2. Composable 状態 と 管理 について(remember rememberSaveable ViewModel) - Ki-Kobayashi/Android-Wiki GitHub Wiki

https://qiita.com/satoru_pripara/items/a62252d7aa6cc1d7aa90#%E5%85%AC%E5%BC%8F%E5%AE%9F%E8%A3%85%E4%BE%8B

🟩 公式実装例

さまざまなパターンのアーキテクチャによるAndroidアプリ(kotlin)の実装例を格納したGoogle公式リポジトリ

android/architecture-samples UI・マテリアルデザインのサンプルアプリ

android/compose-samples Jetpackを利用したサンプル

android/sunflower 上級編 android/architecture-components-samples/GithubBrowserSample/ ネットワークへのアクセスや認証など、実際にストアで公開されるアプリに近い機能を備えたアプリ

android/nowinandroid

.

🟩 公式UIガイドライン

Introducing the Android Design Hub: The ultimate resource for building exceptional user interfaces across all form factors

Material Design (3) androidx.compose.material3

.

🟩 その他、実装例

アーキテクチャ、UI(Jetpack Compose)の参考など kliment-jonceski/JetPackPlaygroundApp

🟩 Intent

特定のアクションをリクエストする時に使用できる、メッセージングオブジェクトのこと。
例えば別の画面を開きたい時に、「開いている画面に対して別の画面を開いてください」とアプリにメッセージとして伝えることで、別の画面を開ける。

今回は基本的な使い方として、以下がある

  • 「Activityを起動する」
  • 「Serviceを起動する」
  • 「ブロードキャストを配信する」

【Androidアプリ開発】Intentとは、Intentの使い方とは。

インテントとインテント フィルタ

.

🟩 Jetpack Compose

Android版の flutter, SwiftUIとでもいうべき、宣言的に簡潔にUIが記述できるライブラリ。https://qiita.com/kota_2402/items/7bbdd87be8024785e25b Compose でのレイアウト

@Composableアノテーションのついた関数によって、宣言的にUIを構築する。
バージョン2と3があるが(2023/7現在)、新しくアプリを作り始めるのであれば最新版を選択すべきだろう。

Compose でマテリアル 2 からマテリアル 3 に移行する

.

🟡基本の考え方

  • 何か一つのパラメータが変更されたからといって全ての @Composable関数 が再描画されるわけではない。
     → それだと膨大なコストがかかってしまう。

  • 一部の @Composable 関数のみが再描画されることがありうる。
    → それどころか、一つの@Composable関数の中のUI部品のうち、必要なUI部品のみが再描画の対象となり、他の部分は再描画されないということさえありうる。

  • 再描画が終わらないうちにもう一度再描画の指示がされた場合は、終わっていなかった再描画はキャンセルされる。

  • 場合によっては、@Composable関数が UI アニメーションのフレームごとに実行され、一秒間に数百回も実行される可能性がある。
    → パフォーマンスが悪化する。

.

🛑そのため、@Composable関数のなかに「UIを描画する」以外の副作用は一切書くべきではない。

例えば、共有の領域にある変数を変更するなど。
そのような副作用が必要な場合は、@Composable関数のなかで直接行うのではなく、

.
onClick などのコールバックを @Composable関数 に渡して行うようにする。

.

🛑NG

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // 🛑【NG】避ける!カラム再構成の副作用
         👆remember または rememberAsSaveableという関数を用いて、
            MutablState<T>という型の変数を設ければ、@Composable関数の中で状態管理をしても良い。
            }
        }
        Text("Count: $items")
    }
}

.

✅ OK

@Composable
fun EditTaskScreen(
    // TODO:ViewModelは基本外から注入、Nav関連もMyApp(NavHostの定義場所)で制御するようにする
    viewModel: EditTaskViewModel,
    back: () -> Unit,
) {
    //  private な Composable は部品として使いまわせるようにしとく
  //        → 処理(✅ callback )は public な Composable から渡すようにする
    //         → ViewModelは、MyAppでさらに外部から渡すようにする(一元管理できるから?)
    //         .
    // 【collectAsState】 : StateFlow や Flow からデータを収集し、そのデータを ComposeUI の State オブジェクトに変換。
    //       これにより、Compose が状態の変更を検知して UI を再描画できるようになる
    val uiState by viewModel.uiState.collectAsState()

    //  uiState を渡して、変化があれば再コンポーズさせる?
    EditTaskScreen(
        uiState = uiState,
        back = back,
        loadData = { viewModel.loadData() },
        changeIdle = { viewModel.changeIdle() },
        updateTask = { title, description ->
            viewModel.updateTask(title, description)
        },
        showDeleteDialog = {
            viewModel.showDeleteDialog()
        },
        deleteTask = {
            viewModel.deleteTask()
        }
    )
}

.

🟩 Stateful と Stateless

  1. ステートフル: @Composable関数内部で、rememberなどを用いて状態を持っている
  2. ステートレス: @Composable関数内部では状態を持たず、むしろ関数への引数として必要な値やクロージャーを渡して運用          (例えばViewModelなど外側で状態管理など)

慣習として、表示内容は同じだが、ステートレスとステートフルな関数2つを用意する場合がある。↓

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

状態と Jetpack Compose

🟡

.

🟩 状態の持ち方3種


💡remember系 の 補足💡
  ・「by」で書くと、.valueに直接アクセスできる(titleだけで中身の取得が可能)。
  ・「=」で書くと、title.valueのように、「.value」とつけないと中身にアクセスできない

🟡【1つ目】 var title by rememberSaveable { mutableStateOf("") }

   → メモリに一時保存し、データの変化を監視
   → Activity の破棄(画面回転、ダーク/ライトモード切替、言語切替)で、データも破棄される

.

🟡 【2つ目】var description by rememberSaveable { mutableStateOf("") }

   → Bundleに保存し、データの変化を監視
   → Bundle のため、シンプルなデータしか保存できない 👉複雑なデータなら次項のViewModelを使用する
      💡【※】    → Activity が破棄されてもデータを復元できる

. ### 💎 【※】rememberSaveable に保存できる型 Bundleに保存できる型が該当。具体的には以下。

  • 基本データ型
  • 配列
  • listOf / HashMap (🛑 不変な型でないと、変更時に再コンポーズをトリガーできない
  • Parcelable オブジェクト
  • Serializable オブジェクト

. ### 💎 【※】rememberSaveable に複雑な値も保存するには?

.

🟡 【3つ目】下記のように ViewModel で管理

📚 ViewModelで StateFlow 管理する場合

private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
// TODO: asStateFlow: MutableStateFlow → StateFlowに変換
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

.

📚 ViewModelで MutableState 管理する場合

var inputMessage by mutableStateOf("")
     private set

🟡

.

.

🟩 mutableStateOf について

. 状態監視している値が変更されると、監視している Conposable で 再コンポジション される

.

val mutableState = remember { mutableStateOf(default) }
// or
var value by remember { mutableStateOf(default) }
// or
val (value, setValue) = remember { mutableStateOf(default) }

remember関数を用いて、MutableStateという型の状態nameを保持。

.

🟡テキストフィールドにユーザーが何か入力すると、その文字によって表示を変える例

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

また、Androidで用いられるオブザーバブルな型から値を持ってきて、Composeで使うMutableStateへと簡単に変換する関数も用意されている:

📚 Flowの場合:

.

📚 LiveDataの場合:

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

⚠️ **GitHub.com Fallback** ⚠️