Topic: 18 Understanding Jetpack Compose in Android

This is our eighteenth topic from learn android from basic to advance series

Β·

17 min read

Topic: 18 Understanding Jetpack Compose in Android

Hello Devs, So in Today's blog we talk about Jetpack Compose. It is an awesome modern UI toolkit that helps you to put your app UI on another level. There are so many things that are not possible in XML. I mean for some complex UI design we need to do so many "Jugaads"(Makeshift Solution) right? Ok, Let's jump on to the topic.

Jetpack Compose

Jetpack Compose is a modern UI toolkit for building native Android UIs with Kotlin. It simplifies and accelerates UI development by providing a declarative way to define UI components, handle state changes, and manage UI elements efficiently.

Key Concepts of Jetpack Compose:

  1. Declarative UI: Unlike the traditional imperative approach of modifying UI elements imperatively, Jetpack Compose follows a declarative paradigm. Developers describe how the UI should look based on the current state, and Compose handles the rendering and updates automatically.

  2. UI as a Function of State: In Compose, UI elements are functions of the application state. Whenever the state changes, Compose re-evaluates these functions to update the UI accordingly, making UI updates more predictable and efficient.

  3. Compose Functions: UI components are created using Compose functions, which are Kotlin functions annotated with @Composable. These functions describe the structure and behavior of UI elements.

  4. Immutable UI Elements: Compose UI elements are immutable, meaning they cannot be modified directly. Instead, whenever the state changes, Compose recomposes the UI, efficiently updating only the components affected by the state change.

  5. Material Design Integration: Jetpack Compose seamlessly integrates with Material Design, providing built-in components and theming capabilities to create beautiful and consistent UIs.

Let's see the annotations of Jetpack Compose:

1. @Composable

  • Purpose: This is one of the most fundamental annotations in Jetpack Compose. It marks a function as a composable function, indicating that it describes a UI component or a part of the UI hierarchy.

  • Usage: You apply this annotation to any function that defines a UI component using Jetpack Compose. Composable functions can call other composable functions or regular Kotlin functions, but all UI manipulation should occur within composable functions.

2. @Preview

  • Purpose: This annotation is used to generate a preview of a composable function in Android Studio's layout editor or in the preview pane.

  • Usage: Apply @Preview to a composable function to generate a visual representation of how the UI will look without running the app on a device or emulator. You can customize the preview appearance by specifying parameters like device configuration, theme, and other attributes.

3. @PreviewParameter

  • Purpose: This annotation allows passing parameters to the preview functions, enabling dynamic previews based on different data or configurations.

  • Usage: You apply @PreviewParameter to a property or parameter in the preview function. This property can then be initialized using the @Preview annotation's ParameterProvider parameter.

4. @ExperimentalComposeApi

  • Purpose: Jetpack Compose is evolving rapidly, and some features might be experimental or subject to change. This annotation marks experimental APIs.

  • Usage: Apply @ExperimentalComposeApi to indicate that you are using experimental features. It's essential to stay updated with the latest Compose releases, as APIs marked as experimental may change or be deprecated in future versions.

5. @Immutable

  • Purpose: This annotation marks classes as immutable, indicating that their instances cannot be changed after creation.

  • Usage: You apply @Immutable to data classes or other classes that represent immutable data. Jetpack Compose relies heavily on immutable data structures to manage UI state efficiently.

6. @Model

  • Purpose: This annotation marks a class as a model, meaning it represents a state that can be observed for changes.

  • Usage: Apply @Model to a class to indicate that it represents a state that can trigger UI updates. Compose automatically subscribes to changes in @Model-annotated classes and recommended UI components when the state changes.

7. @Stable

  • Purpose: This annotation marks values as stable, meaning they do not change over time.

  • Usage: Apply @Stable to variables or objects that do not change during the composition process. This annotation helps Compose and optimize UI rendering by identifying stable values that do not require recomposition.

OK devs, moving on to the UI component. With the help of the UI component, you can make your App UIs. There so many UI components are there in Jetpack compose let's check out some of the components.

UI Components

1. Text

  • Purpose: Displays text on the screen.
Text(text = "Hello, Jetpack Compose!")

2. Button

  • Purpose: Represents a clickable button.
Button(onClick = { /* Handle button click */ }) {
    Text(text = "Click me")
}

3. TextField

  • Purpose: Allows users to input text.
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { newText -> text = newText },
    label = { Text("Enter text") }
)

4. Image

  • Purpose: Displays an image.
Image(
    painter = painterResource(id = R.drawable.my_image),
    contentDescription = "My Image",
    modifier = Modifier.size(100.dp)
)

5. Row

  • Purpose: Arrange composables horizontally.
Row {
    Text(text = "Item 1")
    Text(text = "Item 2")
}

6. Column

  • Purpose: Arrange composables vertically.
Column {
    Text(text = "Item 1")
    Text(text = "Item 2")
}

7. Scaffold

  • Purpose: Provides a layout structure for the app, including app bar, bottom bar, etc.
Scaffold(
    topBar = { /* Top app bar */ },
    bottomBar = { /* Bottom navigation bar */ }
) {
    /* Main content goes here */
}

8. Box

  • Purpose: Provides a simple container with a specified size and optional background color.
Box(
    modifier = Modifier
        .size(100.dp)
        .background(Color.Blue)
)

9. Surface

  • Purpose: Provides a surface for drawing UI elements with elevation, shadows, etc.
Surface(
    modifier = Modifier.padding(16.dp),
    elevation = 4.dp
) {
    /* Composables inside Surface */
}

10. CircularProgressIndicator

  • Purpose: Displays a circular progress indicator.
CircularProgressIndicator(
    modifier = Modifier.size(50.dp)
)

11. Checkbox

  • Purpose: Represents a checkbox that allows users to make binary choices.
var checked by remember { mutableStateOf(false) }

Checkbox(
    checked = checked,
    onCheckedChange = { isChecked -> checked = isChecked },
    modifier = Modifier.padding(16.dp)
)

12. Radio Button

  • Purpose: Represents a group of radio buttons where only one option can be selected at a time.
var selectedOption by remember { mutableStateOf(0) }

RadioGroup(
    options = listOf("Option 1", "Option 2", "Option 3"),
    selectedOption = selectedOption,
    onSelectedOptionChange = { option -> selectedOption = option }
)

13. Slider

  • Purpose: Allows users to select a value from a range by sliding a thumb along a track.
var sliderPosition by remember { mutableStateOf(0f) }

Slider(
    value = sliderPosition,
    onValueChange = { newValue -> sliderPosition = newValue },
    valueRange = 0f..100f
)

14. ProgressBar

  • Purpose: Displays a linear progress bar to indicate the progress of a task.
LinearProgressIndicator(
    progress = 0.5f,
    modifier = Modifier.fillMaxWidth()
)

15. Switch

  • Purpose: Represents a switch that toggles between two states (on/off).
var switchState by remember { mutableStateOf(false) }

Switch(
    checked = switchState,
    onCheckedChange = { isChecked -> switchState = isChecked }
)

16. DropdownMenu

  • Purpose: Displays a dropdown menu with selectable items.
var expanded by remember { mutableStateOf(false) }
var selectedOptionIndex by remember { mutableStateOf(0) }
val options = listOf("Option 1", "Option 2", "Option 3")

Box {
    Text(text = "Select an option")
    DropdownMenu(
        expanded = expanded,
        onDismissRequest = { expanded = false }
    ) {
        options.forEachIndexed { index, option ->
            DropdownMenuItem(onClick = {
                selectedOptionIndex = index
                expanded = false
            }) {
                Text(text = option)
            }
        }
    }
}

17. ScrollableColumn

  • Purpose: Arranges composables vertically in a scrollable column.
ScrollableColumn {
    repeat(20) {
        Text(text = "Item $it", modifier = Modifier.padding(8.dp))
    }
}

18. Card

  • Purpose: Represents a card container with rounded corners, elevation, and optional content padding.
Card(
    modifier = Modifier.padding(16.dp),
    elevation = 4.dp
) {
    Text(text = "This is a card", modifier = Modifier.padding(16.dp))
}

19. Divider

  • Purpose: Renders a horizontal line to visually separate content.
Divider(
    color = Color.Gray,
    thickness = 1.dp,
    modifier = Modifier.padding(vertical = 8.dp)
)

20. FloatingActionButton

  • Purpose: Represents a floating action button for primary user actions.
FloatingActionButton(
    onClick = { /* Handle button click */ }
) {
    Icon(Icons.Default.Add, contentDescription = "Add")
}

21. TabRow

  • Purpose: Displays a row of tabs for navigation between different sections.
val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
var selectedTabIndex by remember { mutableStateOf(0) }

TabRow(
    selectedTabIndex = selectedTabIndex,
    backgroundColor = Color.LightGray,
    contentColor = Color.Black
) {
    tabs.forEachIndexed { index, title ->
        Tab(
            selected = selectedTabIndex == index,
            onClick = { selectedTabIndex = index }
        ) {
            Text(text = title)
        }
    }
}

22. Snackbar

  • Purpose: Displays a transient message at the bottom of the screen.
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) {
    Button(onClick = {
        snackbarHostState.showSnackbar("Snackbar message")
    }) {
        Text(text = "Show Snackbar")
    }
}

23. Tooltip

  • Purpose: Shows a tooltip with additional information when the user interacts with a specific element.
Tooltip(
    text = { Text("This is a tooltip") },
    modifier = Modifier.padding(16.dp)
) {
    Text(text = "Hover over me")
}

24. Drawer

  • Purpose: Represents a sliding panel that contains navigation or additional content.
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()

Scaffold(
    drawerContent = { /* Content of the drawer */ },
    drawerBackgroundColor = Color.LightGray,
    drawerContentColor = Color.Black,
    drawerState = drawerState,
    drawerGesturesEnabled = true,
    drawerElevation = 8.dp
) {
    /* Main content goes here */
}

25. ModalBottomSheetLayout

  • Purpose: Represents a bottom sheet that slides up from the bottom of the screen.
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

ModalBottomSheetLayout(
    sheetState = bottomSheetState,
    sheetContent = { /* Content of the bottom sheet */ },
    sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
    // Main content of the app
}

26. AlertDialog

  • Purpose: Displays a dialog window with a message and optional actions.
val openDialog = remember { mutableStateOf(false) }

if (openDialog.value) {
    AlertDialog(
        onDismissRequest = { openDialog.value = false },
        title = { Text("Dialog Title") },
        text = { Text("Dialog message") },
        confirmButton = {
            Button(
                onClick = { openDialog.value = false }
            ) {
                Text("OK")
            }
        }
    )
}

27. DatePicker

  • Purpose: Allows users to select a date.
var selectedDate by remember { mutableStateOf(LocalDate.now()) }

DatePicker(
    date = selectedDate,
    onDateChange = { newDate -> selectedDate = newDate }
)

28. TimePicker

  • Purpose: Allows users to select a time.
var selectedTime by remember { mutableStateOf(LocalTime.now()) }

TimePicker(
    time = selectedTime,
    onTimeChange = { newTime -> selectedTime = newTime }
)

29. ProgressIndicator

  • Purpose: Represents a generic progress indicator.
ProgressIndicator(
    progress = 0.5f,
    modifier = Modifier.size(50.dp)
)

30. BottomNavigation

  • Purpose: Displays a bottom navigation bar for switching between primary destinations in the app.
val navItems = listOf("Home", "Profile", "Settings")
var selectedNavItem by remember { mutableStateOf(0) }

BottomNavigation {
    navItems.forEachIndexed { index, label ->
        BottomNavigationItem(
            selected = selectedNavItem == index,
            onClick = { selectedNavItem = index },
            label = { Text(label) }
        )
    }
}

OK devs, there are many more components available in the Jetpack component. But it's time to move on to the next topic. You see in Modifier in this component, right? So what is this let's talk about this.

Modifier

Modifier in Jetpack Compose is crucial, as it allows for flexible and powerful layout and styling of UI components. Modifier is a key concept in Jetpack Compose, used to specify how a composable should be displayed, positioned, sized, and interact with other composables. Let's delve into its details:

Purpose of Modifier:

  • Layout: Modifier allows you to define the layout characteristics of a composable, such as its size, padding, alignment, and arrangement within its parent container.

  • Styling: Modifier enables you to apply styling properties to composables, such as background color, border, shape, elevation, and more.

  • Interactivity: A modifier can be used to define interaction behavior, such as clickable areas, focusability, and gesture handling.

Creating Modifier:

You can create a Modifier instance by chaining various modifier functions together using the Modifier factory function. For example:

val modifier = Modifier
    .padding(16.dp)
    .size(200.dp)
    .background(Color.Blue)

Common Modifier Functions:

  1. padding: Adds padding around the composable.

     Modifier.padding(16.dp)
    
  2. size: Specifies the size of the composable.

     Modifier.size(200.dp, 100.dp)
    
  3. fillMaxSize: Fills the maximum available space within its parent.

     Modifier.fillMaxSize()
    
  4. background: Sets the background color or drawable.

     Modifier.background(Color.Red)
    
  5. clickable: Makes the composable clickable.

     Modifier.clickable { /* onClick action */ }
    
  6. border: Adds a border around the composable.

     Modifier.border(2.dp, Color.Black)
    
  7. align: Aligns the composable within its parent.

     Modifier.align(Alignment.Center)
    
  8. offset: Offsets the position of the composable.

     Modifier.offset(x = 20.dp, y = 30.dp)
    

Applying Modifier:

You apply the Modifier to a composable function by passing it as a parameter:

Box(
    modifier = Modifier
        .size(200.dp)
        .background(Color.Blue)
) {
    /* Composable content */
}

Composing Modifier:

You can compose multiple modifiers using the then function to apply them sequentially:

val modifier = Modifier
    .padding(16.dp)
    .then(Modifier.size(200.dp))
    .then(Modifier.background(Color.Blue))

Benefits of Modifier:

  • Declarative: Modifier follows a declarative approach, allowing you to specify the desired properties of a composable without directly manipulating the UI elements imperatively.

  • Composable: Modifier functions are composable themselves, meaning you can combine and reuse them to create complex layouts and styles efficiently.

  • Type-safe: Modifier functions are type-safe, ensuring that only compatible modifiers can be applied to composables, reducing the risk of runtime errors.

Now it's time to check out the remember function.

In Jetpack Compose, remember is a utility function that allows you to retain and manage the state within a composable function. As an experienced Android developer, understanding how to use remember is essential for managing stateful information effectively. Here's a detailed explanation:

Purpose of remember:

  • State Retention: remember is used to retain and manage state information within a composable function across recompositions. It ensures that the state persists as long as the composable function is active.

  • Efficient State Management: By using remember, Jetpack Compose can optimize state updates and recomposition, improving the performance and efficiency of your UI.

Common Use Cases:

  1. Retaining UI State: remember is often used to retain UI state such as whether a button is clicked, the current value of a text field, or the selected item in a list.

  2. Caching Expensive Computations: You can use remember to cache the result of expensive computations or calculations, ensuring that they're only performed when necessary.

  3. Managing Side Effects: remember can also be used to manage side effects, such as performing network requests or accessing external resources, in a composable function.

Creating State with remember:

You can create a state using remember by passing an initial value to it. For example:

val counterState = remember { mutableStateOf(0) }

Accessing State:

Once you've created a state with remember, you can access and modify it within the composable function. For example:

Button(onClick = { counterState.value++ }) {
    Text(text = "Increment")
}

Types of remember:

  1. mutableStateOf: Creates a state holder that can be modified. It returns a MutableState object.

  2. mutableStateListOf: Creates a state holder for a list that can be modified. It returns a MutableListState object.

  3. mutableStateMapOf: Creates a state holder for a map that can be modified. It returns a MutableMapState object.

  4. derivedStateOf: Computes a derived state value based on other states. It returns a DerivedState object.

Lifecycle of remember:

  • remember retains the state for the duration of the composable function's lifecycle.

  • When the composable is recomposed due to changes in its inputs or external factors, the state retained by remember persists, maintaining its value across recompositions.

Considerations:

  • Immutability: It's important to note that the state returned by remember should be treated as immutable, and modifications should be performed using functions provided by the state object (e.g., value property for MutableState).

  • Recomposition Triggers: Changes to the state are managed by remember trigger recomposition of the composable function, ensuring that the UI reflects the updated state.

So devs when you make an app using XML you use Intent to navigate and pass data from one activity to another activity, right? But When you use Jetpack Compose you Create a Composable function, not the activity. So how can you pass data and navigate in Compose? Let's check out.

Navigation

Navigation in Jetpack Compose is crucial for building modern and navigable user interfaces in your Android applications. Jetpack Compose provides the Navigation component, which allows you to navigate between different composables in a declarative and type-safe manner.

The Navigation component in Jetpack Compose provides a way to navigate between different composables within your app, following the principles of a single-activity architecture. It allows you to define navigation routes, handle back navigation, and pass data between composables.

Key Concepts:

  1. NavHost: The NavHost composable serves as the container for hosting navigable destinations within your app. It defines the scope of navigation within a specific area of your UI.

  2. NavGraph: A NavGraph defines the structure of your app's navigation. It contains navigation destinations (screens) and the actions that define how to navigate between them.

  3. NavBackStackEntry: This represents a destination in the navigation stack, allowing you to access information about the current destination and navigate back to previous destinations.

  4. NavHostController: The NavHostController is responsible for managing navigation within a NavHost. It provides functions for navigating to destinations, handling back navigation, and accessing navigation-related information.

Setting up Navigation:

  1. Define Destinations: Define composables representing different screens or destinations in your app.

  2. Create a NavGraph: Create a NavGraph to define the navigation structure of your app, including the destinations and navigation actions.

  3. Set up NavHost: Use the NavHost composable to host your NavGraph, specifying the start destination and providing a NavHostController.

  4. Navigate Between Destinations: Use the NavHostController to navigate between destinations in response to user actions or events.

Example:

Let's see a simple example of setting up navigation in Jetpack Compose:

val navController = rememberNavController()

NavHost(navController = navController, startDestination = "screen1") {
    composable("screen1") {
        Screen1(navController)
    }
    composable("screen2") {
        Screen2(navController)
    }
}

@Composable
fun Screen1(navController: NavController) {
    Button(onClick = { navController.navigate("screen2") }) {
        Text(text = "Go to Screen 2")
    }
}

@Composable
fun Screen2(navController: NavController) {
    Button(onClick = { navController.navigate("screen1") }) {
        Text(text = "Go to Screen 1")
    }
}

Benefits of Navigation in Jetpack Compose:

  • Declarative: Navigation in Jetpack Compose is declarative, making it easy to define and understand the navigation flow of your app.

  • Type-Safe: Navigation actions are type-safe, reducing the risk of runtime errors and providing compile-time checks for navigation destinations.

  • Back Navigation Handling: The navigation component handles back navigation automatically, ensuring a consistent and intuitive user experience.

  • Integration with ViewModel: The navigation component integrates seamlessly with ViewModels, allowing you to pass data between destinations and maintain UI state across the navigation.

In Jetpack Compose Understanding side-effects is essential for handling operations that have external effects, such as fetching data from a network, accessing the file system, or interacting with platform APIs. In Jetpack Compose, side-effects are operations that have observable effects beyond the scope of the composable function itself. Let's delve into side-effects in Jetpack Compose:

Purpose of Side-Effects:

  1. Performing Asynchronous Operations: Side-effects are commonly used to perform asynchronous operations, such as fetching data from a network or reading from a database, without blocking the UI thread.

  2. Interacting with Platform APIs: Side-effects enable interaction with platform-specific APIs, such as accessing device sensors, handling permissions, or invoking system services.

  3. Managing External State: Side-effects are useful for managing external state, such as caching data, updating preferences, or interacting with external services.

Common Side-Effect Scenarios:

  1. Launching Coroutines: Side-effects are often used to launch coroutines for performing asynchronous operations in a non-blocking manner.

  2. Accessing Platform Services: Side-effects enable access to platform-specific services and resources, such as location services, camera, or file system.

  3. Handling Lifecycle Events: Side-effects can be used to handle lifecycle events of a composable or its parent, such as initialization, disposal, or configuration changes.

Side-Effect Functions:

Jetpack Compose provides several side-effect functions for executing operations with side-effects:

  1. LaunchedEffect: Executes a side-effect in a coroutine scope that's tied to the lifecycle of the composable. It's typically used for launching coroutines for performing asynchronous operations.

  2. DisposableEffect: Executes a side-effect when the composable is first created and disposes of the effect when the composable is removed from the composition.

  3. Effect: Executes a side-effect during the composition process, allowing you to perform operations that need to be recomposed when their inputs change.

Example of Side-Effect in Jetpack Compose:

Let's see an example of using LaunchedEffect to fetch data asynchronously:

@Composable
fun DataFetcher() {
    val dataState = remember { mutableStateOf<Result<String>>(Result.Loading) }

    LaunchedEffect(Unit) {
        try {
            val result = fetchDataFromNetwork()
            dataState.value = Result.Success(result)
        } catch (e: Exception) {
            dataState.value = Result.Error(e)
        }
    }

    when (val result = dataState.value) {
        is Result.Success -> {
            // Handle success case
        }
        is Result.Error -> {
            // Handle error case
        }
        Result.Loading -> {
            // Handle loading state
        }
    }
}

In this example, LaunchedEffect is used to launch a coroutine for fetching data from a network asynchronously. The UI can react to different states of the data fetch operation, such as loading, success, or error.

Benefits of Side-Effects in Jetpack Compose:

  • Non-Blocking: Side-effects enable executing operations asynchronously without blocking the UI thread, ensuring smooth user interactions.

  • Lifecycle-Aware: Side-effect functions are lifecycle-aware, automatically handling the lifecycle of the composable and managing resource cleanup.

  • Declarative: Side-effects are declarative, allowing you to define the behavior of side-effectful operations within the context of the composable function itself.

Alright devs, It's time to wrap up this blog. I hope this blog helps you to understand Jetpack Compose. So our next topic belongs to the Design Pattern. we will talk about the different types of design patterns in Android. See you at the next blog.


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. πŸ“²πŸ‘¨β€πŸ’» Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my LinkedIn

Β