Save 37% off PRO during our Black Friday Sale! »

A-Z of Kotlin flow

A-Z of Kotlin flow

6289244df2b7ed2e27ff5dfdd93f058a?s=128

Monika Kumar Jethani

October 21, 2021
Tweet

Transcript

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

    on an Android app.
  2. Julie talks about her challenges… • Lots of callbacks •

    Needs to take care of disposing of the subscriptions
  3. Julie starts exploring Coroutines… • Sequential code for asynchronous operations

    • Can be disposed through scopes
  4. Rx interface StackOverflowService { @GET("/users") fun getTopUsers(): Deferred<List<User>> @GET("/users/{userId}/badges") fun

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

    suspend fun getBadges(@Path("userId") userId: Int): List<Badge> }
  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() }
  7. Coroutines fun load() { viewModelScope.launch { try { val users

    = service.getTopUsers() updateUi(users) } catch (e: Exception) { updateUi(e) } } } private fun updateUi(s: Any) { //... }
  8. Coroutines is just Scheduler part of Rx

  9. None
  10. A-Z of Kotlin flow Monika Kumar Jethani

  11. Who am I ?? • Android Engineer & Kotlin GDE

    • From India • Chef, Reader, Writer, Speaker @monika_jethani • Monika Kumar Jethani
  12. Coroutines + Flow ~ Rx

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

    Backpressure directly • Cold
  14. Kotlin flow supports Kotlin multiplatform

  15. What is Flow? • A stream of data • Can

    emit multiple values sequentially • Can be computed asynchronously
  16. Flow Builders flowOf(4, 2, 5, 1, 7) (1..5).asFlow()

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

    }
  18. Consuming a Flow – Collect operator (1..5).asFlow() .filter { it

    % 2 == 0 } .map { it * it }.collect { Log.d(TAG, it.toString()) }
  19. Consuming a Flow –Reduce operator val result = (1..5).asFlow() .reduce

    { a, b -> a + b } Log.d(TAG, result.toString())
  20. Getting started implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"

  21. None
  22. How will my app architecture look? UI Controller View Model

    Repository Datasource Live Data/ State Flow Flow Flow
  23. Scenario 1: Connecting to Network @GET("movie/popular") suspend fun getPopularMovies(): List<Movie>

  24. Repository fun getMovies(): Flow<List<Movie>> { 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) }
  25. View Model private val _movies = MutableLiveData<List<Movie>>() val movies: LiveData<List<Movie>>

    get() = _movies fun loadMovies() { viewModelScope.launch { moviesRepository.getMovies() .collect { movieItems -> _movies.value = movieItems } } }
  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 } } }
  27. Scenario 2: Connecting to Database @Dao abstract class MoviesDao {

    @Query("SELECT * FROM Movies") abstract fun getMovies(): Flow<List<Movie>> }
  28. State Flow • Stores the state • It’s hot •

    It’s observable
  29. View Model private val movies = MutableStateFlow<Resource<List<Movie>>>(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<Resource<List<Movie>>> { return movies }
  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() } } } }
  31. Flow Binding • for Android's platform and unbundled UI widgets

    • turn traditional callbacks on Android UI widgets into the Flow type.
  32. Getting started with Flow Binding implementation 'io.github.reactivecircus.flowbinding:flowbinding- android:1.0.0'

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

  34. Scenario 3: Search Example fun SearchView.getQueryTextChangeStateFlow(): StateFlow<String> { 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 }
  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 }
  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
  37. Testing Coroutines using MockK • Use coEvery { } for

    mocking • Use coVerify { } to verify call to mocked methods
  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
  39. Verify Emissions using Turbine flowOf("one", "two").test { assertEquals("one", awaitItem()) assertEquals("two",

    awaitItem()) awaitComplete() }
  40. Verify Error using Turbine flow { throw RuntimeException("broken!") }.test {

    assertEquals("broken!", awaitError().message) }
  41. Let’s start with the workshop

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

  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'
  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)
  45. Step 3: Create a MutableStateFlow in view model val commentState

    = MutableStateFlow( CommentApiState( Status.LOADING, CommentModel(), "" ) )
  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) } }
  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} } } }
  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() } }
  49. Step 6: Test success scenario using AAA val mockResponse: CommentApiState<CommentModel>

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

    mockk<ApiService>() coEvery { apiService.getComments(1) } coAnswers { throw IOException() } repo = spyk(CommentsRepository(apiService, coroutinesTestRule.testDispatcher)) coroutinesTestRule.testDispatcher.runBlockingTest { val response: Flow<CommentApiState<CommentModel>> = repo.getComment(1) response.test { assertThat(awaitError(), instanceOf(IOException::class.java)) } }
  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
  52. Thank you