CompositionLocal in Jetpack Compose
Learn about CompositionLocal in Jetpack Compose and implement an efficient way for multiple composables to access data. By Rodrigo Guerrero.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
CompositionLocal in Jetpack Compose
25 mins
- Getting Started
- Introduction to Jetpack Compose Architecture
- Understanding Unidirectional Data Flow
- Implementing Unidirectional Data Flow
- Getting to Know CompositionLocal
- Learning About Predefined CompositionLocals
- Using Existing CompositionLocals
- Dismissing the Keyboard
- Creating Your Own CompositionLocals
- Using staticCompositionLocalOf()
- Providing Values to the CompositionLocal
- Using a Custom CompositionLocal With a Custom Theme
- Using compositionLocalOf()
- Understanding When to Use CompositionLocal
- Alternatives to CompositionLocal
- Where to Go From Here?
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:
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:
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
:
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()
.
LocalSoftwareKeyboardController
that states This API is experimental and is likely to change in the future.
To make the warning go away, add @OptIn(ExperimentalComposeUiApi::class)
outside the definition of SearchScreen()
.
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.
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 thenavController
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:
- Wrap the code with
CompositionLocalProvider
. - 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.