Jetpack Compose: シンプルな方法での MVVM 状態管理

#### はじめに

はじめまして、Hridoy Chandra Das(リド)（@ihridoydas）です。

Jetpack Compose での状態管理を簡素化する方法と、https://jsonplaceholder.typicode.com/users Web API を使用してユーザーを取得する方法を学びます。

こちらでは、状態管理を簡素化したいと考えています。
1 つの状態変数(UserState)だけを使用して画面のすべての状態を管理する方法をご紹介します。
Dagger と MVVM アーキテクチャ パターンを備えた Jetpack Compose を使用します。
#### MVVM アーキテクチャ パターン

:::note warn
警告
独自の方法を使用していますが、お好きなように使ってください。
:::



<img width="400" alt="Screenshot 2023-12-10 at 10.43.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/27b0719a-652a-82a2-a397-ad8e8f030967.png">




ViewModel を使用して、ビジネス ロジックを UI コンポーネントから分離する状態ホルダー。 ViewModel はライフサイクルを認識するコンポーネントであるため、コンポジションよりもライフサイクルが長く、構成が変更されても ViewModel の状態を保持できます。

今回はこの(ViewModel)アプローチについて学びます。 ビューからビジネス ロジックを抽出し、将来の開発者にとって理解しやすく維持しやすい方法で ViewModel 内に配置できます。

では、コーディングしてみましょう…

##### Android Studio で新しいプロジェクトを作成し、Composeアクティビティを選択します。

#### 設定:
#### <ins>プロジェクトに使うデペンデンシーライブラリー:</ins>
```build.gradle.kts
dependencies {
//...Jetpack composeの必要なデペンデンシーライブラリー
     
    // Use Dagger Hilt
    implementation ("com.google.dagger:hilt-android:2.48.1")
    kapt ("com.google.dagger:hilt-android-compiler:2.48.1")
    
    //Hilt Navigation
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
    
    // ViewModel
    implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")

    //kotlin Coroutines
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

    // Retrofit
    implementation ("com.squareup.retrofit2:retrofit:2.9.0")
    implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

}
```

##### Jetpack Composeでナビゲーションアニメーションを使用したい場合こちら。。
https://qiita.com/ihridoydas/items/e97bb646e9affaf27e23


#### <ins>build.gradle.kts(Project)</ins>
```build.gradle.kts
plugins {
    .
    .
    id ("com.google.dagger.hilt.android") version "2.48" apply false
}
```
#### <ins>build.gradle.kts(Module)</ins>
```build.gradle.kts
plugins {
    .
    .
    id ("kotlin-kapt")
    id ("com.google.dagger.hilt.android")
}
```

#### <ins>```AndroidManifest.kt``` にインターネット許可を追加 </ins>
```AndroidManifest.kt
    <uses-permission android:name="android.permission.INTERNET"/>
```

#### <ins>Dependency Injection And Compose View: </ins>
#### <ins>Step 1:</ins> BaseApp.kt
```BaseApp.kt
@HiltAndroidApp
class BaseApp : Application()
```
#### <ins>Step 2:</ins> AndroidManifest.ktに```android:name```を追加
```AndroidManifest.kt
 <application
        android:name=".BaseApp"
        .
        .
        .
 </application>
```
#### <ins>Step 3:</ins> AppModule.ktに```@Module```をdiに提供する（provideRetrofit）と(provideUserServices)
[https://gist.github.com/ihridoydas/f9816487424c325808d684176096ac3c](https://gist.github.com/ihridoydas/f9816487424c325808d684176096ac3c)

```AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    // Provide Retrofit
    @Singleton
    @Provides
    fun provideRetrofit():Retrofit{
        return Retrofit.Builder()
            .baseUrl("https://jsonplaceholder.typicode.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    // Provide User Services(jsonplaceholder)
    @Singleton
    @Provides
    fun provideUserServices(retrofit: Retrofit):UserServices{
        return retrofit.create(UserServices::class.java)
    }

}
```
#### <ins>Step 4:</ins> User データクラスを作成

```User.kt
data class User(
    val id :Int,
    val name:String,
    val username:String,
    val email:String
)
```


#### <ins>Step 5:</ins> UserServices Interfaceを作成
```UserServices.kt
interface UserServices {
    //今回ユーザーデータを取得する
    @GET(value = "/users")
    suspend fun fetchUser() : List<User>
}
```

#### <ins>Step 6:</ins> UI Stateを作成(状態管理)

```UserState.kt
sealed class UserState{
    data object StartState: UserState()
    data object LoadingState: UserState()
    data class Success(val users: List<User>) : UserState()
    data class Error(val errorMessage:String): UserState()
}
```
#### <ins>Step 7:</ins> ここに ViewModel があります。
ここ↑では、デフォルト状態 START で画面の状態を管理するための MutableStateFlow を 1 つ定義します

```JsonPlaceHolderUsersViewModel.kt
@HiltViewModel
class JsonPlaceHolderUsersViewModel @Inject constructor(
    private val userServices: UserServices,

) : ViewModel(){

    val userState = MutableStateFlow<UserState>(UserState.StartState)

    init {
        fetchUsers()
    }
    //データをフェッチする
    private fun fetchUsers(){
        viewModelScope.launch {
            userState.tryEmit(UserState.LoadingState)

            try {
                val users = userServices.fetchUser()
                userState.tryEmit(UserState.Success(users))
            }
            catch (e:Exception){
                e.localizedMessage?.let { UserState.Error(errorMessage = it) }
                    ?.let { userState.tryEmit(it) }
            }
        }
    }

}
```

#### <ins>Step 8:</ins> UserItemViewとUsersListViewを作成。

##### UserItemViewを作成

```UserItemView.kt
@Composable
fun UserItemView(user: User) {
    Row(
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically

    ) {
        Box(
            modifier = Modifier
                .size(50.dp)
                .background(Color.Black, CircleShape),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = user.name.substring(0, 1),
                color = Color.White,
                fontWeight = FontWeight.Bold,
                fontSize = 24.sp,
            )
        }
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 6.dp),
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = user.name,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold,
                color = Color.Black,
            )
            Spacer(modifier = Modifier.padding(top = 3.dp))
            Text(
                text = "Username: ${user.username} (Id:${user.id})",
                fontSize = 10.sp,
                fontWeight = FontWeight.Light,
                color = Color.Black,
            )

            Spacer(modifier = Modifier.padding(top = 4.dp))
            Text(
                text = user.email,
                fontSize = 16.sp,
                color = Color.Black,
            )

        }

    }

}
```
<img width="500" alt="Screenshot 2023-12-10 at 11.25.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/29d96c39-78e7-2a87-813b-1d2da51d486a.png">

##### UsersListViewを作成

```UsersListView.kt
@Composable
fun UsersListView(users:List<User>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        content = {
        items(users){user->
            UserItemView(user = user)
        }
    })
}
```

#### <ins>Step 9:</ins> 次に、この状態を処理するコンポーザブルを構築しましょう。

viewModel の初期化では、まず状態を START から LOADING に変更します。 この状態では、進行状況インジケーターがユーザーに表示されます。 次に、IO スレッドでデータをフェッチする呼び出しを行い、結果を待ちます。 API からデータを受信したら、受信したユーザー リストを使用して状態を LOADING から SUCCESS に変更します。失敗した場合は、状態を FAILURE に設定し、問題が発生したことをユーザーに通知するエラー メッセージを表示します。

```MVVMStateManagementScreenUsers.kt
//MainView
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MVVMStateManagementScreenUsers(
    usersViewModel: JsonPlaceHolderUsersViewModel = hiltViewModel(),
    navController: NavController,
    onBackPress : ()->Unit,
) {
    val context = LocalContext.current
    //Collect Data as a State
    val userState by usersViewModel.userState.collectAsState()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = "MVVM State Management User",
                        fontSize = 12.sp,
                        modifier = Modifier.fillMaxWidth(),
                        textAlign = TextAlign.Left
                    )
                },
                navigationIcon = {
                    IconButton(
                        onClick = {
                            onBackPress()
                        },
                        modifier = Modifier
                    ) {
                        Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
                    }
                },

            )
        }
    ) {
        Box(modifier = Modifier
            .fillMaxSize()
            .padding(it)){
            
            //こちら、UI状態管理
            when(userState){
                //LOADING
                is UserState.LoadingState->{
                    Box(modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center){
                        CircularProgressIndicator()
                    }
                }
                
                is UserState.StartState->{}
                
                //SUCCESS
                is UserState.Success->{
                    val users = (userState as UserState.Success).users
                    UsersListView(users)
                }
                //FAILURE
                is UserState.Error->{
                    val message = (userState as UserState.Error).errorMessage
                    Toast.makeText(context,message,Toast.LENGTH_LONG).show()
                    Column(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(start = 6.dp),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            text = message,
                            fontSize = 15.sp,
                            fontWeight = FontWeight.Bold,
                            color = Color.Black,
                        )
                    }
                }
            }
        }

    }
}
```

ここの↑ MainView() では、画面の状態に従ってビューを設定します。これで、ViewModel が状態を更新するたびに、コンポーザブルは現在の状態に基づいてビューを再構成します。

#### <ins>結果:</ins> [GitHub サンプル](https://github.com/ihridoydas/AndroidBlog/tree/blog1/MVVM_State_management)
##### かなりシンプルです…:blush:
以上、最後までお読みいただきありがとうございました。

<img width="400" alt="MVVM.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/b80006cc-6eaf-9050-93cf-77e61a1214d8.gif">