Android Jetpack Compose Review
During the last Google I/O the Android team officially announced a brand new Jetpack library named Compose. Trust me when I say this library could change the way developers write Android applications in the future. So let's review it and write down some thoughts about it.
Disclaimer: 22 of May 2019, After publishing this blog post the Jetpack Compose team contacted to shed light on some parts of the post. We recommend you to review them carefully because they point some parts of the implementation they couldn't cover during the Google I/O talk or the official documentation.
Jetpack Compose aims to be a declarative framework to build Android user interfaces easily using a Kotlin like this:
@Composable
fun RallyApp() {
RallyTheme {
Scaffold(appBar = { RallyAppBar() }) {
RallyBody()
}
}
}
@Composable
fun RallyAppBar() {
Row {
Text(text = title, style = +themeTextStyle { h4 })
}
}
@Composable
fun RallyBody() {
Padding(padding = 16.dp) {
Column {
RallyAlertCard()
HeightSpacer(height = 10.dp)
RallyAccountsCard()
HeightSpacer(height = 10.dp)
RallyBillsCard()
}
}
}
The idea is cool, isn't it? If you are a web developer, you might be familiar with this idea because, years ago, React by Facebook already did this. Based on the concept of web-components, Facebook developers created a framework to be able to build web-based applications in a similar way. However, there are some small details we would like to review 😃
After watching the talk at Google I/O, reading the documentation, and playing with the official repository where this new library is being developed, we've got some thoughts we'd like to share with you.
API Design
Once you watch the talk, you notice the Jetpack Compose team has been thinking a lot about the current Android API. They remark how the usage of extensions for some components, and bad decisions they made in the past, doesn't let the Android API properly evolve. From talking about code reusability to how the usage of the classic OOP design they followed is not generating a maintainable API over time. The team reviews how the current SDK encourage Android devs to keep the state of the app in the view implementation instead of having a single source of truth. After that quick review, they proposed a solution. Inspired by React/Redux, they suggest Jetpack Compose as the solution where developers will no longer create classes extending from the framework but composing their UI using the framework components. And this is awesome!!! However, once you look closer, you start finding some friction points we hope the Google team will solve before the first public release.
The first point we'd like to review is the usage of functions for the components' declaration. Here you have an example:
@Composable
fun RallyBillsCard(): Unit {
Card(color = cardInternalColor) {
Column {
Padding(padding = 12.dp) {
Column {
Text(text = "Bills", style = +themeTextStyle { subtitle2 })
Text(text = "$1,810.00", style = +themeTextStyle { h1 })
}
}
}
}
}
As you can see, a component can be declared using a function. This might look like an excellent idea, however, we should think about the state of the view. Even when during the talk they mention the usage of a single source of truth and the usage of the lexical scope for the component function, having a local state for our views should be interesting. If instead of using a function for the class declaration we'd use a class we could handle local state in our components easily. With the current implementation, we'd have to wrap all these functions inside a class and keep their state linked to a host component. This is to be able to keep the state in the expected scope outside the setContent
method you will find in activities and fragments. Otherwise, the state will be reset once the UI is rendered again, and this forces us to move all the UI state into the app state.
Another interesting point related to the usage of functions is the lack of components lifecycle. If we don't have a separated lifecycle, how are we going to know if this is the first time we are rendering this component, or it was rendered before? Again, the answer could be to move the state of the view to the app state.
The other small detail can be found in the signature function:
@Composable
fun RallyBillsCard(): Unit {
...
}
Returning Unit will limit the API design from the testing viewpoint. Implementing the API as a huge side effect instead of deferring the computation until the view has to be rendered will not let us test this code using a unit/integration testing approach but the classic UI testing strategy. If you don't believe me, do what I did. Go to the repository and try to test the code. You can find some examples of tests already written by the team inside the androidTests
folder. You'll see how the usage of an activity just for testing purposes let them write tests asking for the size of the view but not for the information the view is rendering or the style being applied in a friendly way.
If we could just get an instance of the components tree we could make assertions using libraries like Kotlin Snapshot, in the same way, React and Vue.js developers have been doing for the last years. We could write regular unit tests or snapshot tests like these:
fun testRendersTheTitleAsPartOfTheRallyAppBarComponent() {
val title = "Any title"
val component = RallyApp(title)
assertEquals(title, component.title.text)
}
fun testRendersTheRallyAppBarComponent() {
val component = RallyApp(title )
component.matchWithSnapshot()
}
@Composable
fun RallyAppBar(title: String) {
Row {
Text(text = title, style = +themeTextStyle { h4 })
}
}
Even when this testing approach is not a silver bullet and we might need to reduce the testing scope to get readable snapshots. Returning the components could simplify the way Android devs test their applications. Even if you don't think snapshot testing could be a good testing strategy, a good design should let the developer choose the scope of the test or the testing strategy we could use for our automated test suite. However, if we keep returning Unit this will not be possible. If you don't know what snapshot testing is, take a look at this link.
On the other hand, the usage of return values for our @Composable
functions would let us create a truly declarative view tree we could optimize and evaluate in run-time or even in build-time when needed. As you can see, there are some benefits we should consider before the first public release of this library.
Additional notes to the component lifecycle:
After publishing this blog post the Jetpack Compose team contacted me to review this part of the content. They sent me a few notes about some parts of the post:
"We also use Effects for dealing with lifecycle related things. We have a few primitives that you can build a lot on top of to handle this. For example the `onCommit` and the `onActive` effects. `onActive` runs the first time the component composes, and never after that. In the callback you can also define an `onDispose` callback which will get called as the component is leaving the hierarchy. Similarly, there’s an `onDispose` effect that will just do the latter. `onCommit` is a little more multi-purpose. With no input parameters, It will run every time the component composes. You can also add input parameters, and it will run any time any of them change. This can be useful for a couple of different scenarios."
@Composable
fun UserProfile(userId: Int) {
val user = +state<User?>(userId) { null }
+onCommit(userId) {
val cancellationToken = UserAPI.get(userId) {
user.value = it
}
onDispose {
UserAPI.cancel(cancellationToken)
}
}
if (user == null) {
Loading()
return
}
Text(text=user.name)
Image(src=user.photo)
}
"Here we used `userId` as a parameter of `onCommit` so that this code will execute only when the parameter passed into `UserProfile` changes, so we don’t call the API too many times, for instance."
"You can do some interesting things with this, including building your own more complex reusable effects (again, the concept of “Effect” I believe will eventually get unified with that of a `@Composable` function, so the syntax and keywords and everything will just be the same, but right now they are different)."
"I actually am a fan of snapshot testing, but just don’t think that snapshot testing virtual DOM is the right thing to be doing. I view virtual DOM as an implementation detail and think there can be a lot of problems relying on assertions of shallow renderings of components. I since have come to the conclusion that shallow rendering is probably a mistake. Similarly, I think that anyone relying on the return values of render functions (or composable functions in this case) in tests is probably a bit of a code smell."
No silver bullets. You still need activities and fragments
During the Google I/O talk, they introduced Compose as a new set of Jetpack UI widget without Activities or Fragments. Just composable components. However, your code will need a component like an Activity, View or Fragment to be able to make it work. This will help you a lot if you want to integrate the usage of the library in just part of your app, but this means any developer using Jetpack Compose will have to learn the old school Android/Fragment/View API. At least, until they develop other components wrapping the navigation or some UI patterns like the usage of tabs into Composable components. This is how an activity using a component would look like:
class RallyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppComponent()
}
}
This part will let you use this library in a friendly and compatible way from the beginning and at the same time will work as a workaround for all the design issues you can find in the library.
Where is my state?
During the talk, the team mentioned a lot the usage of a single source of truth and the suggested the usage of a unidirectional data flow. However, this is not as easy as it looks like. To be able to use something like Redux, we will need to be able to create a way to manipulate the state in a friendly way and keep it in memory, being able to mutate it asynchronously when needed without concurrency issues. However, there are no tools like this right now. And this will not be an easy task to implement. The talk describes the usage of LiveData observable fields and data class marked with @Model
annotations for the UI updates and the usage of mutable variables as part of the data declaration. Something like this:
@Model
//Look at the VAR username and VAR passwords
class LoginState(var username: String, var password: String) {
val valid: Boolean get() = username.length > 0 && password.length > 0
fun login() = Api.login(username, password)
}
Even if we are able to create observable classes using mutable state you still need to keep this class linked to an Activity lifecycle or a Fragment lifecycle because there is no such thing as a unique store you can handle easily for now. So I don't fully understand how they want to get rid of the state being part of activities and fragments when the proposed implementation doesn't handle the state lifecycle as Redux already does. I hope they don't suggest to keep the state as a mutable singleton in the future while using classes with mutable variables annotated with @Model
.
Looking at the current state of the library, the only thing I can think about is all the features they still need to develop before considering the first public release. If you want to compare it with the current React/Redux state of the art you can review how React components lifecycle looks like and how these components have to be linked to Redux in these infographics Sergio Gutiérrez designed in a previous blog post:
The lifecycle of a React component:
Integration between any React component and Redux:
If you compare it with the current state of the library, there are a lot of pull requests they still have to send before the first release. Maybe, in the future, we can see how they connect Jetpack Compose with libraries like kotlin-redux to be able to handle unidirectional data flows and Arrow for the lenses usage, recursion schemes, side effects, and simple data-types.
Additional notes to the state management:
After publishing this blog post the Jetpack Compose team contacted me to review this part of the content. They sent me a few notes about some parts of the post:
"This isn’t where we’d like people to go, but I can see how this could be easily misinterpreted. Will work on messaging here. We may often hear of “top-down data flow” or “unidirectional data flow” to describe this pattern, and the latter is probably the more accurate term. Composable functions can have local state. Data for a composable function (or “props” to use a React term) is provided to the composable via function parameters, but local state scoped to the composable can be introduced by leveraging Compose’s memoization capabilities. Right now in the repo we have a concept called “Effects” (which may be unifying with Composable functions in the future). You can use these to introduce local state now, either by creating an @Model
state class and introducing it with memo
, or by using any type and introducing it using state
. For example:"
@Composable fun Counter() {
// introduce a state value (of type `Int`, with initial value of `0`.
// Note: the `+` syntax is temporary
val count = +state { 0 }
// use it to compose your UI. pass it into other composables as parameters
Text(text="Count: ${count.value}")
// modify the value inside of event handlers, for instance
Button(text="Increment, onClick = { count.value += 1; })
}
"Alternatively, use the @Model
and memo
like I mentioned:"
@Model class CounterState(var count: Int)
@Composable fun Counter() {
// introduce a CounterState value. Instance will be preserved across compositions.
// Note: the `+` syntax is temporary
val count = +memo { Counter(0) }
// use it to compose your UI. pass it into other composables as parameters
Text(text="Count: ${count.count}")
// modify the value inside of event handlers, for instance
Button(text="Increment, onClick = { count.count += 1; })
}
What about the performance?
Even when they talk about the implementation of a list of components, and they take into account the performance during the talk. They don't go deep into the library implementation details. They describe the possible implementation of a ScrollingList but if you review the repository there is no such component implemented for now. They describe a promising component like this:
@Composable
fun NewsFeed(stories: LiveData<List<StoryData>>) {
ScrollingList(stories.observe()) { story ->
StoryWidget(story)
}
}
However, this is not part of the repository right now and we will have to wait until a new release in order to check if the suggested implementation looks like this or not.
Also, related to the usage of Unit as return value has another important implication here. As the team is not designing the components API as a set of data we can manipulate easily, there is no way we can compare an already rendered view with a new version of this view with just a bunch of modified values in order to implement an efficient UI rendering from the outside. And this could be really important depending on the final implementation! If you review the React ecosystem, you'll see how the usage of types for the Virtual DOM design let Facebook engineers implement a smooth UI thanks to the partial update of the device DOM using an efficient and fast diff algorithm. You can read about the Virtual DOM implementation here if you want.
I guess there is an inner mechanism inside the rendering code making all these components efficient when talking about drawing. However, I can't talk about it because it wasn't part of the talk and the code I've reviewed seems to use canvas API for the component's rendering.
What if I don't want to use Jetpack Compose?
Don't you worry if you want to code like this, but you don't have time to wait for the final release. You can always create custom views you can compose in your old school layouts and link them with the Jetpack Data Binding library. If you implement your custom view using a single rendering binding method and link the view with a single source of truth implemented using any Redux library you'll get all the benefits. Don't forget the layout and your custom views is something you can already compose easily. Keeping the state of the app as part of your model and implementing your custom views as a class with a render function will give you a similar result. Even if you don't want to use custom views, you can already do this with your view holders if you don't follow the Google official guidelines and implement your RecyclerView's view holder without exposing the internal views and with a method to render the data passed as a parameter. This solution is not based on a nice Kotlin DSL, but might be worthy for you.
Conclusions
The Android team is doing it great. Believe me when I say that this Compose API is way better than the current state of the art of Android development. If you don't think so try to write a React/ReactNative/Vue application and you'll better understand what using components like these will be in the future.
Keep in mind that even when this library will help us a lot in the future, the rest of the SDK will have to evolve similarly if we want to improve the way we write our apps. Apart from the design changes the Android Team will implement in the future in the Compose API, there are a lot of points to enhance in the rest of the SDK: stop returning the execution of the libraries calls as part of the "onActivityResult" methods, let the users get an instance of an activity without initialising the activity lifecycle (really useful for testing), create a friendly API for the animated transitions, etc.
Last but not least, we think releasing a sneak peek like this of the source code, and the examples, is one of the best things Google did. This talk, documentation, and source code let them gather tons of feedback they use for future improvements. There are a lot of things to develop and I hope we will be able to use Compose in all our apps in the future.
References
Here you can find some useful resources about this topic: