Compose (UI) beyond the UI (Part II): applying changes
In part I we saw how, with Compose UI, we can (should?) get rid of Fragments, Android-ViewModels, and configuration changes to make Android development easier. Here we will see how to apply those changes, so make sure you read part I beforehand, to understand why.
As this series focuses more on architecture and the Android framework we will not discuss how to build the UI, theming, or material components. The main focus will be on not using Fragments and manually handling configuration changes.

Before getting on topic, knowing memory leaks will help understand part of the article. When we have objects with a different life span and the longer-lived object (A) keeps a reference to the shorter-lived one (B), we create a memory leak. That is, B cannot be freed from memory. This can be particularly dangerous if B is in a state where it does not function properly, as A might try to use it leading to a crash or a bug.
In Android, this often occurs with Activities and Fragments, or their Context. For example, when a configuration change occurs the Activity and Fragment are destroyed and new instances are created, but the ViewModel is retained. If we, for example, save the Activity Context in the ViewModel, we have a memory leak. Unless we set up non-trivial code that removes the old instance, accumulates possible operations that rely on the Context, and sets the new one again once it is available.
Replacing Fragments
The replacement of Fragments with Composables is a big change that the new UI toolkit facilitates. Let us see how…
Basics of Fragments and Composables
Fragments are meant to host reusable UI in a modular way, and they need to be hosted by another Fragment or an Activity. Their most typical use is to hold full screens of Android applications; however, they are also used to represent large portions of the UI that do not necessarily match the whole screen.
Composable functions describe the transformation of data into a tree or hierarchy, that typically ends up displaying UI. Note that this already describes or substitutes data binding. They are often compared with suspend functions because they share some characteristics. Basically, the @Composable
annotation, in the same way as the suspend
keyword, alters the function type. This is done to carry a context that is provided by the compiler, and as a consequence, to call a modified function you need to be in a function with the same modifier. That is, you need to call Composables from other composable functions, and you need to call suspend functions from other suspend functions.
Another of the characteristics of composable functions will be useful later: recomposition. It simply means that the system can restart composable functions when it deems necessary. This allows us to have the UI always up to date, as any change to a variable that a composable function depends on triggers recomposition.
UI
As Composables are functions that can receive parameters and invoke other functions, they are perfect for composition: you only need to call another function! Therefore, a composable function can represent the UI of the whole screen, and call other Composables that represent smaller portions of the UI down to a widget as simple as a checkbox. They are the substitute for both Fragments and views!
It is important to note that views can currently be used in this way; however, it involves a lot more boilerplate and complexity.
For example, in the following code, we have a Composable that represents the whole screen -a Fragment in the old system. It uses the header function -which would be a custom view- and the text function -a system view. The result will show the header and a text below the header with some padding.
Non-UI responsibilities
Now, if we consider what Fragments do, we have to acknowledge that they do much more than display UI. First, we all we have put our logic in Fragments. This has been strongly discouraged and in the last years we see more and more developers adopting MVX architectures and moving non-UI logic out of Fragments. In the compose world the same principle applies, and the encouragement to move logic and state out of the view layer is probably even stronger. We can get view controllers -e.g. ViewModels- from composable functions in different ways, and we can pass data and lambdas as parameters of functions down to smaller Composables. This lets us control how much each view knows, making sure it is only what it needs.
Another responsibility Fragments have is to interact with the system APIs. All of this can be done within composable functions as they have access to resources, the context, and Android APIs in general. However, it is probably wise if we break the tradition of having several responsibilities in the view; after all, handling a Bluetooth connection does not have much to do with UI in the same way that doing a network request is not UI either. Instead, we can create an abstraction (like a UseCase) and pass it to the controller as a constructor parameter, like it is typically done with Repositories or UseCases.
One of the main benefits of using controllers in MVX architecture is being able to remove all non UI logic from views, and we can do that by having different classes that are invoked from the controller. Although I will not discuss this further because it is not the topic of this post, I believe that it is relevant if we consider we can handle configuration changes ourselves (we will see how soon). When handling configuration changes ourselves the view is not recreated; therefore, the view and its controller have the same life span. This means that we do not need to use an android-ViewModel (as controller) to retain the user state. And it also means that if the controller uses an object that depends on the Activity to we do not need to worry about memory leaks.
Navigation
Another important aspect to take into account is navigation. Traditionally we used the FragmentManager, and with Jetpack we have recently been able to use the navigation component. With compose we could just create our own backstack and simply call the composable functions that represent each screen ourselves. However, some libraries can help us handle that with less work. The two most relevant options are the compose router, which came out first, and the Jetpack navigation component built for compose. We can see how to use the navigation component in its documentation.
Handling configuration changes
Handling configuration changes does not involve a substitution of some utilities or classes for other ones; instead, it affects the behavior of our application and the lifecycle of certain components. In general, when a configuration change occurs, Activities and Fragments are recreated so that the proper resources are loaded. However, if we set android:configChanges="…"
in the Activity entry in our AndroidManifest, this will not be the case for the configuration changes we select.
How
If we select "orientation|screenSize|screenLayout|keyboardHidden"
and we are using compose, when the user rotates their device or when they resize the window the UI will adapt automatically to the new dimensions. As we have seen, if a variable that Composables observe changes, they recompose to always be up to date. And thanks to this, configuration changes like those affecting size are automatically applied.
Another configuration change we might want to handle is a language change. So, we can add locale|layoutDirection
(adding only locale
is not enough, at least in some devices). Now, when we change the language (or we force RTL) and go back to the app it will adapt automatically without recreating the Activity. Note, though, that this applies to resources resolved by compose. So, if you resolve a string from the Activity, Fragment, or a locale manager that you have in your view controller, that string will not be automatically translated. You need to update it manually.
Given the last point and depending on our situation, we might consider that handling all configuration changes is not worthwhile. So, we could let the Activity recreate for those that are less frequent, like a language change. There are many configuration changes, and more could be added in future Android versions, so we cannot be sure we handle all of them. Also, we may not know how to handle some of them. Therefore, it is probably better if we do not try to handle all configuration changes, and instead, assume that in some cases the Activity will indeed be recreated. This has consequences that will be discussed in the next section.
Using configuration parameters to build our UI
But first, an example of what we can do when handling configuration changes with compose. To get access to configuration values we can use AmbientConfiguration
and access the screen size, among other things. For example, let us suppose we have a view that has to be rendered different when it is in portrait, when it is in landscape, or when it is on a small screen (like multiwindow). Here we can see how we could achieve this with compose:
In this example, we have a view that represents what would typically be a custom view in Android, and there we apply logic based on the current configuration. However, the smaller Composables we call from there do not need to know about screen size, so they just provide a boolean parameter to offer two ways of being displayed. According to our needs, we can decide if we apply the logic higher in the hierarchy and pass down the result or we compute it on-site. So we can introduce this logic at a screen level, in a large view, or in a very granular component like a checkbox.
Unhandled config changes
As we have seen in the previous section, we should probably not assume we will handle all possible configuration changes. Therefore, we need to be ready for the Activity being recreated, which means there are two relevant issues to be aware of.
First, with Activity recreation we would lose user data, so we need to do something about it. We could use a ViewModel to retain it, but we are doing all these changes to avoid going this route.
Another way would be using the SavedStateHandle (formerly SavedInstanceState) to save the state -like user input- and recover it afterward. We would have to retrieve what we have stored in the database or server, which would not be immediate. But taking into account that these config changes are quite rare it should not be a problem if the user has to wait a bit more.
The main advantage of this approach is that you should already be doing this. If Android kills your process to save resources you already lose the state unless you save it this way. And process death is common, only using the camera and going back is enough to trigger it in many devices.
If for your requirements saving user data in process death is not necessary, probably it is not necessary either for these uncommon configuration changes. That being said, we should all care about saving state, users mind more than we may think.
Second, if we go this way we must avoid using the Android ViewModel, or if we use it we have to ensure we do not pass objects bound to the Activity lifecycle as parameters. If, for example, we had a ViewModel with a reference to the Activity Context, when an unhandled configuration change occurred the Activity would be recreated. However, it would be kept in memory, because the ViewModel would retain it. So, we would have a memory leak, and although this would be uncommon, it would also be hard to find.
Therefore, if we decide to handle configuration changes it is probably better to not use the Android ViewModel. Even if we do not pass a reference to the Activity now, we or someone else might, in the future.
So, no ViewModel?
From the discussion above, we can extract that we should decide between three ways forward, regarding the ViewModel.
1) Continue using the ViewModel, and keep interacting with the Android system from the View layer.
2) Keep using the ViewModel and set up a complex system to avoid memory leaks.
3) Stop using the ViewModel and benefit from Compose taking care of configuration changes.
So, for those interested in exploring the last option, the last change to discuss is how we can get rid of the Android ViewModel. Not using it is not so straightforward as you might expect, especially since the navigation component is tied to it. Because it is a bit complex, I will explain how I achieve it in part III.
Conclusion
So, to sum up, we have seen how we can replace Fragments and custom views easily, as well as data binding, by just using composable functions.
We have also briefly seen that we can handle configuration changes ourselves without much effort. To do this properly we should use the SavedStateHandle, but well, we should use it anyway.
Finally, we have discussed how it is convenient to avoid using the Android ViewModel, but we still do not know how to do that. So, in the next episode, we will see how to create and use our own ViewModel or ViewController with the navigation component. And I will also share a sample app that follows the practices discussed in this series.
Here you can find the next one, where we see how to avoid AAC-ViewModels and also see a sample app: Compose (UI) beyond the UI (Part III): no AAC-ViewModel and a sample app.