このチュートリアルでは、AndroidにおけるStateFlowの使用方法について、簡単なステップバイステップの孤立した例を使って学習します。
StateFlowとは?
StateFlow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) はステートホルダーが観測可能なフローで、現在のステートと新しいステートの更新をコレクターに送出します。
Android では、StateFlow
は観測可能な状態を維持する必要があるクラスに適している。
例えば、 StateFlow
を YourViewModel
から公開することで、 View
が UI の状態の更新をリッスンして、画面の状態を設定変更に耐えられるようにすることができる。
以下はコードの使用例です。
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(exception: Throwable): LatestNewsUiState()
}
それでは、いくつかの例を見てみましょう。
例1: Kotlin Android のシンプルなステートフロー例
完全なアプリでStateflowを使用する方法についてのアイデアを提供するための、単純な孤立した例です。
このアプリはまた、次のことを学ぶのに役立ちます。
- Viewmodel
- Kotlin コルーチン
- StateFlow
- ビューバインディング
ステップ1: プロジェクトの作成
まず、空の Android Studio
プロジェクトを作成します。
ステップ2: 依存関係
app/build.gradle`に、以下の依存関係を追加します。
// architectural components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
// activity ktx for viewmodel
implementation "androidx.activity:activity-ktx:1.1.0"
// coroutine lifecycle scopes
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
ステップ3: Java8とViewBindingの有効化
同じapp/build.gradle
内で、android{}
closureの中で、Java8とViewBindingを有効にします。
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
ステップ4: レイアウトの設計
MainActivityのレイアウトをデザインします。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parent_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:hint="@string/login">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/login_field"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="30dp"
android:hint="@string/password">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_field"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/login_b"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="30dp"
android:text="@string/login"
app:elevation="10dp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
ステップ5:ViewModelの作成
Stateflow を使って UI の更新を行う ViewModel を作成します。
MainViewModel.kt`を作成し、インポートの追加から始めます。
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
androidx.lifecycle.ViewModel`クラスを拡張します。
class MainViewModel : ViewModel() {
MutableStateFlowオブジェクトとStateFlowオブジェクトの2つのインスタンスフィールドを定義します。
private val _loginState = MutableStateFlow<LoginUIState>(LoginUIState.Empty)
val loginUIState: StateFlow<LoginUIState> = _loginState
ログイン処理をシミュレートする関数を作成します。
fun login(username: String, password: String) = viewModelScope.launch {
_loginState.value = LoginUIState.Loading
// fake network request time
delay(2000L)
if (username == "raheem" && password == "android") {
_loginState.value = LoginUIState.Success
} else {
_loginState.value = LoginUIState.Error("Incorrect password")
}
}
ログインの状態を保持するために、Sealedクラスを作成します。
sealed class LoginUIState {
object Success : LoginUIState()
data class Error(val message: String) : LoginUIState()
object Loading : LoginUIState()
object Empty : LoginUIState()
}
}
以下がコード全体です。
MainViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
private val _loginState = MutableStateFlow<LoginUIState>(LoginUIState.Empty)
val loginUIState: StateFlow<LoginUIState> = _loginState
// simulate login process
fun login(username: String, password: String) = viewModelScope.launch {
_loginState.value = LoginUIState.Loading
// fake network request time
delay(2000L)
if (username == "raheem" && password == "android") {
_loginState.value = LoginUIState.Success
} else {
_loginState.value = LoginUIState.Error("Incorrect password")
}
}
// login ui states
sealed class LoginUIState {
object Success : LoginUIState()
data class Error(val message: String) : LoginUIState()
object Loading : LoginUIState()
object Empty : LoginUIState()
}
}
ステップ6:MainActivityの作成
以下は、MainActivity.kt
の全コードです。
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collect
import xyz.teamgravity.stateflow.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.apply {
// login button
loginB.setOnClickListener {
viewModel.login(loginField.text.toString().trim(), passwordField.text.toString().trim())
}
// collect data and respond
lifecycleScope.launchWhenCreated {
viewModel.loginUIState.collect {
when (it) {
is MainViewModel.LoginUIState.Loading -> {
progressBar.visibility = View.VISIBLE
}
is MainViewModel.LoginUIState.Success -> {
Snackbar.make(parentLayout, "Successfully logged in", Snackbar.LENGTH_SHORT).show()
progressBar.visibility = View.GONE
}
is MainViewModel.LoginUIState.Error -> {
Snackbar.make(parentLayout, it.message, Snackbar.LENGTH_SHORT).show()
progressBar.visibility = View.GONE
}
else -> Unit
}
}
}
}
}
}
実行
コードをコピーするか、以下のリンクからダウンロードし、ビルドして実行します。
参考文献
以下は参考文献のリンクです。
ダウンロード 例