How to start writing reusable components for Android apps?
The purpose
For anyone, like myself, interested in building custom, reusable view components for an Android app. And those who had problems with finding good guidelines on the topic.
I will provide the basic reasoning behind architecting custom reusable views. There won’t be implementation details (code) for many reasons,
one of those is my trial to focus on the concept rather than technicalities.
Anyway, I hope that after reading this article you will be able to apply those principles to any interface architecture pattern such as MVP, MVC, MVVM
or MVI.
The first part says why and when adapt such technique.
You can jump to the second part for the list of recommendations.
First, ask yourself a question: Do we need reusable components?
IMHO you should be a part of a bigger, long-lasting project to consider doing so. I understand it’s hard to draw a line between MVP/short-term
and a long-term project sometimes. It is sometimes also hard to let go of technical nicety such as custom views but please be mature to do so.
The following list should help you making a decision.
YAYS
- If you’re in a flexible environment where design decisions are done based upon many perspectives (not just your own) so you can pitch for good UX/UI.
- If you see one (or more) view is used (will be used) in many places (reused). Plan for it. Those views should have fairly similar UX/UI to serve as a reusable components. Otherwise, reusing them will be challenging. Again, pitch the team to sustain consistent user experience.
- If your teams plans to build extremely custom view like karaoke,
piano keyboard, fancy animated overlays, etc. - Finally, I have not tested this theory but IMHO if you’re bored and you feel like making an old legacy project interesting.
Since writing reusable components is like writing complex new features. - If you’re able to deliver business value by creating such components.
- If your team plans major refactors and you have time to just play with the product.
NOPE
- POC or MVP cases. Although, MVP cases could be a good playground.
- Small, simple app cases. There are situations where you know an app will be coded once and left as is. BTW Consider going cross-platform with those. Again, nice playground.
- An inflexible environment. Design system gave to you up-front instead
of being discussed with all parties involved. - Your codebase is not ready to handle reusable components.
Please see Prerequisites section at the very end for few tips.
In this case you might have to put some work beforehand in order to enable reusable components.
Ok, so now you’re sure. Let’s go with recommendations!
- Custom views
It could be quite obvious for some and not necessary for some others
as I noticed. Deeply research the case of custom views first and try to imagine them as reusable and self-contained components. Don’t be afraid.
There are plenty of materials to learn how to create simple custom view.
Remember, it doesn’t mean you’re responsible for rendering everything yourself. It doesn’t mean you have to create those components from scratch neither. It will be the better and simpler version of what Fragments suppose to be.
Think of a custom view as a container (f.ex ViewGroup like FrameLayout)
for other components grouped together to serve one purpose.
For example list of results used in couple places, part of product detail screen, or maybe just a grid product item used on different screens of your app,
or a search-bar and corresponding list of results. In the Toolbar/bars case
be careful. We had a lot of work with extracting Toolbar out of custom view after we realised Toolbar should be a separate, reusable component itself.
The important parts are reusability, testability and independency.
Decouple your view and controller from everything else, more on this shortly.
I would suggest to read about Atomic Design in order to imagine the building process better. Basically, think of an any widget like button or label as a basic building block for bigger component. No matter if custom or native one.
Then imagine your custom, reusable view (imagine now list of results with reload button) and just compose those two views together. Handle composed views through your view controller or/and communicate through business logic (f.ex in a reactive way but that is not an only option) and you’re good to go.
Please remember, it is complex stuff. Probably the first place you will choose for refactor will be the wrong one. It was the case in my case.
My tip would be to go with the simplest view first.
DON’TS
Fragments inside “main” fragment.
Parent-child relation between views or view controllers.
Toolbar as a part of custom view.
Navigation bottom bar (iOS Toolbar) as a part of custom view.
Coupling. Activity/Fragments does setup for view.
DOS
Custom view that serves one purpose.
Activity/Fragments just render/show/hide view.
2. Use platform-native components as much as possible
Speaking of custom views, don’t go fully custom if not forced to.
Reusing existing components and widgets just makes your life easier
and enables you to work on actual, unique to your app business value. Work alongside with product/UX/UI/your own team to build native-first design systems. In most cases there is no need to go completely custom
and I’m sure your client is more than happy to get rid of an animation or fancy screen transition just to get business value on the place.
Ours clients are #trueStory.
What do you mean by native components?
Google did a great job introducing Material Components — guidelines with the corresponding implementation you can reuse in your application.
You could also consider not-views related components like Navigation Component which plays nicely with bottom nav bar f.ex. and could simplify your codebase.
You should take a look on how to compose and reuse themes and styles for your application, here is a presentation from Googlers on the topic.
But in the end just reuse old, good widgets and layouts to get things done.
3. Use modern app layer (view layer) designs patterns
Yep, and not only for the app layer. Plenty articles on that matter. I recommend you to read Hannes Dorfmann’s story on MVI since a lot of useful concepts and ideas are there. He elaborates on topics such as how to organise, test, reuse and maintain your code which is not a part of this article but those two topics are strictly related. On the other hand I’m not a fan of tools like Mosby or Moxy because I painfully found out they are rather constraining. Inheriting behaviour, state or plugins from base classes was catastrophic in our cases. Refactors and building custom behaviour for new screens were messy tasks and required much effort. Instead, use lifecycle aware components (again, see Android tools) or setup your architecture the way that you can call your business logic anytime. Some people calls it overhead. I would call it simplicity especially most of us happen to use caching.
4. Use dependency inversion frameworks extensively
Nowadays, there is world beyond Dagger. Dagger itself offers quite good support for Android apps also. You can leverage dependency inversion principle alongside with framework of your choosing to inject views the same way you are injecting other collaborators like controllers, use cases or data sources. That way you’re decoupling actual view from the place it is rendered.
The great news is your custom view is View implementation which means your Fragment or Activity doesn’t have to know what is rendered inside. Providing View subclasses enables you to use many custom views interchangeable as particular a component. Voila! Basic A/B testing setup done. Based on condition X you’re providing View Y or Z. Or you want to support multiple apps with one codebase. No worries, just inject different views for different flavours. There is much flexibility in this approach.
The only thing that Fragment or Activity need to do is to add view
to the layout (programmatically or through inflated xml if you want to
be direct). Don’t over-engineer it since you cannot escape from those
few lines. Just show/hide/gone view first based on the state that must
be rendered on the screen, and then optimise if necessary.
We’re building an app for Android OS 6+ so obviously those devices are quite fast devices. We have not encountered any glitches.
Oh, and really focus on cleaning up dependencies declarations like Dagger Modules. If you don’t understand it — spend time on it. Leave readable
and understandable legacy to you colleagues. Piling up dependencies are the worst enemy of a clean dependency graph, could lower performance and definitely causes headaches while coding the solution.
5. Infamous parent-child dependency
Don’t introduce parent-child dependency in case of custom views. They really need to be testable and independent. You don’t want to create/mock parent view or controller in order to test a child. And you would need to create or mock such instances for UI or unit test eventually. Don’t introduce hidden dependencies like setup of a custom view partially delegated to the Activity/Fragment or parent view. And definitely don’t inject your Activity/Fragment to a custom view which could lead to cycle dependency and memory leaks. That would also mean other developers will not understand how to use your component. It’s not in reusable fashion.
The same goes for view controllers (Presenters, ViewModels, Models, etc). Don’t introduce parent-child link between them. Navigation and analytics are the common mistake — just inject those (hopefully) reusable components to custom view controller instead of calling methods from parent controller in order to do any job like analytics tracking.
In summary, what was the effect of aligning to those principles…
Simply put, reusable components which could be used anywhere in the app with only small changes to the codebase required. By architecting those properly you should be able to decouple from Android lifecycle hell.
TBH we still sometimes need to sync data onResume() or onPause() somehow. That is why we created an abstract class CustomView with onForeground()
and onBackground() methods which are then implemented by custom views. We have done it for simplify, since it’s not breaking good architecture rules. We’re still doing our research on this topic. We know some tricks including lifecycle aware components but in the end our goal was to decouple from lifecycle! I will update this article when and if solved.
So, for example, by creating a list of results which was decoupled completely from other components we were able to switch views inside the screen in couple of lines enabling A/B testing and easy multiple flavour handling. It is also in the testing stage, so stay tuned. I’m definitely eager to edit this or write new article after long-term usage of the solution.
The way to achieve that wasn’t easy. We talked to the client and other devs
a lot in order to simplify and learn what is really needed.
We started refactor in one place and then thrown away code. And started again because system and requirements were quite complex. But that enabled us to understand business requirements and technical dependencies which opened new perspective on the solution. We knew how to simplify.
Prerequisites
I assumed your codebase is prepared for custom views in a way that many other collaborators are already reusable, testable, independent components. Or at least you’re able to inject them where needed. That would mean Analytics, Navigators, business logic, sensor handlers components and much more. That really depends on your application and you need to resolve it for yourself. Hopefully, by using the above mentioned principles as a reference. Although, there are few basic rules:
- Your business logic is separated from views and view controllers.
- Your business logic can be easily injected and reused.
- View controllers are decoupled from views and how views are rendered.
- View controllers are framework agnostic.
- View controllers are easily injectable to views.
- Views are are easily injectable to Fragments/Activities.
DISCLAIMER
That is my story behind architecting reusable components for an Android app. I’m not saying it is silver bullet nor the best practise. Still, I consider above rules as a great approach to organise our system.
It was the first iteration we did in Tigerspike as a part of the larger project so stay tuned for more! Big shout out to my colleagues as they made this article possible. :)