Jetpack Compose is Android’s modern toolkit for building native UIs. With the shift from XML layouts to Kotlin-based UI code, developers are empowered to build declarative and responsive UI components. This guide will help you create a complete Compose-based project step-by-step. We’ll build a responsive layout, manage state effectively, navigate between screens, and test our UI components.

The Basics of Jetpack Compose

Before diving into the code, it’s essential to understand some key concepts in Jetpack Compose, especially if you’re transitioning from XML-based layouts:

  • Composable Functions: These are Kotlin functions that return UI elements. You define UI by creating composable functions instead of writing XML.
  • State Management: Jetpack Compose uses state to control how UI elements are displayed. A change in state re-renders the affected composable.
  • Modifiers: These are powerful tools that allow you to alter the layout, behavior, and appearance of a composable.

For a deeper understanding of these concepts, you can explore the official Jetpack Compose tutorial.

See this project on github. (Please support us by giving stars ❤️)

Check out my other sample projects here

Step 1: Project Setup

First things first, you need to set up your project in Android Studio. Make sure to use the latest version of Android Studio and include the necessary Compose dependencies in your build.gradle file.

    
    // build.gradle (app level)
    dependencies {
        implementation "androidx.compose.ui:ui:1.0.5"
        implementation "androidx.compose.material3:material3:1.0.0-alpha03"
        implementation "androidx.compose.ui:ui-tooling-preview:1.0.5"
        implementation "androidx.navigation:navigation-compose:2.4.0-beta01"
        implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
        // other dependencies...
    }
    
  

By adding these dependencies, you’re including the necessary Compose libraries to start building UI components and handle navigation between screens. For more details on the dependencies and their use, refer to the Compose Setup Guide.

Step 2: Creating a Theme

Every app has a theme, and Jetpack Compose offers a clean way to apply styles and colors globally. For Compose, you define themes using Kotlin code rather than XML. This is done using the MaterialTheme composable.

Example:

    
    @Composable
    fun MyAppTheme(content: @Composable () -> Unit) {
        MaterialTheme(
            colorScheme = lightColorScheme(
                primary = Color(0xFF6200EA),
                onPrimary = Color.White,
                background = Color(0xFFF5F5F5),
                surface = Color(0xFFFFFFFF),
                onSurface = Color.Black
            ),
            typography = Typography,
            content = content
        )
    }
    
  

The MaterialTheme composable wraps the whole app content and ensures a consistent look and feel throughout the app. The color scheme is customizable, and you can define typography to match your brand. Learn more about theming in Compose by visiting the Theming Documentation.

Step 3: Creating a Home Screen Layout

The Home screen will be the primary screen for our app. This is where we’ll introduce various Compose elements like columns, text fields, and lazy grids. Here’s how we structure our home screen layout:

HomeScreen Composable

Mastering compose simple project
FlowWell App

We’ll create a HomeScreen composable function to serve as the entry point of our app. This composable will include a search bar, a category row, and a grid of featured collections. Let’s look at the code:

    
    @Composable
    fun HomeScreen(modifier: Modifier = Modifier) {
        Column(
            modifier = modifier
                .verticalScroll(rememberScrollState())
                .padding(bottom = 56.dp)
        ) {
            var query by remember { mutableStateOf("") }
            SearchBar(
                query = query,
                onQueryChanged = { query = it }
            )
            Spacer(modifier = Modifier.height(16.dp))
            CategoryRow()
            Spacer(modifier = Modifier.height(16.dp))
            FeaturedCollectionsGrid()
        }
    }
    
  

Explanation:

  • Column: This composable arranges its children in a vertical sequence.
  • rememberScrollState(): This enables vertical scrolling within the column, so the content can be scrolled if it overflows the screen height.
  • var query by remember { mutableStateOf("") }: This sets up a state variable to store the search query.
  • Spacer: Adds spacing between elements.

The search bar is an essential UI component in many apps. Let’s build it using Compose:

    
    @Composable
    fun SearchBar(
        query: String,
        onQueryChanged: (String) -> Unit,
        modifier: Modifier = Modifier
    ) {
        TextField(
            value = query,
            onValueChange = onQueryChanged,
            leadingIcon = {
                Icon(Icons.Default.Search, contentDescription = "Search")
            },
            placeholder = {
                Text(text = "Search")
            },
            modifier = modifier
                .fillMaxWidth()
                .padding(8.dp)
        )
    }
    
  

Explanation:

  • TextField: A composable for taking user input. It displays the search query and updates when the user types.
  • leadingIcon: Adds an icon to the left of the input field.
  • placeholder: Placeholder text displayed when the field is empty.

TextField in Compose is highly customizable and supports many functionalities. For further details, you can refer to the TextField Documentation.

LazyHorizontalGrid example

The Featured Collections Grid will display a set of collections dynamically. You can use LazyHorizontalGrid to create a performant grid layout:

    
    @Composable
    fun FeaturedCollectionsGrid(modifier: Modifier = Modifier) {
        LazyHorizontalGrid(
            rows = GridCells.Fixed(2),
            contentPadding = PaddingValues(horizontal = 16.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            modifier = modifier.height(500.dp)
        ) {
            items(
                count = featuredCollections.size,
                itemContent = { index ->
                    val collection = featuredCollections[index]
                    Card(
                        shape = RoundedCornerShape(8.dp),
                        modifier = Modifier.fillMaxSize()
                    ) {
                        Column {
                            Image(
                                painter = painterResource(collection.imageRes),
                                contentDescription = collection.title,
                                modifier = Modifier.size(height = 200.dp, width = 250.dp),
                                contentScale = ContentScale.Crop
                            )
                            Text(
                                text = collection.title,
                                style = MaterialTheme.typography.titleMedium,
                                modifier = Modifier.padding(8.dp)
                            )
                        }
                    }
                }
            )
        }
    }
    
  

Explanation:

  • LazyHorizontalGrid: A performant grid that renders items lazily. Only the visible items are rendered on the screen.
  • Card: A Material container that groups the grid’s content together.
  • Image: Loads and displays the drawable resource for each collection item.

To better understand lazy loading in Compose, check out the Lazy Layout Documentation.

Step 6: Navigation in Compose

Navigation is a key feature in any app. With Compose, we use NavController and NavHost to handle screen transitions. Here’s an example of setting up navigation:

    
    @Composable
    fun MyAppNavHost(navController: NavController) {
        NavHost(navController = navController, startDestination = "home") {
            composable("home") { HomeScreen() }
            composable("profile") { ProfileScreen() }
        }
    }
    
  

Explanation:

  • NavController: Responsible for handling navigation actions within your app.
  • NavHost: Defines the navigation graph, which is the set of screens (composables) and actions (navigation events) in your app.

Step 7: Testing in Jetpack Compose

Jetpack Compose allows you to write UI tests with ease. You can use createComposeRule to create a test environment for your composables. Here’s an example of testing our HomeScreen:

    
    @RunWith(AndroidJUnit4::class)
    class HomeScreenTest {
        @get:Rule
        val composeTestRule = createComposeRule()

        @Test
        fun searchBar_displaysCorrectPlaceholder() {
            composeTestRule.setContent {
                SearchBar(query = "", onQueryChanged = {})
            }

            composeTestRule
                .onNodeWithText("Search")
                .assertExists()
        }
    }
    
  

Explanation:

  • composeTestRule: Sets up the Compose testing environment.
  • onNodeWithText("Search"): Finds the text node with the text “Search” and asserts its existence.

For more testing options and best practices in Compose, visit the Testing in Compose Documentation.

Conclusion

By following this guide, you’ve learned how to build a simple yet powerful Jetpack Compose app. We’ve covered fundamental concepts like creating a theme, laying out components, and navigating between screens. Compose makes UI development much easier and more flexible compared to XML, letting you manage state effectively and write more readable code.

To see the complete project code, visit the GitHub repository: FlowWell GitHub Repository.

For more information and guidance, check out the official Jetpack Compose documentation.

Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *