StateFlow Examples
This tutorial will help you learn about StateFlow usage in Android using simple step by step isolated examples.
What is Stateflow?
StateFlow
is a state-holder observable flow that emits the current and new state updates to its collectors.
In Android, StateFlow
is a great fit for classes that need to maintain an observable mutable state.
For example a StateFlow
can be exposed from the YourViewModel
so that the View
can listen for UI state updates and inherently make the screen state survive configuration changes.
Here is code usage example:
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()
}
Let us now look at some full examples.
Example 1: Kotlin Android Simple Stateflow Example
A simple isolated example to give you an idea about how to use Stateflow in a full app.
The app also helps you learn the following:
- Viewmodel
- Kotlin Coroutines
- StateFlow
- Viewbinding
Step 1: Create Project
Start by creating an empty Android Studio
project.
Step 2: Dependencies
Add the following dependencies in your 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"
Step 3: Enable Java8 and ViewBinding
In the same app/build.gradle
go ahead and enable Java8 and ViewBinding inside the android{}
closure:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
Step 4: Design Layout
Design a MainActivity layout with a bunch of edittexts and a button:
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>
Step 5: Create a ViewModel
Create a ViewModel where we will use Stateflow to emit updates to UI:
Create a MainViewModel.kt
and then Start by adding imports:
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
Extend the androidx.lifecycle.ViewModel
class:
Define two instance fields: a MutableStateFlow and StateFlow objects:
private val _loginState = MutableStateFlow<LoginUIState>(LoginUIState.Empty)
val loginUIState: StateFlow<LoginUIState> = _loginState
Now cretae a function to 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")
}
}
Create a sealed class to hold the login ui states:
sealed class LoginUIState {
object Success : LoginUIState()
data class Error(val message: String) : LoginUIState()
object Loading : LoginUIState()
object Empty : LoginUIState()
}
}
Here is the full code:
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()
}
}
Step 6: Create MainActivity
Here's the full code for the 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
}
}
}
}
}
}
Run
Copy the code or download it in the link below, build and run.
Reference
Here are the reference links:
Number | Link |
---|---|
1. | Download Example |
2. | Follow code author |
Kotlin Android Stateflow + DataBinding
Learn android coroutines StateFlow alongside DataBinding in step by step example project. The example is written in Kotlin.
This example will comprise the following files:
AppBindingComponent.kt
AppViewBinding.kt
AppViewBindingImpl.kt
MainActivity.kt
MainViewModel.kt
Step 1: Create Project
- Open your
AndroidStudio
IDE. - Go to
File-->New-->Project
to create a new project.
Step 2: Add Dependencies
In your app/build.gradle
add dependencies as shown below:
Enable DataBinding as shown below, inside the android{
closure:
We will also enable Java8:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
}
Make sure you have kotlinx-coroutines-android
declared as one of your dependencies:
dependencies {
//..
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha05"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha05"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
}
Step 3: Design Layouts
*(a). activity_main.xml
Create a file named activity_main.xml
and design it as follows:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewModel"
type="com.star_zero.stateflow.databinding.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:onClick="@{() -> viewModel.onClickButton()}"
android:text="TEST"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
app:textStateFlow="@{viewModel.text}"
tools:text="Hello World!" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Step 4: Write Code
Write Code as follows:
(a). AppBindingComponent.kt
Create a file named AppBindingComponent.kt
Here is the full code
package com.star_zero.stateflow.databinding
import androidx.databinding.DataBindingComponent
import androidx.lifecycle.LifecycleCoroutineScope
class AppBindingComponent(private val scope: LifecycleCoroutineScope) : DataBindingComponent {
override fun getAppViewBinding(): AppViewBinding {
return AppViewBindingImpl(scope)
}
}
(b). AppViewBinding.kt
Create a file named AppViewBinding.kt
Here is the full code
package com.star_zero.stateflow.databinding
import android.widget.TextView
import androidx.databinding.BindingAdapter
import kotlinx.coroutines.flow.StateFlow
interface AppViewBinding {
@BindingAdapter("textStateFlow")
fun setText(view: TextView, stateFlow: StateFlow<String>)
}
(c). AppViewBindingImpl.kt
Create a file named AppViewBindingImpl.kt
Here is the full code
package com.star_zero.stateflow.databinding
import android.widget.TextView
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
class AppViewBindingImpl(private val scope: LifecycleCoroutineScope) : AppViewBinding {
override fun setText(view: TextView, stateFlow: StateFlow<String>) {
scope.launchWhenStarted {
stateFlow.collect {
view.text = it
}
}
}
}
(d). MainActivity.kt
Create a file named MainActivity.kt
Here is the full code
package com.star_zero.stateflow.databinding
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.star_zero.stateflow.databinding.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainViewModel>()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(
this,
R.layout.activity_main,
AppBindingComponent(lifecycleScope)
)
binding.viewModel = viewModel
}
}
(e). MainViewModel.kt
Create a file named MainViewModel.kt
Here is the full code
package com.star_zero.stateflow.databinding
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MainViewModel : ViewModel() {
private val _text = MutableStateFlow("Hello World!")
val text: StateFlow<String> = _text
fun onClickButton() {
_text.value = "Hello StateFlow: ${System.currentTimeMillis()}"
}
}
Run
Simply copy the source code into your Android Project,Build and Run.