Why Jetpack Compose Is Not Ready For You

Written by tomerpacific | Published 2022/08/08
Tech Story Tags: debugging | jetpack-compose | android | kotlin | ui | jetpack | android's-jetpack-compose | coding

TLDRAndroid’s Jetpack Compose version 1.0 was [released ](https://://://android-developers.googleblog.com/2021/07/jetpack-compose-announcement) on July 28th. It promised a new way to build UI in our Android applications and it was met with much enthusiasm. Jetpack has been regularly updated and maintained since then and the latest release happened at the end of June (the 29th) The major appeal is the simplicity of having to write code in Kotlin that defines your UI.via the TL;DR App

Android’s Jetpack Compose version 1.0 was released close to a year ago on July 28th. It promised a new way to build UI in our Android applications and it was met with much enthusiasm. It has been regularly updated and maintained since then and the latest release happened at the end of June (the 29th). While only a year has passed since its release, a year is a good enough time frame to inspect things from a better perspective.

I’m sure many of you have contemplated whether it might be a good time to jump on the Jetpack Compose bandwagon. There is always an undeniable desire to try out and embrace new technology and see what we can do with it. But in the back of our minds, we all have a little voice that tells us that it might be too soon. Sort of like wanting to take a bite out of a pizza that just came out of the oven.

Sometimes, we have to wait a bit till we can take that first bite. 🍕


Engines Hot

Jetpack Compose is Android’s newest toolkit for building UI natively. If you have developed applications in the past, you might be familiar with these two options when creating a layout :

  • Creating a XML file
  • Building the UI inside of your class by code

These ways of building UI have been with Android developers for a long time, so they are tried and true. By now, we have gotten used to them, but that doesn’t necessarily mean they are without flaws.

Developers are aware of the hassle we face when creating layouts via XML. Making sure everything is aligned properly, the need to add ids and various other attributes, the fact that you need to connect the UI in the XML with logic in your activity/fragment, the design view vs the text editor.

It can get cumbersome.

It can get messy.

It can get repetitive.

It can get frustrating.

On the other hand, it was always good to see how your UI stacks up without needing to build and run your application. The attributes themselves were pretty self-explanatory and the variety of UI elements was large.

So why even consider moving to Jetpack Compose?

The major appeal of Jetpack Compose is the simplicity of having to write code in Kotlin that defines your UI. No more fussing about choosing a relative layout over the linear layout. No more defining constraints in constraint layout. Just a simple system to lay out your design and make it come to life.

After spending considerable time with Jetpack Compose and converting one of my applications to have layouts that are based solely on it, I have returned from the battle with some scars, and want to share the details.

This isn’t a piece that is going to try and dismiss Jetpack Compose. Its intent is to show the state of Jetpack Compose as of writing this article. Because it is better to go into something with eyes wide open.


Where Is My Jetpack?

Jetpack Compose offers a pretty straightforward lineup of components to use to build your layout. But even if the offering seems simplistic and easy to navigate, there are some noticeable hiccups.

We are all familiar with GridLayout and it makes sense that Jetpack Compose will also have some implementation of it. And it does. It’s named LazyVerticalGrid(or LazyHorizontalGrid). But if you have an application that has a grid layout and want to switch to Jetpack Compose, you need to be aware that currently, this component is still experimental.

The DSL implementation of a lazy grid layout. It composes only visible rows of the grid. This API is not stable, please consider using stable components like LazyColumn and Row to achieve the same result.
Params:
cells - a class describing how cells form columns, see GridCells doc for more information
modifier - the modifier to apply to this layout
state - the state object to be used to control or observe the list's state
contentPadding - specify a padding around the whole content
verticalArrangement - The vertical arrangement of the layout's children
horizontalArrangement - The horizontal arrangement of the layout's children
content - the LazyListScope which describes the content
@ExperimentalFoundationApi
@Composable
fun LazyVerticalGrid(
    cells: GridCells,
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    content: LazyGridScope.() -> Unit

As you can see above, the class is annotated with @ExpirimentalFoundationApi that reads:

This foundation API is experimental and is likely to change or be removed in the future.

and the comments above the class declaration state that:

The DSL implementation of a lazy grid layout. It composes only visible rows of the grid. This API is not stable, please consider using stable components like LazyColumn and Row to achieve the same result.

Meaning, that you are better off creating a grid layout using columns and rows yourself and not this class. And when you are using an API that is experimental, you might get weird bugs or crashes that you have no idea how to fix and might not have a solution (link to a related issue):

java.lang.IllegalStateException: Check failed.
        at androidx.compose.foundation.lazy.LazyGridKt$ItemRow$2.measure-3p2s80s(LazyGrid.kt:451)
        at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:55)
        at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:99)
        at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:98)
        at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:126)
        at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:88)
        at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:76)
        at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:98)
        at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:75)
        at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1308)
        at androidx.compose.foundation.lazy.layout.LazyLayoutPlaceablesProvider.getAndMeasure-0kLqBqw(LazyMeasurePolicy.kt:61)
        at androidx.compose.foundation.lazy.list.LazyMeasuredItemProvider.getAndMeasure-oA9-DU0(LazyMeasuredItemProvider.kt:45)
        at androidx.compose.foundation.lazy.list.LazyListMeasureKt.measureLazyList-wroFCeY(LazyListMeasure.kt:145)
        at androidx.compose.foundation.lazy.list.LazyListKt$rememberLazyListMeasurePolicy$1$1.measure-3p2s80s(LazyList.kt:259)
        at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2.invoke-0kLqBqw(LazyLayout.kt:55)
        at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2.invoke(LazyLayout.kt:44)
        at androidx.compose.ui.layout.SubcomposeLayoutState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:355)
        at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:55)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:306)
        at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
        at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:131)
        at androidx.compose.foundation.layout.FillModifier.measure-3p2s80s(Size.kt:658)
        at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
        at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:99)
        at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:98)
        at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:126)
        at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:88)
        at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:76)
        at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:98)
        at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:75)
        at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1308)
        at androidx.compose.ui.layout.RootMeasurePolicy.measure-3p2s80s(RootMeasurePolicy.kt:38)

The @ExperimentalFoundationApi can be found on other quite common use cases:

Apart from Grid, there are some UI elements that are not yet available in Jetpack Compose. To bypass this problem, the proposed solution is to use the AndroidView composable. Think of it as a wrapper for the View object. You might be thinking that this solution fits for edge cases dealing with sophisticated views, but in reality, Google acknowledges Jetpack Composes’ shortcomings, saying:

AndroidView is commonly needed for using Views that are infeasible to be reimplemented in Compose and there is no corresponding Compose API. Common examples for the moment are WebView, SurfaceView, AdView, etc.

So it is important to be aware of this component and your (very probable) need to use it.


Preflight Check List

Jetpack Compose has a a lot to offer. No doubt about it. But once you start working with it, you realize that even though as a whole, it might appear as a complete package, there are things missing. And those things don’t necessarily have to be something special. For example, tooltips.

As of the latest version of Jetpack Compose, there is no built in support for tooltips. Yes, there is a workaround someone has drafted that you can apply. But this isn’t an official part of Jetpack Compose, so you will always have to make sure this solution works. And in my eyes, I figured that if Jetpack Compose supports Grids, it will surely have support for Tooltips. You might be thinking that I am exaggerating this, but as it stands, there is an issue open that is requesting Tooltips to be added, so there is a need from the community for this.


Testing… 3 2 1

Currently, Jetpack Compose allows you to do instrumentation tests only, which means that in order to run them, you will need a device or an emulator. Also, as part of creating a test, you need to create a test rule. This allows you to test and control composables and applications in your tests. There are two types of test rules:

The difference between the two is that the latter is used to access system related components and logic (I.E. activity, resources).

While the supplied APIs are self-explanatory, you may find yourself puzzled when you have to do something that seems very simple.

For example, if you want to perform a test that clicks on an image, there is an option to do this with onNodeWithContentDescription. But it’s not as immediately understandable as when you are trying to locate a button with onNodeWithText. It also assumes that you add a content description to your Image element.

Sometimes, you will find yourself in cases where none of the available API methods meet your demands and you might end up adding test tags to your elements using testTag. Then you can use onNodeWithTag so you can target your UI correctly. This might make your code a bit less unattractive as there will be .tesTag lines spread around in your layout.

In a hybrid application (one that mixes views and Compose composables) you will need to use Espresso in conjunction with the testing module from Jetpack Compose. This is because, while Espresso can target views by searching via text (for example), it does not have access to Composables’ semantics. While this isn’t a bad thing per se, it is just another dependency and framework you will need to manage and handle.


Grounded

This article’s aim is not to dissuade you from using Jetpack Compose or paint it as something you should altogether avoid. In its own right, Jetpack Compose has plenty new to offer and is only in its infancy.

Nothing is perfect right out of the gate and with time and continued efforts from Google, I am sure Jetpack Compose will be the way we all will be building our UI.

You can even look back at the year since its release and see where the progression has led. It’s just always a good idea to know the capabilities of the technology you are going to use. That way, you can assess if it is the right tool for you to use.

As mentioned at the beginning of this article, I recently made the jump on the Jetpack Compose bandwagon and converted one of my applications to use it.

You can see the source code here:

https://github.com/TomerPacific/LaundrySymbols


Published by HackerNoon on 2022/08/08