JetPack Component:複雑な状態の管理


stateの数が少なければ、グループ内で管理するのに便利です.しかし、数が多くなり、論理も多くなると、他の方法が必要になります.
国家管理には3つの方法がある.
  • Composables
  • State holders
  • ViewModels
  • コンセプト


    State holders


    state holderは、複雑なUIを含むstateとその関連論理のオブジェクトです.単一のUIコンポーネントに関連するか、画面全体に関連する可能性があります.さらにstateholderは、複数のstateを同時に使用する場合に非常に有用な相互合成可能な特徴を有する.
  • UI構成要素は、0〜複数の状態保持者に依存し得る.
  • ビジネスロジックまたはスクリーンステータス(screen state)にアクセスする必要がある場合、一部の州の所有者はView Modelに依存する可能性があります.
  • ビューモデルは、データまたはビジネス層に依存する.

  • ステータスと論理のタイプ


    [state]
  • UI要素ステータス:UIコンポーネントのアップグレードステータス.例えば、ScaffoldのScaffoldState.
  • Screen or UI state:画面に表示されるstate.これらの状態は通常、他のレイヤに関連付ける必要があります.
  • [logic]
  • UI動作or UIロジック:画面にステータスの変化を表示する方法に関するロジック.例えば、ナビゲーションは、次の画面を決定し、Snakbarとして情報を表示するかトーストとして表示するかの論理である.この論理は常に組合せの内部にある必要がある.
  • ビジネスロジック:国の変化に伴って行われる行為です.会員に加入したり、特定の情報を保存したりする行為.
  • 適用例


    状態と論理をComponentableに設定


    非常に簡単なUIロジックとUI elements stateであれば、Composibleに保存するのも良い方法かもしれません.
    @Composable
    fun MyApp() {
        MyTheme {
            val scaffoldState = rememberScaffoldState()
            val coroutineScope = rememberCoroutineScope()
    
            Scaffold(scaffoldState = scaffoldState) {
                MyContent(
                    showSnackbar = { message ->
                        coroutineScope.launch {
                            scaffoldState.snackbarHostState.showSnackbar(message)
                        }
                    }
                )
            }
        }
    }
    ただし、可変状態の場合は、その組み合わせの範囲を超えていることに注意してください.他の組合せでstateを変更すると、エラーを見つけるのが難しいからです.上記の例では、

    stateholderにstateとlogicを配置する


    複数のUIにまたがるUI要素のステータスとUIロジックの場合、Componentableに置くのは賢明ではありません.これらのstateとlogicをstateholderに置くと,複雑さを低減し,注目点分離の原則を遵守することができる.
    state holderは、コンポーネントで作成および削除されるオブジェクトです.state holderは、組合せのライフサイクルに従うため、組合せ依存性を有することができる.以下に、MyAppStateというクラスからStateholderを作成する例を示します.このクラスはCorpose Stateをリソースとして受け入れるので、remembereを介してクラスを返すのは良い操作です.
    class MyAppState(
        val scaffoldState: ScaffoldState,
        val navController: NavHostController,
        private val resources: Resources,
        /* ... */
    ) {
        val bottomBarTabs = /* State */
    
        // Logic to decide when to show the bottom bar
        val shouldShowBottomBar: Boolean
            get() = /* ... */
    
        // Navigation logic, which is a type of UI logic
        fun navigateToBottomBarRoute(route: String) { /* ... */ }
    
        // Show snackbar using Resources
        fun showSnackbar(message: String) { /* ... */ }
    }
    
    @Composable
    fun rememberMyAppState(
        scaffoldState: ScaffoldState = rememberScaffoldState(),
        navController: NavHostController = rememberNavController(),
        resources: Resources = LocalContext.current.resources,
        /* ... */
    ) = remember(scaffoldState, navController, resources, /* ... */) {
        MyAppState(scaffoldState, navController, resources, /* ... */)
    }
    
    @Composable
    fun MyApp() {
        MyTheme {
        	// rememberMyAppState() 를 통해 모든 state 를 받음.
            val myAppState = rememberMyAppState()
            Scaffold(
                scaffoldState = myAppState.scaffoldState,
                bottomBar = {
                    if (myAppState.shouldShowBottomBar) {
                        BottomBar(
                            tabs = myAppState.bottomBarTabs,
                            navigateToRoute = {
                                myAppState.navigateToBottomBarRoute(it)
                            }
                        )
                    }
                }
            ) {
                NavHost(navController = myAppState.navController, "initial") { /* ... */ }
            }
        }
    }

    viewmodelによるビジネスロジック、screen stateへのアクセス


    viewmodelは、ビジネスロジック、データ層にアクセスし、screen stateなどを同時に有するため、stateholderの特殊な形式と見なすことができる.
    ただし、viewmodelはuicomponentよりも長いlifetcycleを有するため、compositionライフサイクルに属する参照を有するべきではない.これは、ViewModelがcontextを必要としないのと同様に、メモリの漏洩を防ぐためです.
    stateholderとしてviewmodelを使用してスクリーンレベルに適した組み合わせを行います.
    data class ExampleUiState(
        dataToDisplayOnScreen: List<Example> = emptyList(),
        userMessages: List<Message> = emptyList(),
        loading: Boolean = false
    )
    
    class ExampleViewModel(
        private val repository: MyRepository,
        private val savedState: SavedStateHandle
    ) : ViewModel() {
    
        var uiState by mutableStateOf<ExampleUiState>(...)
            private set
    
        // Business logic
        fun somethingRelatedToBusinessLogic() { ... }
    }
    
    @Composable
    fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
    
        val uiState = viewModel.uiState
        ...
    
        Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
            Text("Do something")
        }
    }
    state holderとしてviewModelを使用するメリットは、次のとおりです.
  • configが変更された場合でも、実行する必要があるプロセスは良好に保持されます.
  • Navigationとの互換性が良い.
  • hiltとの互換性は良好です.
  • NavigationとViewModel
    ナビゲーション画面がバックグラウンドにあるときにviewModelをキャッシュします.したがって、画面が戻ると、古いデータはそのままになります.一般的なstateholderにとって、これは難しい仕事です.
    バックスタックから画面がポップアップされると、ViewModelも鮮明になり、メモリの漏洩を防ぐために状態も鮮明になります.

    state holderとviewmodel


    以上のことから、state holderとviewModelは異なる責任を負っていることがわかります.
    state holderはUI要素のステータスとUI動作(UIlogic)、viewModelはScreen stateとBusiness logicを担当します.したがって,1つの組合せで2つを同時に使うのはおかしくないが,推奨すべき事項である.
    private class ExampleState(
        val lazyListState: LazyListState,
        private val resources: Resources,
        private val expandedItems: List<Item> = emptyList()
    ) { ... }
    
    @Composable
    private fun rememberExampleState(...) { ... }
    
    @Composable
    fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
    
        val uiState = viewModel.uiState
        val exampleState = rememberExampleState()
    
        LazyColumn(state = exampleState.lazyListState) {
            items(uiState.dataToDisplayOnScreen) { item ->
                if (exampleState.isExpandedItem(item) {
                    ...
                }
                ...
            }
        }
    }