NashTech Blog

Modernizing Android UI: From XML to Jetpack Compose

Old method of creating UI on Android

Previously, Android applications had an interface that mainly used XML. This method will help clearly separate the interface code and logic code, but it also has many problems when compared to today’s new methods.

Some obvious disadvantages when using XML:

XML interfaces are often difficult to manage when you need to make real-time dynamic changes, such as changing the layout or adding/removing views during application execution. This often requires writing additional code in Java/Kotlin to accommodate.

As applications grow large, XML files can become very complex and difficult to maintain, especially if not properly organized. This complexity can increase the likelihood of errors and difficulty in debugging.

XML can cause some performance problems if the interface is complex with many nested layouts. Parsing XML and generating corresponding UI objects can consume time and system resources.

For interfaces with high design requirements or custom interfaces, XML can limit creativity and require you to use a lot of Java/Kotlin code or even create custom views.

XML does not support reuse of interface elements well. While it’s possible to create styles and themes to apply to multiple views, reusing complex interface elements still requires more effort.

To overcome these limitations, we can use a combination of XML with Java/Kotlin code or use more modern interface design tools and methods, like Jetpack Compose, which we will learn about in this article. write this.

<LinearLayout
   android:id="@+id/linearLayout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical" ></LinearLayout>

Must add Java/Kotlin code like below:

LinearLayout linearLayout = findViewById(R.id.linearLayout);

TextView newTextView = new TextView(this);
newTextView.setText("New TextView");
newTextView.setLayoutParams(new LinearLayout.LayoutParams(
   LinearLayout.LayoutParams.MATCH_PARENT,
   LinearLayout.LayoutParams.WRAP_CONTENT));

linearLayout.addView(newTextView);

As an application grows, XML files can become very complex and difficult to maintain, especially without proper organization. This complexity can increase the likelihood of errors and make debugging difficult.

<LinearLayout
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical" >
   <TextView
      android:id="@+id/textView1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="TextView 1" />
   <EditText
      android:id="@+id/editText1"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Enter something" />
   <!-- Many other layouts, they can be nested together -->
</LinearLayout>

XML can cause some performance issues if the interface is complex with many nested layouts. Parsing XML and generating corresponding UI objects can be time consuming and resource intensive.

<LinearLayout
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">
   <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal">
      <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal">
         <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <TextView
               android:id="@+id/textView3"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="TextView 3" />
            <TextView
               android:id="@+id/textView4"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="TextView 4" />
         </LinearLayout>
      </LinearLayout>
   </LinearLayout>
</LinearLayout>

For interfaces with high design requirements or custom interfaces, XML can limit creativity and require you to use a lot of Java/Kotlin code or even create custom views.

CustomView customView = new CustomView(context);
customView.setCustomAttribute(...); // Tùy chỉnh đặc biệt
layout.addView(customView);

XML does not support reusing UI components well. While it is possible to create styles and themes that can be applied to multiple views, reusing complex UI components requires more effort.

<!-- There's no easy way to re-use below code in other layouts -->
<LinearLayout
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical">
   <TextView
      android:id="@+id/commonTextView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Reusable TextView" />
   <Button
      android:id="@+id/commonButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Reusable Button" />
</LinearLayout>

To overcome these limitations, we can use a combination of XML with Java/Kotlin code or use more modern interface design tools and methods, such as Jetpack Compose, which we will learn about in this article.

Jetpack compose approach

Jetpack Compose is a modern Android tool for building user interfaces, and it improves many of the problems that XML has in designing interfaces.

Jetpack Compose uses a declarative programming model, describing interfaces as Kotlin functions. This makes updating the interface easier and more natural. Just change the status, and the interface will automatically update accordingly. This is a huge improvement over having to manually manipulate views in XML. In the example below, when count changes, Text and Button will be updated automatically without calling methods like findViewById() or setText() like in XML.

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

Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}

Jetpack Compose code is generally cleaner and easier to understand, thanks to the direct coupling of UI components and application logic in one place. This makes the code easier to maintain and less prone to spreading UI logic across multiple places. The logic handled in the onClick function is right in the UI code. In the example below, all the UI and logic are in one function, making it easier to maintain.

@Composable
fun UserProfile(user: User) {
    Column {
        Text("Name: ${user.name}")
        Text("Age: ${user.age}")
    }
}

Jetpack Compose is designed for performance. It minimizes parsing and interface construction because everything is done directly in Kotlin code, without the need to convert from XML to Java objects. In addition, optimizations such as updating only the necessary parts of the interface (recomposition) significantly improve performance.

Compose provides powerful and flexible customization. It is easy to create custom UI components without having to create a View class as in XML. Creating complex UIs becomes easier, and managing these components is simpler. CustomButton can be easily created without creating a CustomButton subclass:

@Composable
fun CustomButton(onClick: () -> Unit, label: String) {
    Button(onClick = onClick) {
        Text(label)
    }
}

With Jetpack Compose, it’s easy to reuse UI components by creating simple Compose functions that can be reused in multiple parts of your app. This code reuse is much easier and more efficient than XML. In the example above, we can easily reuse CustomButton in multiple places in the UI.

Material components in Compose

  • Text is the element used to display text on the screen.
@Composable
fun MyText() {
    Text(text = "Hello, Jetpack Compose!")
}
  • Button is a component used to display an interactive button.
@Composable
fun MyButton() {
    Button(onClick = { /* TODO: Handle button click */ }) {
        Text("Click Me")
    }
}
  • TextField is a component used to receive text input from the user.
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { newText -> text = newText },
        label = { Text("Enter your name") }
    )
}
  • Image is a component used to display images.
@Composable
fun MyImage() {
    Image(
        painter = painterResource(id = R.drawable.my_image),
        contentDescription = "My Image"
    )
}
  • Row is a component used to arrange child elements horizontally.
@Composable
fun MyColumn() {
    Column {
        Text("First")
        Text("Second")
        Text("Third")
    }
}
  • Card is a component used to display an interface block with outstanding effects.
@Composable
fun MyCard() {
    Card(
        elevation = 8.dp,
        shape = RoundedCornerShape(8.dp),
    ) {
        Text("This is a card")
    }
}
  • Scaffold is a fundamental layout component that provides a framework for application screens, including components like AppBar, BottomNavigation, FAB, etc.
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text("My App") })
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* TODO: Handle FAB click */ }) {
                Text("+")
            }
        }
    ) {}
}
  • LazyColumn is a component used to display a vertically scrolling list. It only renders the elements that are visible on the screen, saving resources.
@Composable
fun MyLazyColumn() {
    LazyColumn {
        items(100) { index ->
            Text("Item #$index")
        }
    }
}
  • Surface is a component used to wrap UI elements and add properties like background color, shape, or opacity.
@Composable
fun MySurface() {
    Surface(
        color = Color.Gray,
        shape = RoundedCornerShape(8.dp)
    ) {
        Text("This is inside a surface")
    }
} 

Example of using Jetpack compose

Create a screen that shows a list of payment methods. The continue button can be triggered when the user selects any payment method.

class PaymentMethodListActivity.kt

This class is an activity containing a list and a continue button.

class PaymentMethodListActivity  : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyActivityScreen() // Set your composable function as the content
        }
    }
}

interface ChangePaymentMethodCallback {
    fun onPaymentMethodChanged(paymentMethod: String)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyActivityScreen() {
    var selectedMethod by rememberSaveable { mutableStateOf("") }
    val changePaymentMethodCallback = object : ChangePaymentMethodCallback {
        override fun onPaymentMethodChanged(paymentMethod: String) {
            selectedMethod = paymentMethod
        }
    }
    Column (
        modifier = Modifier
            .fillMaxSize()
            .padding(all = 20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ){
        PaymentMethodList(changePaymentMethodCallback)
        Spacer(modifier = Modifier.height(20.dp))
        Button(
            onClick = { /* Handle something */ },
            enabled = selectedMethod != ""
        ) {
            Text("Continue",
                modifier = Modifier.padding(all = 10.dp)
                .fillMaxWidth(),
                textAlign = TextAlign.Center
            )
        }
    }
}

class PaymentMethodList.kt

This class is a custom view that displays a list of payment methods and returns the selected results via a callback to update the UI.

data class PaymentMethod(
    val name: String,
    val icon: Int // Replace with ImageVector if using vector icons
)

@Composable
fun PaymentMethodList(
    changePaymentMethodCallback: ChangePaymentMethodCallback,
) {
    val paymentMethods = listOf(
        PaymentMethod("Visa", R.drawable.visa), // Replace with actual drawable resources
        PaymentMethod("PayPal", R.drawable.paypal),
        PaymentMethod("Alipay", R.drawable.alipay),
        // Add more payment methods as needed
    )
    var selectedMethod by remember { mutableStateOf<PaymentMethod?>(null) }

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 16.dp, bottom = 16.dp)
    ) {
        items(paymentMethods) { method ->
            PaymentMethodItem(
                method = method,
                isSelected = selectedMethod == method,
                onSelected = {
                    selectedMethod = it
                    selectedMethod?.name?.let { methodName ->
                        changePaymentMethodCallback.onPaymentMethodChanged(methodName)
                    }
                }
            )
        }
    }
}

@Composable
fun PaymentMethodItem(
    method: PaymentMethod,
    isSelected: Boolean,
    onSelected: (PaymentMethod) -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable { onSelected(method) },
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
        colors = CardDefaults.cardColors(
            containerColor = if (isSelected) Color.LightGray else Color.White
        )
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // Use Image if you have drawable resources
            Image(
                painter = painterResource(id = method.icon),
                contentDescription = method.name,
                modifier = Modifier.size(32.dp)
            )
            Spacer(modifier = Modifier.width(16.dp))
            Text(text = method.name)
            Spacer(Modifier.weight(1f))
            if (isSelected) {
                Icon(
                    imageVector = Icons.Filled.CheckCircle,
                    contentDescription = "Selected",
                    tint = Color.Green
                )
            }
        }
    }
}

In the above example, remember and mutableStateOf can be replaced by using ViewModel.

Class SelectedMethodViewModel.kt

class SelectedMethodViewModel : ViewModel() {
    private val _selectedMethod = mutableStateOf("")
    val count get() = _selectedMethod

    fun changePaymentMethod(paymentMethod: String) {
        _selectedMethod.value = paymentMethod;
    }
}

Change function MyActivityScreen to use SelectedMethodViewModel class.
@Composable
fun MyActivityScreen() {
//    var selectedMethod by rememberSaveable { mutableStateOf("") }
    var selectedMethodViewModel = SelectedMethodViewModel()
    val changePaymentMethodCallback = object : ChangePaymentMethodCallback {
        override fun onPaymentMethodChanged(paymentMethod: String) {
            selectedMethodViewModel.changePaymentMethod(paymentMethod)
        }
    }
    Column (
        modifier = Modifier
            .fillMaxSize()
            .padding(all = 20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ){
        PaymentMethodList(changePaymentMethodCallback)
        Spacer(modifier = Modifier.height(20.dp))
        Button(
            onClick = { /* Handle something */ },
            enabled = selectedMethodViewModel.selectedMethod.value != ""
        ) {
            Text("Continue",
                modifier = Modifier.padding(all = 10.dp)
                .fillMaxWidth(),
                textAlign = TextAlign.Center
            )
        }
    }
}
Using LiveData or Flow
class SelectedMethodViewModel : ViewModel() {
    private val _selectedMethod = MutableLiveData("")
    val selectedMethod get() = _selectedMethod

    fun changePaymentMethod(paymentMethod: String) {
        _selectedMethod.value = paymentMethod;
    }
}

Ability to integrate with legacy XML projects

Since ViewModels can be used, it is easy to integrate existing ViewModels into new
views using Jetpack Compose, thereby gradually migrating old projects to use Jetpack
compose without having to rewrite the entire application.

To use Jetpack Compose in an existing Android application, you just need to add the Jetpack compose
libraries to the build.gradle as below:

    implementation "androidx.activity:activity-compose:1.7.2"
    implementation "androidx.compose.ui:ui:1.4.3"
    implementation "androidx.compose.material:material:1.4.3"
    implementation "androidx.compose.ui:ui-tooling-preview:1.4.3"
You can create compose views for activities and fragments or use compose views in xml file.

Conclusion

Comparison between XML and Jetpack Compose

CriteriaXMLJetpack Compose
How to build UIUI is built by defining components in XML filesUI is built directly in Kotlin code using @Composable functions
Separate interface and logicInterface is defined in XML and logic is defined in Java/KotlinInterface and logic are tightly integrated, all defined in Kotlin
Dynamic change capabilityDifficulty in changing dynamic UI without writing additional code in Kotlin/JavaSupport dynamic interface changes easily with the support of features such as State, LiveData
Complexity in large projectsXML files can become complex and difficult to maintain as a project grows.Easier to maintain and manage, thanks to clear code structure and good reusability
EfficiencyXML with many nested layouts can affect performanceJetpack Compose optimizes UI rendering, with less performance impact
Reuse componentsComponents can be reused through styles, extensions, and custom views, but it’s complicatedEasy component reuse, thanks to composable features and flexible customization capabilities
UI Customization CapabilitiesLimit yourself to XML attributes and create custom views if necessaryHighly flexible UI customization, easy to create custom interface components

Jetpack Compose is a major step forward in Android UI development, making it easier for developers to build modern, beautiful, and high-performing apps. Its flexibility, customizability, and tight integration with Kotlin make it an attractive option for both new projects and existing projects considering a transition from traditional XML interfaces. However, as with any new technology, learning and getting comfortable with Jetpack Compose requires time and effort on the part of developers.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top