Android MVI architecture with Jetpack & Coroutines/Flow — Part 3
Creating Coroutines/Flow empowered UseCases
If you haven’t read the previous articles of these series, you can find them here:
In this article we will see how to create different types of UseCases that will be used in our ViewModels.
What is a UseCase?
A UseCase is a list of actions or event steps typically defining the interactions between a role (known in the Unified Modeling Language (UML) as an actor) and a system to achieve a goal. The actor can be a human or other external system. In systems engineering, use cases are used at a higher level than within software engineering, often representing missions or stakeholder goals. The detailed requirements may then be captured in the Systems Modeling Language (SysML) or as contractual statements.
This is the definition provided by Wikipedia here.
But what is actually a UseCase in our project?
In many applications there are parts of our business logic that involve the coordination of several local or remote sources in order to achieve a goal.
For example, whenever we make an API call to fetch a Github repository’s information, we also want to store this information or update it in case we already have this Github repository’s information stored. This is a business flow that can be triggered from several places in our application.
Due to that need we need to have it written only once and test that component in isolation :)
Entering UseCases
Our UseCase examples are heavily influenced from Chris Banes’ TIVI repo.
FlowUseCase
We want to extend FlowUseCase whenever we have observable/streamable results (e.g.: database changes), in the form of Flow<T>
.
Our abstract class here, is implementing the ObservableUseCase<T>
which defines a property and a method.
dispatcher
is the Kotlin Coroutines Dispatcher that we will use in order to execute our UseCase’s work. Usually this can beDispatchers.IO
for network or database operations, orDispatchers.Default
if we want to execute CPU intensive tasks.observe
is the method that will return aFlow<T>
containing the results of our UseCase’s actions.
FlowUseCase<Params: Any, Type : Any>
abstract class includes a channel that accepts the Params
type. Params
can be any type of data that our UseCase needs in order to execute its actions and produce the result.
As we can also see here, the invoke
operator is being overriden in order to send the Params
input to our internal channel.
The channel then is converted to a Flow
and is flatMap
‘d in order to execute the method that produces a Flow<T>
result, which is doWork(Params)
.
NoResultUseCase
We want to extend NoResultUseCase whenever we have an action that we want to execute without caring for the actual success or failure of the task since it may not be critical to our business logic, or will not impact our user.
In this UseCase all we need to do, is provide the Dispatcher
that best fits our needs, based on our previous explanation. It will then switch Coroutines context to that Dispatcher
and execute any action given in run
method.
ResultUseCase
We want to extend ResultUseCase whenever we have an action that we want to execute, but we actually care of its result. This can be in cases our product needs to also do error handling or inform the user that the action was not successfully completed.
As previously we need to indicate the Dispatcher
that we want our UseCase to utilize. Here our run
method though has a return type. In our case this type is Result
, which can take 2 different forms. It can be either a Success
or a Failure
. Usually we can create that very simple Result
class with a sealed class, but generally this is based on the project’s needs.
Conclusion
The above base UseCase classes are usually enough to satisfy our needs on a project, but like I always say “it depends”. In a later article we will also explore how easy it is to test our UseCases in isolation.
You can find all of the code above and examples of their usage to the following repo, which will be the one we will cover in this series of articles!