Este tutorial le ayudará a aprender sobre el uso de StateFlow en Android usando simples ejemplos aislados paso a paso.
¿Qué es Stateflow?
StateFlow
es un flujo observable de estado que emite las actualizaciones de estado actuales y nuevas a sus colectores.
En Android, StateFlow
es un gran ajuste para las clases que necesitan mantener un estado mutable observable.
Por ejemplo, un StateFlow
puede ser expuesto desde el YourViewModel
para que el View
pueda escuchar las actualizaciones del estado de la interfaz de usuario e inherentemente hacer que el estado de la pantalla sobreviva a los cambios de configuración.
Aquí está el ejemplo de uso de código:
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()
}
Veamos ahora algunos ejemplos completos.
Ejemplo 1: Kotlin Android Simple Stateflow Example
Un simple ejemplo aislado para darte una idea de cómo usar Stateflow en una aplicación completa.
La aplicación también te ayuda a aprender lo siguiente:
- Viewmodel
- Coroutines Kotlin
- StateFlow
- Viewbinding
Paso 1: Crear un proyecto
Comienza creando un proyecto vacío de Android Studio
.
Paso 2: Dependencias
Añade las siguientes dependencias en tu 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"
Paso 3: Habilitar Java8 y ViewBinding
En la misma app/build.gradle
seguir adelante y habilitar Java8 y ViewBinding dentro de la android{}
closure:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
Paso 4: Diseño del Layout
Diseñar un layout de MainActivity con un montón de edittexts y un botón:
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>
Paso 5: Crear un ViewModel
Crear un ViewModel en el que utilizaremos Stateflow para emitir actualizaciones a la UI:
Crea un MainViewModel.kt
y luego Comienza añadiendo importaciones:
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
Extiende la clase androidx.lifecycle.ViewModel
:
class MainViewModel : ViewModel() {
Definir dos campos de instancia: un objeto MutableStateFlow y StateFlow:
private val _loginState = MutableStateFlow<LoginUIState>(LoginUIState.Empty)
val loginUIState: StateFlow<LoginUIState> = _loginState
Ahora crea una función para simular el proceso de login:
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")
}
}
Crea una clase sellada para mantener los estados de la ui de login:
sealed class LoginUIState {
object Success : LoginUIState()
data class Error(val message: String) : LoginUIState()
object Loading : LoginUIState()
object Empty : LoginUIState()
}
}
Aquí está el código completo:
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()
}
}
Paso 6: Crear MainActivity
Aquí está el código completo para el 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
}
}
}
}
}
}
Ejecutar
Copia el código o descárgalo en el siguiente enlace, construye y ejecuta.
Referencia
Aquí están los enlaces de referencia:
Descargar Ejemplo