Writing Android UI Code in Jetpack Compose
Note: This article was published back in May, 2019, just after Jetpack Compose got announced in Google IO. A lot changed since then, including all the APIs discussed here. And you no longer need to fork AOSP, just to use Compose.
During Google IO 2019, The Android team announced Jetpack Compose, a Kotlin based, unbundled UI toolkit for writing declarative/reactive/functional UI code.
Now, what do we mean by saying declarative/reactive/functional UI code? Instead of writing tough to read, bloated XML files for UI and invoking them with id/tag in Java/Kotlin code, we will be writing self explanatory UI code in Java/Kotlin files itself in Reactive/Functional Programming Style, along with the behavior.
Jetpack Compose isn't yet ready for production, not even in Alpha / Beta, it's in development and if you want to check it out you need to fork the AOSP project, and need to use a special Android Studio.
So, why do we care to check it now? Why not wait for it being atleast in Alpha? The answer is Jetpack Compose will disrupt the way we presently structure code and build architecture, we will need to think in different ways since we will no longer be able to get view instance and will not be able to update UI based on Async operations in current ways, we have to follow different approaches. Gues what, we got sufficient time in our hand to figure out how to structure our code and architecture around Jetpack Compose, before it goes production ready, but in order to do so, we first need to understand how Jetpack Compose works, how to apply themes / styles, how to maintain state, and how to update view states based on async operations / network response, which we will be discussing in this series, so that once we get strong grips on Jetpack Compose, we can start thinking on architectures and code patterns.
For detailed instructions on how to fork AOSP and setup Android Studio, please visit: https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/ui/README.md
So, you are interested in Jetpack Compose and already set up everything, what more are we waiting for? Let's get started.
The @Composable functions
The API is literally named after it, Composable functions are building blocks / smallest units of building UI in Jetpack Compose.
Yes, you read it right, say goodbye to creating numbers of Classes and XML Layout files and attribute-sets.
You just need to write simple Kotlin functions, returning Unit
, to design your UI and annotate them with @Composable
, withing this function, you can use pre-existing @Composable
functions like DSL, these pre-existing composable functions can be again created by you or can be shipped with Jetpack Compose API, or any other plugin/library for that matter.
For instance, take the below example.
@Composable
fun Buttons(onClick: ()->Unit) {
Button(onClick = onClick, text = "Button 1")
Button(onClick = onClick, text = "Button 2")
}
This is a Composable function, which in turn calls another Composable function - Button
(from package androidx.ui.material
) twice to display two buttons in the screen.
I can now write another composable function as follows and call the Buttons
function.
@Composable
fun ButtonsDemo(onClick: () -> Unit) {
Text(text = "Text 1")
Buttons(onClick)
}
This Composable function ButtonsDemo()
will display one text and two button components in the screen when called.
As you've already noticed, the Composable functions can have parameters, based on your requirements, but are not expected to return any value.
Set Content to Activity
So, we've created composables based on our UI requirements, now it's time to display them inside the Activity.
While using XML, we call setContentView(R.layout.layout_name)
or setContentView(viewInstance)
to display a view instance / layout resource to the screen, with Jetpack Compose, we need to call setContent
and pass a @Composable
lambda to it, from where we can call the @Composable
functions, like the below example.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text= "Hello World")
}
}
Below is the definition of setContent
extension function.
fun Activity.setContent(composable: @Composable() () -> Unit) =
setContentView(FrameLayout(this).apply { compose(composable) })
It basically creates a FrameLayout
, composes our @Composable
lambda and thus all @Composable
functions within it and draws it inside the FrameLayout
for display.
So, our Hello World application is done. Now, let us wrap the text in our custom Composable, as follows.
class SimpleActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CustomText(text = "Hello World")
}
}
}
@Composable fun CustomText(text: String) {
Text(text = text, style = +themeTextStyle { h4 })
}
Should work like a charm, right? Wrong, as soon as you install the app and start the Activity, you'll get this crash: https://stackoverflow.com/q/56050892/4284706, workaround? You just need to put your composable inside a CraneWrapper
and a Theme, just as follows.
class SimpleActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CraneWrapper {
MaterialTheme {
CustomText(text = "Hello World")
}
}
}
}
}
@Composable fun CustomText(text: String) {
Text(text = text, style = +themeTextStyle { h4 })
}
Run it now, and it'll work as expected.
Now, why does that happen? Why do we need the CraneWrapper
? As said by Ragunath Jawahar, Romain Guy and Leland Richardson in compose channel of Kotlinlang Slack, the CraneWrapper
is a Composable, that hosts the compose tree with help of AndroidCraneview
grants access to the Context
, Density
, FocusManager
and TextInputService
implicitly to all of its children, without CraneWrapper
the composables doesn't gets access to these resources thus throws exception causing your app to crash.
Note: I believe (also hinted by the Google team working on Jetpack Compose), in future we will probably not need to use CraneWrapper
, as they will fix this soon enough, by probably adding CraneWrapper
inside setContent {}
or otherwise.
Style it your way
So, we created Hello World app and created+used our first custom Composable, now let's take one more step forward, how to define your own styles / text styles / theme? Let's figure it out together.
Generally, with our present structure, we define Themes and Styles in a XML file and keep it inside styles
folder, and to use it we refer it from the manifest file (to set default theme for an Activity), and from Views in XML, to set a style to that particular view, sometimes we also call them from Java/Kotlin file and pass it to an instance of a view.
Defining and using styles/themes are easier with Jetpack Compose, more over a brilliant use of Kotlin receiver types ensure that the IDE suggest you available styles / textstyles when using them. In the last example, I used MaterialTheme
(which comes along with the ui-material
module of Compose) as the default theme for the activity, I also used h4
from MaterialTheme
and applied it to Text
inside CustomText
, as CustomText
was used inside MaterialTheme
, all the styles inside MaterialTheme
became available to it, thanks to function composing, and then we just used the h4
text style from it.
Enough of contexts, now let's get hands dirty and define our new Custom Theme.
val primary = Color(0xFF021AEE.toInt())
val primaryText = Color(0xFFEE0290.toInt())
val secondary = Color(0xFFD602EE.toInt())
val white = Color(0xFF26282F.toInt())
val black = Color(0xFFFFFFFF.toInt())
@Composable
fun CustomTheme(@Children children: @Composable() () -> Unit) {
val colors = MaterialColors(
primary = primary,
secondary = secondary,
surface = white,
onSurface = black
)
val textStyles = MaterialTypography(
h1 = TextStyle(fontFamily = FontFamily("RobotoCondensed"),
fontWeight = FontWeight.w100,
color = primaryText,
fontSize = 96f),
h2 = TextStyle(fontFamily = FontFamily("RobotoCondensed"),
fontWeight = FontWeight.w100,
color = primaryText,
fontSize = 60f),
h3 = TextStyle(fontFamily = FontFamily("Eczar"),
fontWeight = FontWeight.w500,
color = primaryText,
fontSize = 48f),
h4 = TextStyle(fontFamily = FontFamily("RobotoCondensed"),
fontWeight = FontWeight.w700,
color = primaryText,
fontSize = 34f),
h5 = TextStyle(fontFamily = FontFamily("RobotoCondensed"),
fontWeight = FontWeight.w700,
color = primaryText,
fontSize = 24f),
h6 = TextStyle(fontFamily = FontFamily("RobotoCondensed"),
fontWeight = FontWeight.w700,
color = primaryText,
fontSize = 20f)
)
MaterialTheme(colors=colors, typography = textStyles) {
val value = TextStyle(color = black) // set default textstyle when not specified
CurrentTextStyleProvider(value = value) {
children()
}
}
}
That's it, we created our own custom Theme, now in order to use it in the last example, we just need to make the below changes.
class SimpleActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CraneWrapper {
CustomTheme {
CustomText(text = "Hello World")
}
}
}
}
}
@Composable fun CustomText(text: String) {
Text(text = text, style = +themeTextStyle { h4 })
}
That's right, we just need to call CustomTheme
from the activity, instead of MaterialTheme
and everything else will be taken care of.
Update: No XML Syntax
As Suggested by Romain Guy in the comments section, XML Syntax is going away, and we shouldn't use that with Jetpack Compose, so I updated all the code here with DSL syntax and asking all of you to not use the XML syntax, rather use it the way it is, a DSL.
While invoking @Composable
functions, if you're getting compile time error saying: "Stateless function shouldn't be invoked" then, you need to add the below lines in module level build.gradle
inside android
block. This is a temporary workaroud, until the team ships a proper fix.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { compile ->
compile.kotlinOptions.freeCompilerArgs +="-P"
compile.kotlinOptions.freeCompilerArgs += "plugin:androidx.compose.plugins.kotlin:syntax=FCS"
}