๐ Learning Kotlin Coroutines and Flows for Android Development by Example
๐ Sample implementations for real-world Android use cases
๐ Unit tests included!
This repository is intended to be a "Playground Project". You can quickly look up and play around with the different Coroutine and Flow Android implementations.
In the playground
package you can play around with Coroutines and Flow examples that run directly on the JVM.
Every use case is using its own Activity
and JetPack ViewModel
. The ViewModel
s contain most of the interesting Coroutine-related code.
Activities
listen to LiveData
or StateFlow
events of the ViewModel
and render received UiState
s.
This project is using retrofit/okhttp together with a MockNetworkInterceptor
. This lets you define how the API should behave.
Everything can be configured: http status codes, response data and delays. Every use case defines a certain behaviour of the Mock API.
The API has 2 endpoints. One returns the names of the most recent Android versions and the other one returns the features of a certain
Android version.
- Perform single network request
- Perform two sequential network requests
- Perform several network requests concurrently
- Perform variable amount of network requests
- Perform a network request with timeout
- Retrying network requests
- Network requests with timeout and retry
- Room and Coroutines
- Debugging Coroutines
- Offload expensive calculation to background thread
- Cooperative Cancellation
- Offload expensive calculation to several Coroutines
- Exception Handling
- Continue Coroutine execution even when the user leaves the screen
- Using WorkManager with Coroutines
- Performance analysis of dispatchers, number of coroutines and yielding
- Perform expensive calculation on Main Thread without freezing the UI
- Flow Basics
- Basic Flow intermediate operators
- Flow Exception Handling
- Exposing Flows in the ViewModel
This use case performs a single network request to get the latest Android Versions and displays them on the screen.
[code]
This use case performs two network requests sequentially. First, it retrieves recent Android Versions and then it requests the features of the latest version.
There are also 2 alternative implementations included. One is using old-school callbacks. The other one uses RxJava. You can compare each implementation. If you compare all three implementations, it is really interesting to see, in my opinion, how simple the Coroutine-based version actually is.
[code]
Performs three network requests concurrently. It loads the feature information of the 3 most recent Android Versions. Additionally, an implementation that performs the requests sequentially is included. The UI shows how much time each implementation takes to load the data so you can see that the network requests in the concurrent version are indeed performed in parallel. The included unit test is also interesting, as it shows how you can use virtual time to verify that the concurrent version gets performed in parallel.
[code]
Demonstrates the simple usage of map()
to perform a dynamic amount of network requests. At first, this use case performs a network request to load all Android versions.
Then it performs a network request for each Android version to load its features. It contains an implementation that performs the network requests sequentially and another one that performs them concurrently.
[code]
This use case uses the suspending function withTimeout()
from the coroutines-core library. It throws a TimeoutCancellationException
if the timeout was exceeded.
You can set the duration of the request in the UI and check the behaviour when the response time is bigger than the timeout.
General networking timeouts can also be configured in the okhttp client.
[code]
Demonstrates the usage of higher-order functions together with coroutines. The higher-order function retry()
retries a certain suspending operation for a given amount of times.
It uses an exponential backoff for retries, which means that the delay between retries increases exponentially. The behavior of the Mock API is defined in a way that it responses
with 2 unsuccessful responses followed by a successful response.
[code]
Unit tests verify the amount of requests that are performed in different scenarios. Furthermore, they check if the exponential backoff is working properly by asserting the amount of elapsed virtual time.
Composes higher level functions retry()
and withTimeout()
. Demonstrates how simple and readable code written with Coroutines can be.
The mock API first responds after the timeout and then returns an unsuccessful response. The third attempt is then successful.
Take a look at the included callback-based implementation to see how tricky this use case is to implement without Coroutines.
I also implemented the use case with RxJava.
[code]
This example stores the response data of each network request in a Room database. This is essential for any "offline-first" app.
If the View
requests data, the ViewModel
first checks if there is data available in the database. If so, this data is returned before performing
a network request to get fresh data.
[code]
This is not really a use case, but I wanted to show how you can add additional debug information about the Coroutine that is currently running to your logs.
It will add the Coroutine name next to the thread name when calling Thread.currentThread.name()
This is done by enabling Coroutine Debug mode by setting the property kotlinx.coroutines.debug
to true
.
[code]
This use case calculates the factorial of a number. The calculation is performed on a background thread using the default Dispatcher.
Attention: This use case does not support cancellation! UseCase#11 fixes this!
[code]
In the respective unit test, we have to pass the testDispatcher to the ViewModel, so that the calculation is not performed on a background thread but on the main thread.
UseCase#10 has a problem. It is not able to prematurely cancel the calculation because it is not cooperative regarding cancellation. This leads to wasted device resources and
memory leaks, as the calculation is not stopped and the ViewModel is retained longer than necessary. This use case now fixes this issue. The UI now also has a "Cancel Calculation"
Button. Note: Only the calculation can be canceled prematurely but not the toString()
conversion.
There are several ways to make your coroutines cooperative regarding cancellation: You can use either use isActive()
, ensureActive()
or yield()
.
More information about cancellation can be found here
[code]
The factorial calculation here is not performed by a single coroutine, but by a number of coroutines that can be defined in the UI. Each coroutine calculates the factorial of a sub-range.
[code viewmodel] [code factorial calculator]
This use case demonstrates different ways of handling exceptions using try/catch
and CoroutineExceptionHandler
. It also demonstrates when you should use supervisorScope{}
: In situations when you don't want a failing coroutine to cancel
its sibling coroutines. In one implementation of this use case, the results of the successful responses are shown even though one response wasn't successful.
[code]
Usually, when the user leaves the screen, the ViewModel
gets cleared and all the coroutines launched in viewModelScope
get canceled. Sometimes, however, we want a certain coroutine operation to be continued
when the user leaves the screen. In this use case, the network request keeps running and the results still get inserted into the database
cache when the user leaves the screen. This makes sense in real-world applications as we don't want to cancel an already started background "cache sync".
You can test this behavior in the UI by clearing the database, then loading the Android version and instantly closing the screen. You will see in LogCat that the response
still gets executed and the result still gets stored. The respective unit test AndroidVersionRepositoryTest
also verifies this behavior. Check out this blogpost for details of the implementation.
[code viewmodel] [code repository]
Demonstrates how you can use WorkManager together with Coroutines. When creating a subclass of CoroutineWorker
instead of Worker
,
the doWork()
function is now a suspend function
which means that we can now call other suspend functions. In this
example, we are sending an analytics request when the user enters the screen, which is a nice use case for using WorkManager.
[code viewmodel] [code worker]
This is an extension of use case #12 (Offload expensive calculation to several coroutines). Here it is possible to additionally define the dispatcher type you want
the calculation to be performed on. Additionally, you can enable or disable the call to yield()
during the calculation. A list of calculations is displayed on the bottom in order to be able to compare them in a convenient way.
This example shows how you can perform an expensive calculation on the main thread in a non-blocking fashion. It uses yield()
for every step in the calculation so that other work, like drawing the UI, can be performed
on the main thread. It is more a "showcase" rather than a use case for a real application, because of performance reasons you should always perform expensive calculations on a background thread (See UseCase#10).
See [this blog post] for more information!
You can play around and check the performance of different configurations!
This simple use case shows how to consume values from a DataSource
that emits live stock information and how to display them in the UI.
The datasource exposes a Flow
which is built with the flow{}
flow builder. It fetches fresh stock information every 5 seconds from a mocked endpoint.
A LiveData
property that exposes the UiState
in the ViewModel
is created by using the .asLiveData()
terminal operator.
This use case also shows how to use the map
intermediate operator and the onStart
lifecycle operator.
[code viewmodel] [code datasource]
The second use case is an extension of the first one.
It uses some basic intermediate operators, like withIndex
, map
, take
and filter
.
[code viewmodel] [code datasource]
The third use case shows how to properly implement exception handling with flows.
It uses the catch
operator to handle exceptions of our flow in the ViewModel
and uses the retry
operator to retry failed network requests in the DataSource
.
[code viewmodel] [code datasource]
This use case shows how to expose flows (a StateFlow
to be precise) in the ViewModel
instead of a LiveData
property.
The statIn
operator is used to convert the ordinary, cold Flow
into a hot StateFlow
.
In the Activity
, the repeadOnLifecycle
suspend function is used to collect emissions of the StateFlow
in a lifecycle-aware manner.
[code viewmodel] [code datasource]
Licensed under the Apache License, Version 2.0 (the "License"). You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
You agree that all contributions to this repository, in the form of fixes, pull-requests, new examples etc. follow the above-mentioned license.