Jetpack Compose 🚀 Beginner’s Series

Narayan Panthi
Kt. Academy
Published in
11 min readJan 29, 2024

--

A straightforward insight into Android Jetpack compose will lead you from foundational basics to advanced.

Jetpack Compose Android Tutorial Series

🙏 Namaste!

Welcome, Let’s build a Jetpack Compose App from the ground up. This will be a quick, no-nonsense guide for you, covering various sections:

Basics — Episode I (You are Here)

  • Composable Functions & Compose Layouts
  • Material 3: Themes & Guidelines
  • Design a Card UI & Login Page in Compose

Navigation — Episode II

  • Basic Navigation in Jetpack Compose
  • Pass Argument with Navigation
  • Dialog Navigation, Nested Navigation
  • Bottom Navigation that supports Nested & Multiple Backstack

State Management — Episode III

  • Core Concepts of Jetpack Compose & State Management
  • Launch Effects & Side Effects in Compose
  • Kotlin Flow

LazyColumn & LazyRow — Episode IV

  • Display List with LazyColumn from API Network Response
  • Offline First, Caching with Room Database

Bonus —

  • Capturing & Uploading Images with Compose
Final Output of Firefly App from this Series

Free access to code repository in Episode II🤤

We Android developers spent years crafting our user interfaces using XML-based layouts. It’s time to shift gears and explore this declarative UI toolkit that promises to make our life as a developer a whole lot easier.

For those of us rooted in XML habits, the thought of getting into Jetpack Compose might seem too complicated. But trust me, the benefits in terms of code clarity, development speed, and maintenance are worth it.

Basics things to get started

In Jetpack Compose, there’s a bunch of stuff like UI Components, Layouts, State Management, StateFlow, SideEffects. But when we’re just starting, we don’t have to understand everything at once. As we get used to designing with Compose, things like passing data and managing states will make more sense. So, let’s not stress — start with the basics, and we’ll figure things out step by step.

1. Composable functions

In Jetpack Compose, building UIs is as simple as defining a function. All you need to do is add the @Composable annotation on your function, and voila! You’ve entered the world of UI creation.

@Composable
fun FancyButton(label: String) {
Button(onClick = { /* Handle button click */ }) {
Text(text = label)
}
}

To preview it, like we used to write XML and preview it in the Design Tab, we need to add the following code just to define what to show in the Preview Tab.

@Preview(showBackground = true, name = "Text preview")
@Composable
fun YourPreviewName() {
YourAppTheme {
FancyButton(label = "Android")
}
}

2. Compose Layout ➡️

In Jetpack Compose, UI elements form a hierarchy, arranged by calling composable functions within one another. Think of them as invisible containers that hold your views or other layouts.

Column

If you prefer a vertical arrangement, A Column is a way to go. It stacks your views on top of each other, creating a vertical cascade.
Similar to LinearLayout — orientation = “vertical”.

@Composable
fun UserCard(user: User) {
Column {
Text(text = user.firstName)
Text(text = user.email)
}
}

Row

This layout is your go-to for arranging views horizontally. Lining up your elements side by side.

Similar to LinearLayout — orientation = “horizontal”

@Composable
fun UserCard(user: User) {
Row {
Text(text = user.firstName)
Text(text = user.lastName)
}
}

Scaffold

Scaffold is a Compose function for building your app’s layout based on Material Design. It takes parameters like topBar, bottomBar, and floatingActionButton to structure key components of your application efficiently.

Scaffold(
topBar = {
TopAppBar(
title = {
// Placeholder for top app bar content
}
)
},
bottomBar = {
BottomAppBar {
// Placeholder for bottom app bar content
}
},
floatingActionButton = {
FloatingActionButton(onClick = { /* Handle click */ }) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
) { innerPadding ->
Column() {
// Main content
}
}

LazyCoumn

Using a Column for a large number of items can lead to performance issues, as all items are laid out regardless of visibility. Compose provides efficient alternatives like LazyColumn and LazyRow, which only handle visible items in the viewport.

  • Similar to recyclerview
LazyColumn {
items(messages) { message ->
ProductItemRow(message)
}
}

Additionally, We can use items() extension function known as itemsIndexed() to get index number. Also, The LazyVerticalGrid and LazyHorizontalGrid composables are available for grid layouts.

Modifiers

Modifiers enhance Compose UI elements by providing decoration or adding behavior. Examples include backgrounds, padding, and click event listeners for rows, text, or buttons.

Modifiers in Compose serve a similar purpose of XML attributes like id, padding, margin, color, alpha, ratio, and elevation etc.

@Composable
private fun UserProfile(fullName: String) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = "Name")
Text(text = fullName
}
}

TextField

A TextField is a UI component designed for users to input text or numbers, facilitating the collection of user-provided data.

Similar to EditText in XML.

@Composable
fun UserInputField() {
// to save user input value
var userInput by remember { mutableStateOf(TextFieldValue("")) }

TextField(
value = userInput,
onValueChange = {
userInput = it
},
label = { Text(text = "Enter Your Text") },
placeholder = { Text(text = "Type something here...") },
)
}

These are the basic things you need to get started, we will learn more about them in the upcoming part of this series.

II. First Compose Project with Material3

Let’s create our first project by selecting → New Project →Jetpack Compose Empty Activity

Jetpack Compose Empty Activity

Let’s see the Project Structure & files created by Android Studio,

Color.kt — Contains color of App

The color file contains all the colors related to our app. We can add our app color here. But, As recommended by Material3 Guidelines, we should auto-generate our app color theme from Material3ThemeBuilder to follow the “Material You” personal for every style.

Theme.kt — Contains style/theme of App

Our app features two primary states: Light and Dark. Moreover, in alignment with “Material You” principles, the color palettes now dynamically adjust based on the selected wallpaper. To harmonize with this functionality, we need to configure the theme colors according to the chosen color palettes.

Material Theme — Primary & OnPrimary Color Concept

There are 26+ color roles mapped to Material Components. Our selected app color will map to the Material Theme Color Roles. Explore the documentation for a comprehensive array of color options.

private val DarkColorScheme = darkColorScheme(
primary = YourDarkPrimaryColor,
secondary = YourAppDarktSecondaryColor,
tertiary = YourAppDarkTertiaryColor
)

private val LightColorScheme = lightColorScheme(
primary = YourAppLightPrimaryColor,
secondary = YourAppLightSecondaryColor,
tertiary = YourAppLightTertiaryColor

/* Other default colors roles to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)

Material Theme will adjust your color scheme depending on the wallpaper style or selection.

val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)

Styling with Material3 might seem a little tricky at first, but don’t worry — it gets easier as you get the hang of it.

Type.kt — Contians Text Typography of App

Just like changing colors, adjusting the type means replacing the usual style of the Material3 Theme.

// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

MainActivity.kt — Main Screen of App

In Compose, an Activity still acts as the starting point for an Android app. Rather than using an XML file, as you would in the traditional View system, you utilize setContent to outline your layout. Inside it, Composable functions are called instead.

Fig: Jetpack Compose MainActivity

Let’s observe quickly what is happening here.

The onCreate method, where the initialization logic forMainActivity takes place.

🔵 → The setContent block sets up the Compose UI. It uses a Surface composable as a container, applying the background color from the theme.

🟡 → Greeting composable function. It takes a name parameter and an optional modifier parameter with a default value of Modifier.

🔴 → The GreetingPreview composable is a preview function that is useful for visualizing how the UI component looks during development.

III. Design — A Simple Card UI

Let’s create a TravelCard.kt file and start writing our code. We’ll learn about some UI elements.

Simple Travel Nepal Card UI with Jetpack Compose

Before we start, add these dependencies for Compose Material 3 and Coil [Image Loading Library].

// Material Compose
implementation "androidx.compose.material3:material3:1.1.2"

// Image Loading
implementation("io.coil-kt:coil-compose:2.5.0")

Think In Compose Way 🤔

The above UI can be made with the following elements.

travel_card.xml == CardView > LinearLayout > ImageView > TextView123…

Similarly, In compose we can divide them into the following sections

@Composable
fun TravelCard() {

Card() {
Column() {
Image()
Column() {
Text(text = Your Category".uppercase())
Text( text = "Your Title")
Text(text = "Your Description")
}
}
}

}

Now, let's make our first card by adding some modifiers,

@Composable
fun TravelCard() {

Card(
modifier = Modifier
.padding(10.dp)
.shadow(
elevation = 5.dp,
spotColor = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.medium
),
shape = MaterialTheme.shapes.medium
) {
//... card contianer
}

}

Then, add a Column, Image, and Text.

@Composable
fun TravelCard() {

Card(
modifier = Modifier
.padding(10.dp)
.shadow(
elevation = 5.dp,
spotColor = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.medium
),
shape = MaterialTheme.shapes.medium
) {
Column(
Modifier
.fillMaxWidth(),
) {
Image(
painter = painterResource(id = R.drawable.ic_travel_dummy),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(8.dp)
.height(150.dp)
.size(84.dp)
.clip(MaterialTheme.shapes.medium)
)

Column(
Modifier
.padding(10.dp),
) {
Text(
text = "yourText".uppercase(),
style = appTypography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(8.dp)
)

Text(
text = "Your Title",
style = appTypography.titleLarge,
maxLines = 2,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.padding(8.dp)
)

Text(
text = "Your Description",
style = appTypography.bodySmall,
maxLines = 3,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.padding(8.dp)
)

Spacer(modifier = Modifier.height(8.dp))

}
}
}


}

@Preview(showBackground = true)
@Composable
fun TravelCardPreview() {
TestArticleTheme {
TravelCard()
}
}

You can replace the Image component with AsyncImage to load images from the URL:

 AsyncImage(
model = travel.thumbnail,
contentDescription = productEntity.title,
modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()
.height(150.dp),
contentScale = ContentScale.Crop,
)

Finally, To display TravelCard in our app, Add the following in MainActivity.kt

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ArticleTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TravelCard()
}
}
}
}
}

@Preview(showBackground = true)
@Composable
fun Preview() {
TestArticleTheme {
TravelCard()
}
}

Now, let's create a simple login page.

IV. Complex Design — Login

Create a new file LoginScreen.kt and LoginComponents.kt for login page design and their components.

Login Screen — Home Screen Design from Firefly App

From the image above, there are components such as the email textfield, password textfield, SignIn button, and Signup Text. Now, let’s incorporate the email textfield component in a way that it can be utilized not only in the registration section but also across various pages of the app.

EmailInput & PasswordInput


@Composable
fun EmailInput(
label: String,
icon: ImageVector,
currentValue: String,
focusRequester: FocusRequester? = null,
keyboardActions: KeyboardActions,
onValueChange: (String) -> Unit
) {

TextField(
value = currentValue,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester ?: FocusRequester()),
leadingIcon = { Icon(imageVector = icon, contentDescription = label) },
label = { Text(text = label) },
shape = Shapes.medium,
singleLine = true,
keyboardActions = keyboardActions,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
)
}

@Composable
fun PasswordInput(
label: String,
icon: ImageVector,
currentValue: String,
focusRequester: FocusRequester? = null,
keyboardActions: KeyboardActions,
onValueChange: (String) -> Unit
) {

var passwordVisible by remember { mutableStateOf(false) }

TextField(
value = currentValue,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester ?: FocusRequester()),
leadingIcon = { Icon(imageVector = icon, contentDescription = label) },
trailingIcon = {
val passwordIcon = if (passwordVisible) {
AppIcons.PasswordEyeVisible
} else {
AppIcons.PasswordEyeInvisible
}
val description = if (passwordVisible) {
"Hide Password"
} else {
"Show Password"
}
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = passwordIcon, contentDescription = description)
}
},
label = { Text(text = label) },
shape = Shapes.medium,
singleLine = true,
keyboardActions = keyboardActions,
visualTransformation = if (passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),

)
}

Some Parameters Info

label: String: It is a string that provides context or guidance to the user about the expected input.

icon: ImageVector: It could be an image or a vector graphic providing a visual cue related to the email input.

Or For Icons, You can use this dependency.

    implementation 'androidx.compose.material:material-icons-extended:1.5.4'
object AppIcons {
val Email = Icons.Default.Email
val Password = Icons.Default.Lock
val PasswordEyeVisible = Icons.Default.Visibility
val PasswordEyeInvisible = Icons.Default.VisibilityOff
}

currentValue: String: This parameter holds the current value of the email input field. It represents the text that is currently entered or selected in the input field.

onValueChange: (String) -> Unit: This is a higher-order function parameter. It takes a lambda function as an argument, where the lambda function receives a String parameter. This function is a callback that is invoked when the value of the email input changes. The (String) -> Unit syntax specifies that the lambda function should take a String argument and return Unit (similar to void in other languages).

Creating a personalized login user interface is a straightforward process. Simply arrange your elements within a column and apply the desired modifiers as needed.

LoginScreen.kt

@Composable
fun LoginScreen() {

// Sperate this function as we have to addd viewmodel, declear variables here
// Identify keys actions and listener we may require for login screens.

LoginContent(
email = "apple@gmail.com",
password = "password",
onEmailChange = {
// listen changes of email field
},
onPasswordChange = {
// listen changes of password field
},
onLoginClick = {
// when onLogin Button is Clicked
},
onSignUpClick = navigateToSignUp, // signUp Click
)
}

@Composable
fun LoginContent(
email: String,
password: String,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLoginClick: () -> Unit,
onSignUpClick: () -> Unit
) {
val passwordFocusRequester = FocusRequester()
val focusManager: FocusManager = LocalFocusManager.current

Column(
Modifier
.padding(MaterialTheme.dimens.extraLarge)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally
) {

Box(
modifier = Modifier
.weight(2f)
.padding(MaterialTheme.dimens.medium), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_app_logo),
contentDescription = "logo",
Modifier.padding(10.dp)
)
}

Box(
modifier = Modifier.weight(3f),
) {
Spacer(modifier = Modifier.height(20.dp))

Column(verticalArrangement = Arrangement.Center) {
EmailInput(
currentValue = email,
keyboardActions = KeyboardActions(onNext = { passwordFocusRequester.requestFocus() }),
onValueChange = onEmailChange,
icon = AppIcons.Email,
label = stringResource(id = R.string.label_email),
)

Spacer(modifier = Modifier.height(20.dp))

PasswordInput(
currentValue = password,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
focusRequester = passwordFocusRequester,
onValueChange = onPasswordChange,
icon = AppIcons.Password,
label = stringResource(id = R.string.label_password),
)

Spacer(modifier = Modifier.height(30.dp))

Button(
onClick = {
onLoginClick()
},
Modifier
.fillMaxWidth()
.disableMutipleTouchEvents()
) {
Box {
Text(text = "Sign In", Modifier.padding(8.dp))
}
}
}
}

Box(
modifier = Modifier.weight(0.5f)
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Don’t have an account?", color = Color.Black)
TextButton(onClick = {
onSignUpClick()
}) {
Text(text = "Sign Up")
}
}
}
}
}
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
FireflyComposeTheme {
LoginScreen(
navigateToHome = {},
navigateToSignUp = {})
}
}

That’s all we need to create a simple login page. It’s quite impressive right?

Feel free to create one or two more UI components to familiarize yourself with the design process.

Thank you… Share your thoughts in the comments. Happy Reading 🐱

Navigation in Jetpack Compose — Episode II

Next Episode

Find more articles on the www.kt.academy

--

--