ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Anemic Repositories, MVI and RxJava-induced design damage, and how AAC ViewModel is silently killing your app

--

We’ve reached the one year anniversary of me not writing any articles! Clearly, this is a streak that’s ought to be broken. So, it’s time for me to share a few things I’ve been seeing the past couple of months, that actually disturb me a bit each time I see them. Time to do something about that, after all 😉

Anemic Repositories

I’ve definitely heard the term “anemic domain model”, but I haven’t really heard “anemic repository”. Might be an Android-specific thing, but here it’s fairly common.

It all boils down to this simple image from the Jetpack “Guide to App Architecture”:

From https://developer.android.com/topic/libraries/architecture/images/final-architecture.png

This image clearly states that “the ViewModel should be communicating with a Repository”.

That doesn’t mean that you ALWAYS need a Repository in your code.

What is a repository anyway?

The Repository (in Android lingo) is meant to have a very specific job to do in the following scenario: providing the means to observe the database for changes, while also triggering cache invalidation and re-fetching new data from a remote API when needed (then saving that to the local DB, thus triggering a DB fetch for the newly inserted data).

If an app does not have a remote API, it does not need a Repository.

If an app does not have a local database, it might also not need a Repository.

If your app also uses “UseCase” classes like GetUserUseCase or GetUserFetchTask, then you definitely don’t need a Repository, as those “UseCase” classes are already doing what a Repository would.

An app also does not need a Repository if “there is a chance that maybe later someone might potentially need it, but right now it’s just forwarding requests”. That’s what refactoring steps are for.

How are repositories meant to work?

An interesting tidbit about Repositories is that if we check the recommendation from Google’s samples for how the Android Architecture Components are meant to be used, we can find that the recommended Repository implementation is stateless.

It does not serve as a singleton global in-memory cache. The caching is done by the LiveData<T> inside ViewModel. In fact, the Repository also doesn’t handle the cache invalidation directly: that is implemented inside LiveData.onActive, leveraging the powers of MediatorLiveData. (This particular implementation of this behavior is called a NetworkBoundResource).

Therefore, in Jetpack’s recommendation, the Repository is a ResourceFactory. But, it is not a DataSourceSelector, and most likely shouldn’t be written as such.

As mentioned by Yigit Boyar in the talk “App Development — Pragmatic Best Practices” (Droidcon SF 2017):

The important thing you need to understand is, whether it comes from the network that goes into the disk, it NEVER goes back to the UI. This makes the disk your single source of truth, any time where this changes, your UI always shows the latest information. If you do this RxJava operation “pull from network, pull from disk, whoever comes last return it to the user” you are doing it wrong.

Therefore, there is most likely no reason for the Repository to have logic that “directly sends data from the network to the UI” unless you don’t actually have a local database.

If you don’t have local disk persistence, you’re now troubled with handling the scenario when you come back from process death, and your app has no data, and no internet access, as you’re on an airplane or in London Underground.

Of course, you can always keep that in mind, and design your app so that this won’t cause you too many headaches or crashes, but it takes additional consideration.

But even in this scenario, what you most likely want is an in-memory cache (to make up for the lack of a local database), that you update with the remote fetch. You could even fake a reactive database with a few BehaviorRelays. So even when you don’t have a dedicated local database, you still don’t need to do a “data source selection”, instead, trigger a refresh to update the local cache.

So what’s an anemic repository like?

When people don’t really know what to put in a Repository, because their app just doesn’t need them, it typically looks like this:

This Repository does not do anything useful. The only thing it does is forward the exact same method call to a Dao.

This case is not better when there are UseCase classes for each method call, as the Repository is still doing only forwarding.

In fact, even the UseCases are just doing forwarding.

When you have code that does nothing except delegate the method call someplace else, then it’s a code smell, and it has a name: Middleman.

If you’re trying to shoe-horn in a Repository just for the sake of “following the Jetpack Guide to App Architecture”, and you’re adding code smells in your code, that doesn’t make it better. That makes it worse. In fact, if UserDao is an interface, I could theoretically replace the above code with something like this:

Which is alright if that’s what you actually need, but you probably don’t, and you definitely don’t need it for the sake of having it.

You’re doing your code a favor if you make it less smelly — just delete your Repository, then add one if it actually matches your requirements. If all your code you added does is delegate it to some other class, and NOTHING else, then adding those code isn’t “cleaner”: it’s a code smell, and detracts from the value of your code.

And, even if you might need UseCases (or as we called them when we needed them, FetchTasks) in place of the Repository, in that case you definitely don’t want a Repository that just delegates 8 method calls to 8 UseCases, just for the sake of coupling them, and not doing anything else with them beyond that. I think you get the idea.

MVI, and RxJava-induced design damage

Another subject I feel like it’s worth touching on is MVI as a “popular architectural pattern”.

The gist of MVI is that you use RxJava everywhere. People tend to put this nice image next to it to make it feel like a good idea. Circles are pretty, we need more of them.

A common image that depicts a “unidirectional data flow (UDF)”

What they don’t tell you is that you can achieve most of this without RxJava, and you’ll end up with code that is easier to read, easier to write, easier to understand, and therefore easier to manage.

Let’s focus on the communication from User -> Intent -> Model (which is what differentiates MVI from MVVM).

You see, the trick is that the “Intent” from the User is an “event” that we already had: click listeners, text changes, and so on. We can add listeners for it.

What MVI does is represent these “events” triggered by a listener as a class or object instance.

So we take code that reads like this:

And replace it with the following:

On the other side of the equation, there is someone listening to these events.

Or with RxJava

We are doing pretty much the same thing. But have we really gained anything?

The answer is, if you ask me, is that we definitely have more baggage: having to manage CompositeDisposable, Observable.merge, mapping existing Listener events into our own classes, describing them as our own sealed class hierarchy.

Just using a regular Listener interface would have been easier to use.

Why do people try to replace such synchronous method calls with RxJava then, in pursuit of “MVI” and “UDF”? In this particular case, they probably prefer writing more code to achieve the same thing 😉

On the other hand, when using BehaviorRelays to replace a regular field + listener in a ViewModel, we can actually leverage the benefit of RxJava, when we use methods such as Observable.combineLatest to observe if any of our fields have changed, using a single observer.

Interestingly, we don’t need either one of publish().replay(1).autoConnect(0) or scan() to achieve this — a BehaviorRelay is enough.

But when you’d use BehaviorRelay, you can also use LiveData, which may be easier to understand, and easier to manage the lifecycle of its subscribers (as with LiveData, this is automatic). You don’t actually need Rx to handle this scenario either. Prefer Rx if using it actually makes your code simpler, and makes it easier to understand.

The more state you put in RxJava using operators such as buffer or scan or replay(1), the harder it becomes to “tap” the stream, and persist your current state to the onSaveInstanceState(Bundle) — which is a must if you want to follow the Core App Quality Guidelines (see FN-S2).

The dark secret of AAC ViewModel

ViewModel was introduced into the Android Developer’s app toolkit in 2017, with the first release of Jetpack Architecture Components.

In its simple design, it was meant to be a “data holder”, that would persist the query results obtained from an underlying database across configuration changes (orientation change, multi-window mode, locale change, etc). It wasn’t really meant to be more than that.

However, people were desperate to move out common logic that resided in a cumulative blob of an Activity and its child Fragments. ViewModel seemed like an easy way to share data, state, and functionality — and by using LiveData, keeping them in sync across these multiple Fragment screens.

This approach was the new, recommended approach to share state across screens.

The problem hiding in the background

When using this technique to share data or state across screens, we must consider the fact that when the app is put in background, Android can mercilessly murder it at any time in order to reclaim resources.

$ adb shell am kill com.acme.app.deadmeat

This is also known as “low memory condition”, or “process death”.

When this happens, all static variables are cleared, the whole app is restarted, but you have the last-most Activity and its Fragments recreated automatically.

(Developers often claim that “this scenario is rare and you shouldn’t care about it”, but they’re probably iOS users.)

However, the ViewModel does not persist the selected state. You might be returning on a detail screen, and the selected state is lost.

The proposed solution?

Since Google I/O 2019, there has been a new tool in the works, called viewmodel-savedstate, which last week finally reached 1.0.0-RC2. Hopefully a stable release will come soon, as it would be rather important for any users of ViewModel.

Using an AbstractSavedStateViewModelFactory instead of your usual by viewModel in Koin or your usual by viewModels<T> in ktx, you can obtain a reference to a SavedStateHandle.

When you obtain a SavedStateHandle, and pass it to your ViewModel, you can obtain a MutableLiveData that will auto-persist and auto-restore the data you store in it (as long as it is a supported type).

The take-away here is that if:

  • you don’t have a saveState/restoreState function on your ViewModel
  • you don’t have an initialState: Bundle? constructor argument on your ViewModel (passed in from the ViewModelProvider.Factory in your Activity/Fragment as a dynamic argument)
  • you don’t use SavedStateHandle.getLiveData("key") nor SavedStateHandle.get("key")/set("key")

Then your app is most likely broken, and you should try checking how it behaves after being put to background sometime. People take pictures, get phone calls, and so on. You might want to support that in your applications.

Conclusion

Hopefully, this article has helped you find ways you can reduce complexity in your code, identify bugs, and make your apps better. If you feel like there’s anything I’ve left out, I should be thinking about, or you agree or disagree with, feel free to comment!

(And you can also follow the discussion thread on Reddit: https://www.reddit.com/r/androiddev/comments/e2kh68/anemic_repositories_mvi_and_rxjavainduced_design/)

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Gabor Varadi

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.

Responses (15)

Write a response