StateFlow APIs in Kotlin

StateFlow APIs in Kotlin

With the rise in popularity of Coroutines and Flow, Kotlin released Stateflow API as part of v1.3.6.

You need to know how Flow API works. To learn about it click here.

In this blog, we are going to discuss what are StateFlow APIs and how it works. We are going to cover,

  • What are StateFlow?
  • How is it different from ConflatedBroadcastChannel?
  • How to use StateFlow in Android?

So, let's start.

What are StateFlow?

StateFlow is like a way to use Kotlin Flow to manage and represent a state in an application.

StateFlow is a type of interface, which is only a read-only and always returns the updated value. And to receive the updated value we just collect the value from the implemented Flow.

StateFlow only returns if the value has updated and doesn't return the same value. In simpler terms, consider two values x and y where x is the value initially emitted and y is the value to be emitted.

StateFlow makes sure, if (x == y) the do nothing but if (x !=y) then only emit the new value i.e. y in this case.

Note: A regular Flow is cold but StateFlow is hot. It means that the regular Flow does not have the concept of the last value and it only becomes active when it gets collected, whereas StateFlow has the concept of the last value and it becomes active as soon as we create it.

We can think of StateFlow like the Subject in RxJava, and the regular Flow like normal observables in RxJava.

How is it different from ConflatedBroadcastChannel?

In coroutines, ConflatedBroadcastChannel was used to emit the latest value, and that were observed by multiple different sources.

StateFlow is pretty much the same and it also emits the latest value to be collected from the flow. There are few notable differences between them as,

  • When collecting value from StateFlow, we always get the latest value as it always has a value that makes it read-safe because at any point in time the StateFlow will have a value, unlike ConflatedBroadcastChannel where we first needed to create an instance without value and that is why it was not read-safe.
  • ConflatedBroadcastChannel implements the Channel APIs to work but in StateFlow APIs, Channel APIs are not used and that is the reason a faster execution happens.
  • StateFlow works on a concept of the operator distinctUntilChanged , so it only returns the updated value as it filters out the same value.

How to use StateFlow in Android?

We are going to discuss this, by taking an example to fetch a list of users from API. Let's discuss this in steps.

Step 01.

First, let's setup out the gradle file, we will add the coroutine dependency in the app's build.gradle file like,

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"

Step 02.

We will create an activity with a viewmodel. We will create MainActivity and MainViewModel. We will also create ApiHelper class which will have a function returning Flow called getUsers(). And finally, to manage the state of API calls we will create a Resource file that looks like,

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {

    companion object {

        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T?): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }
    }
}

and Status looks like,

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

Step 03:

Now, we will design the XML of MainActivity which would contain a recyclerView and progress bar. It will look like,

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

and finally, we will create an Adapter class for the recyclerView. We will create a file called ApiUserAdapter and it will look like,

class ApiUserAdapter(
    private val users: ArrayList<ApiUser>
) : RecyclerView.Adapter<ApiUserAdapter.DataViewHolder>() {

    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(user: ApiUser) {
            itemView.textViewUserName.text = user.name
            itemView.textViewUserEmail.text = user.email
            Glide.with(itemView.imageViewAvatar.context)
                .load(user.avatar)
                .into(itemView.imageViewAvatar)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        DataViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_layout, parent,
                false
            )
        )

    override fun getItemCount(): Int = users.size

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
        holder.bind(users[position])

    fun addData(list: List<ApiUser>) {
        users.addAll(list)
    }

}

Here, the item_layout.xml file looks like,

<androidx.constraintlayout.widget.ConstraintLayout 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/container"
    android:layout_width="match_parent"
    android:layout_height="60dp">

    <ImageView
        android:id="@+id/imageViewAvatar"
        android:layout_width="60dp"
        android:layout_height="0dp"
        android:padding="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserName"
        style="@style/TextAppearance.AppCompat.Large"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="MindOrks" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserEmail"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textViewUserName"
        app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
        tools:text="MindOrks" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now, let's setup the Activity file.

Step 04:

Let's update the code in our MainActivity like,

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel
    private lateinit var adapter: ApiUserAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupUI()
        setupViewModel()
        setupObserver()
    }
}

Here you can see we have two variables viewModel and adapter of type MainViewModel and ApiUserAdapter respectively. We also have three function calls in onCreate().

First, let's update the setupUI(), Here will update the code like,

private fun setupUI() {
    recyclerView.layoutManager = LinearLayoutManager(this)
    adapter =
        ApiUserAdapter(
            arrayListOf()
        )
    recyclerView.addItemDecoration(
        DividerItemDecoration(
            recyclerView.context,
            (recyclerView.layoutManager as LinearLayoutManager).orientation
        )
    )
    recyclerView.adapter = adapter
}

Now, we will setup the viewModel in setupViewModel function like,

private fun setupViewModel() {
    viewModel = ViewModelProviders.of(
        this,
        ViewModelFactory(
            ApiHelper()
        )
    ).get(MainViewModel::class.java)
}

Here, MainViewModel takes ApiHelper as a parameter in the constructor via ViewModelFactory.

Step 05.

Let's update the ViewModel to fetch the data from API.

@ExperimentalCoroutinesApi
class MainViewModel(
    private val apiHelper: ApiHelper
) : ViewModel() {

    @ExperimentalCoroutinesApi
    private val users = MutableStateFlow<Resource<List<ApiUser>>>(Resource.loading(null))


    @ExperimentalCoroutinesApi
    fun fetchUsers() {
        viewModelScope.launch {
            apiHelper.getUsers()
                .catch { e ->
                    users.value = (Resource.error(e.toString(), null))
                }
                .collect {
                    users.value = (Resource.success(it))
                }
        }
    }

    @ExperimentalCoroutinesApi
    fun getUsers(): StateFlow<Resource<List<ApiUser>>> {
        return users
    }

}

Here, we first created a variable user of type MutableStateFlow. MutableStateFlow always takes always a default value, so here it would be the loading state.

Then we can, do the API call in fetchUser function. Inside catch block, we will update the state to error and in collect, we will update the state value to success and pass the successful response to it.

MutableStateFlow has read and write property but StateFlow is read-only. So, we update the value via MutableStateFlow but always collect value via StateFlow.

Here the successful response is the list of users. Now we see the users of getUsers function which is of type StateFlow.

Step 07:

Now, we will call the fetchUser function in the Activity to do the Api call like,

viewModel.fetchUsers()

Now, to observe the changes inside setupObservers and we will collect the data from getUsers functions like,

private fun setupObserver() {
    lifecycleScope.launch {
        val value = viewModel.getUsers()
        value.collect {
            when (it.status) {
                Status.SUCCESS -> {
                    progressBar.visibility = View.GONE
                    it.data?.let { users -> renderList(users) }
                    recyclerView.visibility = View.VISIBLE
                }
                Status.LOADING -> {
                    progressBar.visibility = View.VISIBLE
                    recyclerView.visibility = View.GONE
                }
                Status.ERROR -> {
                    //Handle Error
                    progressBar.visibility = View.GONE
                    Toast.makeText(
                        this@SingleNetworkCallActivity,
                        it.message,
                        Toast.LENGTH_LONG
                    ).show()
                }
            }
        }
    }
}

Here, inside the collect we get the data as of type Resource and based on the status we will setup the list inside renderList() which looks like,

private fun renderList(users: List<ApiUser>) {
    adapter.addData(users)
    adapter.notifyDataSetChanged()
}

Now, when we run the app we get the desired output.

This is how we can use the StateFlow to manage state in Android Application.

If you want to learn how to work with Kotlin Flow, click here

Happy learning

Team MindOrks