image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Jetpack Compose is a declarative GUI framework proposed to build activities on Android (available only using Kotlin).
Graphical interfaces are built using composition of functions that emit graphical nodes; functions are automatically called again to refresh the interface when some of their parameters change or when an underlying state is modified.
Contrary to the legacy view API that works with XML files describing the layout that are inflated and actions that are coded inside the activity, if we use the Compose API, all the code describing the GUI including rendering and actions are present in a single file, the activity.

With Compose, we do not use several activities or fragments inside activities. Reusable components are expressed as composable functions. A single activity is used for the application that calls composable functions according to the state that must be displayed.

Other GUI APIs similar to Jetpack Compose

More and more API to design graphical user interfaces rely nowadays on a declarative style to specify the displayed elements. Here are some representents of such kinds of frameworks :

JetBrains (editor of the IntelliJ IDE) proposes an extension to Jetpack Compose (in beta status) that supports developement on desktop and web platforms. It permits to develop an Android and desktop app with the same code base (minus some adaptations to configure the window of the desktop app). Concerning the web version, it does not use exactly the same components but rather components that are more close of HTML tags with the ability to use CSS to configure the rendering.

How to choose the most relevant framework for our needs?

  1. If for us the most important criterion is the multiplatform support, Flutter may be a good choice since it is a relatively mature framework that allow to use a single codebase to develop mobile, desktop and web apps (few special adaptations are needed to port the app for several platforms). The programming language Dart allow compiling to native code or JavaScript for web apps.
  2. If our primary goal is to develop web apps, React is more relevant; the application could be later ported to mobile or desktop platforms but keeping the JavaScript code; thus a JavaScript interpreter is used that involves performance penalty. However rather than using React native, we keep the possibility to use a framework packaging web applications to mobile packages like Cordova.
  3. If the main target for our application is Android devices, Jetpack compose is more adapted. The Jetpack Compose for desktop proposed by Jetbrains offer new outlooks for the app; note however that the support for desktop is experimental. Concerning the web version, it will require some adaptations on the codebase.
  4. Finally SwiftUI is a proprietary solution of Apple (the projet is not Open Source). For now we can only use it to develop MacOS and iOS applications without the ability to port them to Android, the web or non-Apple desktop platforms (Windows and Linux).

The requirements to use Jetpack Compose

An Hello World activity with Compose

If we want to display an Hello World! message, on can emit a Text element with a customized name (taken as parameter of the function) inside a Box layout that fills all the space, the Text is centered inside the box with a red border and a padding of 5dp:

@Composable
fun HelloWorld(name: String) {
    Box(modifier=Modifier.fillMaxSize()) {
        Text(
            "Hello world $name!",
            modifier = Modifier.align(Alignment.Center).border(width = 5.dp, color = Color.Red)
                .padding(10.dp),
            fontSize = 40.sp,
            textAlign = TextAlign.Center
        )
    }
}

We can include inside compose activities preview functions (without parameters): Android Studio allows to view directly the graphical rendering of the function (modifications are live updated if they are related to litterals, otherwise a refresh requiring a compilation is needed):

@Preview(showBackground = true)
@Composable
fun HelloWorldPreview() {
    CoursandTheme {
        HelloWorld("foobar")
    }
}

Note that the theme (here CoursandTheme) has been automatically created by the assitant of Android Studio dedicated to the creation of compose activities. It is implemented with a set of files in the ui.theme subpackage. It is possible to change the colors, shapes or typographies declared in this theme by editing the files.

One can use the previouly written composable function as the main component that will be displayed on our activity:

class ComposeHelloWorldActivity : ComponentActivity() {

    @ExperimentalFoundationApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CoursandTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    HelloWorld("foobar")
                }
            }
        }
    }
}

The steps and principles of graphical rendering with Compose

Rendering the GUI is made in three distinct steps with the Compose API:

  1. Composition
    • In this step the composer system call the main function and all the functions called directly or indirectly.
    • These functions emit graphical nodes that are collected by the composer.
    • This way the composer obtain a description of the graphical interface to render taking into account parameters and state.
    • Since the functions used for the composition are Kotlin functions, one can use all the abilities of the programming language to make computations, use conditional rendering, use loops and so on.
  2. Layout
    • Once the composer knows the elements to render it must position them on the screen.
    • In the layout phase, each graphical element is measured to obtain its size; this size depends on its children, so the measuring process is recursively lead
    • Each parent element place these children in its space using the measured sizes.
  3. Drawing
    • When the positions of each element is known they can be drawn on the screen.
    • Each element is reponsible to draw itself in its boundaries (like with the legacy View system using the method onDraw(Canvas c))

Instances of composables

var MYTEXT_INSTANCE_COUNTER = 0

@Composable
fun MyText(text: String) {
  val myTextId by remember { mutableStateOf(MY_TEXT_INSTANCE_COUNTER++) }
  Log.v("MyText", "Composition of MyText #${myTextId}")
  Text(text)
}

@Composable
fun ParentComposable() {
  var counter by remember { mutableStateOf(0) }
  TwoTexts("first", "$counter")
  Button(onClick={ counter++ }) { Text("Increment") }
}

@Composable
fun TwoTexts(first: String, second: String) {
  Log.v("MyText", "Composition of TwoTexts")
  MyText(first) // first call site
  MyText(third) // second call site, only this text will be recomposed since only third changes
}

@Composable
fun SeveralTexts(texts: List<String>) {
  for (t in texts) {
    MyText(t)
  }
}

The composer will identify each instance of composable using its call rank:

@Composable
fun SeveralTexts(texts: List<String>) {
  for (t in texts) {
    key(t) {
      MyText(t)
    }
  }
}

How to access to the Context ?

Typically composable functions are written directly in files at top level (not into an Activity class). To access to the Context (useful for numerous Android APIs), one must use LocalContext.current.

Example: a component displaying the level of the battery

@Composable
fun BatteryDisplayer() {
    val context = LocalContext.current
    var batteryLevel by remember { mutableStateOf(Float.NaN) }
    val broadcastReceiver = remember { object: BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1).toFloat() /
                    intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
    } }
    DisposableEffect(true) {
        val intent = context.registerReceiver(broadcastReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
        onDispose {
            Log.d("BatteryDisplayer", "Unregistered displayer")
            context.unregisterReceiver(broadcastReceiver)
        }
    }
    Text("Battery level: $batteryLevel")
}

The accompanist library

Google proposes Accompanist an additional library for Jetpack Compose for features that have not been already embedded in the official library.
Here are some of the supported features:

Compatibility with the legacy API

How to embed a Compose component inside a XML layout?

Declare a ComposeView inside the XML layout (with the relevant dimensions):

<androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

And don't forget to call in onCreate (for an activity) or `onCreateView (for a fragment) the method setContent`` to supply the composable:

fun onCreate(b: Bundle?) {
	...
	val cv = findViewById<ComposeView>(R.id.compose_view)
	cv.setContent {
		MaterialTheme {
			MyBeautifulComposeFunction()
		}
	}
	// Dispose the Composition when the view's LifecycleOwner
  // is destroyed
  cv.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
}

How to use a legacy View inside a Compose component?

A component inheriting from the class View can be embedded inside a Compose component. One must supply:

Example to use a TextView (not really useful, it will be easier to use the composable Text):

@Composable
private fun LegacyTextView(text: String) {
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = { // update will be called again if text changes
            it.text = text
        }
    )
}