Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Julie talks about her challenges… • Lots of callbacks •

    Needs to take care of disposing of the subscriptions
  2. 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() }
  3. Coroutines fun load() { viewModelScope.launch { try { val users

    = service.getTopUsers() updateUi(users) } catch (e: Exception) { updateUi(e) } } } private fun updateUi(s: Any) { //... }
  4. Who am I ?? • Android Engineer & Kotlin GDE

    • From India • Chef, Reader, Writer, Speaker @monika_jethani • Monika Kumar Jethani
  5. What is Flow? • A stream of data • Can

    emit multiple values sequentially • Can be computed asynchronously
  6. Consuming a Flow – Collect operator (1..5).asFlow() .filter { it

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

    { a, b -> a + b } Log.d(TAG, result.toString())
  8. How will my app architecture look? UI Controller View Model

    Repository Datasource Live Data/ State Flow Flow Flow
  9. 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) }
  10. 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 } } }
  11. Handling Exceptions fun loadMovies() { viewModelScope.launch { moviesRepository.getMovies() .onStart {

    /* _movie.value = loading state */ } .catch { exception -> /* _movie.value = error state */ } .collect { movieItems -> _movies.value = movieItems } } }
  12. Scenario 2: Connecting to Database @Dao abstract class MoviesDao {

    @Query("SELECT * FROM Movies") abstract fun getMovies(): Flow<List<Movie>> }
  13. 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 }
  14. 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() } } } }
  15. Flow Binding • for Android's platform and unbundled UI widgets

    • turn traditional callbacks on Android UI widgets into the Flow type.
  16. 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 }
  17. 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 }
  18. 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
  19. Testing Coroutines using MockK • Use coEvery { } for

    mocking • Use coVerify { } to verify call to mocked methods
  20. 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
  21. 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)
  22. Step 3: Create a MutableStateFlow in view model val commentState

    = MutableStateFlow( CommentApiState( Status.LOADING, CommentModel(), "" ) )
  23. 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) } }
  24. 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} } } }
  25. 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() } }
  26. 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() } }
  27. 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)) } }
  28. 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