Open In App

Kotlin Flow in Android with Example

Improve
Improve
Like Article
Like
Save
Share
Report

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:

  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.

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
    xmlns:tools="http://schemas.android.com/tools"
    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
    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)
}


 

 

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.

 



Last Updated : 16 Aug, 2021
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads