Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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() }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Coroutines is just Scheduler part of Rx

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

A-Z of Kotlin flow Monika Kumar Jethani

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Coroutines + Flow ~ Rx

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Kotlin flow supports Kotlin multiplatform

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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) }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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 }

Slide 30

Slide 30 text

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() } } } }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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 }

Slide 35

Slide 35 text

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 }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Let’s start with the workshop

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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'

Slide 44

Slide 44 text

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)

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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) } }

Slide 47

Slide 47 text

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} } } }

Slide 48

Slide 48 text

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() } }

Slide 49

Slide 49 text

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() } }

Slide 50

Slide 50 text

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)) } }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Thank you