トップ画像
Jetpack Composeで、Mutable系統をStateに登録したらあかん

執筆者: オキリョウ

最終更新: 2021/09/17

タイトルで書いてあることを悟るまでに数日潰したので、怒鳴り散らしながらメモとして残します。

Jetpack Composeとは

Android開発において新たな時代の到来を感じさせるUI Toolkitです。
Androidは十数年前に登場して以降、様々なアプリが開発されてきています。
その影響もあり、Android開発を行うときは十数年前のクソみたいな古い風習が残っています。
例えばUIを作成するときです。
Androidでは命令型UIを採用しており、XMLと呼ばれる言語で書いていく必要がありました。
以下のような奴です

<?xml version="1.0" encoding="utf-8"?>
	<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
	    android:id="@+id/LinearLayout"
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content"
	    android:layout_margin="10dp"
	    android:background="@android:color/white"
	    android:elevation="4dp"
	    android:orientation="horizontal">
	

	    <androidx.appcompat.widget.AppCompatTextView
	        android:id="@+id/title"
	        android:layout_weight="1"
	        android:gravity="center"
	        android:layout_gravity="center_vertical"
	        android:textSize="30sp"
	        android:layout_height="wrap_content"
	        android:layout_width="0dp"
	        android:clickable="true"
	        android:text="@string/title"
	        android:focusable="true" />
	

	    <androidx.appcompat.widget.AppCompatImageButton
	        android:id="@+id/delete_button"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:src="@drawable/ic_delete_title"
	        android:contentDescription="@string/delete" />
	

	</androidx.appcompat.widget.LinearLayoutCompat>


こんなコードから以下のようなUIが出力されます


こんなのコードだけ見ても何を表しているのか、慣れていない人にとってはさっぱりわかりません。慣れててもよくわかりません。


これだけならいいのですが、ここからデータを表示するとなると、KotlinやJavaでコードを書く必要があります。
その書き方がかなりめんどくさいわけです。例えば、以下のような感じになります。

val binding = ChecklistTitleBinding.inflate(inflater, parent, false)
val title: TextView = binding.title

//要素の文字の部分の設定
title.apply {
    text = "Hello World"
    textSize = getTextSize(context).toFloat()

    //タイトル名をクリックした際の処理
    setOnClickListener {
        clickTitleOnListener(title.text.toString())
    }
}


この通り、部品一つずつ指定して書いていく必要があります。
これで、部品の名前とかはXMLで書いているのでそっちを見ないといけないし、指定するのを忘れていたらバグにつながります。

そこでJetpack Composeの出番です。
これは最近はやっている宣言的UIを採用しており、パッと見たらわかる感じになっています。
例えば、以下のように書くと、先ほど紹介したコードと同じことができます。

@Composable
fun LazyCard(text: String = "Hello World"){
    Card(
        modifier = Modifier
            .padding(10.dp)
            .fillMaxWidth()
    ) {
        Row(
            modifier = Modifier.padding(12.dp)
        ) {
            Text(
                text = text,
                modifier = Modifier.weight(1f)
            )
            Icon(
                painter = painterResource(R.drawable.ic_baseline_delete_24),
                contentDescription = null
            )
        }
    }
}


以下のような感じですね

これだと、コードを見ただけで、先ほどよりは断然分かりやすいと思います。
どのような見た目をしているかだったり、どこにデータが流れていくかであったりが分かりやすいので非常に使いやすいです。

このように、Jetpack Composeは、Androidの新時代を感じさせるUI Toolkitになっています。

本題

やっと本題です。
Kotlinには複数のデータを扱うために、標準ライブラリの中にいくつかの型(class)が存在します
例えば配列を扱うためのList型、複数のPair型(keyとvalueを持った型)を扱うためのMap型などがそれにあたります。

これらの型は、一度値を作成したら値を追加、削除することができません

しかし、これらを継承した型である、MutableList型、MutableMap型等、Mutable系統であれば可能です

val list = listOf("は", "ひ", "ふ")

//コンパイルエラー
list.add("へ")


val mutableList = mutableListOf("は", "ひ", "ふ")

//これはOK
mutableList.add("へ")


結構扱いやすく、私もよく利用している型です。

そのため、Jetpack Composeにおいても何気なく使ったことが運の尽きでした。



ある日、こんな感じの、+ボタンを押したらカードが追加されていくアプリを作成しました。
ソースコードは以下の通りです。

@Composable
fun List(){

    //副作用
    var list by remember { mutableStateOf((0..3).toMutableList()) }

    //UI部分
    LazyColumn(Modifier.fillMaxSize()){
        item{
            //カード表示部分
            for(i in list){
                LazyCard(text = i.toString())
            }
        }
        item {
            //ボタン
            AddButton(
                //ボタンを押したときの処理
                onClick = {
                    val tempList = list
                    tempList.add(4)
                    list = tempList
                }
            )
        }
    }
}


(副作用ってなんやねんて話ですが、話し出すと記事が一つできるような内容ですので省きますが、簡単に言うと、「関数外のところに影響を及ぼす部分」のことです。)


このremember{}の部分が何をしているかというと、画面に表示するためのMutableListを保存しています。
rememberのところでState型を保有しておき、それを返してくれます。
これを変数に入れておき、値が変更されると、UIを更新してくれる優れものです。

今回の場合ですと、Listの中身が追加されれば、表示されるリストの数も追加されて以下のようになるはずです。


そして、画面内の+ボタンを押すと、Listの中身が追加されるようになっています。

//ボタン
AddButton(
    //ボタンを押したときの処理
    onClick = {
        //別の変数に代入
        val tempList = list
        
        //変数を追加
       tempList.add(4)

        //listに反映
        list = tempList
    }
)

つまり、このボタンを押せば増えてくれるはずです。

しかし、実際には増えてくれません。
この挙動のせいで数日間頭を抱える事態に陥ってしまいました

原因

結論から書くと、rememberで渡された値が参照渡しになっていることでした。

私の意図した動きは以下の通りです

//副作用の部分
var list by remember { mutableStateOf((0..3).toMutableList()) }

AddButton(
    onClick = {
        //別の変数に、実際の値のコピーを代入
        val tempList = list
        
        //tempListのリストに、変数を追加
       tempList.add(4)

        //本体のlistに反映(ここで差分を検知してUIの再描画が走る)
        list = tempList
    }
)


しかし、実際にはこう動いていました。

//副作用の部分
var list by remember { mutableStateOf((0..3).toMutableList()) }

AddButton(
    onClick = {
        //別の変数に、listの参照先を代入
        val tempList = list
        
        //tempListのリストに、変数を追加(ここで、listの方にも変数が追加される。この追加方法ではUIの再描画が走らない)
       tempList.add(4)

        //本体のlistとtempListの両方に値が追加されたせいで、差分が一切ないため、UIの再描画が走らない
        list = tempList
    }
)


いろいろと調べてると、どうやらKotlin(というよりJava)では、クラス型は参照渡しを採用しているらしいです。
http://syrinx.q.t.u-tokyo.ac.jp/tori/java/basic/entity_ref.html
Mutable系統もクラス型なので参照渡しということですね。

解決策

値を新しく作って、ぶち込めば解決です。

//副作用の部分
var list by remember { mutableStateOf((0..3).toList()) }

AddButton(
    onClick = { list = list + listOf(list.size) }
)


listに、新しくList型を作り直して、それをぶち込んでいます。

ここで重要なのが、Mutable型の特徴であるadd()関数では差分を検知してくれないということです。
ですので、Mutable型を使うことに対するメリットがないばかりか、意図せず使ってしまい、バグを誘発する危険性があります。

だからこそ、Mutable型をStateの部分(rememberのところ)では避けて、普通の型を使うべきでしょう。

以上

しかし、こんな記事俺以外に誰が見るんだろうか・・・

取得に失敗しました

2020年度 入部

Twitter GitHub