Benefits of Compose with Examples

Benefits of Compose with Examples

As of?now?(19 August, 2023) the latest Jetpack Compose library version is 1.5.0. The compose 1.0 version was released in July 2021.

Here are some of practical benefits of using Compose in your project.


1 — Reduced line of codes

Compose is precise and to the point, which means there is no need to use extra functions or adapters to make your views. One of the famous example is of the?RecycleView. Whenever you try to make a multi-item-view recycle view, you have to make adapters and various viewholders according to the type of the view in the?RecycleView. So, approximately the number of class files can go from 3 to 10 class files according to the different views.

Where as in Compose, its just a small line of function in a single class or in couple of classes to show a list with different type of views in it. Here is a small example of a list with different items in compose. In xml approach we used view component?RecycleView?here for a vertical list we use?LazyColumn


@Composable
fun SectionedListView() {
    val itemList = generateItemList()    LazyColumn {
        items(itemList) { section ->
            when (section) {
                is Section.Header -> {
                    Text(
                        text = section.title,
                        fontSize = 20.sp,
                        fontWeight = FontWeight.Bold,
                        textAlign = TextAlign.Center,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp)
                    )
                }
                is Section.Item -> {
                    ListItem(text = section.content)
                }
            }
        }
    }
}

sealed class Section {
    data class Header(val title: String) : Section()
    data class Item(val content: String) : Section()
}

@Composable
fun generateItemList(): List<Section> {
    val itemList = mutableListOf<Section>()
    itemList.add(Section.Header("Section 1"))
    for (i in 1..5) {
        itemList.add(Section.Item("Item $i in Section 1"))
    }
    itemList.add(Section.Header("Section 2"))
    for (i in 1..3) {
        itemList.add(Section.Item("Item $i in Section 2"))
    }
  return itemList
}

@Preview
@Composable
fun PreviewSectionedListView() {
    SectionedListView()
}        


In the above example, the?SectionedListView?composable uses?LazyColumn?to create a list with different sections. The?Section?sealed class defines two types of sections:?Header?and?Item. The?generateItemList?function creates a list of sections, alternating between header and item sections. The?LazyColumn?iterates through the list and displays the appropriate content based on the section type.

2 — Reusability

The reusability of the Compose Views is just superb. It is the key component in the Compose library. We can define common views with different params in those?Composables?and it can be used in all over your project. It makes your code more modular and maintainable.

While creating these views we can either take some dynamic params like strings or values to show in that view or we can encapsulate the logic and other UI elements within according to your use-case.

Following is an example of a reusable?Text on a?Card?view.

@Composable
fun ReusableListItem(content: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = 4.dp
    ) {
        Text(
            text = content,
            fontSize = 16.sp,
            fontWeight = FontWeight.Normal,
            textAlign = TextAlign.Start,
            modifier = Modifier.padding(16.dp)
        )
    }
}

@Preview
@Composable
fun PreviewReusableListItem() {
    ReusableListItem(content = "This is a reusable ListItem")
}        

In this example, the?ReusableListItem?composable encapsulates the appearance and layout of a list item. It uses the?Card?composable to create a material card with a title. You can customise the?ReusableListItem?with different parameters or modifiers to suit your needs.

You can then use the?ReusableListItem?component in your?LazyColumn?or any other?Composables?that require list items:

@Composable
fun SectionedListView() {
    val itemList = generateItemList()

    LazyColumn {
        items(itemList) { section ->
            when (section) {
                is Section.Header -> {
                    Text(
                        text = section.title,
                        fontSize = 20.sp,
                        fontWeight = FontWeight.Bold,
                        textAlign = TextAlign.Center,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp)
                    )
                }
                is Section.Item -> {
                    ReusableListItem(content = section.content)
                }
            }
        }
    }
}        

By creating reusable components like?ReusableListItem, you can maintain a consistent design across your app, improve code readability, and easily make updates or changes to the UI in a centralised manner.

3 — Various Previews of your work

You can preview your developer views in the Android Studio as well. The best thing I like about this is that how can we view the same UI components in different density sizes (ldpi, mdpi, hpdi, xhdpi, xxhdpi, xxxhpdi) with different fonts (very large, large, medium, small, very small).

After checking your view with the above probabilities you can confidently publish your views to other developers.

All these previews are real time and come with an interaction mode as well. For example if you want to render a list and you want to scroll the list as well, then you can interact with it real time and perform your actions as well.

Following is some code to see your views with different probabilities. So instead of using?@Preview?you can use?@DevicePreviews?on your views to see your view in different probabilities defined below.


@Preview(name = "small_font_hdpi_phone", group = "phone_hdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=240", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_hdpi_phone", group = "phone_hdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=240", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_hdpi_phone", group = "phone_hdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=240", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_hdpi_phone", group = "phone_hdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=240", fontScale = 1.30f, showSystemUi = true)

@Preview(name = "small_font_xhdpi_phone", group = "phone_xhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=320", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_xhdpi_phone", group = "phone_xhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=320", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_xhdpi_phone", group = "phone_xhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=320", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_xhdpi_phone", group = "phone_xhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=320", fontScale = 1.30f, showSystemUi = true)

@Preview(name = "small_font_xxhdpi_phone", group = "phone_xxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=480", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_xxhdpi_phone", group = "phone_xxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=480", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_xxhdpi_phone", group = "phone_xxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=480", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_xxhdpi_phone", group = "phone_xxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=480", fontScale = 1.30f, showSystemUi = true)

@Preview(name = "small_font_xxxhdpi_phone", group = "phone_xxxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=640", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_xxxhdpi_phone", group = "phone_xxxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=640", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_xxxhdpi_phone", group = "phone_xxxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=640", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_xxxhdpi_phone", group = "phone_xxxhdpi", locale = "en", device = "spec:width=411dp,height=891dp,dpi=640", fontScale = 1.30f, showSystemUi = true)

@Preview(name = "small_font_xxhdpi_tablet", group = "tablet_xxhdpi", locale = "en", device = "spec:width=1280dp,height=800dp,dpi=480", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_xxhdpi_tablet", group = "tablet_xxhdpi", locale = "en", device = "spec:width=1280dp,height=800dp,dpi=480", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_xxhdpi_tablet", group = "tablet_xxhdpi", locale = "en", device = "spec:width=1280dp,height=800dp,dpi=480", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_xxhdpi_tablet", group = "tablet_xxhdpi", locale = "en", device = "spec:width=1280dp,height=800dp,dpi=480", fontScale = 1.30f, showSystemUi = true)

@Preview(name = "small_font_xxhdpi_foldable", group = "foldable_xxhdpi", locale = "en", device = "spec:width=673.5dp,height=841dp,dpi=480", fontScale = 0.85f, showSystemUi = true)
@Preview(name = "medium_font_xxhdpi_phone", group = "foldable_xxhdpi", locale = "en", device = "spec:width=673.5dp,height=841dp,dpi=480", fontScale = 1.00f, showSystemUi = true)
@Preview(name = "large_font_xxhdpi_phone", group = "foldable_xxhdpi", locale = "en", device = "spec:width=673.5dp,height=841dp,dpi=480", fontScale = 1.15f, showSystemUi = true)
@Preview(name = "largest_font_xxhdpi_phone", group = "foldable_xxhdpi", locale = "en", device = "spec:width=673.5dp,height=841dp,dpi=480", fontScale = 1.30f, showSystemUi = true)
annotation class DevicePreviews        

4 — State Management

Another key feature of compose is that it comes with?state management?which means, you define a state flow in your?ViewModel?and you can pass the state in your compose functions.

Now in your compose function you have some logic to handle different types of states like loading, no content found, show content, error state. When ever your?ViewModel?adds some state then your compose functions are notified and your relative state is rendered on your app without using any?callback?or calling the function again or using?notifyItemChange?like functions.

@Composable
fun StateFlowExample() {
    val textStateFlow: MutableStateFlow<String> = remember { MutableStateFlow("Initial Value") }
    val textState: State<String> = textStateFlow.collectAsState()

    Column {
        BasicTextField(
            value = textState.value,
            onValueChange = { newValue ->
                textStateFlow.value = newValue
            },
            modifier = Modifier.padding(16.dp)
        )
        DisplayText(text = textState.value)
    }
}

@Composable
fun DisplayText(text: String) {
    Text(text = "Current Text: $text", modifier = Modifier.padding(16.dp))
}        

  1. StateFlowExample Composable: This is the entry point of the example. When you call?StateFlowExample()?in your Composable hierarchy, it initializes a?MutableStateFlow?named?textStateFlow?and a?State?object named?textState. The?textState?is derived from?textStateFlow?using?collectAsState().
  2. BasicTextField Composable: Inside the?StateFlowExample?composable, a?BasicTextField?is created. This Composable takes the current value from the?textState?and uses it as the initial value for the text field.
  3. User Interaction and onValueChange: When the user interacts with the?BasicTextField?by typing or changing its content, the?onValueChange?callback is triggered. In this callback, the?textStateFlow.value?is updated with the new value entered by the user.
  4. DisplayText Composable: The?DisplayText?composable is also part of the?StateFlowExample. It displays the current value of the text state. The value of the?textState?(which comes from the?textStateFlow) is automatically updated whenever?textStateFlow?changes, and this triggers the recomposition of the?DisplayText?composable.

The order of function calls can be summarised as follows:

  1. StateFlowExample()?is called, initialising the state flows and the UI.
  2. The?BasicTextField?is created with the initial value from?textState.
  3. User interaction in the?BasicTextField?triggers the?onValueChange?callback.
  4. The value of?textStateFlow?is updated, triggering a recomposition.
  5. The?DisplayText?composable is recomposed with the new value from?textState.

This cycle continues whenever the user interacts with the text input, causing the UI to reactively update based on the changes in the?StateFlow.

4 — Less boiler plate code

Compose reduces the amount of boilerplate code required for creating UI elements. In compose the functions are defined in the class, so you can directly access them. There’s no need for findViewById or ViewHolder patterns, which leads to cleaner and more concise code.

Here’s a simple example that demonstrates how Compose minimises boilerplate:

Traditional View-based UI using XML and findViewById:

Xml:

<!-- activity_main.xml -->
<LinearLayout
    xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, World!" />
        
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me" />
        
</LinearLayout>
        

Class:

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var textView: TextView
    private lateinit var button: Button
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        textView = findViewById(R.id.textView)
        button = findViewById(R.id.button)
        
        button.setOnClickListener {
            textView.text = "Button Clicked"
        }
    }
}        

The equivalent UI using Jetpack Compose:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun ComposeUI() {
    var text by remember { mutableStateOf("Hello, World!") }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text)
        Button(onClick = { text = "Button Clicked" }) {
            Text("Click Me")
        }
    }
}

@Preview
@Composable
fun PreviewComposeUI() {
    ComposeUI()
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeUI()
        }
    }
}        

As you can see, the Compose example requires significantly less code. There’s no need for findViewById, separate XML layout files, or complex UI setup. Jetpack Compose allows you to directly define UI elements and their behaviour within a single composable function. This results in a more concise, readable, and maintainable codebase.

5 — Performance

Compose’s architecture is designed to be efficient and optimized for performance, resulting in smoother UI interactions and reduced UI thread blocking.

  1. Minimised UI Thread Blocking:?Compose aims to minimize the time spent on the UI thread by offloading work to background threads where possible. This helps ensure smoother interactions and responsiveness, especially in scenarios involving heavy computations.
  2. State-of-the-Art Reconciliation:?Compose uses a virtual view hierarchy and employs advanced reconciliation techniques to efficiently determine the differences between the current and new UI states. This results in optimised updates and reduced layout calculations.
  3. Animation Performance:?Compose provides a more straightforward way to work with animations and automatically optimizes animations by leveraging the underlying rendering engine. This can lead to smoother and more performant animations compared to manually managing animation updates.
  4. Efficient UI Updates:?Compose utilizes a declarative UI model, where you describe the UI’s state and appearance. The framework automatically calculates the minimal set of changes needed to update the UI based on changes in the underlying data. This can lead to more efficient updates and reduce unnecessary redraws, improving UI performance.
  5. Lazy Evaluation:?Compose follows a lazy evaluation approach, meaning that only the parts of the UI that are currently visible or have changed will be evaluated and recomposed. This can lead to faster rendering and less memory consumption, especially for complex layouts.

6 — Efficient Development

Let’s discuss how Compose contributes to reducing coupling and improving cohesion in your app’s codebase:

1. Reduced Coupling:?Coupling refers to the degree of interdependence between different components or modules in a system. High coupling can make code more difficult to maintain and modify. Compose helps reduce coupling through the following ways:

  • Composable Functions:?Compose promotes the creation of self-contained, reusable composable functions. Each composable encapsulates a specific UI element or behavior. This separation reduces the need for tight coupling between UI elements.
  • Single Source of Truth:?Compose encourages the use of a single source of truth for UI state. This reduces the need for sharing data directly between components and minimises the dependencies between them.
  • Decoupling UI and Logic:?In Compose, UI logic is decoupled from the underlying UI framework. This allows you to write UI components independently of the UI implementation details, making the components more modular and less tightly coupled to the framework.

2. Improved Cohesion:?Cohesion refers to how closely related the responsibilities of a module or component are. High cohesion implies that a component focuses on a single, well-defined task. Compose enhances cohesion through the following approaches:

  • Declarative UI:?Compose’s declarative approach encourages designing UIs in a way that closely aligns with the app’s data and logic. This improves the cohesion between the UI components and the underlying data they represent.
  • Single Responsibility Principle:?Composable functions in Compose follow the single responsibility principle, where each function is responsible for rendering a specific UI element or behavior. This naturally improves the cohesion of the codebase.
  • Encapsulation of Behaviour:?Compose allows you to encapsulate UI behaviour within composable functions. This helps ensure that related UI behaviour is kept together and doesn’t leak into unrelated components.


7 — Compose using existing views

While Compose encourages building the UI from scratch, it also provides ways to integrate with existing View-based components, allowing a gradual migration to the new framework. Here is an example:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun ExistingViewIntegration() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Compose UI
        Text(text = "Count: $count")
        
        // Existing Button View integrated into Compose
        AndroidView(
            factory = { context ->
                android.widget.Button(context).apply {
                    text = "Click Me (View)"
                    setOnClickListener { count++ }
                }
            },
            modifier = Modifier.padding(16.dp)
        )

        // Compose UI
        Button(onClick = { count++ }) {
            Text("Click Me (Compose)")
        }
    }
}
        

In this example, we integrate an existing Android?Button?view into the Compose layout using the?AndroidView?composable. The?AndroidView?composable takes a factory lambda that creates the native Android view and sets its properties and behaviours. The?modifier?parameter allows you to apply Compose modifiers to the integrated view.

You can see that the existing?Button?view is seamlessly integrated into the Compose layout alongside Compose UI elements. This allows you to reuse your existing Views while progressively transitioning to a Compose-based UI.

Keep in mind that while this integration provides flexibility, the long-term goal is to migrate towards using Compose’s composable functions to build UI components. Integrating existing Views should be used selectively, especially for cases where Compose doesn’t provide a direct equivalent or for complex custom views.


8 — Existing Views using Compose

Integrating Jetpack Compose into existing Android Fragments is possible using the?ComposeView?widget, which serves as a container for Compose content within your Fragment's layout. Here's how you can do it:

First you can create a composable function which you want to integrate in your fragment.

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*

@Composable
fun ComposeContent() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "This is a Compose Fragment")
        Button(onClick = { /* Handle button click */ }) {
            Text("Click Me")
        }
    }
}        

In your existing Fragment’s layout XML, add a?ComposeView?to host the Compose content. You can place it wherever you want within your layout.

<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".YourFragment">

    <!-- Other Views or Layouts -->

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

</FrameLayout>        

In your Fragment code, you can set the content of the?ComposeView?using the?setContent?method. This will inflate and display your Compose content within the?ComposeView.

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.compose.ui.platform.ComposeView

class YourFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return inflater.inflate(R.layout.fragment_your, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val composeView = view.findViewById<ComposeView>(R.id.compose_view)
        composeView.setContent {
            ComposeContent()
        }
    }
}        

In this example,?ComposeContent?is the Composable content that you want to display within your existing Fragment. You use the?ComposeView?in your Fragment's layout XML to host the Compose content and then set the content of the?ComposeView?in the?onViewCreated?method of your Fragment.

This way, you can integrate Jetpack Compose content into your existing Android Fragments. Keep in mind that Compose is designed to provide a consistent UI framework, so using it across your app’s UI can lead to a more cohesive and modern user experience.

Muhammad Ali

Lead Software Engineer | Mobile | Android | solution Architect

1 年

Moment of success, when your words become reference.

回复

要查看或添加评论,请登录

社区洞察

其他会员也浏览了