Once upon a time, one could write an entire Android app in a single humongous activity. Google provided us with a bunch of fairly low-level building blocks and basically no guidance on how to use them together in a maintainable and scalable way. (“So you want to build a house? Here is a shovel, a saxophone and a kitten, we thought they might be useful, but do absolutely what you want with them, go on! And please mind the lifecycles, thank you.”)
Fortunately, things have changed a bit and now the official docs mention stuff like view models, lifecycle observers, navigation graphs, repositories or single-activity applications. And there are even official
opinions on how to combine them!
Nevertheless, activities and fragments, the remnants of those dark times, are apparently here to stay. When you look at API surfaces of these...
things, one question surely comes to mind: How do I work with that and stay sane at the same time?
A match made in a place other than heaven
Before we get to that, a little disclaimer is needed: This article is very opinionated. Your mileage and needs may vary. The following recommendations are best suited to “ordinary” form-based apps containing quite a lot of screens and user flows, with modest performance requirements. Games, specialized single-purpose apps, apps with dynamic plug-ins, high-performance, or UI-less apps might benefit from completely different approaches.
With that out of the way, the first question we should ask is: Do we even
need activities and fragments?
With activities being the entry points to the app's UI, the answer is obviously yes. There’s no way around the fact that we need at least one activity with
android.intent.action.MAIN and
android.intent.category.LAUNCHER in its intent filter. But do we need
more than one? The answer to that is a resounding no and we’ll see why in a future post.
Fragments are a different matter. First introduced in Android 3.0, when tablets were a thing, they were hastily put together as a kind of reusable mini-activities so that larger tablet layouts could display several of them simultaneously (think master-detail flows and such). Unfortunately, they inherited many design flaws of activities and even added some very interesting new ones. To say they are controversial would be an understatement.
On top of that, we don’t really need them in the way that we need that one launcher activity. Bigger, reusable pieces of UI can be served using good old views and there are 3rd party frameworks that do just that (and even several others that achieve the same thing in other ways, like using RecyclerViews to compose the UI from different “cells” etc.); and don't even forget that Jetpack Compose is coming...
However! Fragments are still developed, supported, documented, advocated for, integrated with other highly useful libraries (like Jetpack’s Navigation component) and in some places, they’re quite irreplaceable (yes, this is more of a design flaw of such APIs, but we need to work with what we have). Love them or hate them, they are the standard, well-known official solution, so let’s just be pragmatic: We’ll give them a hand, but won’t let them take the whole arm.
And so, we’ve arrived at the second question: One activity and possibly many fragments—but what can we use them
for? And if we can,
should we?
Less is more
This is where the opinions begin, so brace yourself.
Architecture-wise, what is an activity (and to an extent, a fragment, since they share many similarities)? My answer is this: A textbook violation of the single responsibility principle.
The main problem with an activity/fragment is that it is:
- a giant callback for dozens of unrelated subsystems
- which is created and destroyed outside of our control
- and we cannot safely pass around or store references to it.
Typical consequences of these issues (when not handled in a sensible way) include activity subclasses several thousand lines long, full of untestable spaghetti (1), UI glitches and crashes (2) and memory leaks (3).
Open any activity in your current project, type
this. and marvel at the endless list of methods. The humble activity handles UI lifecycle and components, view models, data and cache directories, action bars and menus, assets and resources, theming, permissions, windows and picture-in-picture, navigation and transitions, IPC, databases and much, much more.
How Android got to this point isn’t important right now, but your code doesn’t have to suffer the same bloated fate. We need to chip away at the responsibilities and one way to do that is this: Use fragments exclusively for UI duties and that single activity for system call(back)s (and absolutely no UI).
Fragments of imagination
Each fragment should represent one screen (or a significant part of one) of your application. A fragment should only be responsible for
- rendering the view model state to the UI and
- listening for UI events and sending them to its view model.
That’s all. Nothing more. Really.
View model states should be tailored to concrete UI, should be observable and idempotent. It’s alright for view models and fragments to be quite tightly coupled (but view models mustn’t know anything about the fragments).
Because fragments are much harder to test than view models, the view model should pre-format the displayed data as much as possible, so the fragment can be kept extremely simple and just directly assign state properties to its view properties. There shouldn’t be any traces of formatting or any other logic in the fragments.
The opposite way should be equally simple—the fragment just attaches listeners to its views (our current favorite is the
Corbind library which transforms Android callbacks to handy and most importantly unified
Flows) and sends these events directly to the view model.
That is what fragments should do. But what they shouldn’t do is perhaps even more important:
- Fragments shouldn’t handle navigation between screens, permissions nor any other system stuff, even if the APIs are conveniently accessible from right inside the fragment.
- Fragments shouldn’t know about each other and shouldn’t depend on or interact with their parent activity.
- Fragments shouldn’t know about how they are instantiated and how they are injected (if your DI framework allows this); they also shouldn’t know about fragment transactions, if possible.
- Data should be passed to fragments
exclusively through their view models and that should be just the data to be displayed in the UI—forget about fragment arguments and rich domain models in them.
- This almost goes without saying, but fragments shouldn’t do any file, database or network IO (I know, inside the fragment, the almighty Context is sooooo close… Just a small peak into SharedPrefs, please? No, never!).
- Since Android view models got
SavedStateHandle, fragments even shouldn’t persist their state to handle process death.
- And for heaven’s sake, never ever use abominations such as headless or retained fragments.
Some other tips include:
- Fragments should handle only the very basic lifecycle callbacks like
onCreate/onDestroy,
onViewCreated/onDestroyView,
onStart/onStop and
onPause/onResume. If you need the more mysterious ones, you’re probably going to shoot yourself in the foot in the near future.
- If possible, don’t use the original
ViewPager with fragments—that road leads to madness and memory leaks. There's a safer and more convenient
ViewPager2 which works much like
RecyclerView.
- Make dialogs with
DialogFragments
integrated with Jetpack Navigation component. It’s much easier to handle their lifecycle (those dismissed dialogs popping on the screen again after device rotation, anyone?) and they can have their own view models. This way, there’s almost no difference between part of your UI being a dialog or a whole screen.
- Sometimes it’s OK for fragments to include other fragments (e.g., a screen containing a
MapFragment), but keep them separate—no direct dependencies and communication between them, no shared view models etc.
- To make your life easier, your project probably has some sort of
BaseFragment which simplifies plumbing, sets up scopes, and what have you. That’s fine, but resist the temptation to pollute it with some “handy” little methods for random things like toasts, snackbars, toolbar handling etc. YAGNI! Don’t misuse inheritance as a means to share implementation—that’s what composition is for.
- Our favorite way to access views from fragments is the relatively new and lovely ViewBinding library. It’s simple to integrate, straightforward to use, convenient, type-safe, and greatly reduces boilerplate. No other solution (findViewById, Butter Knife, kotlin-android-extensions plugin or Data Binding library) possesses all these qualities.
- Speaking of Data Binding, even when it isn’t throwing a wrench into your build, we don’t think that making our XMLs smarter than they need to be is a good idea to begin with. And don’t let me start about the testability of such implementations.
- Use
LeakCanary! The recent versions require practically no setup and automatically watch Activity, Fragment, View and ViewModel instances out of the box.
After following all this advice (and a little bit of coding), your
complete fragment could look like this (the implementation details aren’t important, just look at the amount and
intention of the code):
internal class ItemDetailFragment :
BaseFragment<ImteDetailViewModel, ItemDetailViewModel.State, ItemDetailFragmentBinding>() {
// take advantage of reduced visibility if possible so you don’t pollute your project’s global scope
// required by DI framework
override val viewModelClass = ItemDetailViewModel::class
// layout inflation with the lovely ViewBinding library
override fun onCreateViewBinding(inflater: LayoutInflater) =
ItemDetailFragmentBinding.inflate(inflater)
// initialization of view properties that cannot be set in XML
override fun ItemDetailFragmentBinding.onInitializeViews() {
detailContainer.layoutTransition?.disableTransitionType(DISAPPEARING)
}
// render the view model state in the UI; kept as simple as possible
// state properties should preferably be primitive or primitive-like types
// no DataBinding :)
// notice the receiver - we don’t have to reference the binding on every single line
override fun ItemDetailFragmentBinding.onBindState(state: ItemDetailViewModel.State) {
loading.isVisible = state.isLoadingVisible
itemTitle.text = state.item.title
itemCategory.textResId = state.item.categoryResId
itemFavorite.isChecked = state.item.isFavorite
itemPrice = state.item.price // price is a String and is already properly formatted
/* ... */
}
// the other way around: catch UI events and send them to the view model
override fun ItemDetailFragmentBinding.onBindViews() {
toolbar.navigationClicks().collect { viewModel.onBack() }
toolbar.menu.findItem(R.id.checkout).clicks().collect { viewModel.onCheckout() }
favorite.checkedChanges().collect { isChecked -> viewModel.setFavorite(isChecked) }
addToWishList.clicks().collect { viewModel.onAddToWishList() }
addToCart.clicks().collect { viewModel.onAddToCart() }
/* ... */
}
}
That’s not that bad, is it?
If you can’t beat them, join them
Although hardly an elegant or easy-to-use API, fragments are here to stay. Let’s make the best of this situation: Pragmatically utilize them for their useful integrations and focus on the single real responsibility they have—handling the UI. Ignore the rest and KISS—this principle is extremely important when working with fragments. That way, you’re going to have small, simple, focused fragments—and more importantly, a lot less less headaches.