
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
)
}
}
}
UsingLiveDataorFlow
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
| Criteria | XML | Jetpack Compose |
| How to build UI | UI is built by defining components in XML files | UI is built directly in Kotlin code using @Composable functions |
| Separate interface and logic | Interface is defined in XML and logic is defined in Java/Kotlin | Interface and logic are tightly integrated, all defined in Kotlin |
| Dynamic change capability | Difficulty in changing dynamic UI without writing additional code in Kotlin/Java | Support dynamic interface changes easily with the support of features such as State, LiveData |
| Complexity in large projects | XML 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 |
| Efficiency | XML with many nested layouts can affect performance | Jetpack Compose optimizes UI rendering, with less performance impact |
| Reuse components | Components can be reused through styles, extensions, and custom views, but it’s complicated | Easy component reuse, thanks to composable features and flexible customization capabilities |
| UI Customization Capabilities | Limit yourself to XML attributes and create custom views if necessary | Highly 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.