Open In App

Kotlin Flow in Android with Example

Last Updated : 25 May, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

Kotlin Flow is a tool that helps developers work with data that changes over time like search results, live updates, or user input. It’s part of Kotlin’s coroutines, which are used for writing code that doesn’t block the app while waiting for something to finish, like a network call or a file to load. Flow gives you a simple and clean way to deal with streams of data that comes in bit by bit instead of all at once.

What is exactly Kotlin Flow?

It is a way to handle data that comes in slowly or over time like lazy flow. Instead of using callbacks, it gives you a better way to work with this kind of data using coroutines. A Flow doesn’t start doing anything until you ask it to. For example, if you have a Flow that gets data from the internet, it won’t start downloading until you actually start using it.

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:

  1. Good knowledge of Android
  2. Knowledge of Kotlin
  3. Basics of MVVM Architecture
  4. Basics of Retrofit Library
  5. Basics of Kotlin coroutines
  6. 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.

kotlin-flow-dir
Project structure


Step 2: Add required dependencies

Navigate to the Gradle Scripts > build.gradle(Module:app) and add the below dependency in the dependencies section for Retrofit2 and Gson Converter for Api Calls.

dependencies {
...
implementation ("com.google.code.gson:gson:2.9.1")
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
}

Step 3: Allow Internet Permission in Manifest

Navigate to app > manifests > AndroidManifest.xml and add the following line of code to provide internet permission for api calls.

<uses-permission android:name="android.permission.INTERNET" />

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. This file will contain a editText to enter the id as a query and display data fetched from api in the textviews.

activity_main.xml:

XML
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:background="@color/white"
    tools:context=".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" />

    <com.google.android.material.divider.MaterialDivider
        android:layout_width="1dp"
        android:layout_height="0dp"
        android:layout_marginTop="24dp"
        android:layout_marginBottom="-16dp"
        app:layout_constraintBottom_toBottomOf="@+id/comment_textview"
        app:layout_constraintEnd_toStartOf="@+id/guideline2"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toBottomOf="@+id/search_edit_text" />

    <EditText
        android:id="@+id/search_edit_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:autofillHints="Search Comments By Id"
        android:inputType="number"
        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="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="20dp"
        android:text="Id"
        android:textAlignment="viewEnd"
        android:textStyle="bold"
        app:layout_constraintEnd_toStartOf="@+id/guideline2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/search_edit_text" />

    <TextView
        android:id="@+id/comment_id_textview"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/textView" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="20dp"
        android:textAlignment="viewEnd"
        android:text="Name"
        android:textStyle="bold"
        app:layout_constraintEnd_toStartOf="@+id/guideline2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/comment_id_textview" />

    <TextView
        android:id="@+id/name_textview"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/textView2" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="20dp"
        android:textAlignment="viewEnd"
        android:text="Email"
        android:textStyle="bold"
        app:layout_constraintEnd_toStartOf="@+id/guideline2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/name_textview" />

    <TextView
        android:id="@+id/email_textview"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/textView3" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="20dp"
        android:text="Comment"
        android:textAlignment="viewEnd"
        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="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/textView4" />

</androidx.constraintlayout.widget.ConstraintLayout>


Design UI:

home-design-kotlin-flow


 

Step 5: Create a model to save data from API calls

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. Create a kotlin file named CommentModel and create a dataclass 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

CommentModel.kt:

Kotlin
package org.geeksforgeeks.demo

import com.google.gson.annotations.SerializedName

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
)

  

Step 6: Create an API Service

We need to create an API interface to call the API using a retrofit. Create a kotlin file named ApiService.kt and add the following code

ApiService.kt:

Kotlin
package org.geeksforgeeks.demo

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
}

  

Step 7: Create a helper class from saving States of API calls

Let's add some helper classes to handle the loading or error state of API. Create a kotlin file named CommentApiState.kt. Refer to the comment in the code for an explanation.

CommentApiState.kt:

Kotlin
package org.geeksforgeeks.demo

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
}


Step 8: Create a Retrofit Instance for network calls

Create a kotlin file named RetrofitInstance.kt and add code to create API service, which will be used to make API calls.

RetrofitInstance.kt:

Kotlin
package org.geeksforgeeks.demo

import com.google.gson.GsonBuilder
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    // Base url of the api
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    // create retrofit service
    fun apiService(): ApiService =
        Retrofit.Builder().baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
            .build()
            .create(ApiService::class.java)
}


Step 9: Create a Repository

Create a kotlin file with the name CommentsRepository.kt. Add the following code. Refer to the comments for an explanation. This repository calls the ApiService and returns the data as a flow in the IO Thread.

CommentsRepository.kt:

Kotlin
package org.geeksforgeeks.demo

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn

class CommentsRepository(private val apiService: ApiService) {
    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 10: Working with the ViewModel 

Create a kotlin class file named CommentViewModel.kt .Add the following code. Refer to the comments for an explanation.

CommentViewModel.kt:

Kotlin
package org.geeksforgeeks.demo

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

class CommentsViewModel : ViewModel() {
    // initialize the repo and pass the api service as parameter
    private val repository = CommentsRepository(
        RetrofitInstance.apiService()
    )

    // MutableStateFlow to store comment data
    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) {
        // initialize state as loading
        commentState.value = CommentApiState.loading()

        // use coroutine scope to let the api call run asynchronously
        viewModelScope.launch {
            // Collecting the data emitted by the function in repository
            repository.getComment(id)
                // handle errors like 404 not found, invalid id etc
                .catch {
                    commentState.value =
                        CommentApiState.error(it.message.toString())
                }
                // on the success response, data is set to state for observing and updating
                .collect {
                    commentState.value = CommentApiState.success(it.data)
                }
        }
    }
}


Step 11: Working with MainActivity.kt 

Open presentation > MainActivity.kt. Add the following code, refer to the comments for explanation. We now need to call the API from view(MainActivity) and show the data on the screen.

MainActivity.kt:

Kotlin
package org.geeksforgeeks.demo

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.launch
import org.geeksforgeeks.demo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    // initiate the viewmodel
    private val viewModel: CommentsViewModel by lazy {
        ViewModelProvider(this)[CommentsViewModel::class.java]
    }
    // create a view binding variable
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // set on click listener for search button
        binding.button.setOnClickListener {
            val id = binding.searchEditText.text.toString().trim()
            // check to prevent api call with no parameters
            if (id.isEmpty() || id == "0") {
                Toast.makeText(this, "Id Can't be 0 or empty", Toast.LENGTH_SHORT).show()
            } else {
                // if id isn't empty, make the api call
                viewModel.getNewComment(id.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) {
                    // on loading, show the progress bar
                    Status.LOADING -> {
                        binding.progressBar.isVisible = true
                    }
                    // on success, update ui and make progress bar invisible
                    Status.SUCCESS -> {
                        binding.progressBar.isVisible = false
                        // handle 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
                        }
                    }
                    // on error, show toast with error message
                    else -> {
                        binding.progressBar.isVisible = false
                        Toast.makeText(this@MainActivity, "${it.message}", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
}


Output:

Now run the app, put some numbers and click search. Enter any number from 1-500, It will return a successful state. 



Next Article
Article Tags :

Similar Reads