Composition Local: Creating your own composition locals - devrath/ComposeAlchemy GitHub Wiki
Sometimes you need to implement your composition locally.
- Providing
NavigationController
to different composables in your application. - Implementing a
CustomTheme
for your application.
Depending on the use case and the frequency of data changes
- When we use this, Any change in the
composition-local
will cause the entire UI to be re-drawn. - When the value of the
composition-local
does not change often, we usestaticCompositionLocalOf
. - A good place to use it is a
navController
in the app. - Several composables might use the
navController
but passing thenavController
in the hierarchy in several places might become inconvenient especially if multiple places there are many navigations happen.
Providing values to the composition local: Example usage of NavController
Using custom composition local: Example with a custom theme
CompositionLocals.kt
val LocalNavigationProvider = staticCompositionLocalOf<NavHostController> { error("No navigation host controller provided.") }
- This creates a static
compositionLocal
of typeNavHostController
- During creation, you can assign a default value to use. In this case, you can’t assign a default value to
CompositionLocal
because thenavigationController
lives within the composables ofMainActivity.kt
. Instead, we throw an error. - Remember, It is important to decide if the
compositionLocal
needs a default value now or to be provided later. So we can do such a way that if it's not provided later, We throw an error. -
Best practice is to start the variable name with
Local
so that other developers are aware of its purpose it.
MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Wrap your theme with the composition-local-provider block and provide a default value.
CompositionLocalProvider(LocalNavigationProvider provides rememberNavController()) {
ToReadListTheme {
// Get the navController from the local-navigation-provider
val navController = LocalNavigationProvider.current
NavHost(navController = navController, startDestination = "booklist") {
composable("booklist") {
// Individual screen
BookListScreen(books)
}
}
}
}
}
}
}
-
CompositionLocalProvider
helps bind your CompositionLocal with its value. -
LocalNavigationProvider
is the name of your own CompositionLocal. - provides 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.
BookListScreen.kt
@Composable
fun BookListScreen(
books: List<Book>
) {
val navigationController = LocalNavigationProvider.current
FloatingActionButton(onClick = { navigationController.navigate("search") }) { ... }
}
- Define your custom colors Colors.kt
data class MyReadingColors(
val primary100: Color = Color(0xFF108575),
val primary90: Color = Color(0xFF22AE9A),
val primary10: Color = Color(0xFFCDFDEE)
)
- Define your custom fonts and add them to
textstyle
variables Type.kt
val ubuntuLight = FontFamily(
Font(resId = R.font.ubuntu_light, style = FontStyle.Normal)
)
val ubuntuRegular = FontFamily(
Font(resId = R.font.ubuntu_regular, style = FontStyle.Normal)
)
data class MyReadingTypography(
val H5: TextStyle = TextStyle(
fontFamily = ubuntuRegular,
fontSize = 20.sp,
lineHeight = 22.sp,
fontWeight = FontWeight.Normal
),
val subtitle: TextStyle = TextStyle(
fontFamily = ubuntuLight,
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Normal
)
)
- Define it in our composition locals file. One holds the
custom-colors
and another holdscustom-theme
CompositionLocals.kt
val LocalColorsProvider = staticCompositionLocalOf { MyReadingColors() }
val LocalTypographyProvider = staticCompositionLocalOf { MyReadingTypography() }
- Define the local provider in the Theme file Theme.kt
// Create the object MyReadingTheme that holds two style-related variables.
object MyReadingTheme {
// Add the colors variable of type MyReadingColors.
val colors: MyReadingColors
// Create a custom getter for colors. This method provides the current value of your LocalColorsProvider.
@Composable
get() = LocalColorsProvider.current
// Add the typography variable of type MyReadingTypography.
val typography: MyReadingTypography
// Add a custom getter for typography. This method provides the current value of your LocalTypographyProvider.
@Composable
get() = LocalTypographyProvider.current
}
- Now access the colors and topography as below
@Composable
fun ToReadListTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalColorsProvider provides MyReadingColors(),
LocalTypographyProvider provides MyReadingTypography()
) {
MaterialTheme(
colors = lightColors(
primary = MyReadingTheme.colors.primary100,
primaryVariant = MyReadingTheme.colors.primary90,
secondary = MyReadingTheme.colors.secondary100,
secondaryVariant = MyReadingTheme.colors.secondary90
),
content = content
)
}
}
- Use it in composable
Column {
Text(text = book.title, style = MyReadingTheme.typography.H5)
Spacer(modifier = Modifier.height(4.dp))
Text(text = book.author, style = MyReadingTheme.typography.subtitle)
Spacer(modifier = Modifier.height(4.dp))
if (showAddToList) {
Button(
onClick = {
onAddToList(book)
Toast.makeText(context, "Added to list", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Add to List")
}
}
}
- Contrary to
staticCompositionLocalOf()
, ThecompositionLocalOf()
will only invalidate the composables that read its current value. - It is tempting to use
compositionLocalOf
in all places but good practice is- You can provide a value through CompositionLocal when the value is a UI tree-wide value.
- The
staticCompositionLocalOf
is theme-related values and paddings you implemented in the previous sections can be used by all composables, a subset, and even several composables at once. - You need to provide a good default value, or as you learned, throw an error if you forget to provide a default value.
- Add the data class in the Theme.kt file Theme.kt
data class MyReadingPaddings(
val small: Dp,
val medium: Dp
)
- Next add the
compositionlocal
in ourCompositionLocals.kt
file. Also we have already provided the default value.
val LocalPaddings = compositionLocalOf { MyReadingPaddings(small = 8.dp, medium = 16.dp) }
- Below is how it is being used
Card(
modifier = modifier
.fillMaxWidth()
.padding(all = LocalPaddings.current.small),
elevation = 12.dp,
shape = RoundedCornerShape(size = 11.dp)
) {
// Other content
}