Jetpack Compose official logo.

Last Updated: 2022-10-20

Jetpack Compose

Throughout these past two years we've got our timelines flooded with posts about Jetpack Compose. It all started in February-March 2021 with the #AndroidDevChallenge and from then on we've kept seeing new developers joining this Composable new world!

What's Jetpack Compose?

"Jetpack Compose is Android's modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.", as mentioned on the official website.

Along with this, it's also worth mentioning that Android features are no longer chained to a specific API version. Starting in API 21, all developments are backward compatible. This means that if, for instance, you want to use the IME animation that was released in Android 11, you can use them back until lollipop. Yes! When Android was still named after popular desserts.

More about Jetpack Compose

Although Jetpack Compose brings a lot of advantages, it's important to mention that it has an associated learning curve. If you've been developing for Android for quite some time and so you've been using imperative UI, making the change to declarative will take you a few weeks/months to master - this is normal.

You'll need to learn this new development paradigm along with an entire new API. We've all taken months, so don't worry if things might be a bit blurry in the beginning, learning something new takes time.

Fortunately, there are a lot of materials available that will help you along the way:

A few more notes

Before we jump into the Codelab, remember that Jetpack Compose is in version 1.3.0. If we compare it with XML it has just given its first steps, so you might find a couple of bugs along the way, or perhaps some API's don't support the full set of attributes/functionalities that you're used to. This is perfectly fine, for the first one you always have Google's issue tracker from where you can check if anyone found the same problem, for the second one, if there's no library that solves the problem you can always use XML on that Activity (they're interoperable), or better - you can give it support!

What you'll build

In this codelab, you're going to build the Unsplash app! It's a simple application that communicates with the Unsplash servers in order to download the most beautiful images out there.

What you'll learn

What do I need to start?

How to create a Compose project?

The latest versions of Android Studio already have a new template for Jetpack Compose. Although in this codelab you're going to start with an existing project that already communicates with the Unsplash servers in order to get the latest trending images. It's worth mentioning that you can easily create a new project via the New Project wizard on Android Studio.

To do this, open Android Studio and click in New Project.

You'll be prompted with a similar screen where you find a grid with several templates. Select the Empty Compose Activity.

In order to create your new project, you'll need to enter its name, package, etc. - similar to what you're already familiar with.

After filling all the fields, click on Finish and wait for the project to synchronize.

Once done, click on compile and run in a couple of seconds you'll see an app on your phone/emulator welcoming you to this Composable new world!

Get a key for the Unsplash API

The images that you're going to see in the app comes from the Unsplash API. In order to use it, you'll need to request an API key. It's easy to use, and free for non-commercial projects.

Register for API Key

Get the code

Everything you need for this project is in this git repository. To get started, you'll need to

  1. Clone the project locally
git clone https://github.com/cmota/a-composable-new-world.git
  1. Now open the Starter Project on Android Studio.
  1. You'll need to wait until gradle runs all the tasks. You can get up and stretch your legs a bit, this is going to take some minutes.

  1. Compile and run the app. You should see a screen similar to this one:

Alternative: Download code & work locally

The source code of this codelab is available on GitHub. It follows the same structure of this Codelab.

Let's start! Open the content of Starter Project on Android Studio.

Download: A Composable New World

  1. Unpack the downloaded zip file.
  2. Open Android Studio
  3. Click in Open
  4. Navigate to the Starter Project folder and open it
  5. Wait for the project to synchronize

When you open 00.Starter Project and start to navigate throughout the source code you'll see two separated folders:

This one contains all of the business logic for the Unsplash app

With all the code required to design the UI

For the initial sections of this codelab you'll only need to work with the ui folder. shared is left for the last - But wait! There's more: Alicerce, and as you'll see it was built to help you move your existing code to multiplatform. But that's a secret for now! 🤐

After synchronizing and running the project you look at your device/emulator and see... a black screen. Don't worry, everything is working as expected!

It's now time to start adding content to it.

About Screen

If you open the AndroidManifest.xml you'll see that MainActivity.kt is the activity that's going to be launched when the app starts (it's also the only activity of the project).

You can already spot a couple of differences between Compose and XML. The one that immediately jumps out is when you look at the setContent function and see that it no longer receives a layout resource. Instead a Composable function is required in order to design your screen.

Having readability in mind, the project is organized into subfolders where each one of them correspond to a specific screen or feature:

Defines the Composable functions for the about screen code

Contains a set of classes that together define the theme and styles for the application

As you're moving through the sections you'll see new folders being added.

Adding Text

Ready to dive into Compose? Let's start!

For now you're seeing a black screen, so let's start by adding some text! To do this, there is a function called Text. It receives a couple of parameters, for now let's focus on defining a specific string.

Inside the Composable of the AboutContent.kt file add:

@Composable
fun AboutContent() {

  Text(
    text = stringResource(id = R.string.about_text)
  )
}

Don't forget that you'll also need to import the correspondent classes:

import androidx.compose.material.Text
import androidx.compose.ui.res.stringResource
import com.cmota.unsplash.R

stringResource can be seen as the equivalent function of getString but for Composables. Typically to access a string resource you would either use the activity or application context to call getString. With stringResource this is easier and the only condition to call it is that the code is inside a Composable function.

Now compile and run the project. What did you see differently?

Nothing right? That's because the default color for the Text is black - the same one that's being used on the surface. Let's change that!

Customizing Text

Once the default color of the text is the same as the background it's time to change one of them, otherwise it won't be possible to read anything.

In the same file, set the following parameter in the Text function.

@Composable
fun AboutContent() {

  Text(
    text = stringResource(id = R.string.about_text),
    color = Color.White
  )
}

Better, right?

Text supports all of these parameters to be defined:

@Composable
fun Text(
   text: String,
   modifier: Modifier = Modifier,
   color: Color = Color.Unspecified,
   fontSize: TextUnit = TextUnit.Unspecified,
   fontStyle: FontStyle? = null,
   fontWeight: FontWeight? = null,
   fontFamily: FontFamily? = null,
   letterSpacing: TextUnit = TextUnit.Unspecified,
   textDecoration: TextDecoration? = null,
   textAlign: TextAlign? = null,
   lineHeight: TextUnit = TextUnit.Unspecified,
   overflow: TextOverflow = TextOverflow.Clip,
   softWrap: Boolean = true,
   maxLines: Int = Int.MAX_VALUE,
   onTextLayout: (TextLayoutResult) -> Unit = {},
   style: TextStyle = LocalTextStyle.current
)

For now, define the text to be bigger and so easier to read and align it to center instead of left.

@Composable
fun AboutContent() {

  Text(
    text = stringResource(id = R.string.about_text),
    color = Color.White,
    fontSize = 21.sp,
    textAlign = TextAlign.Center
  )
}

You'll see a similar screen to this one:

Feel free to play with the rest of these parameters and see how they affect your text.

Defining your layout

You've already customized your text, but it still feels that this screen has everything too close, there's almost no space between the status bar and the app. Let's update this!

First, you'll need to use a new function called Column. Before continuing, the following image from the Android documentation is important to better understand this Composable.

A good example on why we need to use these functions is to add another Text and see what happens:

The screen will render both texts one on top of the other. Now, to overcome this you can either add Column if you want them to be one below the other or Row if instead you want them aligned side by side.

Using Column:

@Composable
fun AboutContent() {

   Column {
       Text(
           text = stringResource(id = R.string.app_name),
           color = Color.Red,
           fontSize = 21.sp,
           textAlign = TextAlign.Center
       )

       Text(
           text = stringResource(id = R.string.about_text),
           color = Color.White,
           fontSize = 21.sp,
           textAlign = TextAlign.Center
       )
   }
}

Integrating your Text with the surroundings

Now imagine that you want to center your text on the screen. There are different possibilities to achieve this, but since you've just seen how to use Column you're going to follow this approach.

In the section above you've seen that Column allows you to stack components. This function has a lot more functionalities that you can use to build your UI's.

@Composable
inline fun Column(
   modifier: Modifier = Modifier,
   verticalArrangement: Arrangement.Vertical = Arrangement.Top,
   horizontalAlignment: Alignment.Horizontal = Alignment.Start,
   content: @Composable ColumnScope.() -> Unit
)

One of these features is the use of Modifier. Here you can define a never ending set of attributes: width, height, background, click event, padding, etc. And of course, you can always create extensions with new behaviors 🙂.

Along with it, you also have the verticalArrangment and horizontalAlignment that allow you to define how content should be displayed in the screen: align to the start, end, top, bottom, etc.

You'll use these four parameters to center your text.

@Composable
fun AboutContent() {

   Column(
       modifier = Modifier
           .fillMaxSize()
           .padding(16.dp),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(id = R.string.about_text),
           color = Color.White,
           fontSize = 21.sp,
           textAlign = TextAlign.Center
       )
   }
}

You should see a similar screen to this one:

In the previous screen you've added and customized text on an empty screen. It's time to dive in and build our app.

Adding your second screen

Start by creating a new package called home inside ui. The correct path should be:

Here is where you're going to create the second screen for your app. After creating the folder add a new Kotlin file inside and call it HomeContent.kt.

Update it's content to be a Composable function called HomeContent.

@Composable
fun HomeContent() {
  // Your code will be here
}

Don't forget to import androidx.compose.runtime.Composable in order to use @Composable.

Adding images

This screen contains a list of images that are going to be fetched from Unsplash API. So the first step here is to define our image. Create a new function called AddUnsplashImage and add:

@Composable
fun AddUnsplashImage(image: Image) {

}

It's going to receive an Image object which is defined inside shared/data/model and it's used to serialize the response received from the API. Here, it's going to be used to populate this function.

You'll need to import Image from com.cmota.unsplash.shared.data.model.Image.

You're not going to load these images from the device local storage, Image contains a url from where the image needs to be downloaded so it can be displayed.

There are already a lot of libraries that do this for you: Glide, Picasso, Coil, etc. Here, you're going to use Coil. Why? Well, it was the first library that I've noticed that already supported Compose and it has been working perfectly since then.

Start by adding the library to the project. Open build.gradle.kts and in the dependencies section add:

implementation("io.coil-kt:coil-compose:1.3.2")

Now click on synchronize and wait a bit for the system to fetch Coil.

Once done, create a new package called components. It should be inside com.cmota.unsplash. Here create a new class and call it ImagePreview.kt. This is a component that you can reuse throughout your app screens, so instead of adding it to HomeContent.kt increasing the number of lines of that file and reducing readability I've decided to add it inside components.

Now, if you want to add images on another screen instead of looking for the correct function inside HomeContent.kt you just need to browse your components folder.

It's now time to define the image integration with Coil.

@Composable
//1
fun AddImagePreview(
   url: String,
   modifier: Modifier
) {
   //2
   val request = ImageRequest.Builder(LocalContext.current)
       .data(url)
       .crossfade(true)
       .build()
   //3
   val painter = rememberAsyncImagePainter(
       model = request
   )

   Box {
       //4
       Image(
           painter = painter,
           contentScale = ContentScale.Crop,
           contentDescription = stringResource(id = R.string.description_preview),
           modifier = modifier
       )
       //5
       when (painter.state) {
           is AsyncImagePainter.State.Loading -> {
               AddImagePreviewEmpty(modifier)
           }
           is AsyncImagePainter.State.Error -> {
               AddImagePreviewEmpty(modifier)
           }
           else -> {
               // Do nothing
           }
       }
   }
}

A lot is being done here, so let's go step by step:

  1. AddImagePreview needs to receive a url and a modifier. The first one is used to define where the image should be fetched and the second one to define the attributes for the image component.
  2. The request is done through the Coil image library. Here, the url from the image is set and the crossfade parameter enabled. This is set just to create a smooth animation for when the image is fetched.
  3. The painter object needs to be set using rememberImagePainter, so recomposition can take place when the image is downloaded. Otherwise, the screen will show AddImagePreviewEmpty from Loading state.
  4. Image Composable, responsible to draw the image on the screen. It's worth mentioning that you should always set contentDescription for accessibility purposes.
  5. Finally, depending on the current state of the image one might want to show different screens. For instance, if an image fails to load, you may want to add a retry button. Here, you're going to fallback and use always the same - AddImagePreviewEmpty

Add the AddImagePreviewEmpty function:

@Composable
fun AddImagePreviewEmpty(modifier: Modifier) {
  Image(
    painter = painterResource(id = R.drawable.ic_launcher),
    contentDescription = stringResource(id = R.string.description_preview_error),
    modifier = modifier
  )
}

Now that you've created the application image components and logic. It's time to navigate to HomeContent.kt and add some images!

Adding lists

First, update the HomeContent to receive a list of images:

@Composable
fun HomeContent(
   images: List<Image>
) {
   // Your code will be here
}

Now that this is done and since it's receiving a list, this means that you'll need to implement a list in Compose. Doing this in XML would mean that you're going to use RecyclerView so you needed to create it along with an Adapter and the XML files for the entries.

In Compose this is really simple, to create a list you just need to use:

For vertical lists

For horizontal lists

With this in mind add:

@Composable
fun HomeContent(
   images: List<Image>
) {
   LazyColumn(
       modifier = Modifier.fillMaxWidth(),

       content = {
           items(images) {

               AddUnsplashImage(image = it)
           }
       }
   )
}

Now that the list is already added and is calling the AddUnsplashImage function that was created before, it's time to call the Composables from ImagePreview.

@Composable
fun AddUnsplashImage(image: Image) {

    AddImagePreview(
            url = image.urls.regular ?: "",
            modifier = Modifier
              .fillMaxWidth()
              .height(200.dp)
              .clip(RoundedCornerShape(8.dp))
              .background(color = colorContentSecondary)
        )
}

In this Modifier, you're setting the clip function with a shape - RoundedCornerShape. This is going to transform the image and apply round corners to it.

Now that the UI is ready, it's only missing connecting to the existing view model - UnsplashViewModel - so the data can be retrieved.

Open MainActivity.kt and declare a property with the UnsplashViewModel type after the class declaration.

private val unsplashViewModel: UnsplashViewModel by viewModels()

Once done, it's time to call fetchImages and send the result to HomeContent.

class MainActivity : AppCompatActivity() {

    //1
    private val unsplashViewModel: UnsplashViewModel by viewModels()

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

        //2
        unsplashViewModel.fetchImages()

        setContent {
            //3
            val images = unsplashViewModel.images.observeAsState()

                //4
                HomeContent(
                    images = images.value ?: emptyList()
                )
        }
    }
}

Going through this newly updated code:

  1. Declare the UnsplashViewModel property
  2. Once done, inside onCreate call fetchImages, so the Unsplash API is queried for the latest trending images
  3. Create an observer for the images field that will hold the images
  4. Remove the AboutContent call and change it instead to HomeScreen where you're going to send the newly retrieved images

Every time the images field is updated, MainScreen will recomposite and update the UI with the new content.

If you try to compile the project now, you'll see that observeAsState is missing. This is because it's part of the runtime-livedata library. So before you compile the project, go over to the build.gradle.kts file and under dependencies add:

implementation("androidx.compose.runtime:runtime-livedata:1.1.0-alpha05")

Compile and run the project and let's see which beautiful images the Unsplash API sends to you.

Creating more complex layouts

Now that you're seeing all the images, it's time to improve the layout. Every image is glued to the next one, there are no margins, and there's no information about the author and the name of the image. So let's improve this layout.

Open HomeContent.kt and scroll to the AddUnsplashImage function.

@Composable
fun AddUnsplashImage(image: Image) {

   //1
   Surface(
       modifier = Modifier.padding(
           start = 16.dp,
           end = 16.dp,
           top = 8.dp,
           bottom = 8.dp
       ),
       color = Color.Transparent
   ) {
      //2
       AddImagePreview(
           url = image.urls.regular ?: "",
           modifier = Modifier
               .fillMaxWidth()
               .height(200.dp)
               .clip(RoundedCornerShape(8.dp))
               .background(color = colorContentSecondary)
       )
       //3
       val verticalGradientBrush = Brush.verticalGradient(
           colors = listOf(
               colorContent20Transparency,
               colorContent85Transparency
           )
       )
    
       //4
       Column(
           modifier = Modifier
               .fillMaxWidth()
               .height(200.dp)
               .background(brush = verticalGradientBrush),
           verticalArrangement = Arrangement.Bottom
       ) {

           //5
           Column(
               modifier = Modifier.padding(16.dp)
           ) {

               Text(
                   text = image.description ?: "",
                   fontSize = 17.sp,
                   color = colorAccent
               )

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

               //6
               Text(
                   text = image.user.username,
                   fontSize = 15.sp,
                   color = colorAccent,
                   maxLines = 2,
                   overflow = TextOverflow.Ellipsis
               )
           }
       }
   }
}

Let's analyze this code step-by-step:

  1. You're using a Surface here because there are three different layers of the app that need to be put on top of each other:
  1. Image
  2. Gradient
  3. The author's name and the picture title
  1. The AddImagePreview Composable that you'd already defined
  2. A color gradient that's going to be used as an overlay on the image so the text is easier to read
  3. The previously defined gradient is added as a background on the Column. Also it's content is aligned to the bottom.
  4. A second Column is used here to hold the image description and the authors' name as well to define a padding on these components. If this was done on the previous Column, the background wouldn't fill the entire image.
  5. Since an image description can be of several lines, here you're limiting it to be at maximum two. If there's more, the end text is going to be replaced by ellipsis.

It's time to compile and run the app. Let's see how it looks after all of these changes 🎨.

Themes and styles are great for two things. First, they allow us to keep our components consistent. If we need to increase the font size we can just update it's value on the style and all the text that makes use of it will be updated. As apps get with more and more screens this is great to avoid missing that one text that was left with the old font size. Secondly, it seems night mode was the greatest feature two years ago, since most of the platforms and apps mentioned it's support. Having the possibility to use themes makes this implementation simpler (and faster).

Creating a specific font

If you open the font folder inside the resource files you'll see that there's a big_noodle_titling.ttf file over there. In this section you're going to update the app to use this font as default.

Start by creating a new file called Type.kt inside the ui/theme folder.

Now, it's time to load this font and apply it to all the Text Composables in the app.

private val BigNoodleTitlingFontFamily = FontFamily(
    Font(R.font.big_noodle_titling)
)

Define a set of font sizes to use afterward:

private val fontSizeLarge = 21.sp
private val fontSizeMedium = 17.sp
private val fontSizeSmall = 15.sp

Now that the attributes are defined it's time to set the typography:

val typography = Typography(
    h1 = TextStyle(
        color = colorAccent,
        fontFamily = BigNoodleTitlingFontFamily,
        fontWeight = FontWeight.Bold,
        fontSize = fontSizeMedium
    ),

    h2 = TextStyle(
        color = colorAccent,
        fontFamily = BigNoodleTitlingFontFamily,
        fontWeight = FontWeight.Normal,
        fontSize = fontSizeSmall
    ),

    h3 = TextStyle(
        color = colorAccent,
        fontFamily = BigNoodleTitlingFontFamily,
        fontSize = fontSizeLarge
    )
)

Setting a style

With everything defined it is now time to open HomeScreen.kt file again and update the Text Composables to use this newly defined TextStyle.

The first Text that you're going to find is where the image description is used, replace the fontSize and color for the corresponding typography.h1 and then the next Text Composable for typography.h2.

Text(
   text = image.description ?: "",
   style = typography.h1
)

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

Text(
   text = image.user.username,
   style = typography.h2,
   maxLines = 2,
   overflow = TextOverflow.Ellipsis
)

Finally, don't forget that there's still an AboutContent.kt file. Open it and update the same attributes to typography.h3.

Text(
   text = stringResource(id = R.string.about_text),
   style = typography.h3,
   textAlign = TextAlign.Center
)

Setting a theme

After defining a style, you can also define the theme that's going to be used in your app. This is particularly useful to avoid defining attributes like background, size, etc. for every component. It also makes it simple to define a light and a night theme.

Create a Theme.kt file in ui/theme. Here add the following code:

private val DarkColorPalette = darkColors(
   background = colorContent,
   onBackground = colorAccent
)

private val LightColorPalette = lightColors(
   background = colorAccent,
   onBackground = colorContent
)

@Composable
fun UnsplashTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {

   val colors = if (darkTheme) {
       DarkColorPalette
   } else {
       LightColorPalette
   }

   MaterialTheme(
       colors = colors,
       typography = typography,
       shapes = Shapes,
       content = content
   )
}

First you've defined two different color palettes that can be used throughout the app. In this case they correspond to the dark and light palettes which should be used in case the device is in night mode or light, respectively.

In order to decide which one the system should use, you'll check the isSystemInDarkTheme function, that inside looks at the uiMode. Depending, on each one is selected one of those two palettes will be used to set the background color of a Composable.

Now open MainActivity.kt and add the HomeContent invocation to be inside the UnsplashTheme Composable:

UnsplashTheme {
   HomeContent(
       images = images.value ?: emptyList()
   )
}

Finally, to see how you could apply this functionality, open HomeContent.kt and look for the LazyColumn call. Here, for now you were just defining it to fill the entire width of the screen. Add a background to it that will use the MaterialTheme.

LazyColumn(
   modifier = Modifier
       .fillMaxWidth()
       .background(color = MaterialTheme.colors.background),

   content = {
       items(images) {

           AddUnsplashImage(image = it)
       }
   }
)

Now compile and run the app. Switch between the light and night mode and see how your app adjusts 🌓.

Adding a top bar

Let's keep adding new features in the app, this time a top bar.

To do this, you're going to use a function called Scaffold that already creates the layout structure for the app. If you look at the parameters it receives:

@Composable
fun Scaffold(
   modifier: Modifier = Modifier,
   scaffoldState: ScaffoldState = rememberScaffoldState(),
   topBar: @Composable () -> Unit = {},
   bottomBar: @Composable () -> Unit = {},
  snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
   floatingActionButton: @Composable () -> Unit = {},
   floatingActionButtonPosition: FabPosition = FabPosition.End,
   isFloatingActionButtonDocked: Boolean = false,
   drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
   drawerGesturesEnabled: Boolean = true,
   drawerShape: Shape = MaterialTheme.shapes.large,
   drawerElevation: Dp = DrawerDefaults.Elevation,
   drawerBackgroundColor: Color = MaterialTheme.colors.surface,
   drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
   drawerScrimColor: Color = DrawerDefaults.scrimColor,
   backgroundColor: Color = MaterialTheme.colors.background,
   contentColor: Color = contentColorFor(backgroundColor),
   content: @Composable (PaddingValues) -> Unit
)

You can find the topBar that you're going to use, as well as other components that you typically can find on an Android app, for instance, the floatingActionButton.

Start by creating a new package called main that should be inside the ui folder. Followed by a new file called MainScreen.kt.

Once again, depending on, how you created the file remove the class declaration if it exists and add:

@Composable
//1
fun MainScreen(
   images: List<Image>
) {
  //2
   Scaffold(
       //3
       topBar = {
           TopAppBar(
               title = {
                   Text(
                       text = stringResource(id = R.string.app_name),
                       style = typography.h3,
                       //4
                       color = MaterialTheme.colors.onBackground
                   )
               },
               modifier = Modifier.fillMaxWidth(),
               backgroundColor = MaterialTheme.colors.background,
               elevation = 0.dp
           )
       },
       //5
       content = {
           HomeContent(
               images = images
           )
       }
   )
}

Let's dive in into this new function:

  1. As the name mentions, this is going to be the first screen of the app. So in a few steps you'll need to update the MainActivity.kt.
  2. Although this could be done manually, since there's Scaffold that already defines how an application should look, you're going to use it.
  3. There are two arguments that you're going to define here, the first one is the topBar and content. Starting with the first one, as you saw before, this argument receives a Composable function, so you'll need to create it. Fortunately, there's already a TopAppBar that you can use to implement this feature. You just need to define the Text.
  4. In the previous section you've defined a theme for the application, which sets the layout as white if the light mode is on or as black if it's night mode instead. Here, you're going to use onBackground color, because it's the exact opposite of the color used to set a background. If you were going to use the same, well... you wouldn't be able to read the text.
  5. The screen that should be used with the Scaffold.

I've mentioned earlier that this new function is going to be the app starting point, so head over MainActivity.kt and change the HomeScreen call to MainScreen.

UnsplashTheme {
   MainScreen(
       images = images.value ?: emptyList()
   )
}

Time to compile the app and see how it looks now!

Adding a navigation bar

Now that you've added the top bar, it's time to add the navigation bar. And yes, you're also going to use the Scaffold introduced before for that 🙌.

Start by creating a BottomNavigationScreen.kt file inside the main folder.

Here you're going to create a class that's going to be used to define each tab - a tag, it's the label text and the icon that should be displayed. In this newly created file add:

sealed class BottomNavigationScreens(
    val route: String,
    @StringRes val stringResId: Int,
    @DrawableRes val drawResId: Int
) {

    object Home : BottomNavigationScreens("Home", R.string.navigation_home, R.drawable.ic_home)
    object About : BottomNavigationScreens("About", R.string.navigation_about, R.drawable.ic_about)
}

Finally go back to the MainContent.kt file and update the Composable function to use the NavHost.

This component is part of the androidx.navigation library, so you need to add it to the build.gradle.kts inside the dependencies section.

implementation("androidx.navigation:navigation-compose:2.4.0-alpha10")

Wait for the project to synchronize. Once done, before the function declaration add:

private val DEFAULT_SCREEN = BottomNavigationScreens.Home

This field corresponds to the first tab that should be visible, in other words, selected.

Now you need to define a navigation controller. Since the app needs to keep track of the tab that's currently selected, you're going to use rememberNavController. After the function declaration add:

val navController = rememberNavController()

In the previous section when looking at the Scaffold function (you can scroll up, I'll wait 👀) you saw that there's a bottomBar parameter. Let's define it here.

Scaffold(
...
bottomBar = {
   val bottomNavigationItems = listOf(
       BottomNavigationScreens.Home,
       BottomNavigationScreens.About
   )

   MainBottomBar(
       navController = navController,
       items = bottomNavigationItems
   )
},
...
)

Here you're defining which tabs need to be added.

MainBottomBar is not yet defined, it's a custom Composable created to define a tab, so let's create it! Inside the main folder add a new file - MainBottomBar.kt.

Now create the Composable function - MainBottomBar:

private lateinit var selectedIndex: MutableState<Int>

@Composable
fun MainBottomBar(
   navController: NavHostController,
   items: List<BottomNavigationScreens>
) {

   BottomNavigation(
       backgroundColor = colorPrimary
   ) {

       //1
       selectedIndex = remember { mutableStateOf(0) }

       //2
       items.forEachIndexed { index, screen ->

           val isSelected = selectedIndex.value == index

           //3
           BottomNavigationItem(
               modifier = Modifier.background(MaterialTheme.colors.background),
               icon = {
                   Icon(
                       painter = painterResource(id = screen.drawResId),
                       contentDescription = stringResource(id = screen.stringResId)
                   )
               },
               label = {
                   Text(
                       stringResource(id = screen.stringResId),
                       style = typography.subtitle1
                   )
               },
               selected = isSelected,
               //4
               selectedContentColor = colorPrimary,
               unselectedContentColor = colorAccent,
               alwaysShowLabel = true,
               //5
               onClick = {
                   if (!isSelected) {
                       selectedIndex.value = index
                       navController.navigate(screen.route)
                   }
               }
           )
       }
   }
}

Going through the code:

  1. In order for the tab to be updated, when the user selects a different one, it needs to keep track of the one that's selected. For that you're going to remember selectedIndex so recomposition can occur and the color of the icon and text changes depending if it's selected or not.
  2. This list of items is the one that you've defined before in MainScreen.kt - it should contain the home and about screens.
  3. For each of these items a new BottomNavigationItem is created with the corresponding icon and label.
  4. Another cool feature here is that BottomNavigationItem can handle the selected and unselected states and apply the corresponding defined color.
  5. The onClick event that's responsible for updating the index and changing the screen.

Since we're using styles and themes don't forget to add at Type.kt the subtitle1 definition.

subtitle1 = TextStyle(
   fontFamily = BigNoodleTitlingFontFamily,
   fontWeight = FontWeight.Normal,
   fontSize = fontSizeSmall
)

Finally, get back to MainScreen.kt it's time to make the final adjustment. For that you need to update the content block of the Scaffold with:

    NavHost(navController, startDestination = DEFAULT_SCREEN.route) {
        composable(BottomNavigationScreens.Home.route) {
            HomeContent(
                images = images
            )
        }
        composable(BottomNavigationScreens.About.route) {
            AboutContent()
        }
    }

This way when the user presses a different tab then the one that's selected, it will automatically recreate this part of the screen.

Compile and run the app to see how it looks!

Your app is coming along quite nicely! Well done 🙌!

In this chapter you're going to see how you can interact with it. The goal is for the user to enter some text and the app should search photos related to that word in the Unsplash repository.

Start by opening the MainContent.kt file and lets add a new item to the list:

content = {
  item {
    AddSearchField(onSearchAction)
  }
...
}

So you now need to define the AddSearchField function and onSearchAction action.

Let's start by adding the search field:

@Composable
fun AddSearchField(onSearchAction: (String) -> Unit) {
   //1
   val search = remember { mutableStateOf("") }

   //2
   val focused = remember { mutableStateOf(false) }

   //3
   OutlinedTextField(
       //4
       value = search.value,
       //5
       onValueChange = { value ->
           search.value = value
       },
       modifier = Modifier
           .fillMaxWidth()
           .padding(16.dp)
           .onFocusChanged {
               focused.value = it.isFocused
           },
       //6
       placeholder = {
           Text(
               text = stringResource(id = R.string.search_hint),
               style = typography.h3,
               color = MaterialTheme.colors.onBackground
           )
       },
       //7
       leadingIcon = {
           val icon = painterResource(id = R.drawable.ic_search)
           val description =
               stringResource(R.string.description_search)

           Icon(
               painter = icon,
               contentDescription = description
           )
       },
       //8
       colors = TextFieldDefaults.outlinedTextFieldColors(
           focusedBorderColor = colorPrimary,
           unfocusedBorderColor = colorAccent,
           leadingIconColor = colorAccent,
           cursorColor = colorAccent,
           textColor = colorAccent
       ),
       //9
       keyboardOptions = KeyboardOptions.Default.copy(
           imeAction = ImeAction.Done
       ),
       //10
       keyboardActions = KeyboardActions {
           onSearchAction(search.value)
       }
   )
}

I know that is a bit of code, so let's go step-by-step:

  1. You'll need to remember the text that the user has added. This is reflected in point 4 where this text is set. Everytime a new character is added it triggers the OutlinedTextField to recompose.
  2. If the OutlinedTextField is focused, it should show a different color. Similar to property defined on 1, focused is used to recomposite this field when the user interacts with it.
  3. The Composable function to be used. There's also TextField, being the difference between both - that this one creates an outline around the field.
  4. The text that should be shown.
  5. Every time the user adds or removes a character this function is triggered. You'll need to update search with this new value so the field can be recomposed.
  6. The equivalent of using hint in an EditText. Tells the user what action is expected, in this case it will search for an image that corresponds to that topic.
  7. An icon that's used before the text that's going to be added.
  8. A set of colors used to define the look of the TextField.
  9. Setting the KeyboardOptions with the ImeAction Done will change the keyboard layout to show the bottom right key as a tick.
  10. Finally the KeyboardActions defines what should happen when the user clicks on the key mentioned in 9. In this scenario it will look for images that belong to the text that was just entered.

Now that the function is done, you'll need to update the HomeContent declaration, so it receives the onSearchAction:

fun HomeContent(
   images: List<Image>,
   onSearchAction: (String) -> Unit
)

All the calls before, so in MainScreen.kt:

fun MainScreen(
   images: List<Image>,
   onSearchAction: (String) -> Unit
)

and in the content parameter:

   HomeContent(
       images = images,
       onSearchAction = onSearchAction
   )

And in MainActivity.kt:

MainScreen(
   images = images.value ?: emptyList(),
   onSearchAction = { topic -> unsplashViewModel.searchForATopic(topic) }
)

Now that everything is done, compile and run the project and let's see how it looks!

As you've reached the last chapter of this codelab, I wanted to share something that I believe that's going to help you develop your next apps - alicerce.

Alicerce is a template that I've created that automatically setups an android and a desktop application that shares most of its code via:

Give it a go! You can find the repository here.

Alicerce GitHub repository

There's also a full version of Unsplash using this template in this repository.

Unsplash (shared code) GitHub repository

If you have any questions just reach me at twitter. All the feedback is more than welcome.

Follow me on twitter 🐦

See you in the next event! 🖖