CompositionLocal in Jetpack Compose

Learn about CompositionLocal in Jetpack Compose and implement an efficient way for multiple composables to access data. By Rodrigo Guerrero.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Learning About Predefined CompositionLocals

Jetpack Compose provides multiple predefined CompositionLocal implementations that start with the word Local, so it’s easy for you to find them:

Predefined composition locals

Using Existing CompositionLocals

For this exercise, you’ll add a book image to each book in your reading list by using the current context.

Open Book.kt. Add the following as the first line in the BookRow() composable:

val context = LocalContext.current

Android provides the LocalContext class that has access to the current context. To get the actual value of the context, and any other CompositionLocal, you access its current property.

Make the following code the first element of Row(), right before Column().

AsyncImage(
  modifier = Modifier
    .width(120.dp)
    .padding(end = 8.dp),
  model = ImageRequest
    .Builder(context)
    .data(book.coverUrl)
    .error(context.getDrawable(R.drawable.error_cover))
    .build(),
  contentScale = ContentScale.Crop,
  contentDescription = book.title
)

This code adds and loads an image to each book row using the Coil library. It uses the context provided by LocalContext.

Build and run. Now you can see those covers:

Books with images

Next, you’ll use a Toast message to give feedback whenever you add a book to the list.

Open Book.kt and replace the Button code at the end of BookRow() composable with the following:

Button(
  onClick = {
    onAddToList(book)
    Toast.makeText(context, "Added to list", Toast.LENGTH_SHORT).show()
  },
  modifier = Modifier.fillMaxWidth()
) {
  Text(text = "Add to List")
}

This code displays the Toast message by using the context that you obtained previously with LocalContext.current. You didn’t have to pass the context down to this composable to use it.

Build and run. Add a book to your reading list. Notice the Toast:

Toast when adding a book

Did you notice the keyboard stays on screen after you search for books in the search screen? You’ll fix that next!

Dismissing the Keyboard

Android provides LocalSoftwareKeyboardController that you can use to hide the soft keyboard when needed.

Open SearchScreen.kt and add the following line of code below the searchTerm definition:

val keyboardController = LocalSoftwareKeyboardController.current

To make the warning go away, add @OptIn(ExperimentalComposeUiApi::class) outside the definition of SearchScreen().

Note: You’ll see a warning after adding LocalSoftwareKeyboardController that states This API is experimental and is likely to change in the future.

Update keyboardActions inside the OutlinedTextField composable as follows:

keyboardActions = KeyboardActions(
  onSearch = {
    // 1.
    keyboardController?.hide()
    onSearch(searchTerm)
  },
  onDone = {
    // 2.
    keyboardController?.hide()
    onSearch(searchTerm)
  }
),

You just added the necessary code in sections one and two to hide the soft keyboard when the user presses the search or done buttons on the keyboard.

Build and run. Navigate to the search screen and search for a book. After you press the search key on the keyboard, the keyboard will disappear. Great work!

As you saw in this section, there are several existing CompositionLocal implementations for your use. You also have the option to create your own and will dig into that concept next.

Creating Your Own CompositionLocals

In some scenarios, you may want to implement your own CompositionLocal. For example, to provide the navigation controller to the different composables in your UI or implement a custom theme for your app.

You’re going to work through these two examples in the following sections.

Jetpack Compose provides two ways to use CompositionLocal, depending on the frequency that the data changes:

  • staticCompositionLocalOf()
  • compositionLocalOf()

Using staticCompositionLocalOf()

One way to create your own CompositionLocal is to use staticCompositionLocalOf(). When using this, any change on the CompositionLocal value will cause the entire UI to redraw.

When the value of your CompositionLocal doesn’t change often, staticCompositionLocalOf() is a good choice. A good place to use it is with the navController in the app.

Several composables may use the controller to perform navigation. But passing the navController down to all the composables can quickly become inconvenient, especially if there multiple screens and places where navigation can take place.

Besides, for the entire lifetime of the app, the navigation controller remains the same.

So now that you understand its value, you’ll start working with CompositionLocal.

Open CompositionLocals.kt, and add the following code:

val LocalNavigationProvider = staticCompositionLocalOf<NavHostController> { error("No navigation host controller provided.") }

This line creates your static CompositionLocal of type NavHostController. During creation, you can assign a default value to use.

In this case, you can’t assign a default value to CompositionLocal because the navigation controller lives within the composables in MainActivity.kt. Instead, you throw an error.

It’s important to decide wether your CompositionLocal needs a default value now, or if you should provide the value later and plan to throw an error if it’s not populated.

Note: A best practice is to begin the name of your provider with the prefix Local so that developers can find the available instances of CompositionLocal in your code.

Open MainActivity.kt then replace the creation of the navController with the following line:

val navController = LocalNavigationProvider.current

You get the actual value of your CompositionLocal with the current property.

Now, replace the call to BookListScreen() with the following:

BookListScreen(books)

This composable doesn’t need to receive the navController anymore, so you remove it.

Open BookListScreen.kt, and remove the navController parameter, like this:

@Composable
fun BookListScreen(
  books: List<Book>
) {

You removed the parameter, but you still need to provide the navController to handle the navigation.

Add the following line at the beginning of the method:

val navController = LocalNavigationProvider.current

You get the current value of your navigation controller, but instead of passing it explicitly, you have implicit access.

Build and run. As you’ll notice, the app crashes.

Open Logcat to see the following error:

2022-07-02 15:55:11.853 15897-15897/? E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.rodrigoguerrero.toreadlist, PID: 15897
  java.lang.IllegalStateException: No navigation host controller provided.

The app crashes because you didn’t provide a value for the LocalNavigationProvider — now you know you still need to do that!

Providing Values to the CompositionLocal

To provide values to your CompositionLocal, you need to wrap the composable tree with the following code:

CompositionLocalProvider(LocalNavigationProvider provides rememberNavController()) {

}

In this code:

  • CompositionLocalProvider helps bind your CompositionLocal with its value.
  • LocalNavigationProvider is the name of your own CompositionLocal.
  • provides is the infix function that you call to assign the default value to your CompositionLocal.
  • rememberNavController() — the composable function that provides the navController as the default value.

Open MainActivity.kt and wrap the ToReadListTheme and its contents with the code above. After you apply these changes, onCreate() will look as follows:

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

  setContent {
    // 1.
    CompositionLocalProvider(LocalNavigationProvider provides rememberNavController()) {
      ToReadListTheme {
        // 2.
        val navController = LocalNavigationProvider.current
        NavHost(navController = navController, startDestination = "booklist") {
          composable("booklist") {
            val books by bookListViewModel.bookList.collectAsState(emptyList())
            bookListViewModel.getBookList()
            BookListScreen(books)
          }
          composable("search") {
            val searchUiState by searchViewModel.searchUiState.collectAsState(SearchUiState())
            SearchScreen(
              searchUiState = searchUiState,
              onSearch = { searchViewModel.search(it) },
              onAddToList = { searchViewModel.addToList(it) },
              onBackPressed = {
                searchViewModel.clearResults()
                navController.popBackStack()
              }
            )
          }
        }
      }
    }
  }
}

Here, you:

  1. Wrap the code with CompositionLocalProvider.
  2. Read the current value of your CompositionLocal.

The value you provide is now available to the entire UI tree that CompositionLocalProvider surrounds.

Build and run once again — it shouldn’t crash anymore. Navigate to the search screen to observe that the navigation still works.