Jetpack Compose: The Android Developer Roadmap – Part 5

Jetpack Compose represents the future of the UI toolkit in Android development. In this post, we will explore the key components of Jetpack Compose, such as Compose UI, State, Side-effects, and CompositionLocal, along with strategies for migrating to Compose.

Jaewoong E.
Jaewoong E.
Published January 16, 2024

We've segmented our Android Developer Roadmap into a multi-part series, delving into crucial Android fundamentals and the current ecosystem.

In our previous post, we delved into crucial aspects of contemporary Android development, covering design patterns, architecture, asynchronous operations, networking, and local databases on Android.

In part five, we'll explore the fundamental concepts of Jetpack Compose, including Compose UI, State management, handling Side-Effects, CompositionLocal, and strategies for migrating from XML to Compose UI.

If you'd like to be notified as we release future posts, follow the author on GitHub and follow Stream on Twitter.

Let’s get started!

Jetpack Compose

Following the announcement of Jetpack Compose stable 1.0 by the Android team, the adoption of Jetpack Compose in production has rapidly gained momentum. According to Google, there are now over 125,000 apps built with Jetpack Compose that have been successfully published on the Google Play store.

When discussing Jetpack Compose, our focus often gravitates toward Compose UI. However, it's essential to note that Jetpack Compose technically comprises three primary components:

  • Compose Compiler: The Compose Compiler stands as a pivotal component within Jetpack Compose, written in Kotlin with targeting on Kotlin Multiplatform. Diverging from conventional annotation processing tools like KAPT and KSP, the Compose compiler plugin directly engages with FIR (Frontend Intermediate Representation). This distinctive approach empowers the plugin to analyze static code during compile time with more information, dynamically modify Kotlin source code, and ultimately produce Java bytecode. Essentially, annotations from Compose libraries, such as @Composable, collaborate intricately with a multitude of tasks performed by the Compose Compiler under the hood.

  • Compose Runtime: The Compose Runtime serves as the cornerstone of Compose's model and state management. This library operates by memoizing the state of compositions using a slot table, a concept derived from the gap buffer data structure. Under the hood, this runtime undertakes various crucial tasks. It handles essential features we commonly use in project development, including managing side-effects, preserving values with remember, triggering recomposition, storing CompositionLocal, and constructing Compose Layout nodes.

  • Compose UI: Compose UI, a vital component of Jetpack Compose, encompasses a suite of UI libraries empowering developers to craft layouts by emitting UI through Composable functions. Compose UI libraries offer a variety of components facilitating the construction of Compose layout trees. These layout trees, once created, are consumed by the Compose Runtime. Leveraging the capabilities of Kotlin Multiplatform, JetBrains released a stable of Compose Multiplatform, and now you can build the same layout with the same Compose UI libraries for each different platform, such as Android, iOS, desktop, and web assembly.

In this article, we’ll focus on Compose UI, State, Side-effects, CompositionLocal, and migration strategies to Jetpack Compose. Instead, we won't dive into the intricacies of the Compose compiler and runtime as they involve a substantial amount of internal workflows. If you're keen on exploring the internal workings of Compose, consider checking out the references below:

Compose UI

As we discussed in the previous section, Compose UI is an integral component of Jetpack Compose, a comprehensive suite of components designed to simplify the creation of Compose layout trees, with a focus on Compose Multiplatform compatibility.

You can create UI layouts in a declarative manner, which means you can write layouts in Kotlin logically, enabling you to maintain your UI layouts without the need for imperative manipulation of frontend views. This approach facilitates the development of highly reusable and intuitive UI layouts.

In this section, you will delve into essential concepts for utilizing Compose UI libraries, including theming, Modifiers, creating layouts, implementing lists, and animations.

Theming

Jetpack Compose makes it easy to implement a design system, ensuring your app maintains a consistent appearance by offering predefined themes in a source-of-truth manner.

Compose UI provides two primary theming libraries: the implementation of Material Design 2 and Material Design 3. These theming libraries offer a variety of essential design components, including buttons, cards, switches, bottom sheets, and more, all of which reflect the respective theme styles internally.

You can maintain a consistent appearance for your application by utilizing these UI components within the MaterialTheme Composable, as demonstrated in the sample below:

kt
MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // content details
}

If your application's UI specifications diverge from Google's material design guidelines, you have the flexibility to implement your own custom design systems, akin to the MaterialTheme composable. In such cases, you can refer to Stream Video SDK's custom theming systems. This approach empowers users to maintain consistent styles for each provided UI component while also facilitating easy customization of component styles from the user's perspective.

Modifier

The Modifier in Jetpack Compose is a crucial component for UI development. It provides developers with the flexibility to alter and enhance the properties of Composable elements, such as adjusting width/height, padding size, background color, adding a click callback, and so on. The complete list of Compose modifier functions is available at List of Compose Modifiers.

Also, modifiers can be effectively passed down through the layout tree hierarchy, maintaining and extending the properties from the root Composable. This capability ensures a consistent and efficient propagation of layout configurations and styling across the UI elements.

Modifiers are standard Kotlin objects and stateless, so you can simply create a modifier by chaining a builder class like the below:

kt
@Composable
private fun Greeting(name: String) {
    Row(modifier = Modifier.padding(12.dp)) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

Most Compose UI components include a modifier parameter, enabling users to customize properties and control their styles. Similarly, when creating your own Composable functions, it's beneficial to expose the modifier parameter. This practice allows for modifying the styling directly at the call site, offering greater flexibility and customization in your UI design.

kt
@Composable
public fun Header(modifier: Modifier = Modifier) {
    Row(modifier = modifier.fillMaxWidth()) {
        ..
    }
}

From the example provided, it's evident that you can pass a modifier to the Header Composable function. This approach allows for external control of the component's styles, enabling you to adapt and apply different styles based on varying requirements. This flexibility in styling outside the function enhances the customization and adaptability of your UI components.

Another critical aspect to consider with modifiers is the significance of the order in which modifier functions are applied. Chaining these functions involves sequentially layering the modifiers, starting from the highest one and working downwards.

Each step in this process effectively wraps the existing modifiers, building a new composite modifier. This progressive folding over the chain ensures that each modifier influences the final appearance and behavior of the component in a specific, controlled manner. This process resembles the traversal of a tree, moving methodically from the top to the bottom.

The example code below shows you how to implement a simple online indicator by tweaking the order of the modifier functions:

kt
@Composable
fun OnlineIndicator(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .size(60.dp)
            .background(VideoTheme.colors.appBackground, CircleShape)
            .padding(4.dp)
            .background(VideoTheme.colors.infoAccent, CircleShape),
    )
}

Once you've constructed the Composable function and set up a preview in Android Studio, you'll be able to view the outcome as shown below:

The Modifier plays a pivotal role in Jetpack Compose, and while this lesson touches on its essentials, it doesn't encompass all its facets. For a more comprehensive understanding of modifiers, you can explore the Android official documentation on Compose modifiers.

Lists and Grids

It's rare to encounter an application that doesn't utilize lists or grids to effectively display items to users. In traditional XML-based Android development, creating performant and efficient lists often involved considerable challenges, particularly with RecyclerView. This approach required managing complex usages and customizations.

Conversely, Jetpack Compose simplifies the implementation of lists and grids. It offers a more straightforward approach, enabling highly reusable components for each item. Composable functions in Jetpack Compose are inherently independent, offering a high degree of reusability and reducing the complexity typically associated with list and grid implementations.

In the example provided below, implementing a vertical list is straightforward using the Column Composable function in Jetpack Compose. This demonstrates how simple and efficient it is to create structured layouts.

kt
@Composable
fun MessageList(messages: List<Message>) {
    Column(modifier = Modifier.verticalScroll()) {
        messages.forEach { message ->
            MessageItem(message)
        }
    }
}

@Composable
fun MessageItem(message: Message) {
  .. 
}

You can also implement a horizontal list by using the Row Composable function.

While the Column and Row Composables in Jetpack Compose render all items simultaneously, they may not be suitable for displaying a large number of items due to performance concerns. For such scenarios, Compose UI recommends using LazyColumn and LazyRow for lists or LazyVerticalGrid and LazyHorizontalGrid for grids. These components efficiently handle large datasets by only rendering visible items, optimizing performance and resource usage.

Lazy lists and grids in Jetpack Compose, such as LazyColumn, serve a similar purpose to the traditional RecyclerView, offering enhanced performance compared to standard Column or Row Composables. Here's an example demonstrating the use of LazyColumn:

kt
@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageItem(message)
        }
    }
}

@Composable
fun MessageItem(message: Message) {
  .. 
}

In the example shown above, lazy lists and grids in Jetpack Compose offer a unique scope with Domain-Specific Language (DSL) styling. Within this scope, you can render your Composable items using the provided items function. This feature allows for more structured and intuitive readability and is much simpler than the traditional RecyclerView.

You've successfully created a list in Jetpack Compose without the complexity of RecyclerView.Adapter and RecyclerView.ViewHolder, and remarkably, it took less than 15 lines of code! This simplicity and efficiency highlight one of the most appealing aspects of using Jetpack Compose for Android UI development.

Animation

Animation serves as a highlight of UI design, offering enriched user experiences. Traditionally, in XML-based layouts, creating sophisticated animations posed significant challenges. This complexity stemmed from the need to navigate through intricate Android view systems and handle measurements, coupled with the use of complex animation APIs like ValueAnimator and ObjectAnimator.

Jetpack Compose simplifies this process by offering a dedicated animation library. This library features a variety of user-friendly APIs, making the implementation of dynamic animations much more straightforward compared to the traditional XML-based layouts:

  • AnimatedVisibility: This Composable function facilitates the animation of its content's appearance and disappearance. By default, content appears with a fade-in and expansion effect, and disappears with a fade-out and shrink effect. However, these transitions can be tailored to your preferences by specifying custom EnterTransition and ExitTransition parameters.
  • Corssfade: Crossfade provides an animation transition between two Composable functions using a crossfade effect. This feature is particularly valuable for creating smooth and visually appealing transitions between different screens.
  • AnimatedContent: The AnimatedContent composable dynamically animates its content based on changes in a given target state, adhering to specified animation spec parameters. This functionality is especially useful for creating animations between two distinct Composable functions.
  • animate*AsState: The animate*AsState functions represent the most straightforward animation APIs in Jetpack Compose for animating a single value. To use them, you simply specify the target or end value. The API then automatically initiates the animation, transitioning smoothly from the current value to the provided target value. You can use the following animate*AsState functions, which produce different value types: animateDpAsState, animateFloatAsState, animateIntAsState, animateOffsetAsState, animateSizeAsState, and animateValueAsState.
  • animateContentSize: The animateContentSize modifier offers a simple and efficient way to animate changes in size. This modifier eliminates the need for verbose callbacks, streamlining the animation process. You can simply apply this modifier to your Composables and let it handle size transitions automatically based on different content sizes determined by your predefined states.

You've explored a variety of powerful animation APIs available in Jetpack Compose. Their ease of use, compared to the traditional XML-based layout systems, stands out as a compelling reason to adopt Jetpack Compose in your application development. For a deeper dive into animations within Compose, you can refer to the comprehensive guide on Animations in Compose.

State

In programming, the term "state" refers to the in-memory representation of information at a specific moment, which dictates the program's behavior. This concept is predominantly associated with user interactions, influencing how the program responds and operates based on various inputs and actions performed by the user.

Let's consider a scenario where you have a button on a screen designed to fetch user information from a network. In this context, the state of the operation can be represented as a progression from Idle to Loading and finally to Completed. This sequence starts with Idle (nothing happened), moves to Loading (data is being fetched from the network upon clicking the button), and culminates in Completed (successful receipt of the payload from the network).

As the state changes, the UI's role is primarily to observe these changes from a state manager (also known as a view model or similar state machine) and adjust its layout accordingly. In essence, the state is deeply intertwined with the UI, with the latter responding dynamically to the former's alterations.

In Jetpack Compose, the Compose Runtime library offers specialized state APIs, enabling efficient management of UI states within Compose.

State and Recomposition

Before delving into the concept of recomposition, it's important to understand the three primary phases in Jetpack Compose:

  1. Composition: This initial stage in the Compose phase involves your Composable functions being incorporated into the Composition. During this phase, descriptions of your Composable functions are created, along with multiple in-memory slots. These slots are used to memoize each Composable function, enabling their efficient utilization during runtime.

  2. Layout: During this phase, the placement of each Composable node within the Composable tree is determined. Essentially, the Layout process involves measuring each Composable node and positioning them appropriately, ensuring that every element is correctly arranged in the overall structure of the UI.

  3. Drawing: This phase involves rendering your Composable nodes onto a Canvas, typically the device screen. It's the stage where the visual representation of your Composables is actually created and displayed.

Now, you might be curious about how to update the UI layout that has already been rendered through those three primary phases. To address this, Jetpack Compose introduces a mechanism for redrawing, starting from the first phase (composition, technically, your Composable nodes notifies to the composition about the UI changes), to updating your UI layout in response to state changes. This process is known as recomposition.

As mentioned earlier, most mobile applications maintain states, which are in-memory representations of your data model. It's essential for your UI to be updated in response to changes in these states. To facilitate this, Jetpack Compose offers two primary methods to trigger recomposition, ensuring that the UI reflects the latest state changes:

  • Input Changes: This is the most fundamental method for triggering recomposition in your Composable functions. The Compose runtime uses the equals function to detect changes in your arguments. If equals returns false, the runtime interprets this as a change in the input data. Consequently, it initiates a recomposition to update your UI representation in line with these data changes.

  • Observing State Changes: Jetpack Compose offers an effective mechanism to trigger recomposition by monitoring state changes using the State API provided by the Compose runtime library. Typically, this is used in conjunction with the remember Composable function. This approach helps in preserving state objects in memory and restoring them during recomposition, thereby ensuring that the UI reflects the current state.

To deepen your understanding of State in Jetpack Compose, explore the official Android documentation, State and Jetpack Compose and Recomposition.

While recomposition is a necessary mechanism in Jetpack Compose, it does incur a performance cost as it restarts the Compose phases. If you're looking to optimize the performance of your app developed with Jetpack Compose, read the article 6 Jetpack Compose Guidelines to Optimize Your App Performance. This resource offers valuable tips and best practices for improving your Compose app performance.

Stateful vs Stateless

In the realm of Jetpack Compose, Composable functions are often described as either Stateful or Stateless, based on how they manage state.

  • Stateful: As discussed earlier, a Composable function is considered 'Stateful' when it utilizes the remember function. This approach allows the function to store an object in memory, effectively preserving its state across multiple recompositions. The remember function ensures that the state remains consistent even when the Composable function is invoked repeatedly.

  • Stateless: On the other hand, a Composable function is termed 'Stateless' when it does not employ the remember function and instead receives its state through arguments. In this scenario, the function does not hold any internal state; it relies entirely on the data passed to it, making it more predictable and easier to reuse in certain scenarios.

Understanding the distinction between Stateful and Stateless Composables is crucial for efficient and effective UI development in Jetpack Compose, impacting how state is handled and maintained within an application.

The choice between Stateful and Stateless Composable functions depends on your specific requirements, but generally, it's best to lean toward creating Stateless Composables. The rationale for this recommendation is rooted in maintainability and reusability:

  • Maintainability: While Stateful Composables might seem convenient to use at the call site, they can obscure the understanding of their internal states and behaviors. This lack of transparency can make it challenging for developers to predict how the Composable will behave in different scenarios, leading to potential difficulties in maintenance.

  • Reusability: Stateless Composables, relying on external state management, tend to be more reusable across different application parts. They function more like pure functions, where the output is predictable based on the inputs, and they don't maintain any internal state that could affect their behavior unexpectedly.

In summary, opting for Stateless Composables can lead to a cleaner, more modular codebase where components are easier to test, maintain, and reuse in various contexts. Transforming your stateful composable functions into stateless ones can be effectively accomplished through a technique known as state hoisting.

State Hoisting

State hoisting in Jetpack Compose is a method used to convert a Stateful Composable function into a Stateless one by transferring the state management to the call site. Essentially, the core concept of state hoisting involves replacing the state variable, typically managed within remember, with two parameters in the Composable function.

This process effectively shifts the responsibility of maintaining the state from the Composable function to its caller, thereby making the function stateless. As illustrated in the figure below, the MyTestField Composable function operates in a stateless manner. It receives its state directly from the call site and, in turn, communicates events back to that same call site.

Next, we'll contrast Stateful and Stateless Composable functions using sample code snippets, while referring to the figure above for a clearer understanding. Consider a custom text field named MyTextField designed to handle user input. If you're looking to implement MyTextField as a Stateful Composable, here's an example of how it could be done:

kt
@Composable
fun HomeScreen() {
  MyTextField()
}

@Composable
fun MyTextField() {
  val (value, onValueChanged) = remember { mutableStateOf("") }

  TextField(value = value, onValueChange = onValueChanged)
}

From the code example above, you can observe that MyTextField maintains its state by using the remember Composable function to store its state in memory and track input changes. This implementation makes MyTextField a Stateful Composable, as it internally manages its state.

There are pros and cons here. The call site(HomeScreen) doesn't need to manage the state for MyTextField, simplifying its implementation. However, this also means that understanding and controlling the behavior of MyTextField from outside of the box becomes more challenging. As a result, it may be difficult to reuse MyTextField in different contexts due to its internally managed state.

Now, let's take a look at another example that accomplishes the same task, as shown in the example below:

kt
@Composable
fun HomeScreen() {
  val (value, onValueChanged) = remember { mutableStateOf("") }

  MyTextField(
    value = value,
    onValueChanged = onValueChanged
  )
}

@Composable
fun MyTextField(
  value: String,
  onValueChanged: (String) -> Unit
) {
  TextField(value = value, onValueChange = onValueChanged)
}

In this example, the MyTextField Composable is designed to reflect changes in its value through its arguments, with the call site(HomeScreen) managing all states for MyTextField. While this approach might result in longer code compared to the previous stateful example, it offers a clear advantage: the enhanced reusability of the MyTextField Composable across various use cases.

kt
@Composable
fun HomeScreen() {
  val (value, onValueChanged) = remember { mutableStateOf("") }
  val processedValue by remember(value) { derivedStateOf { value.filter { !it.isDigit() } } }

  MyTextField(
    value = processedValue,
    onValueChanged = onValueChanged
  )
}

@Composable
fun MyTextField(
  value: String,
  onValueChanged: (String) -> Unit
) {
  TextField(value = value, onValueChange = onValueChanged)
}

This approach is known as "State Hoisting", which refers to the practice of elevating or 'hoisting' the state management from the callee (in this case, MyTextField) to the caller (such as HomeScreen). This means the state is managed higher up in the hierarchy, allowing for more flexible and controlled state management.

Building upon the stateless example above, let's explore one more use case. Imagine you want to create a text field that restricts users from entering any digits. This scenario showcases the versatility and adaptability of a stateless approach. Then you might able to implement it like the example below:

kt
@Composable
fun HomeScreen() {
  val (value, onValueChanged) = remember { mutableStateOf("") }
  val processedValue by remember(value) { derivedStateOf { value.filter { !it.isDigit() } } }

  MyTextField(
    value = processedValue,
    onValueChanged = onValueChanged
  )
}

@Composable
fun MyTextField(
  value: String,
  onValueChanged: (String) -> Unit
) {
  TextField(value = value, onValueChange = onValueChanged)
}

Moreover, you can continue to use the MyTextField Composable in a variety of ways, tailored to your specific needs. This flexibility should clarify why state hoisting enhances the reusability of your Composable functions, as it allows external control and customization of the component's behavior in diverse contexts.

Side-Effects

Now, let's discuss the concept of side-effects. In the programming world, side-effects commonly refer to unintended or unexpected situations that arise due to changes made in one part of the program affecting another part. These effects are often not immediately apparent, making them challenging to anticipate and manage.

In Jetpack Compose, one of the most common scenarios where side-effects may occur is within a Composable function. Differing from standard functions, Composable functions can be re-invoked to update the UI due to various triggers, such as recomposition. Therefore, executing domain logic directly inside Composable functions can be risky, as it may lead to unexpected behaviors or inconsistencies in the UI due to these repeated invocations.

For instance, consider a scenario where you want to create a combination of text and a text field, and you wish to display a toast with a default message the first time it appears. You might come up with a simple implementation like the one in this example:

kt
@Composable
fun HomeScreen(defaultValue: String = "Hello, Compose!") {
  Column {
    val (value, onValueChanged) = remember { mutableStateOf(defaultValue) }

    val context = LocalContext.current
    Toast.makeText(context, defaultValue, Toast.LENGTH_SHORT).show()

    Text(text = value)

    TextField(value = value, onValueChange = onValueChanged)
  }
}

However, upon running this code, you'll likely notice an issue. Every time you modify the text field's input, the default message ("Hello, Compose!") reappears as a toast. This happens because the state changes for the text field trigger recomposition, which in turn updates the Compose UI and inadvertently causes the toast to be shown repeatedly.

This phenomenon is known as "side-effects", which refers to the occurrence of unexpected behaviors triggered within the program. However, it's often necessary for the UI to engage with domain logic, such as fetching network data, querying databases, and performing other key operations. This interaction is essential in most applications to ensure the UI reflects the current state and data processed by the underlying system.

To address the challenge of side-effects, Jetpack Compose introduces a concept known as the "effect handler." This approach is specifically designed to manage and control side-effects within the Compose framework, ensuring that UI interactions and updates occur in a controlled and predictable manner with the APIs below:

  • SideEffect: This function enables the execution of non-composable functions following each successful recomposition. However, it does not guarantee that these executions will occur prior to a successful recomposition. Essentially, it is designed to trigger actions post-recomposition.
  • LaunchedEffect: This function allows for the safe execution of suspend functions from within a Composable function, initiating from the initial composition phase. Importantly, if the LaunchedEffect is removed from the composition, its coroutine scope will be automatically canceled. To manage another execution based on varying conditions, you can control the execution flow by providing a unique key parameter to LaunchedEffect. This approach enables the cancellation of a current execution and the initiation of a new suspend function, depending on the specific circumstances or state changes.
  • DisposableEffect: This function permits the execution of non-composable functions while also providing the capability to cancel or dispose of ongoing tasks when they leave the composition so that it efficiently prevents issues such as resource leaks or redundant processing.

To gain a deeper understanding of side-effects, check out the Side-effects in Compose, which is an official documentation.

CompositionLocal

Jetpack Compose is designed using a declarative approach, enabling the creation of highly reusable and intuitive UI layouts. However, a challenge arises when the information provided at the root of the layout is needed at the very end of the node. There's no issue with simple UI structures, but consider a scenario like the figure below where you need to construct a highly complex layout involving more than ten levels of various Composable function calls.

In such a scenario, you would need to relay the necessary information from the root to the end, and all Composable functions situated in between must also transmit this information, even if they don't actually utilize it. This approach complicates the entire set of Composable functions, making them more extensive and burdened with unrelated information. Consequently, it could potentially reduce the maintainability of your code.

To tackle this issue, Jetpack Compose introduces the CompositionLocal mechanism. It allows for the implicit passage of data down the Compose tree, enabling access to the required information at any time and at any level of the node whenever needed like the figure below:

In many scenarios, CompositionLocal is used to pass relatively "static" information that doesn’t change much once created. A familiar example might be the MaterialTheme object, which aids in ensuring consistency across your Compose UI components. Let’s see the implementation of it.

kt
@Composable
fun MaterialTheme(
    ..
) {
    ..
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography
    ) {
        ..
    }
}

In the internal implementation of MaterialTheme, it defines all aspects of the theme, including color, alpha, indication, shapes, and typography. Consequently, all Jetpack Compose Material UI components used within the MaterialTheme Composable will consistently apply the theme's properties. This uniformity is maintained regardless of the developers' usage patterns, as they can access the theme information via CompositionLocal.

On the other hand, overusing CompositionLocal can lead to difficulties in debugging and maintaining your code. Therefore, using it judiciously and avoiding excessive reliance on it in your codebase is crucial. If you want to learn more about CompositionLocal, check out Locally scoped data with CompositionLocal.

Migrate to Compose

If your project is still based on XML, you can transition gradually to Jetpack Compose. As Jetpack Compose comprises multiple modular libraries, they are easily portable and can be seamlessly integrated into your existing project. You can begin the migration by simply importing these libraries into your project.

Additionally, Jetpack Compose offers practical APIs that facilitate the transition from XML to Jetpack Compose. Notable examples include ComposeView and ViewCompositionStrategy, which simplify the integration process.

For more insights into migrating to Jetpack Compose, refer to the resources provided below:

Conclusion

This marks the conclusion of part five of the Android Developer Roadmap. This part delves into the components of Jetpack Compose, including Compose UI, State, Side-effects, CompositionLocal, and strategies for migrating to Compose, all of which are crucial elements in understanding and utilizing Jetpack Compose effectively.

If you missed the previous sections of the Android Roadmap, it would be helpful to read those:

The next post will be updated on our blog. If you’d like to stay up to date with Stream, follow us on Twitter @getstream_io or the author @github_skydoves for more great technical content.

And as always, happy coding!

Jaewoong