-
Notifications
You must be signed in to change notification settings - Fork 6
3. Application Business Rules Layer
In most clean architecture implementations you see that use cases are just a class with a single method. I don’t know where that concept comes from. But I don’t think that this concept is related to clean architecture. UseCase is much more than a class with a single method. It is your user story. your business rules. You basically need to translate the user story that you have in your Jira ticket to a UseCase class in your Application Business Rules layer.
Back to our template in the base package, you will see InputPort, OutputPort And UseCase, base classes.
As you can see the ViewModel has an object of the UseCase that has an InputPort type. And then the UseCase also has an Object of ViewModel with the type OutputPort. This will reverse the dependency between UseCase and ViewModel. Let’s look at the actual code.
interface InputPort<in O : OutputPort> {
suspend fun registerOutputPort(outputPort: O)
}
Our InputPort
has a general type of OutputPort
. We will be using this general type in the registerOutputPort
function to register
the ViewModel
in the UseCase
as an outputPort
. You might think that this is better to be done with dependency injection. Well, you are
absolutely right, But I faced some limitations with android ViewModels that made me not able to use dependency injection to bind the ViewModel to the
outPort. So I’m sticking to this function for now. Maybe in the future, I will find a better solution for this.
The OutputPort
is a simple interface.
interface OutputPort
Now coming to the main course, UseCase has a general type of OutputPort
that we use for the outputPort variable. The onReady function will be called
right after the outputPort
is registered. So you can run your setup code. We also have the default implementation for registerOutputPort
.
abstract class UseCase<O : OutputPort> : InputPort<O> {
protected lateinit var outputPort: O
protected open suspend fun onReady() = Unit
override suspend fun registerOutputPort(outputPort: O) {
this.outputPort = outputPort
onReady()
}
}
Then last but not least we have the base Repository interface. Which is an empty interface that might be useful in future for some polymorphism implementation. We will discuss Repository in more detail later on. But for now, you just need to know that Usecases will use a Repository to access data in our android framework. Like data in remote API or local DB.
interface Repository
For this template, we only have one repository called MovieRepository
interface MovieRepository : Repository {
suspend fun updateMovieList()
suspend fun observeMovies(): Flow<List<Movie>>
suspend fun loadMovieSize(): Int
}
Let's look at LoadMovieListInputPort. Our input port has one command that starts updating a movie list in the system cash.
interface LoadMovieListInputPort : InputPort<LoadMovieListOutputPort> {
suspend fun startUpdatingMovieList()
}
Then we have LoadMovieListOutputPort which has two
commands showLoading
that hide and show loading while the movie is loading. And observeMovies
provides a flow that can be used to observe the list
of movies in our cash.
interface LoadMovieListOutputPort : OutputPort {
suspend fun showLoading(loading: Boolean)
suspend fun observeMovies(flow: Flow<List<Movie>>)
}
Now let's check the LoadMovieList. Our usecase has a
MovieRepository
object that will be used to access systems data. In onReady we are making our output port observe the locally cached movie list
using our MovieRepository
object. When the usecase is ready we also load new movies if we don’t have any movies in our cash.
The startUpdatingMovieList
is used to start the loading of a new movie process. First, we tell our output port to show the loading. Then we try to
update our movies locally using our repository, then we hide the loading again. Notice that if we fail to update movies we throw an exception. But we
are throwing the exception after we hide the loading.
class LoadMovieList @Inject constructor(
private val movieRepository: MovieRepository,
) : UseCase<LoadMovieListOutputPort>(), LoadMovieListInputPort {
override suspend fun onReady() {
observeMovies()
loadMoviesIfNeeded()
}
override suspend fun startUpdatingMovieList() {
showLoading()
val e = tryTo { updateMovies() }
hideLoading()
e?.let { throw e }
}
private suspend fun loadMoviesIfNeeded() {
if (movieRepository.loadMovieSize() <= 0) startUpdatingMovieList()
}
private suspend fun observeMovies() = outputPort.startObserveMovies(movieRepository.observeMovies())
private suspend fun showLoading() = outputPort.showLoading(true)
private suspend fun hideLoading() = outputPort.showLoading(false)
private suspend fun updateMovies() = movieRepository.updateMovieList()
}
Copyright (c) <2021> <Muhammad Khoshnaw>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.