In modern day Android projects, it’s typical to see a Repository , UseCase or xyz abstract component exposing a business logic like
interface SomeRepository{
fun getSomeData(): Flow<Result<List<SomeObject>>>
}
Where Result looks something like this
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Error) : Result<Nothing>()
data object Loading : Result<Nothing>() // This is a UI state, why would domain care about this anyway ?
}
You’ll have some issues when you want to use this wrapper in your reactive return types, mainly
- You will write a lot of boilerplate checks ( where you ignore the cases except
Successmost of the time ) - Your return types become awkwardly long like and hard to read, like
Flow<Result<List<FinallySomeData>>>— which I’ll use in examples to make it look even worst. Is there any worse ? - Instead of a simple collection logic, you need to do a type check every single time you need a value.
- Reactive operations become cumbersome due to unnecessary
whenstatements and such. Stop shooting yourself on the foot.
As a simple example, let’s compare two methods from each to see what we end up with. Imagine we have following class
interface FancyRepository {
fun provideFoos(): Flow<Result<List<Foo>>>
fun provideBars(): Flow<Result<List<Bar>>>
}
Oh boy, aren’t you in for a treat !? Let’s say you simply want to combine these two lists into some other data:
fun simpleCombineOperation(fancyRepository: FancyRepository): Result<List<Baz>> {
fancyRepository
.getFoos()
.combine(fancyProvider.getBars()) { foo, bar ->
when (foo) {
is Result.Failure -> { /* Ignore or rethrow */}
is Result.Loading -> { /* Extra line */}
is Result.Success -> {
val foos = foo.data
// Time to check for bar !
when (bar) {
is Result.Failure -> { /* Keep ignoring or rethrowing */}
is Result.Loading -> { /* Yep, again */}
is Result.Success -> {
val bars = bar.data
// Oh my god finally I can do stuff with the data
// Now time to create a Result from result of results
val thingINeedtToProvide = Result.Success(...)
...
}
}
}
}
}
}
}
Wow.. This code is even less maintainable if you’re on a business layer.
Imagine trying combine two UseCases that return Flow<Result<Data>. You need to check Result cases for both UseCases, then generate a new Result and then check that Result again on the UI layer. This paragraph is shorter than that.
Well, somebody has to maintain this !

Now let’s take a look how it is when we don’t wrap the objects
interface FancyRepository {
fun provideFoos(): Flow<List<Foo>>
fun provideBars(): Flow<List<Bar>>
}
How do you feel reading the following code ?
fun simpleCombineOperation(fancyRepository: FancyRepository) =
fancyRepository
.getFoos()
.combine(fancyRepository.getBars()) { foo, bar ->
// Where's my wife ? ( ̄ω ̄)
foo + bar
}
.catch { /* Handle errors, or let the collector handle */ }
Try, fail, improve !
Don’t keep doing things that doesn’t make sense just because its mainstream or in official docs. It may be very well this stuff is useful in your project and business logic. But if it doesn’t, nuke it.
Mixing monads is not a good practice. It increases code complexity and reduces readability and maintainability. Especially when business logic begins to intertwine. You end up with too many flatMaps ( which don’t work the same for different containers ).Result etc. containers are useful when you need to know about the operation state so you can recover or proceed. In my opinion, there are only a few cases where you actually need to recover. In these cases, you don’t show a “retry” dialog ( or something).
Follow me on twitter. I randomly tweet my awkward ideas about mostly android stuff. Then, I disappear for long periods. Follow me on github, where I have projects that contradict the things I just advocated against.
(ノ◕ヮ◕)ノ*:・゚✧.

Leave a comment