Kotlin Flow in Android with Example
Kotlin Flow is one of the latest addition to the Kotlin Coroutines. With Kotlin Flow we can handle streams of data asynchronously which is being executed sequentially.
What are we going to build in this article?
We will build a simple app that fetches some data from API and shows it on the screen. It’s a simple app demonstrating the Kotlin flow working. It will use MVVM architecture.
Prerequisites:
- Good knowledge of Android
- Knowledge of Kotlin
- Basics of MVVM Architecture
- Basics of Retrofit Library
- Basics of Kotlin coroutines
- Basics of View Binding
Step by Step Implementation
Step 1: Create a New Project
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Kotlin as the programming language.
Step 2: Project Structure
We will be following some patterns to keep our files. Create folders and files according to this project structure. Uses will be explained later in this article.

Project structure
Step 3: Add required dependencies
Navigate to the Gradle Scripts > build.gradle(Module:app) and add the below dependency in the dependencies section.
// Retrofit dependency
implementation ‘com.squareup.retrofit2:retrofit:2.9.0’
// json convertor factory
implementation ‘com.squareup.retrofit2:converter-gson:2.1.0’
// Coroutines(includes kotlin flow)
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0’
// lifecycle components
implementation ‘androidx.lifecycle:lifecycle-extensions:2.2.0’
implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1”
implementation “androidx.lifecycle:lifecycle-runtime-ktx:2.3.1”
Step 4: Working with the activity_main.xml
Navigate to the app > res > layout > activity_main.xml and add the below code to that file. Below is the code for the activity_main.xml file.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < androidx.constraintlayout.widget.ConstraintLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:padding = "16dp" tools:context = ".presentation.MainActivity" > < androidx.constraintlayout.widget.Guideline android:id = "@+id/guideline2" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:orientation = "vertical" app:layout_constraintGuide_percent = "0.3" /> < EditText android:id = "@+id/search_edit_text" android:layout_width = "0dp" android:layout_height = "wrap_content" android:autofillHints = "Search Comments By Id" android:inputType = "numberSigned" app:layout_constraintLeft_toLeftOf = "parent" app:layout_constraintRight_toLeftOf = "@id/button" app:layout_constraintTop_toTopOf = "parent" /> < Button android:id = "@+id/button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "Search" app:layout_constraintRight_toRightOf = "parent" app:layout_constraintTop_toTopOf = "parent" /> < ProgressBar android:layout_width = "match_parent" android:id = "@+id/progress_bar" android:layout_height = "wrap_content" app:layout_constraintBottom_toTopOf = "@+id/textView" app:layout_constraintEnd_toEndOf = "parent" android:visibility = "gone" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toBottomOf = "@id/search_edit_text" /> < TextView android:id = "@+id/textView" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginTop = "20dp" android:layout_marginEnd = "20dp" android:layout_marginRight = "20dp" android:text = "Comment Id" android:textStyle = "bold" app:layout_constraintEnd_toStartOf = "@+id/guideline2" app:layout_constraintHorizontal_bias = "1.0" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toBottomOf = "@id/search_edit_text" /> < TextView android:id = "@+id/comment_id_textview" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginStart = "20dp" android:layout_marginLeft = "20dp" android:layout_marginTop = "20dp" android:textStyle = "bold" app:layout_constraintBottom_toBottomOf = "@+id/textView" app:layout_constraintEnd_toEndOf = "parent" app:layout_constraintHorizontal_bias = "0.0" app:layout_constraintStart_toEndOf = "@+id/guideline2" app:layout_constraintTop_toBottomOf = "@id/search_edit_text" /> < TextView android:id = "@+id/textView2" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginTop = "20dp" android:layout_marginEnd = "20dp" android:layout_marginRight = "20dp" android:text = "Name" android:textStyle = "bold" app:layout_constraintEnd_toStartOf = "@+id/guideline2" app:layout_constraintHorizontal_bias = "1.0" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toBottomOf = "@id/comment_id_textview" /> < TextView android:id = "@+id/name_textview" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginStart = "20dp" android:layout_marginLeft = "20dp" android:textStyle = "bold" app:layout_constraintBottom_toBottomOf = "@+id/textView2" app:layout_constraintEnd_toEndOf = "parent" app:layout_constraintHorizontal_bias = "0.0" app:layout_constraintStart_toEndOf = "@+id/guideline2" app:layout_constraintTop_toTopOf = "@+id/textView2" /> < TextView android:id = "@+id/textView3" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginTop = "20dp" android:layout_marginEnd = "20dp" android:layout_marginRight = "20dp" android:text = "Email" android:textStyle = "bold" app:layout_constraintEnd_toStartOf = "@+id/guideline2" app:layout_constraintHorizontal_bias = "1.0" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toBottomOf = "@id/name_textview" /> < TextView android:id = "@+id/email_textview" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginStart = "20dp" android:layout_marginLeft = "20dp" android:layout_marginTop = "20dp" android:textStyle = "bold" app:layout_constraintBottom_toBottomOf = "@+id/textView3" app:layout_constraintEnd_toEndOf = "parent" app:layout_constraintHorizontal_bias = "0.0" app:layout_constraintStart_toEndOf = "@+id/guideline2" app:layout_constraintTop_toBottomOf = "@id/name_textview" /> < TextView android:id = "@+id/textView4" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginTop = "20dp" android:layout_marginEnd = "20dp" android:layout_marginRight = "20dp" android:text = "Comment" android:textStyle = "bold" app:layout_constraintEnd_toStartOf = "@+id/guideline2" app:layout_constraintHorizontal_bias = "1.0" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toBottomOf = "@id/email_textview" /> < TextView android:id = "@+id/comment_textview" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginStart = "20dp" android:layout_marginLeft = "20dp" android:layout_marginTop = "20dp" android:textStyle = "bold" app:layout_constraintBottom_toBottomOf = "@+id/textView4" app:layout_constraintEnd_toEndOf = "parent" app:layout_constraintHorizontal_bias = "0.0" app:layout_constraintStart_toEndOf = "@+id/guideline2" app:layout_constraintTop_toBottomOf = "@id/email_textview" /> </ androidx.constraintlayout.widget.ConstraintLayout > |
It UI looks like this:
Step 5: Working with the API
We will be using https://jsonplaceholder.typicode.com/comments API, It gives some JSON data when id is passed as a path. For example, https://jsonplaceholder.typicode.com/comments/2 gives JSON which contains some random data. We will be using this data and show it on the screen using kotlin flow. Open model > CommentModel and create a model class to parse data that is received from the API.

Example Response
We need to create a dataclass for this response. Add the following code in CommentModel
Kotlin
data class CommentModel( val postId: Int?= null , val id: Int?= null , val email: String?= null , val name:String?= null , @SerializedName ( "body" ) val comment: String?= null ) |
Creating API Interface
We need to create an API interface to call the API using a retrofit. Open network > ApiService and add the following code
Kotlin
import retrofit2.http.GET import retrofit2.http.Path interface ApiService { // Get method to call the api ,passing id as a path @GET ( "/comments/{id}" ) suspend fun getComments( @Path ( "id" ) id: Int): CommentModel } |
Let’s add some helper classes to handle the loading or error state of API. Open network > CommentApiState. Refer to the comment in the code for an explanation.
Kotlin
// A helper class to handle states data class CommentApiState<out T>(val status: Status, val data: T?, val message: String?) { companion object { // In case of Success,set status as // Success and data as the response fun <T> success(data: T?): CommentApiState<T> { return CommentApiState(Status.SUCCESS, data, null ) } // In case of failure ,set state to Error , // add the error message,set data to null fun <T> error(msg: String): CommentApiState<T> { return CommentApiState(Status.ERROR, null , msg) } // When the call is loading set the state // as Loading and rest as null fun <T> loading(): CommentApiState<T> { return CommentApiState(Status.LOADING, null , null ) } } } // An enum to store the // current state of api call enum class Status { SUCCESS, ERROR, LOADING } |
Open utils > AppConfig and add code to create API service, which will be used to make API calls.
Kotlin
import com.google.gson.GsonBuilder import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory object AppConfig { // Base url of the api // create retrofit service fun ApiService(): ApiService = Retrofit.Builder().baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) .build() .create(ApiService:: class .java) } |
The API part of the App is complete. Now we need to work on ViewModel and repository.
Step 6: Working with the Repository
Open repository > CommentsRepository. Add the following code. Refer to the comments for an explanation.
Kotlin
class CommentsRepository( private val apiService: ApiService) { suspend fun getComment(id: Int): Flow<CommentApiState<CommentModel>> { return flow { // get the comment Data from the api val comment=apiService.getComments(id) // Emit this data wrapped in // the helper class [CommentApiState] emit(CommentApiState.success(comment)) }.flowOn(Dispatchers.IO) } } |
Step 7: Working with the ViewModel
Open ViewModel > CommentViewModel.Add the following code. Refer to the comments for an explanation.
Kotlin
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow. catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class CommentsViewModel : ViewModel() { // Create a Repository and pass the api // service we created in AppConfig file private val repository = CommentsRepository( AppConfig.ApiService() ) val commentState = MutableStateFlow( CommentApiState( Status.LOADING, CommentModel(), "" ) ) init { // Initiate a starting // search with comment Id 1 getNewComment( 1 ) } // Function to get new Comments fun getNewComment(id: Int) { // Since Network Calls takes time,Set the // initial value to loading state commentState.value = CommentApiState.loading() // ApiCalls takes some time, So it has to be // run and background thread. Using viewModelScope // to call the api viewModelScope.launch { // Collecting the data emitted // by the function in repository repository.getComment(id) // If any errors occurs like 404 not found // or invalid query, set the state to error // State to show some info // on screen . catch { commentState.value = CommentApiState.error(it.message.toString()) } // If Api call is succeeded, set the State to Success // and set the response data to data received from api .collect { commentState.value = CommentApiState.success(it.data) } } } } |
we are almost done, we now need to call the API from view(MainActivity) and show the data on the screen.
Step 8: Working with the view (MainActivity.kt)
Open presentation > MainActivity.kt. Add the following code, refer to the comments for explanation.
Kotlin
import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { // create a CommentsViewModel // variable to initialize it later private lateinit var viewModel: CommentsViewModel // create a view binding variable private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // instantiate view binding binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // initialize viewModel viewModel = ViewModelProvider( this ).get(CommentsViewModel:: class .java) // Listen for the button click event to search binding.button.setOnClickListener { // check to prevent api call with no parameters if (binding.searchEditText.text.isNullOrEmpty()) { Toast.makeText( this , "Query Can't be empty" , Toast.LENGTH_SHORT).show() } else { // if Query isn't empty, make the api call viewModel.getNewComment(binding.searchEditText.text.toString().toInt()) } } // Since flow run asynchronously, // start listening on background thread lifecycleScope.launch { viewModel.commentState.collect { // When state to check the // state of received data when (it.status) { // If its loading state then // show the progress bar Status.LOADING -> { binding.progressBar.isVisible = true } // If api call was a success , Update the Ui with // data and make progress bar invisible Status.SUCCESS -> { binding.progressBar.isVisible = false // Received data can be null, put a check to prevent // null pointer exception it.data?.let { comment -> binding.commentIdTextview.text = comment.id.toString() binding.nameTextview.text = comment.name binding.emailTextview.text = comment.email binding.commentTextview.text = comment.comment } } // In case of error, show some data to user else -> { binding.progressBar.isVisible = false Toast.makeText( this @MainActivity , "${it.message}" , Toast.LENGTH_SHORT).show() } } } } } } |
Now run the app, put some numbers and click search. Enter any number from 1-500, It will return a successful state.
Output:
Get the complete project from GitHub.
Please Login to comment...