【 Jetpack Compose 】お気に入りの項目だけ表示させる


## 今回実装したもの

お気に入りに登録した観光地のみを表示させる機能を実装しました。

トグルスイッチをオンにするとお気に入りだけが表示されて、オフにすると全ての観光地が表示されます。

| 実装した機能 |
|---|
|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747809/cea5f63f-e355-cc2a-e053-4f73e0e1da3a.gif width=400>|


今回は、その実装方法をかねてより作成している、観光地リストアプリをベースにして紹介します。

https://github.com/jtakumi/jetpackList/tree/toggle 

## 使用するもの
お気に入り機能を実装するために使用したものは以下のものです。

- LandmarkViewModel.kt
    - 観光地情報を保持する viewModel ですが、今回お気に入りかどうかの情報も保持するようにします
- FavoriteData.kt
    - お気に入り情報を取得する時のデータの型を指定します
- Favorite.json
    - お気に入り情報が入った JSON ファイルです
- FavoriteToggle.kt
    - トグルスイッチを実装します
- LandmarkList.kt
    - 観光地リストを表示させます
- MainActivy.kt
    - アプリのライフサイクルを管理します。 viewModel の宣言はここで行います



## FavoriteData
ViewModel 取り掛かる前に data class を設定します。

今回は観光地の `id` とお気に入りか否かの `Boolean` を読み出します。

```FavoriteData.kt
data class FavoriteData(
    val id:Int,
    val isFavorite:Boolean
)
```

## ViewModel

ViewModel では新たにお気に入り情報を取り扱うように変数を追加します。

`build.gradle (Module:app)` では以下のものを `dependencies` に追加してください。

```build.gradle
    implementation(libs.gson)
    implementation(libs.androidx.runtime.livedata)
```
そして、 viewModel のコードでは Gson を用いて JSON ファイルから取得したデータをパースして、 `LiveData` として保持します。

```LandmarkViewModel.kt

class LandmarkViewModel(application: Application) : AndroidViewModel(application) {
    // landmarks は観光地情報保持 ( 既存 ) 
    private val _landmarks = MutableLiveData<List<LandmarkData>>()
    val landmarks: LiveData<List<LandmarkData>> = _landmarks
    // favorites はお気に入り情報保持
    private val _favorites = MutableLiveData<List<FavoriteData>>()
    val favorites: LiveData<List<FavoriteData>> = _favorites

    init {
        loadLandmarks()
        loadFavorites()
    }

    // loadLandmarks() 観光地情報読み込み
   
    private fun loadFavorites() {
        try {
            // asset フォルダにアクセスします。
            val assetManager = getApplication<Application>().assets
            val inputStream = assetManager.open("Favorites.json")
            val jsonString = inputStream.bufferedReader().use { it.readText() }
            val favorites = object : TypeToken<List<FavoriteData>>() {}.type
            _favorites.value = Gson().fromJson(jsonString, favorites)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

```

## FavoriteToggle
トグルスイッチのコンポーネントを作成します。

スイッチが操作されると、その度に `Boolean` 値を返すようになっています。

```FavoriteToggle.kt
fun favoriteToggle(): Boolean {
    var isFavorite by remember {
        mutableStateOf(false)
    }
    Switch(
        checked = isFavorite,
        onCheckedChange = { isChecked -> isFavorite = isChecked },
        // スイッチの色はテーマをカラースキームで設定すると本体のテーマカラーに応じて変化します
        colors = SwitchDefaults.colors(
            checkedThumbColor = MaterialTheme.colorScheme.primary,
            checkedTrackColor =MaterialTheme.colorScheme.primaryContainer,
            uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
            uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer
        )
    )
    return isFavorite
}

```

## Favorite.json
お気に入り情報が入っている JSON を見てみましょう。

今回はお気に入りだけが表示されるか見るために初期値を埋め込んであります。

ゆくゆくはアプリ側でも操作したいところです。

```Favorite.json
[
    // 観光地と同じ個数のデータを入れます。
  {"id": 1,"isFavorite": true},
  {"id": 2, "isFavorite": false},
  {"id": 3, "isFavorite": false},
  {"id": 4, "isFavorite": true},
  {"id": 5, "isFavorite": false},
  {"id": 6,"isFavorite": false},
  {"id": 7, "isFavorite": false},
  {"id": 8, "isFavorite": true},
  {"id": 9, "isFavorite": false},
  {"id": 10, "isFavorite": false},
  //...
]

```

## LandmarkList
お待たせしました！いよいよ画面の実装に入ります。

今回はリストの上にトグルスイッチを配置します。

トグルスイッチはスクロールしても画面の中に留まり続けます。

```LandmarkList.kt
// お気に入りスイッチの初期値は OFF
private var isOnFavoriteToggle = false

sealed class Destination(val route: String) {
    data object Landmarks : Destination("landmarks")
}

@Composable
fun SetLandmarkView(landmarkViewModel: LandmarkViewModel,navController: NavHostController){
    NavHost(navController = navController, startDestination = Destination.Landmarks.route) {
        composable(Destination.Landmarks.route) {
            LandmarkList(viewModel = landmarkViewModel,navController)
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LandmarkList(viewModel: LandmarkViewModel,navController: NavController) {
    //viewModel が持っている情報を使用する
    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 = favoriteToggle()
        }
        
        LazyColumn {
            items(landmarks.value.size) { index ->
            // トグルスイッチが OFF もしくは、その観光地がお気に入り登録されていれば表示
                if (isOnFavoriteToggle.not() || favoriteDataList.value[index].isFavorite) {
                        ListItem(landmark = landmarks.value[index], navController = navController)
                }
            }
            item { Spacer(modifier = Modifier.padding(24.dp)) }
        }
    }
}

    @Composable
    fun ListItem(landmark: LandmarkData,navController: NavController) {
        Column(modifier = Modifier
            .fillMaxWidth()
            .border(1.dp, Color.Gray)
            .padding(16.dp)) {
            // 観光地の　id や名前などが表示される
            Text(text = "No.${landmark.id}:${landmark.name}", style = MaterialTheme.typography.titleLarge)
            Text(text = landmark.description,style = MaterialTheme.typography.bodyLarge)
            }
    }


```

## MainActivy
viewModel は Activity にライフサイクルを管理してもらうために宣言しなければなりません。

忘れがちですが行いましょう。

build.gradle(Module:app) `dependencies` に以下のライブラリを追加してください。
```build.gradle
implementation(libs.androidx.lifecycle.runtime.ktx)
```
Activity はこのように実装します。

```MainActivity.kt
class MainActivity : ComponentActivity() {

    //Android のライブラリである viewModels を使います。
    private val landmarkViewModel: LandmarkViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            JetpackListTheme {
                Surface {
                    val navController = rememberNavController()
                    //リスト画面をセッティングするメソッドに viewModel を継承します。
                    SetLandmarkView(landmarkViewModel,navController = navController)
                }
            }
        }
    }
}
```

以上で今回の実装は終わりです。

もし、アプリの全体像が見たい方はリポジトリへお越しください。

お疲れ様でした。