How I Learned to Develop Android Apps With Jetpack Compose (2023)
IN ONLY 3 MONTHS
Introduction to Jetpack Compose and Android Development
In July of 2021, Google launched version 1.0 of their brand new toolkit for building native UI for Android apps. Jetpack Compose is a game changer for Android developers because it moves away from the UI being designed through XML, to completely dynamic, declarative programming.
Declarative programming is an approach to programming where the developer defines the desired output or result, rather than specifying the steps required to achieve it. In the context of Android Jetpack Compose, declarative programming means that the developer describes the UI elements and their behavior, and the framework automatically takes care of rendering and updating them as needed.
This approach to writing code is extremely convenient as it leaves a lesser margin for error as compared to XML.
In 2023 as an Android developer, Jetpack Compose is a mandatory tool that you should add to your toolkit, as more companies are adapting it by the day.
How to start learning Jetpack Compose (Roadmap)
When I first decided to learn Android development at the start of 2023, I had no experience with Android whatsoever. This made the first few steps towards learning difficult, as I came across a lot of terminologies that overwhelmed me.
Just to name a few, some of the terminologies that overwhelmed me were:
- MVVM (Model View ViewModel)
- State
- State Hoisting
- DataStore
- Hilt (Dependency Injection)
- Room (SQLite)
These terminologies can be overwhelming, but trust me, if you tackle them one at a time, slowly all these concepts will become clearer.
And the biggest advice I can give is, “Android documentation is your friend”. Honestly, I cannot emphasize this enough! The Android development documentation is done so well that 9/10 times all one has to do is give it a read, and any concept becomes clear.
And to make things easier for you, I will be sharing how you can start, what concepts you should tackle, and in what order.
1. How to Manage Activities in Android With Jetpack Compose
In Jetpack Compose, the recommended architecture pattern is the single-activity pattern, where you have a single Activity that hosts all your app’s screens. Instead of using multiple Activity classes, each screen is implemented as a composable function, and the navigation between screens is managed by the Jetpack Navigation component.
Here’s an example of how to implement the single-activity pattern in Jetpack Compose:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = "screen1" ) {
composable("screen1") { screen1(navController) }
composable("screen2") { screen2(navController) }
composable("screen3") { screen3(navController) }
}
}
}
}
2. Composable Functions
In Jetpack Compose, functions are the building blocks for designing declarative UI.
Every Composable function starts with the @Composable annotation. And composable functions can only be called from the context of other composable functions.
In Jetpack Compose you can call a bunch of different predefined Composable functions to design your UI elements. You can also create your Composable function like the example above. The best resource to get started with Composable functions is the Android Basics With Compose course. This course is a very easy-to-follow starting point to get familiar with Jetpack Compose and Kotlin.
@Composable
fun ComposeUI(){
Box(ContentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Text("This is a Composable Function)
}
}
In Jetpack Compose, functions are the building blocks for designing declarative UI.
Every Composable function starts with the @Composable annotation. And composable functions can only be called from the context of other composable functions.
In Jetpack Compose you can call a bunch of different predefined Composable functions to design your UI elements. You can also create your Composable function like the example above. The best resource to get started with Composable functions is the Android Basics With Compose course. This course is a very easy-to-follow starting point to get familiar with Jetpack Compose and Kotlin.
3. UI State in Jetpack Compose
UI state refers to the current state or condition of a user interface. It includes properties such as visibility, text, color, and other attributes of UI elements. The UI state can change dynamically based on user interactions or program logic.
In the context of Android Jetpack Compose, UI state management is an essential part of building dynamic and interactive UIs. Jetpack Compose offers various state management mechanisms, such as State and MutableState, to manage the state of UI elements.
State is a type of value holder that can hold a single value and can be observed for changes. When a State value changes, the Compose framework automatically triggers the recomposition of the affected UI elements to reflect the updated state. MutableState is a mutable version of State that allows you to update its value directly.
Here is an example of using State to manage the visibility of a button in Jetpack Compose:
@Composable
fun MyScreen() {
var isButtonVisible by remember { mutableStateOf(true) }
Column {
Button(
onClick = { isButtonVisible = !isButtonVisible }
) {
Text(text = "Toggle Button")
}
if (isButtonVisible) {
Button(
onClick = { /* do Something */}
) {
Text(text = "My Button")
}
}
}
}
In this example, we declare a mutableState variable called isButtonVisible using the remember function.
The remember function is used for state management. It allows you to remember a value across recompositions of the UI.
In the above code, each time the Toggle Button is pressed the button below it, labeled as “My Button” disappears and reappears.
4. State Hoisting in Jetpack Compose
State hoisting is a common design pattern in designing apps in Jetpack Compose. It allows you to separate the UI and the State management of the UI. It’s a common practice that reduces the chances of bugs and errors.
To apply state hoisting to your composable functions, you must simply move the mutableState variable outside the function and pass the variable and an event listener as parameters to that function.
@Composable
fun MyScreen() {
var isButtonVisible by remember { mutableStateOf (true) }
MyButton(isButtonVisible) {
isButtonVisible = !isButtonVisible
}
}
@Composable
fun MyButton(isVisible: Boolean, onClick: () -> Unit) {
Button(
onClick = onClick,
) {
Text(text = "My Button")
}
if(isVisible) {
Text(text= "Text is Visible")
}
}
In the above code, isButtonVisible mutableState variable has been hoisted from the MyButton composable function to the MyScreen Composable Function. This way the MyButton function does not have direct access to change the value of the state variable.
Once you get comfortable with state hoisting, eventually you will move all state variables of one screen to a separate file/class. This will make it so the UI itself is only observing the state, instead of defining it.
5. Navigation in Jetpack Compose
Once you get comfortable with designing UI and managing state in an Android App, through Jetpack Compose, the natural step forward is to figure out how you can navigate from one screen to the other.
When I started learning Jetpack Compose, Compose Navigation was one of the most head-scratching concepts I had ever seen in code. When you start learning Compose navigation you will hear terms like NavHost, NavController, NavGraph, NavDestinations, etc. And honestly, it can get confusing to keep up with what is what. So I’ll break it down for you.
Before we start, the first step is to add the Jetpack Compose Navigation dependency to your build.gradle app module.
dependencies {
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-compose:$nav_version"
}
Be sure to add the latest version of the navigation dependency. You can find the latest version here.
For now, all you need to understand is what a NavHost is, and what a NavController is. The rest is irrelevant as a beginner. You can research these topics later on your own, but for now, let’s not bite off more than we can chew.
NavController:
The NavController is a Jetpack Compose tool, using which we can easily manage navigation between different screens in our app without having to worry about managing the back stack or implementing complex logic ourselves.
We must pass the navController as an argument to every screen which is responsible for navigating to other screens.
NavHost:
The NavHost is a container that holds the different destinations that you can navigate to. It is the starting point of navigation within your app.
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "screen1") {
composable("screen1") { Screen1(navController = navController) }
composable("screen2") { Screen2(navController = navController) }
}
}
@Composable
fun Screen1(navController: NavController) {
Column {
Text("This is Screen 1")
Button(onClick = { navController.navigate("screen2") }) {
Text("Go to Screen 2")
}
}
}
@Composable
fun Screen2(navController: NavController) {
Column {
Text("This is Screen 2")
Button(onClick = { navController.popBackStack() }) {
Text("Go back to Screen 1")
}
}
}
To initialize the navController we call the rememberNavController function.
val navController = rememberNavController()
After this, we can create the navigation entry point which is our NavHost.
NavHost(navController = navController, startDestination = "screen1") {
composable("screen1") { Screen1(navController = navController) }
composable("screen2") { Screen2(navController = navController) }
}
Notice that the string “screen1” is passed as the parameter for startDestination this tells the navHost that the first screen it will load when composing the UI is Screen1(). The destination string is bound to the UI Screen (composable function) through the composable(“destination_string_here”) function.
6. Persisting Data with DataStore
DataStore is an Android library that provides a more flexible and efficient way to store key-value pairs of data than SharedPreferences. DataStore is designed to work seamlessly with Kotlin coroutines and Jetpack Compose. It also has built-in support for types such as nullable types and lists.
In Jetpack Compose, DataStore can be used to manage state across different composables. By using the observable feature of DataStore, developers can update UIs automatically when the data changes. Also, DataStore can be used to store user preferences, such as the user’s language preference, font size, or theme. It is also commonly used to store small amounts of app state, such as whether the user has seen a particular tutorial or introduction screen.
7. Data Persistence with Room in Jetpack Compose
Room is a powerful library in the Android Jetpack suite that makes it easy to implement data persistence in your Android app. Room is built on top of SQLite and provides a high-level API for working with databases.
To start persisting data with room locally, you should have a good understanding of how databases and SQL work. Without this understanding, there is a good chance that you will find yourself overwhelmed. I would recommend diving into this topic only after you have a good understanding of the topics above.
8. Implementing Model View ViewModel With Jetpack Compose
MVVM or Model View ViewModel is a software architecture pattern that separates an application into three main components: the Model, which represents the data and business logic, the View, which represents the user interface, and the ViewModel, which acts as a mediator between the Model and View.
In MVVM, the ViewModel exposes data from the Model to the View through data-binding mechanisms and also receives input events from the View to update the Model. This separation of concerns makes the code easier to maintain and test and allows for better modularity and flexibility.
Using MVVM with Jetpack Compose is a natural fit, as Compose is designed to be declarative and reactive. The ViewModel can hold the state of the UI and expose it to the Composable functions, which can then render the UI based on the current state. This allows for a clean separation of concerns between the UI and business logic and makes it easy to test and maintain the code.
Some benefits of using MVVM with Jetpack Compose include:
- Separation of concerns: MVVM helps to separate the UI and business logic, making the code easier to understand and maintain.
- Testability: MVVM makes it easier to write unit tests for the business logic and ViewModel, as they can be tested independently of the UI.
- Flexibility: MVVM allows for greater flexibility in how the data is presented and manipulated, as the ViewModel can transform and expose the data in different ways to the UI.
- Re-usability: MVVM promotes modularity and re-usability, making it easier to use the same business logic and data across multiple screens or components.
In summary, using MVVM with Jetpack Compose is a best practice that can lead to cleaner, more maintainable code and a better overall user experience.
9. Dependency Injection with Hilt in Jetpack Compose
Hilt is a dependency injection (DI) library for Android app development that is part of the Android Jetpack suite of libraries. It is built on top of Dagger and provides a simpler, more streamlined API for DI in your Android app.
Hilt aims to make it easy to manage and provide dependencies to your app’s components, such as Activities, Fragments, and ViewModels. It does this by using annotations to declare dependencies and generating the necessary code at compile-time.
Dependency injection can streamline your development/debugging process. It helps you avoid writing buggy code and does a lot of the heavy lifting for you. It is a slightly advanced concept, albeit not too difficult, so I recommend getting into it after you’ve covered everything else.
Conclusion
To conclude, Jetpack Compose is an extremely powerful and useful library to write declarative UI for Android. It comes with its quirks and there are updates to it now and then
There are a lot of concepts unique to Android that we must understand to develop good Android apps. I’ve only covered the basic few concepts that can help you get your foot through the door. After covering these topics you should be on the lookout for tools you can add to your kit to become a better developer.
Thanks for reading!
Happy Coding!