Getting our apps ready for Jetpack Compose
Since the announcement of Jetpack Compose, followed by the developer and alpha releases of the framework, excitement has been building around getting this into our apps. With the migration to Compose, we will see huge improvements in developer productivity, application stability and maintainability, as well as other side affects in things such as hiring (who wouldn’t want to work on a team adopting these new technologies!). However, alongside all of these topics, one question I often get asked around Jetpack Compose is when is there going to be a stable release? It is also something I often wonder too! Whilst this is not likely to happen just yet, I feel it’s important to focus on what we can do now to get our team and apps ready for compose adoption — there are a collection of things that we can start or continue doing to make this transition smoother when the time comes. It’s likely that we’re not going to migrate our whole app right away (as tempting as it may be), anything we can do to prepare our app will help to reduce any friction in this eventual transition — and at the same time naturally improving our codebase.
Note: Throughout this article I talk about Jetpack Compose and some of the principles behind its workings. If you have not yet explored Compose or how it works, I would recommend some additional reading from the official documentation.
START THINKING IN A DECLARATIVE UI MINDSET
Even though we aren’t currently using the declarative approach that Jetpack Compose gives us, we can start trying to take the declarative mindset onboard when working on our applications. One of the big concepts with declarative UI is state — our UI components are composed using the data values that we provide it with, rather than manually controlling view properties whenever values change (setText, setVisibility etc). This concept of state becomes much smoother to manage with state that represents a single source of truth, i.e all of the different components that make up your UI reference the same state object rather than being individually modifier.
Before this approach started to become more common (pre-Android ViewModel and livedata), we may have been in the habit of individually setting view properties from different sources. For example, triggering an operation that caused some data to be emitted and once this data became available, the result would eventually be trickled down to some view. With multiple operations taking place, the different parts of state for a screen become quite decoupled as they are being individually manipulated
The thing is here is that your UI has no single representation of what your screen should be made up of — as these individual streams of data are populating UI components, it’s easy for content to become out of sync and also become quite unpredictable, as your data could be changed from anywhere. This becomes even more of a problem when it comes to Compose, because your entire UI will be re-rendered with any state change, it’s important that your data is in sync so that if it ever changes, you can be sure that the rest of your UI will be rendered in a predictable fashion. Having a single source of truth for your state helps to not only ensure this, but helps to prepare the concerning screen for Jetpack Compose.
We can see here that now we have a single state reference that is manipulated by any of the operations for our screen. This state reference is then used to control all of the state for the components on our screen, rather than each of those components being managed individually when it comes to state. This helps to keep these components stateless, using them to simply portray what our state represents. This approach is perfect for a composable UI — because composables are rendered using the state that they are provided with, the use of a single source of truth means that when we re-render our screen we can compose the content from the same source, ensuring that the representation of our state is always correctly reflected in the UI.
UNIDIRECTIONAL DATA FLOW
Unidirectional data flow itself is a whole separate topic that could probably have it’s own blog post, but with the talk of state it feels appropriate to mention this concept too — it also ties in very closely with the idea of a single source of truth. You can learn more about unidirectional data flow and android in this talk by David González.
In a nutshell, the aim with unidirectional data flow is that data should have only one way to be transferred to other parts of a screen or an application. So in regards to data flow between the view and data layer, let’s say the user presses a button that has its interaction caught by a listener, which triggers a function in a viewmodel, which triggers a request in a repository. This request comes back, sets the state in our view model which then emits the result to be displayed in response to that button press. We can see here that the data flow has been operated in a circular motion — the actual implementation of this can look very different depending on the architecture and frameworks used, but the key concept is that the cycle is always adhered to.
The same also applies when applying this to our UI components — after triggereing that button state when our UI is re-rendered, the parent component that receives the new state should pass that state down to any of it’s child components — again, keeping our UI components stateless and using them to simply portray the state. And then if those child components wish to trigger any events themselves, such as their own button presses, those would be passed up to the parent to then trigger that whole event cycle again.
With this enforced, interactions from any components must trigger events through the parent component, which in turn manipulates our state, which in turn updates our UI. Here our UI and its state become much more predictable, as we know for sure where and how our state is being manipulated. When it comes to compose, this concept is key. For Jetpack Compose, composables are rendered from state and any of its changes — so ensuring predictability and validity here can be achieved via the use of unidirectional data flow. Adhering to the stateless principle in UI components also helps to keep a low friction when it comes to automated testing, as we only need to be concerned with their rendering and behaviour, without a concern for state management.
DECOUPLING UI COMPONENTS
Jetpack Compose is focused on rendering the UI of our application, meaning that it has no responsibility around the data flow, business logic or any other concern of our application. The aim is to have lightweight composables that are decoupled from these other concerns within our project. With that said, this isn’t really any different from what we should aim for our view classes to currently be like in our applications — as even without compose, this helps to decouple these classes, therefore increasing their maintainability and testability. When it comes to adopting compose, having our existing view classes match the responsibility of a compose UI component (as closely as possible) will hugely reduce any friction in switching out that component for a composable. Leading up to compose, there are a couple of thing that we can do in this area.
When working with existing code in your project, small refactors can be made now to reduce the friction in future when it comes to adopting compose. As an exercise, maybe take a quick look at a view component in your app and think, what would it take to convert this component to a composable? If there is nothing that comes to mind, that’s great! However, in a lot of cases it’s likely that that component might be managing it’s own state, triggering a repository directly, displaying a dialog or triggering some other action which affects that screens state. Because these affect state, these are all things that will need to be changed if you wish to adhere to the other things that are mentioned in this post.
Even though these could all be done in one go during compose migration, this not only adds friction but also increases the scope of change — therefore increasing the chance of regressions in our code. What we can do in the meantime is make small refactors to help ease our transition — these can be as small as moving that dialog to the parent and triggering its display via a callback, or moving the view managed state to the parent and have it passed into the child component.
When adding new view classes to your project before your compose adoption, be sure to question the responsibilities of that view. With this in mind, some questions that might come to mind during technical specs and code reviews:
- Should this piece of business logic be contained within this view? Could we utilise this logic in a parent class and have the data that is passed in reflect this?
- Rather than the view managing this piece of state, would it be possible to pass it down from its parent?
- Why don’t we trigger an event via a listener for this and pass an event upstream, rather than executing the request directly inside the view?
As we can see, these example questions relate directly to some of the concepts previously mentioned in this post. There are likely some other things that will come to mind, I just wanted to provide a couple of examples here. The important thing to think about is for the things that we are writing now, how much change will be needed in-order to adopt compose here and what can we do now to reduce our compose debt in future?
DON’T STRESS EVERY DETAIL
Whilst we may be trying to decouple our UI components and start thinking about the compose equivalents, it’s likely that either some things might not be possible to recreate in compose from day one or you simply won’t know how to achieve certain things right away. With the interoperability that compose has with Android Views, we will be able to plug existing components directly into our composable UI. Therefore, if you decide to migrate a screen that has a pretty complex UI, then you will be able to tackle that piece by piece. Whilst it can seem to nice to have tackled everything for the task that is in front of you, it’s important to be pragmatic at the same time. Having some adoption in place is good for team momentum — so if utilising interoperability is needed to add Compose support for a screen in your app, that is better than not adding compose at all.
START PLANNING YOUR ADOPTION
With all of the above in mind, we know what we can do to our code to start preparing for Jetpack Compose. However, it’s important to plan your adoption path as it easy to get caught up in the bigger picture. This can be even more true if you are working in a larger codebase — it can be very daunting to think of where the app is at now, compared to how it will transform to a Compose based project. But, remember to take a step back and think about things in smaller chunks. What needs to be done in a step-by-step fashion to get from where we are now, to where we are comfortably using compose. Remember, converting the UI to the declarative approach is only a part of compose migration — without some of the concepts mentioned in the rest of this post, adopting compose will be increased in difficulty.
Part of this plan could include insuring that all new features adhere to the principles of unidirectional data flow and decoupled UI components, as the more you can start adopting this now, the less work it will be to drop in compose when the time comes. When reviewing pull requests or preparing a technical specific for a feature, try to ensure that what you are merging in will not make your work more difficult in future. Whilst this is something we strive to do anyway, the declarative mindset changes things a bit here. With this thought about and some form of standards in place for how your app manages state and data flow, introducing compose to you codebase will come more naturally.
Adopting Compose in your UI is only a small piece of the puzzle, you’ll also need to think about things such as ramping up the team with the above concepts, the Compose framework and any other changes that are introduced (such as writing tests for composables).
PLANNING FOR MINSDK 21 (LOLLIPOP)
Jetpack Compose requires minSDK 21, which is Android Lollipop. Whilst we have had many versions of Android released since then, there are still a collection of users who are running less than that requirement on their devices, as well as apps that support it. Regardless, without creating too much work for yourself, this will likely be a blocker for your adoption of compose. Because a stable release is still going to be some time away, there are some things to think about in the meantime if you are currently supporting below minSdk 21.
- Why are we still supporting lower than minSdk 21? Is there some form of business need there, or is it simply something we have not revisited in some time?
- What % of our users make up those on lower than minSdk 21? Do we know anything about those users and their behaviours with our app? Do they align with our target user?
- Are there currently other issues we are aware of or have planned to tackle that are caused by devices running pre-21?
- Do the benefits that Jetpack Compose will bring our application outweigh the cons of keeping support or pre-21?
- If we were to update to minSdk 21, what can we do now to ensure that those who cannot update can still enjoy the current set of features in our application?
Supporting older version of the Android SDK can definitely cause difficulties during the development and testing process. Sometimes we may be unable to utilise certain libraries, or face issues which specifically occur on older SDK versions. These things can not only harm developer productivity, but also affect the output which comes from the team. With that in mind, it’s important to visit why that support is still in place, questioning whether it is something that you still need in place. If the decision is made to update the minSdk version, you can start planning what you may need to do to ensure that those who cannot update can still enjoy your app (e.g fixing any critical bugs and ensuring a stable release before support is dropped). Having any work done sooner to change that minimum support will also help to make the transition to compose much smoother.
BUILD MOMENTUM IN YOUR TEAMS
Because Jetpack Compose requires not only a shift in thinking with how we build the UI of our app, it is also a new set of APIs for us to learn and understand. Whilst the framework is still in alpha and things may change slightly, the concepts and ways in which compose will be used will likely remain the same, as will those of declarative UI in general. Because of this, there will be no loss of time from exploring compose and trying it out for yourself.
One step further than that would be to use this to build momentum within your team for Compose. Maybe you could hold a small presentation to introduce some of the concepts to your team, or even host some hack-days to allow people to learn and use compose in a hands-on fashion. Because Compose will be the way in which we build apps, this is a worthy investment for your company. For example, Snapp mobile have hosted several of these hackdays to allow their teams to explore compose in a collaborative environment. Because compose will not be stable for some time yet, this allows people to gradually prepare for the shift and learn about the framework together as a team.
Over at Compose Academy I am hosting a range of workshops for Jetpack Compose, which are a great way to get your team comfortable with this new framework. If this is something that your team is interested in, please drop us an email!
And most importantly, be patient with the adoption of this new way of building UI. Whilst this is an exciting time, the current Android UI framework is not disappearing anytime soon — so there will be plenty of time to migrate over to compose. This is important to not feel overwhelmed with the shiny new things — you likely won’t migrate your whole codebase right away, and you may not even be using it for every new feature of our app from day one. Not only will there be some things that compose won’t support right away, but they’ll also likely be other priorities in your work that still deliver value to your users.
Whilst Compose will eventually become the standard for building UI in our Android apps, this will be a marathon and not a sprint — so enjoy the journey!
Whilst there are likely a range of things we can do to get ready for Jetpack Compose, the above outlines some of the things that have been on my mind as to which could be key to a successful adoption. Are you already thinking about your teams adoption of Jetpack Compose, or have you had any learnings which are helping to put you in the right direction? I’d love to hear about them if so!