$30 off During Our Annual Pro Sale. View Details »

A-Z of Kotlin flow

A-Z of Kotlin flow

Monika Kumar Jethani

October 21, 2021
Tweet

More Decks by Monika Kumar Jethani

Other Decks in Technology

Transcript

  1. Meet Julie…
    • Android Engineer at XYZ company.
    • Working on an Android app.

    View Slide

  2. Julie talks about her challenges…
    • Lots of callbacks
    • Needs to take care of disposing of the subscriptions

    View Slide

  3. Julie starts exploring Coroutines…
    • Sequential code for asynchronous operations
    • Can be disposed through scopes

    View Slide

  4. Rx
    interface StackOverflowService {
    @GET("/users")
    fun getTopUsers(): Deferred>
    @GET("/users/{userId}/badges")
    fun getBadges(@Path("userId") userId: Int): Deferred>
    }

    View Slide

  5. Coroutines
    interface StackOverflowService {
    @GET("/users")
    suspend fun getTopUsers(): List
    @GET("/users/{userId}/badges")
    suspend fun getBadges(@Path("userId") userId: Int): List
    }

    View Slide

  6. Rx
    private val disposable = CompositeDisposable()
    fun load() {
    disposable +=
    service.getTopUsers()
    .subscribeOn(io())
    .observeOn(mainThread())
    .subscribe(
    { users -> updateUi(users) },
    { e -> updateUi(e) }
    )
    }
    private fun updateUi(s: Any) {
    //...
    }
    override fun onCleared() {
    disposable.clear()
    }

    View Slide

  7. Coroutines
    fun load() {
    viewModelScope.launch {
    try {
    val users = service.getTopUsers()
    updateUi(users)
    } catch (e: Exception) {
    updateUi(e)
    }
    }
    }
    private fun updateUi(s: Any) {
    //...
    }

    View Slide

  8. Coroutines is just Scheduler part of Rx

    View Slide

  9. View Slide

  10. A-Z of
    Kotlin flow
    Monika Kumar Jethani

    View Slide

  11. Who am I ??
    • Android Engineer & Kotlin GDE
    • From India
    • Chef, Reader, Writer, Speaker
    @monika_jethani
    • Monika Kumar Jethani

    View Slide

  12. Coroutines + Flow ~ Rx

    View Slide

  13. Why Flow?
    • Null-safe
    • Plethora of operators
    • Handles Backpressure directly
    • Cold

    View Slide

  14. Kotlin flow supports Kotlin multiplatform

    View Slide

  15. What is Flow?
    • A stream of data
    • Can emit multiple values sequentially
    • Can be computed asynchronously

    View Slide

  16. Flow Builders
    flowOf(4, 2, 5, 1, 7)
    (1..5).asFlow()

    View Slide

  17. Flow Builders
    flow = flow {
    (0..10).forEach {
    emit(it)
    }
    }

    View Slide

  18. Consuming a Flow – Collect operator
    (1..5).asFlow()
    .filter {
    it % 2 == 0
    }
    .map {
    it * it
    }.collect {
    Log.d(TAG, it.toString())
    }

    View Slide

  19. Consuming a Flow –Reduce operator
    val result = (1..5).asFlow()
    .reduce { a, b -> a + b }
    Log.d(TAG, result.toString())

    View Slide

  20. Getting started
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"

    View Slide

  21. View Slide

  22. How will my app architecture look?
    UI Controller View Model Repository Datasource
    Live Data/
    State Flow
    Flow Flow

    View Slide

  23. Scenario 1: Connecting to Network
    @GET("movie/popular")
    suspend fun getPopularMovies(): List

    View Slide

  24. Repository
    fun getMovies(): Flow> {
    return flow {
    // exectute API call and map to UI object
    val moviesList = api.getPopularMovies()
    .map {
    //do something
    }
    // Emit the list to the stream
    emit(moviesList)
    }.flowOn(Dispatchers.IO)
    }

    View Slide

  25. View Model
    private val _movies = MutableLiveData>()
    val movies: LiveData> get() = _movies
    fun loadMovies() {
    viewModelScope.launch {
    moviesRepository.getMovies()
    .collect { movieItems ->
    _movies.value = movieItems
    }
    }
    }

    View Slide

  26. Handling Exceptions
    fun loadMovies() {
    viewModelScope.launch {
    moviesRepository.getMovies()
    .onStart { /* _movie.value = loading state */ }
    .catch { exception -> /* _movie.value = error state */ }
    .collect { movieItems ->
    _movies.value = movieItems
    }
    }
    }

    View Slide

  27. Scenario 2: Connecting to Database
    @Dao
    abstract class MoviesDao {
    @Query("SELECT * FROM Movies")
    abstract fun getMovies(): Flow>
    }

    View Slide

  28. State Flow
    • Stores the state
    • It’s hot
    • It’s observable

    View Slide

  29. View Model
    private val movies = MutableStateFlow>>(Resource.loading(null))
    fun loadMovies() {
    viewModelScope.launch {
    moviesRepository.getMovies()
    .onStart { /* _movie.value = loading state */ }
    .catch { exception -> /* _movie.value = error state */ }
    .collect { movieItems ->
    movies.value = movieItems
    }
    }
    }
    @ExperimentalCoroutinesApi
    fun getMovies(): StateFlow>> {
    return movies
    }

    View Slide

  30. UI lifecycleScope.launch {
    val value = viewModel.getMovies()
    value.collect {
    when (it.status) {
    Status.SUCCESS -> {
    progressBar.visibility = View.GONE
    it.data?.let { movies -> renderList(movies) }
    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@DisplayMoviesActivity,
    it.message,
    Toast.LENGTH_LONG
    ).show()
    }
    }
    }
    }

    View Slide

  31. Flow Binding
    • for Android's platform and unbundled UI widgets
    • turn traditional callbacks on Android UI widgets into the Flow type.

    View Slide

  32. Getting started with Flow Binding
    implementation 'io.github.reactivecircus.flowbinding:flowbinding-
    android:1.0.0'

    View Slide

  33. Button clicks with FlowBinding
    findViewById(R.id.button)
    .clicks()
    .onEach {
    }
    .launchIn(uiScope)

    View Slide

  34. Scenario 3: Search Example
    fun SearchView.getQueryTextChangeStateFlow(): StateFlow {
    val query = MutableStateFlow("")
    setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    override fun onQueryTextSubmit(query: String?): Boolean {
    return true
    }
    override fun onQueryTextChange(newText: String): Boolean {
    query.value = newText
    return true
    }
    })
    return query
    }

    View Slide

  35. Apply Operators searchView.getQueryTextChangeStateFlow()
    .debounce(300)
    .filter { query ->
    if (query.isEmpty()) {
    textViewResult.text = ""
    return@filter false
    } else {
    return@filter true
    }
    }
    .distinctUntilChanged()
    .flatMapLatest { query ->
    dataFromNetwork(query)
    .catch {
    emitAll(flowOf(""))
    }
    }
    .flowOn(Dispatchers.Default)
    .collect { result ->
    textViewResult.text = result
    }

    View Slide

  36. Testing Kotlin Flow
    • Use MockK for mocking data
    • Use runBlockingTest scope & TestCoroutineDispatcher for testing
    suspend functions
    • Use turbine library for better assertions on flow results

    View Slide

  37. Testing Coroutines using MockK
    • Use coEvery { } for mocking
    • Use coVerify { } to verify call to mocked methods

    View Slide

  38. Writing better assertions with Turbine
    Turbine gives you an API to collect items, errors and verify that nothing
    else has been emitted from your Flow
    https://github.com/cashapp/turbine

    View Slide

  39. Verify Emissions using Turbine
    flowOf("one", "two").test {
    assertEquals("one", awaitItem())
    assertEquals("two", awaitItem())
    awaitComplete()
    }

    View Slide

  40. Verify Error using Turbine
    flow { throw RuntimeException("broken!") }.test {
    assertEquals("broken!", awaitError().message)
    }

    View Slide

  41. Let’s start with the workshop

    View Slide

  42. • Sample on Github - https://github.com/MonikaJethani/FlowSample/

    View Slide

  43. Step 1: Add the dependencies
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
    testImplementation "io.mockk:mockk:1.12.0"
    testImplementation 'app.cash.turbine:turbine:0.6.1'

    View Slide

  44. Step 2: Write network call in Repository
    suspend fun getComment(id: Int) = 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(dispatcher)

    View Slide

  45. Step 3: Create a MutableStateFlow in
    view model
    val commentState = MutableStateFlow(
    CommentApiState(
    Status.LOADING,
    CommentModel(), ""
    )
    )

    View Slide

  46. Step 3: Create a MutableStateFlow in
    view model
    commentState.value = CommentApiState.loading()
    viewModelScope.launch {
    repository.getComment(id)
    .catch {
    commentState.value =
    CommentApiState.error(it.message.toString())
    }
    .collect {
    commentState.value = CommentApiState.success(it.data)
    }
    }

    View Slide

  47. Step 4: Collect StateFlow in UI
    lifecycleScope.launch {
    viewModel.commentState.collect {
    when (it.status) {
    Status.LOADING -> {
    binding.progressBar.isVisible = true
    }
    Status.SUCCESS -> {
    binding.progressBar.isVisible = false
    it.data?.let { comment ->
    binding.commentIdTextview.text = comment.id.toString()
    binding.nameTextview.text = comment.name
    }
    else -> {
    binding.progressBar.isVisible = false}
    } } }

    View Slide

  48. Step 5: Create CoroutineTestRule
    class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher =
    TestCoroutineDispatcher()) : TestWatcher() {
    override fun starting(description: Description?) {
    super.starting(description)
    Dispatchers.setMain(testDispatcher)
    }
    override fun finished(description: Description?) {
    super.finished(description)
    Dispatchers.resetMain()
    testDispatcher.cleanupTestCoroutines()
    }
    }

    View Slide

  49. Step 6: Test success scenario using AAA
    val mockResponse: CommentApiState =
    CommentApiState.success(
    CommentModel(1, 1, "abc", "xyz")
    )
    coEvery { repo.getComment(any()) } returns flowOf(mockResponse)
    coroutinesTestRule.testDispatcher.runBlockingTest {
    //response.collect { assertNotNull(it) }
    val response: Flow> =
    repo.getComment(1)
    response.test {
    assertEquals(awaitItem(), mockResponse)
    awaitComplete()
    }
    }

    View Slide

  50. Step 7: Test error scenario using AAA
    val apiService = mockk()
    coEvery { apiService.getComments(1) } coAnswers {
    throw IOException()
    }
    repo = spyk(CommentsRepository(apiService,
    coroutinesTestRule.testDispatcher))
    coroutinesTestRule.testDispatcher.runBlockingTest {
    val response: Flow> =
    repo.getComment(1)
    response.test {
    assertThat(awaitError(),
    instanceOf(IOException::class.java))
    }
    }

    View Slide

  51. Good reads
    • MindOrks Flow Guide -
    https://github.com/MindorksOpenSource/Kotlin-Flow-Android-
    Examples
    • Testing Flow - https://speakerdeck.com/heyitsmohit/unit-testing-
    kotlin-channels-and-flows-android-summit

    View Slide

  52. Thank you

    View Slide