【Kotlin】ComposeUIがStateFlowの値を取得できていなかったので、基礎から勉強してみた

## はじめに
Androidアプリを自作している時に、つまづいたところを備忘や整理の意味を込めて記事に残します。今回は 「StateFlow×JetpackCompose」です


## 実現したかったこと
私の作成しているアプリで、アカウントの削除機能を実装していました。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760174/e4be5152-67e3-7980-d7a8-87ddcd5faa8f.png" width=50% >

- アカウント削除ボタンを押すと、DBからユーザ情報を削除
- 削除が成功したら、完了ポップアップを出力する

## 当初の実現方式


概要図（以降に参考コードも載せますが、長いので、読み飛ばしてOK）
![アカウント削除.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760174/2b0ef265-05be-44f1-de53-99bfec93e87d.jpeg)


- 画面A（アカウント削除ボタン画面）

    ````

    //省略


          modifier = Modifier
                    .padding(top = 32.dp)
                    .clickable {
                        DeleteViewModel.reAuthForDel(context,activityResultLauncher)
                    }
            ) {


                Text(
                    text = "アカウント削除",
                    color = Color.Red,
                    fontSize = 24.sp,
                    modifier = Modifier



    ````

<br>


- ViewModel（ユーザ情報の削除と状態管理用）


    ````
    
    
    @HiltViewModel
    class DeleteViewModel @Inject constructor(
        private val db: FirebaseFirestore,
        private val storage: FirebaseStorage,
    ) : ViewModel() {
    
        val user = FirebaseAuth.getInstance().currentUser
        private var _resultFlag = MutableStateFlow(false)
        val resultFlag: StateFlow<Boolean> = _resultFlag
    
        fun chengeResultFlag(flag: Boolean) {
            _resultFlag.value = flag
        }
    
        //Google再認証
        fun reAuthForDel(
            context: Context,
            activityResultLauncher: ActivityResultLauncher<Intent>,
        ) {
            //省略
        }
    
    
    
        //ユーザ情報削除
        fun allUserInfoDeleAft(
            navController: NavController,
            idToken:String?
        ) {
    
            if (idToken !==null) {
    
               //省略
                    user.reauthenticate(googleProvider).addOnSuccessListener {
                        Log.d(TAG,"reauth is success")
    
                        viewModelScope.launch {
                        //user情報の削除を実施
                            val result: Boolean = allUserInfoDel()
                            if (result) {
                            //成功したらFlagをtrueに
                                _resultFlag.value = true 
                                navcontoroler.navigate("LogIn")
                            } else {
                            }
    
                        }
                    }.addOnFailureListener {Exception ->
                    }
                }
            }
        }
    
        //削除用のfunction
        suspend fun allUserInfoDel(): Boolean {
    
    }
    
    ````

    <br>

- 画面B（LogInのUI部分）
    
    ````


    @Composable
    fun LogInView(
        loginViewModel: LoginViewModel = viewModel(),
        currentUserViewModel: CurrentUserViewModel = viewModel(),
        GoogleAuthViewModel: GoogleAuthViewModel = viewModel(),
        navController: NavController,
        DeleteViewModel: DeleteViewModel = viewModel()
    
    ) {
    

    val deleteFlag by DeleteViewModel.resultFlag.collectAsState()
    //省略

    //削除成功後、画面遷移→ポップアップ
        if (deleteFlag) {
        Log.d("DeleteFlag", "deleteFlag: $deleteFlag")
        AlertDialog(
            onDismissRequest =
            { DeleteViewModel.chengeResultFlag(false) },
            title = {
                Text("削除完了のお知らせ")
            },
            text = {
                Text(text = "ユーザアカウントが削除されました。")
            },
            confirmButton = {
                Button(onClick = {
                    DeleteViewModel.chengeResultFlag(false)
                })
                {
                    Text(text = "OK")
                }
            })
    }
    
    
    ````

## 困ったこと
概要図の再掲ですが、画面BでviewModelが定義しているStateFlowの状態を拾えず「削除完了」のポップアップが表示できませんでした。

![アカウント削除 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760174/3ee1391b-bf4b-49c2-d579-7eb6919c0795.jpeg)


## 原因調査の結果
調査の結果、「アクティブではない画面B」に「StateFlowの収集」を任せていることが原因だとわかりました。


ざっくりとした理解にはなってしまいますが、「ComposeUI」「Navigation」で画面遷移を管理している場合は、以下のような各composeで定義されている画面が管理されており、画面AからBに遷移する際は、画像内のリストが入れ替わるはずです。
![スクリーンショット 2025-01-05 11.14.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760174/10d63e0d-e9bc-e826-2c18-e2a17367a5ad.png)

また、アクティブな画面においては、composeが再描画できるような状態（Compose は自動的に UI を更新できる状態）になっていますが、画面B〜Dのようにアクティブでない画面はそうではありません。

そのため、アクティブではない画面B（アプリのログイン画面）でアカウントの削除結果を元にしたStateFlowの値を監視するということはできなかったのです。

## 改善
なので、以下のように現在アクティブである「アカウント削除の画面A」でStateFlowを監視し、ポップアップで「削除完了！」と表示させ、その後に画面Bに遷移させることで、composeUIがStateFlowを監視しない問題を回避することができました。

![アカウント削除 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760174/7120a75a-7a2a-00d7-c8c0-9854f1fb855d.jpeg)

以下に改修後のコードを参考までに載せておきます

- 画面A（アカウント削除ボタン画面）

    ````

    //省略

    val deleteFlag by DeleteViewModel.resultFlag.collectAsState()


          modifier = Modifier
                    .padding(top = 32.dp)
                    .clickable {
                        DeleteViewModel.reAuthForDel(context,activityResultLauncher)
                    }
            ) {


                Text(
                    text = "アカウント削除",
                    color = Color.Red,
                    fontSize = 24.sp,
                    modifier = Modifier)

                    
    if (deleteFlag) {
                    Log.d("DeleteFlag", "deleteFlag: $deleteFlag")
                    AlertDialog(
                        onDismissRequest =
                        { DeleteViewModel.chengeResultFlag(false) },
                        title = {
                            Text("削除完了のお知らせ")
                        },
                        text = {
                            Text(text = "ユーザアカウントが削除されました。")
                        },
                        confirmButton = {
                            Button(onClick = {
                                DeleteViewModel.chengeResultFlag(false)
                                navController.navigate("LogInView")
                            })
                            {
                                Text(text = "OK")
                            }
                        })
                }


    ````

<br>

- ViewModel（ユーザ情報の削除と状態管理用）
     　#navigatioをコメントアウト。それ以外は変更なし


    ````
    
    
    @HiltViewModel
    class DeleteViewModel @Inject constructor(
        private val db: FirebaseFirestore,
        private val storage: FirebaseStorage,
    ) : ViewModel() {
    
        val user = FirebaseAuth.getInstance().currentUser
        private var _resultFlag = MutableStateFlow(false)
        val resultFlag: StateFlow<Boolean> = _resultFlag
    
    
    
        //ユーザ情報削除
        fun allUserInfoDeleAft(
            navController: NavController,
            idToken:String?
        ) {
    
            if (idToken !==null) {
    
               //省略
                    user.reauthenticate(googleProvider).addOnSuccessListener {
                        Log.d(TAG,"reauth is success")
    
                        viewModelScope.launch {
                        //user情報の削除を実施
                            val result: Boolean = allUserInfoDel()
                            if (result) {
                            //成功したらFlagをtrueに
                                _resultFlag.value = true 
                            //画面遷移は画面A内で実装するためコメントアウト
                            //navcontoroler.navigate("LogIn")
                            } else {
                            }
    
                        }
                    }.addOnFailureListener {Exception ->
                    }
                }
            }
        }
    
        //削除用のfunction
        suspend fun allUserInfoDel(): Boolean {
    
    }

    ````

<br>

- 画面B
    - StateFlowを監視するロジックを削除
    - alertDialogを削除（画面Aにて実装）


## おわりに
誰かの役に立てば嬉しいです。
誤り等あれば、ご指摘ください。