Android Jetpack Compose Tutorial

From Zero to Hero in Jetpack Compose for 2025 | Part 1

Dhananjay Trivedi
12 min read3 days ago
  • Jetpack Compose is a modern, native Android UI toolkit for building UIs using Kotlin, eliminating the need for traditional XML layouts.
  • It replaces the traditional View class hierarchy with Composable functions, which are simple Kotlin functions annotated with @Composable.
  • Composable functions allow developers to define UI components in a declarative and flexible manner, improving readability and reducing boilerplate code.
  • Unlike XML-based layouts, which are tightly coupled with the Android framework, Jetpack Compose is a library and is not dependent on a specific Android version.
  • This approach offers a more intuitive and concise way to build UIs, while ensuring compatibility across different Android versions.
`@Composable
fun saySomething(name : String) {
Text(text = "Welcome $name")
}

Understand Imperative vs Declarative

  • In the Imperative approach, we define how the work will be done, outlining each step of the process.
  • In contrast, the Declarative approach focuses on defining what needs to be done, abstracting away the specific steps involved.
  • Before Jetpack Compose, UI development was done imperatively. For example, if we had JSON data for a user, we would manually specify the sequence of steps to display that data on the screen.
{
"isActive" : true,
"imageURL" : "url",
"name" : "DeeJay",
"role" : "Developer"
}

Imperative Approach:

  • We manually bind views using findViewById()
  • Then, we update each view by calling methods like .setText()
  • Each step must be explicitly defined for every UI element.

Declarative Approach (Jetpack Compose):

  • We pass data directly to a Composable function.
  • Jetpack Compose automatically handles the rendering of the UI based on the data.
  • No need for multiple function calls or manual binding of views, making the code cleaner and more concise.

Composition Vs Inheritance Design Pattern (Extra Theory)

  • Composition over Inheritance is a software design principle that encourages using composition (building classes by combining smaller, reusable components) rather than inheritance (building new classes by extending existing ones).

Understanding Composition over Inheritance

  • Inheritance: This is when one class derives from another, inheriting its behavior and properties. In Android, this is common with extending classes like Activity, Fragment, or View.
  • Composition: Rather than extending a class, you build new functionality by combining different, smaller classes, each with specific responsibilities.

Why Prefer Composition?

  • Flexibility: Inheritance tightly couples your code to the superclass, making it difficult to reuse in different contexts. Composition is more flexible and allows objects to be combined in many ways.
  • Avoiding Deep Hierarchies: Long inheritance chains are harder to understand and maintain, while composition keeps classes shallow and modular.
  • Single Responsibility Principle: Composition allows each class to focus on a single responsibility, making them easier to understand and test.

Benefits in Android Development

  • Improved Reusability: Composable classes can be reused across various activities, fragments, or views without relying on inheritance hierarchies.
  • Better Testing: Composable classes are easier to test in isolation since they are smaller and have focused responsibilities.
  • Enhanced Flexibility: With composition, you can easily change or extend behavior without affecting other parts of the class hierarchy.

Now, Let's see some code for Jetpack Composable

Our first Composable Function:

@Composable
fun Greeting(name: String) {
Text(
text = "Hello $name!"
)
}

See the function is annotated by @Composable and to see its output we don’t have to run the app just yet, we can use the @Preview annotation to see the output.

When working with @Preview you have to pass some default value which will be used in the renderer to show you the output along with showBackground = true to see the default white background, you can also pass the name to name your previews.

Text Composable

If you jump inside the Text() composable functions you will see all the parameters it accepts.

Let's add some customization to our text, you can see the output in the Preview Window right here.

Image Composable

When adding Image Composable you will see 4 different suggestions in autocomplete through which we can add images.

First, let's jump inside the Painter Image Resource, we can see the parameters to be passed here.

I have added an image inside the drawable folder, and now we will create our first Image Composable.

I had to rebuild the project for the renderer to render this image, so you may have to as well.

Let's see other customization options.

Layout Composable

Column composable — For Vertical Arrangement.

@Preview(showBackground = true, widthDp = 300, heightDp = 300)
@Composable
fun columnComposable() {
Column (verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Hello")
Text("Medium")
}
}

Row Composable — For Horizontal Arrangement.

@Preview(showBackground = true, widthDp = 300, heightDp = 300)
@Composable
fun rowComposable() {
Row (verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly) {
Text("Hello")
Text("Medium")
}
}

BoxComposable — For Stack Arrangement.

@Preview(showBackground = true, widthDp = 300, heightDp = 300)
@Composable
fun rowComposable() {
Box(verticalAlignment = Alignment.CenterVertically) {
Text("Hello")
Text("Medium")
}
}

Text Input Field Composable

@Preview
@Composable
private fun textInputField() {
// We maintain the state like this
val enteredText = remember { mutableStateOf("") }

TextField(
modifier = Modifier.height(56.dp).background(color = Color.Red),
value = enteredText.value, // Value is the text to show
onValueChange = {enteredText.value = it}, // To update the state
label = {Text(text = "Enter Text Here")}
)
}

Quick Exercise

Let’s Build our UI similar to the above in Jetpack Compose

  1. Design for each item
With minimal code we can understand the structure of our composable

Now we will add some modifiers to make it look prettier.

Here is the full code:

@Preview
@Composable
private fun showContactList() {
Column {
contactListItem("John Jacobs", "How are you doing?")
contactListItem("Emily Smith", "Hey, long time no see!")
contactListItem("Michael Brown", "Let's catch up soon.")
contactListItem("Sarah Johnson", "Are you free this weekend?")
contactListItem("Chris Lee", "Just got back from vacation!")
contactListItem("Emma Wilson", "Meeting rescheduled to 3 PM.")
contactListItem("David Martinez", "Got any updates on the project?")
contactListItem("Olivia Taylor", "Happy Birthday! 🎉")
contactListItem("Daniel Anderson", "Did you see the news today?")
contactListItem("Sophia Thomas", "Lunch tomorrow?")
}
}

@Preview
@Composable
private fun contactListItem(contactName : String = "John Jacobs", contactStatus : String = "Hey There! How you doing?") {
Row ( horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.background(color = Color.White)
.padding(8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,){
Image(
painter = painterResource(R.drawable.avatar_icon),
contentDescription = "avatar_icon",
Modifier
.size(48.dp)
.weight(0.2f),
)
Column (modifier = Modifier
.weight(0.8f)
.padding(4.dp, 8.dp), verticalArrangement = Arrangement.SpaceEvenly){
Text(text = contactName, fontWeight = FontWeight.W600, fontSize = 18.sp)
Box(modifier = Modifier.height(4.dp))
Text(text = contactStatus, fontWeight = FontWeight.W500, color = Color.DarkGray, fontSize = 14.sp)
}
}
}

It's not perfect, but you get the idea.

What are Modifiers?

Modifiers in Jetpack Compose:

  • Purpose: Modifiers are used to apply general-purpose customizations to UI elements in Jetpack Compose, such as adjusting size, padding, alignment, or adding gestures.
  • Composition over Inheritance: Unlike the traditional Android view system, which relies on inheritance (e.g., creating custom views by extending the View class), Jetpack Compose uses composition, making the UI more flexible and modular.
  • Chaining Modifiers: Multiple modifiers can be chained together to apply different customizations to a Composable. For example, you can set padding, background color, and size in one statement.
  • Order of Modifiers: The order in which modifiers are applied is important. The final result depends on the sequence of modifier calls, as each modifier alters the UI based on the previous modifications.

Recomposition

  • When the state of the app changes, Jetpack Compose triggers recomposition automatically.
  • The state represents the data at a given point in time that determines what is displayed on the UI, similar to how it works in Flutter and React Native.
  • Recomposition ensures that only the parts of the UI that depend on the changed state are re-rendered, optimizing performance.
  • Composable functions can be executed in any order so don’t assume them to run in the same order you call them.
  • We do not run any heavy operations in our composition, we have Coroutines for that.
  • Composable functions can be called in parallel.
  • Hence, they should be Pure Functions.
  • Jetpack will try to minimize the call for Recomposition, it is optimistic and can be canceled.

Key Points:

  • Automatic Trigger: Recomposition is triggered automatically when state changes, eliminating the need for manual UI updates.
  • Efficient: Only the composables dependent on the changed state are recomposed, rather than the entire UI.
  • Declarative: Compose handles UI updates declaratively, making the code more efficient and easier to maintain.
  • State Management: State in Jetpack Compose is managed through variables and functions, which automatically reflect changes in the UI when updated.

State in Jetpack Compose

We maintain the data in the state object, which is an observable state so we can observe/get it from where we have to show data.

Let's see with an example:

@Preview
@Composable
private fun clickCounter() {
// Lets create a state variable
val counter = remember { mutableIntStateOf(0) }

Column (
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxHeight().fillMaxWidth()){
Text(text = "You have clicked ${counter.value} times", fontSize = 32.sp)
Box(Modifier.height(32.dp))
Button(onClick = { counter.intValue++ }) {
Text(text = "Click Me ${counter.value}")
}
}
}

On Screen rotation to maintain the state we must save it in a bundle, so simply instead of just `remember` we call `rememberSavable`.

val counter = rememberSaveable() { mutableIntStateOf(0) }

Sharing State between two Composables — State Hoisting

State Hoisting in Jetpack Compose refers to the practice of moving the state from a Composable function to a higher level in the composable tree, allowing the parent composable to control and manage the state.

Key Points:

  • State Hoisting is a design pattern where the state is declared outside of the Composable that needs to read or modify it, typically in a parent composable.
  • The parent composable manages the state and passes it down to child composables as parameters. The child composables can modify the state through callback functions, but the state itself remains “hoisted” to the parent.

Why Use State Hoisting?

  • It enables reusability: Child composables become more reusable because they don’t rely on internal state, making them easier to test and compose.
  • It improves separation of concerns: The state management is handled by the parent, while the child is only responsible for presenting the UI based on the state passed down.
  • How It Works: The state is “hoisted” by defining a state variable in the parent composable, and the child composable receives the state and a function to update it.

We will now look into how to share state within these three Composables.

@Preview
@Composable
private fun parentComposable() {
val counter = rememberSaveable() { mutableIntStateOf(0) } // Hoisting
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
clickCounter(counter.intValue) { // Passing the value and callback
counter.intValue++
}
cardToShowText(counter.intValue) // Passing the value
}
}

@Preview
@Composable
private fun cardToShowText(int: Int = 0) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "The Counter is at $int",
fontSize = 24.sp,
modifier = Modifier.align(Alignment.Center)
)
}
}
}

@Composable
private fun clickCounter(counter : Int, onClick: () -> Int) {
Column (
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.height(200.dp)
.fillMaxWidth()){
Text(text = "You have clicked $counter times", fontSize = 32.sp)
Box(Modifier.height(32.dp))
Button(onClick = {onClick()}){ // onClick functions gets called from here
Text("Clicked $counter times", fontSize = 20.sp)
}
}
}

Note that the data flows downwards and the callback events flow upwards.

Exercise — Quotes App

Let's build the UI for the Single Quote Item.

    @Composable
fun QuoteListItem(
quote: QuoteModel = QuoteModel("Beautiful Quote", "Beautiful Author"),
onClick: (quote: QuoteModel) -> Unit
) {
Card(
modifier = Modifier
.padding(16.dp)
.clickable { onClick(quote) },
elevation = CardDefaults.cardElevation(16.dp)

) {
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.background(color = Color.White)
.padding(8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.quote),
contentDescription = "avatar_icon",
Modifier
.size(48.dp)
.weight(0.2f),
)
Column(
modifier = Modifier
.weight(0.8f)
.padding(4.dp, 8.dp), verticalArrangement = Arrangement.SpaceEvenly
) {
Text(text = quote.text, fontWeight = FontWeight.W600, fontSize = 18.sp)
Box(modifier = Modifier.height(4.dp))
Text(
text = quote.author,
fontWeight = FontWeight.W500,
color = Color.DarkGray,
fontSize = 14.sp
)
}
}
}
}

Then we have to load this card in a list, until now we used a Column which is not the most ideal way just like ListView in Android. We have recylerView so here we have LazyColumn.

@Composable
fun ShowFullList(quoteArray: Array<QuoteModel>, onClick: (quote: QuoteModel) -> Unit) {
LazyColumn {
items(quoteArray) {
QuoteListItem(it) {
onClick
}
}

}
}

UI is Ready now lets focus on the Data Layer. Create an asset folder and add a file quote.json and add the below content.

[
{ "text": "Genius is one percent inspiration and ninety-nine percent perspiration.", "author": "Thomas Edison" },
{ "text": "You can observe a lot just by watching.", "author": "Yogi Berra" },
{ "text": "A house divided against itself cannot stand.", "author": "Abraham Lincoln" },
{ "text": "Difficulties increase the nearer we get to the goal.", "author": "Johann Wolfgang von Goethe" },
{ "text": "Fate is in your hands and no one else's.", "author": "Byron Pulsifer" },
]

Now we create the model class.

data class QuoteModel (val text: String, val author: String)

Finally our DataManager Class:

object DataManager {
var data = emptyArray<QuoteModel>()
var isDataLoaded = mutableStateOf(false)

fun loadDataFromJsonArray(context: Context) {
val inputStream = context.assets.open("quote.json")
val size = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
inputStream.close()
val json = String(buffer, Charsets.UTF_8)
val gson = Gson()
data = gson.fromJson(json, Array<QuoteModel>::class.java)
isDataLoaded.value = true
}

}

With everything now ready, we can call our Composable in our UI after we get the data.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Calling Data Manager to be ready with data from the file
DataManager.loadDataFromJsonArray(applicationContext)

setContent {
MyApplicationTheme {
App()
}
}
}

@Composable
fun App() {
if (DataManager.isDataLoaded.value) {
// We pass the data to the Compose like this.
ShowFullList(DataManager.data) {
// handle onClick here
}
}
}

Fun Fact: States are thread-safe. No matter which context you read and update them in, they will always be thread safe.

Navigating Between Two Screens without Navigation Framework

So we will continue with the quotes App and add a QuotesDetail Composable.

@Preview
@Composable
fun QuoteDetails(
quote: QuoteModel = QuoteModel("Beautiful Quote", "Beautiful Author"),
) {
BackHandler {
DataManager.currentPage.value = Screens.QUOTE_SCREEN
DataManager.switchPages()
}
Card(
modifier = Modifier
.padding(16.dp).fillMaxSize(),
elevation = CardDefaults.cardElevation(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp).background(Color.White)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.background(color = Color.White)
.padding(16.dp)
.fillMaxWidth().align(Alignment.Center),
) {
Image(
painter = painterResource(R.drawable.quote),
contentDescription = "avatar_icon",
Modifier
.size(48.dp),
)
Spacer(modifier = Modifier.size(8.dp))
Text(text = quote.text, textAlign = TextAlign.Center, lineHeight = 48.sp, fontWeight = FontWeight.W600, fontSize = 32.sp)
Box(modifier = Modifier.height(32.dp))
Text(
text = quote.author,
fontWeight = FontWeight.W500,
textAlign = TextAlign.Center,
color = Color.DarkGray,
fontSize = 24.sp
)
}
}
}}

Now lets update the Data Manager Class by adding these two functions.

enum class Screens{
HOME_SCREEN,
QUOTE_SCREEN
}
fun switchPages() {
if (currentPage.value == Screens.HOME_SCREEN) {
currentPage.value = Screens.QUOTE_SCREEN
} else {
currentPage.value = Screens.HOME_SCREEN
}
}

At last, we update the logic to show the correct screen inside App() function.

@Composable
fun App() {
if (DataManager.isDataLoaded.value) {
if(DataManager.currentPage.value == DataManager.Screens.HOME_SCREEN) {
ShowFullList(DataManager.data) {
DataManager.currentQuote = it
DataManager.switchPages()
}
} else {
DataManager.currentQuote?.let { QuoteDetails(it) }
}
}
}

Thanks for reading!

--

--