Jetpack Compose: First Impressions

Jetpack Compose: First Impressions

·

5 min read

Introduction

Jetpack Compose is Google's newest declarative UI Kit for Android app development. All along, we have been using XML layouts and binding the views to activities or fragments. Compose is more like Flutter where you can just declare a view in code and it will appear on screen. This makes developing UI much simpler than using the traditional XML layouts.

In this post, I will be sharing my first experiences with Compose.
Note: I am a student studying IT, so take my opinion with a grain of salt :)

What I enjoyed

Single Source of Truth

First, let's take a look at how the Compose code would look like for a screen like below: image.png

Compose code:

@Composable
fun LoginScreen(navController: NavController?) {
    val mAuth = FirebaseAuth.getInstance()
    val context = LocalContext.current
    var inputEmail by rememberSaveable { mutableStateOf("") }
    var inputPassword by rememberSaveable { mutableStateOf("") }
    var isEmailError by remember { mutableStateOf(false) }
    var isPassError by remember { mutableStateOf(false) }
    var errorMessage by remember { mutableStateOf("") }
    var isChecked by rememberSaveable { mutableStateOf(false) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 40.dp)
    ) {
        AppLogo()
        SquareBox {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.padding(10.dp)
            ) {
                Text(
                    "Login",
                    textAlign = TextAlign.Center,
                    fontSize = 40.sp,
                    modifier = Modifier.fillMaxWidth()
                )
                Box(Modifier.height(10.dp))
                InputTextField(
                    inputValue = inputEmail,
                    inputValueOnChange = {
                        inputEmail = it
                    },
                    label = "Email",
                    isVisible = true,
                    isEmailError
                )

                InputTextField(
                    inputValue = inputPassword,
                    inputValueOnChange = {
                        inputPassword = it
                    },
                    label = "Password",
                    isVisible = false,
                    isPassError
                )
                Box(Modifier.height(10.dp))

                Button(onClick = {
                    isEmailError = false
                    isPassError = false
                    if (inputEmail.isEmpty()) {
                        isEmailError = true
                        errorMessage = "Email cannot be empty"
                        return@Button
                    } else if (inputPassword.isEmpty()) {
                        isPassError = true
                        errorMessage = "Password cannot be empty!"
                        return@Button
                    }
                    mAuth.signInWithEmailAndPassword(inputEmail, inputPassword)
                        .addOnCompleteListener { task ->
                            if (task.isSuccessful) {
                                if (isChecked)
                                    context.getSharedPreferences("SignedIn", Context.MODE_PRIVATE).edit()
                                        .putBoolean("SignedIn", true).apply()
                                else
                                    context.getSharedPreferences("SignedIn", Context.MODE_PRIVATE).edit()
                                        .putBoolean("SignedIn", false).apply()
                                navController!!.navigate(HOME_ROUTE) { navController.popBackStack() }
                            } else {
                                task.exception?.let {
                                    errorMessage = it.localizedMessage!!
                                }
                            }
                        }
                }, Modifier.fillMaxWidth()) {
                    Text(text = "LOGIN")
                }

                Row {
                    Checkbox(checked = isChecked, onCheckedChange = { isChecked = it })
                    Text("Stay signed in")
                }



                // error message
                if (isEmailError || isPassError)
                    Text(errorMessage, color = Color.Red)

                Text(text = "No account? Sign up here", modifier = Modifier.clickable {
                    navController!!.navigate("RegisterScreen")
                }, textAlign = TextAlign.Center)
            }

        }
    }
}

Wow, that looks like a lot of things. The first thing you'll notice is that there are many variables at the very top such as inputEmail and isEmailError. These variables store state. You can think of state as values that change over time. The first thing I like about Compose is that it allows you to write UI directly in the Kotlin file. We can make sure there is a single source of truth for our data by hoisting the state to a parent composable. In traditional XML, we had to manage the UI state and data state separately, which was one of its pain points.

Flexible and Efficient

Another thing I like about Compose is how easy it is to build UI. Take a look at the above code again. To build the same layout in XML, I would probably have to define a separate shape XML file for the square border and deal with constraint layout. For a simple list of items, I would need to build a layout in XML and a separate adapter. This can get really annoying when the app requires many lists that look different. With Compose, all I have to do is:

  1. Store data in a variable
    val itemList = listOf("hello", "there")
    
  2. Define the layout inside the list composable (LazyColumn)
    LazyColumn {
     Text(text = item)
    }
    
  3. Give the list to the list composable
    LazyColumn {
     items(itemList) { item ->
         Text(text = item)
     }
    }
    
    and I'm done. All this code can be written in the same Kotlin file. You could extract the layout to a separate file but it doesn't really change how flexible and efficient Compose is. Furthermore, even with a more complicated list, it is probably easier to build the UI in Compose than XML.

What I struggled with

Now, as much as I loved building UI as Compose, there are some things that just turn me off, but it might just be a personal thing, so take my opinion with a grain of salt.

Small Community

Because Compose is relatively new, you might find yourself lost. Usually when I get stuck, I would search Google for a solution. The solution is very often a StackOverflow Post or YouTube video. I was stuck on a bug that caused my whole UI to flicker every time I navigated from my Login screen to my Home screen. After days of searching, I wanted to give up. I took a last shot at asking at a Discord group that had a Compose channel. It turns out that my UI kept recomposing because I made it check for sharedpreferences to display the UI every time the Login screen was composed. But I would never have known if I did not go out of my way to find a Discord group or even post on StackOverflow for help.

Navigation

Because I was new to this UI Kit, I struggled with structuring my Nav Graph. With XML layouts, activities and fragments, I would usually just do a startActivity() or use fragment manager if I only need to navigate to 1 other page. To navigate in Compose, I had to put every single route in my Nav Graph and navigate to a route like /login. Usually I would put serialisable data inside the intent to startActivity() if the page is just to display data from the previous page. I could not do that between composables as everything passed between routes has to be after the route name. For example if I want to pass a person's name to the next composable, the route would be /login/Jean. I could not pass a whole object to the composable. Perhaps there is a way, but I could not find the solution.

Conclusion

In essence, I think Compose is great at building complex UIs in Android. However, I think I will only use Compose for my side projects as it will take time for me to get used to a declarative approach in building UI. I will still use XML layouts for my school assignments and any side gig I have. If you're looking to use Compose in a large project, I suggest you try building a simple app with it first to get a feel of how different the experience is as compared to XML.

I'm curious to know what you guys think of Jetpack Compose :)