Benefits of Compose with Examples
Ahmad Shahwaiz
Senior Android Developer ??| Certified Scrum Master? | FinTech | Compose | Android Kotlin | Testing
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))
}
The order of function calls can be summarised as follows:
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.
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:
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:
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.
Lead Software Engineer | Mobile | Android | solution Architect
1 年Moment of success, when your words become reference.