DataStore を使ってタスクキルしても大丈夫なお気に入りフィルターを作った with Jetpack Compose



## 今回行ったこと
トグルスイッチの状態をタスクキルしたり、画面回転させてもリセットされないようにしました。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747809/d6c3218f-1871-b514-5f40-662592c419f2.gif width=330>


## 👯‍♂️登場人物？紹介
このデータ保持をする実装をするために以下の登場人物たち？が登場します。 (登場プログラムと言えば良かったかな・・・)

- viewModel
    -  DataStore を使ってデータを保存するために使用
    - Activity が破棄されるイベント(画面回転など)があっても生き残り続けるため重要人物
- ToggleComponent
    - トグルスイッチを実装するコンポーネント
    - タップされるたびに状態を上書き保存
    - デフォルト値は false
- ListScreen
    - リスト画面にトグルスイッチを配置
    - コンポーネントはここから呼び出し

その中で使用するメソッドたちです。
- Preference  DataStore
    - 実装が簡単な方の  DataStore
    - キーを設定して読み出しや書き出しを行う
    -  DataStore よりも前に広く使われていた Shared Preference に使用感が似ている
- corutineScope
    - 非同期で処理を行うために使用
    - これを使って読み込みと書き込みを行う
- LaunchedEffect
    - 今回は起動時のスイッチの状態読み込みで使用

## 👩‍💻実装コード
まずは、 DataStoreの書き込みと読み込み処理をどこで行っているのかをみてみましょう。
### 📝 DataStoreの読み込みと書き込み
 DataStore への読み込みと書き込みはデータベースのようにキーを設定し、それを元に操作していきます。
 DataStore を使おうとすると`suspend fun` を使う必要があり、取得できるデータの型が`Flow` になるためちょっとした工夫が必要です。

**読み込み処理**


```ViewModel.kt
//最初の宣言を忘れずに
val Context. DataStore:  DataStore<Preferences> by preferences DataStore(name = "UserPreferences")
class LandmarkViewModel(application: Application) : AndroidViewModel(application) {
    //...
suspend fun readToggleState(context: Context): Boolean {
        val toggleKey = booleanPreferencesKey("TOGGLE_KEY")
        val toggleFlow = context. DataStore.data.map {
            preferences -> preferences[toggleKey] ?: false
        }
        // first() を使用することでFlow<Boolean>を Boolean に変換できる
        return toggleFlow.first()
    }
     //...
}
```

**書き込み処理**

```ViewModel.kt
suspend fun saveToggleState(context: Context, isChecked: Boolean) {
         val toggleKey = booleanPreferencesKey("TOGGLE_KEY")
         // isChecked はトグルスイッチがタップされた時に渡される値です
         context. DataStore.edit { preferences ->
             preferences[toggleKey] = isChecked
         }
     }
     //...
```

繋げるとこんか感じです。
```ViewModel.kt
//最初の宣言を忘れずに
val Context. DataStore:  DataStore<Preferences> by preferences DataStore(name = "UserPreferences")
class LandmarkViewModel(application: Application) : AndroidViewModel(application) {
    //...
suspend fun readToggleState(context: Context): Boolean {
        val toggleKey = booleanPreferencesKey("TOGGLE_KEY")
        val toggleFlow = context. DataStore.data.map {
            preferences -> preferences[toggleKey] ?: false
        }
        // first() を使用することでFlow<Boolean>を Boolean に変換できる
        return toggleFlow.first()
    }

    suspend fun saveToggleState(context: Context, isChecked: Boolean) {
         val toggleKey = booleanPreferencesKey("TOGGLE_KEY")
         // isChecked はトグルスイッチがタップされた時に渡される値です
         context. DataStore.edit { preferences ->
             preferences[toggleKey] = isChecked
         }
     }
     //...
}
```

### 📱読み込んだ状態を UI へ反映、状態変化を書き込み
次に読み込んだ状態をUI に反映させる方法とタップされて状態変化が起きた時に状態を保存する方法をご紹介します。
トグルスイッチは `Switch` で作り出すことができます。
他の言語では分岐としてお馴染みですが、ここではUIのコンポーネントを設定するために登場します。

💡ちなみに Kotlin には Switch 文に相当するものとして `When`があります。

**読み込んで反映**
アプリを立ち上げた時に DataStore からトグルスイッチの状態を読み出して反映させます。

```ToggleComponent.kt
@Composable
fun favoriteToggle(viewModel: LandmarkViewModel = viewModel()): Boolean {
    val context = LocalContext.current
    var isFavorite by remember { mutableStateOf(false) }
    var isChecked by remember { mutableStateOf(false) }

    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        //  DataStore からデータを取得
        // スマホを横向きにしても、タスクキルしても状態が残るようにする
        isFavorite = viewModel.readToggleState(context)
        isChecked = isFavorite
    }
    //...

}

```

**状態変化を書き込み**
トグルスイッチをタップした時に今の状態を保存する処理を埋め込みます。
```ToggleComponent.kt
Switch(
        checked = isChecked,
        onCheckedChange = { checked -> isChecked = checked
                          coroutineScope.launch {
                              //タップされた時に非同期で状態を保存
                              viewModel.saveToggleState(context,checked)
                          }
                          },
        // トグルスイッチの色を設定
        colors = SwitchDefaults.colors(
            checkedThumbColor = MaterialTheme.colorScheme.primary,
            checkedTrackColor =MaterialTheme.colorScheme.primaryContainer,
            uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
            uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer
        )
    )
    return isChecked
```
繋げるとこんな感じです。

```ToggleComponent.kt
@Composable
fun favoriteToggle(viewModel: LandmarkViewModel = viewModel()): Boolean {
    val context = LocalContext.current
    var isFavorite by remember { mutableStateOf(false) }
    var isChecked by remember { mutableStateOf(false) }

    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        //  DataStore からデータを取得
        // スマホを横向きにしても、タスクキルしても状態が残るようにする
        isFavorite = viewModel.readToggleState(context)
        isChecked = isFavorite
    }

    Switch(
        checked = isChecked,
        onCheckedChange = { checked -> isChecked = checked
                          coroutineScope.launch {
                               //タップされた時に非同期で状態を保存
                              viewModel.saveToggleState(context,checked)
                          }
                          },
        // トグルスイッチの色を設定
        colors = SwitchDefaults.colors(
            checkedThumbColor = MaterialTheme.colorScheme.primary,
            checkedTrackColor =MaterialTheme.colorScheme.primaryContainer,
            uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
            uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer
        )
    )
    return isChecked
}
```

## スイッチの置き場所
ここまで実装できたらいよいよ画面にトグルスイッチを設置します。
トグルスイッチがどうやってお気に入りフィルターとして機能するのかもコメントの中に記載しました。


```ListScreen.kt
// 絞り込みの際に使用
private var isOnFavoriteToggle = false
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LandmarkList(viewModel: LandmarkViewModel,navController: NavController) {
    val landmarks = viewModel.landmarks.observeAsState(initial = emptyList())
    val favoriteDataList = viewModel.favorites.observeAsState(initial = emptyList())
    Column {
        CenterAlignedTopAppBar(
            title = {
                Text(
                    text = stringResource(id = R.string.app_name),
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
            },
            colors = TopAppBarDefaults.topAppBarColors(
                containerColor = MaterialTheme.colorScheme.primaryContainer,
                titleContentColor = MaterialTheme.colorScheme.primary
            )
        )
        Row(modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically) {
            Text(text = stringResource(id = R.string.favoriteTitle), style = MaterialTheme.typography.bodyLarge)
            // 中央のスペースを開けるためのスペーサー
            Spacer(modifier = Modifier.weight(1f))
            //ここでトグルスイッチを設置
            //今の状態を取得
            isOnFavoriteToggle = ToggleComponent(viewModel)
        }
        
        LazyColumn {
            items(landmarks.value.size) { index ->
            //トグルスイッチが OFF なら全て表示
            //その項目がお気に入り登録されていればお気に入りフィルターが ON でも表示する
                if (isOnFavoriteToggle.not() || favoriteDataList.value[index].isFavorite) {
                        ListItem(landmark = landmarks.value[index],favoriteData=favoriteDataList.value[index].isFavorite, navController = navController)
                }
            }
            item { Spacer(modifier = Modifier.padding(24.dp)) }
        }
    }
}
```

## 💭改善したい点
今回はトグルの状態を保存してタスクキルされても適用できるように実装を行いました。

しかし、まだまだ反省点は多いです。

- コンポーネントの中に LanchedEffect などを書いてしまって UI と データ層の分離がうまくできてなかった
- 初期状態の読み込みをもっとスマートなコードにしたい
- そもそも、Fragment とか正しく使えてなかった

などなどありますが、また順次学習して改善していこうと思います。

閲覧いただきありがとうございました!🙇