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.

Different ways of providing composition locals

Depending on the use case and the frequency of data changes

staticCompositionLocalOf()

  • 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 use staticCompositionLocalOf.
  • A good place to use it is a navController in the app.
  • Several composables might use the navController but passing the navController in the hierarchy in several places might become inconvenient especially if multiple places there are many navigations happen.

Example implementations of staticCompositionLocalOf

Providing values to the composition local: Example usage of NavController

CompositionLocals.kt

val LocalNavigationProvider = staticCompositionLocalOf<NavHostController> { error("No navigation host controller provided.") }
  • This creates a 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 navigationController lives within the composables of MainActivity.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") }) { ... }
}

Using custom composition local: Example with a custom theme

  • 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 holds custom-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")
    }
  }
}

compositionLocalOf()

  • Contrary to staticCompositionLocalOf(), The compositionLocalOf() 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 our CompositionLocals.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
}
⚠️ **GitHub.com Fallback** ⚠️