Local Composition for More Efficient Code on Your Android Composable App

Muhammad Utsman
Gravel Product & Tech
5 min readJul 8, 2024

CompositionLocal is a mechanism in Compose used to share data hierarchically within Compose components without explicitly passing it as a parameter. This makes the code cleaner and more efficient.

With CompositionLocal, you can store and access data at a specific composition level without needing to pass the data through parameters. This is useful in situations where you want to share data among multiple components within a composition hierarchy without having to send it separately to each component.

There are several advantages to implementing CompositionLocal:

  1. Isolation of State at Composition Level: CompositionLocal allows you to isolate state or data at a specific composition level. This means that certain state or data can only be accessed by components within that composition hierarchy, reducing the risk of unwanted changes.
  2. Better Code Understanding: With CompositionLocal, the usage of state or data within components doesn’t need to be explained through repeated parameters. This enhances code understanding, making it cleaner and more consistent.
  3. Separation Between Components: CompositionLocal helps separate dependencies among components within the UI hierarchy. This allows you to replace or update a component without affecting other components within the same hierarchy.
  4. Flexible Usage: You can easily use CompositionLocal to share data or state related to UI presentation, such as color themes, languages, text scales, and much more. This provides flexibility in managing more specific displays.
  5. Avoiding Global Dependencies: CompositionLocal helps avoid the use of widely shared global variables or states, which can lead to confusion and difficulty in tracing the source of issues.
  6. Simplified Dependency Management: CompositionLocal can serve as a simpler alternative to managing dependencies compared to Dependency Injection. This is particularly useful in smaller projects or simple UI compositions.
  7. Ease of Use: Using CompositionLocal is straightforward. You only need to define the CompositionLocal key, provide the value through CompositionLocalProvider, and access it within components within the hierarchy.

However, it’s important to remember that CompositionLocal has its limitations. It’s not used for managing complex dependencies or for storing data that needs to exist outside the UI composition lifecycle. In such cases, Dependency Injection might still be a more suitable solution.

Overall, CompositionLocal is a powerful tool for managing state and dependencies at the composition level within the Compose framework. It helps create more structured, clean, and readable code.

Since CompositionLocal can store the state of an instance, there's no need to worry about memory leaks because new instances won't be recreated during recomposition. This characteristic is similar to the functionality of Dependency Injectors and Service Locators in terms of accessibility.

With this functionality, the usage of DI (Dependencies Injection) libraries like Dagger and Koin can be minimized. Why minimize it? That’s a good question. There are several impacts of excessive use of DI or Service Locators, including:

  1. Performance: Using DI (Dependencies Injection) or Service Locators can negatively impact application performance. The mechanism of injection or dependency lookup can require extra time at runtime, potentially disrupting the application’s responsiveness, especially if not well optimized.
  2. Overhead and Memory: The usage of DI or Service Locators can introduce extra memory overhead, especially when dealing with complex object hierarchies or heavy dependencies. This can affect memory consumption and lead to performance issues.
  3. Code Readability: Too many dependencies in the codebase can make it difficult to read and understand. Excessive openness of dependencies or unstructured distribution can make the execution flow convoluted and confusing.
  4. Architectural Impact: Overusing DI or Service Locators can obscure the application’s architecture and create complex interdependencies between components. This can make development harder and less structured.
  5. Security: Injecting or locating too many dependencies can open up potential security issues. Sensitive data might be exposed or misused if dependencies aren’t managed properly.

To avoid these issues, careful consideration is needed when using DI or Service Locators. With CompositionLocal, many aspects of DI or Service Locator usage can be replaced. Let’s consider a case.

Suppose we have a CartViewModel class that has a dependency on CartUseCase at the constructor level.

class CartViewModel(private val useCase: CartUseCase) : ViewModel() {

val cartState get() = useCase.cartReducer.dataFlow.asStateFlow()

val cartUiConfig: MutableStateFlow<CartUiConfig> = MutableStateFlow(CartUiConfig())
}

The common approach is to inject the CartUseCase instance into the constructor. However, with CompositionLocal, we can manually inject it into the node that requires it using the previously provided CartUseCase instance. This is an original technique with a fairly efficient trick, eliminating the need for additional libraries for DI or Service Locator.

At the node level, the implementation will look like this:

@Composable
fun Cart() {
// get instance with local composition
val useCase = LocalCartUseCase.current
val viewModel = rememberViewModel { CartViewModel(useCase) }

// composable layout
}

The CartUseCase will be obtained from the CompositionLocal named LocalCartUseCase. Of course, LocalCartUseCase is defined beforehand and provided within the same CompositionLocalProvider scope.

// define with error default value
val LocalCartUseCase = compositionLocalOf<CartUseCase> { error("Not provided") }

@Composable
fun App() {
// create instance
val cartUseCase = remember { CartUseCase.newInstance() }

// provide instance into local composition
CompositionLocalProvider(
LocalCartUseCase provides cartUseCase
) {
CommonTheme {
// content
}
}
}

In this structure:

  • The LocalCartUseCase is declared using compositionLocalOf, which defines a CompositionLocal key with a default value that throws an error if the value is not provided.
  • Within the App() composable function, an instance of CartUseCase is created using remember to ensure it persists across recompositions.
  • The created cartUseCase instance is then provided to the CompositionLocalProvider using the syntax LocalCartUseCase provides cartUseCase. This means that any composable function within the scope of this provider can access the provided cartUseCase instance using LocalCartUseCase.current.

This way, the useCase variable in the Cart() composable function can retrieve the CartUseCase instance that was provided and stored in the LocalCartUseCase CompositionLocal.

However, despite this efficiency, it can’t entirely replace DI or Service Locators at the architecture layer level, especially when dealing with the creation of UseCases or Repositories that require classes outside of the Compose node.

You can found example implementation in my playground Kotlin Compose Multiplatform here.

Conclusion

In conclusion, CompositionLocal is a mechanism in Jetpack Compose for sharing data hierarchically between Compose components without having to pass it explicitly as parameters. This makes the code cleaner and more efficient.

CompositionLocal has several advantages such as isolating state at certain composition levels, better code understanding, separation between components, flexible use, avoiding global dependencies, and simpler dependency management.

However, CompositionLocal has limitations. It is not used to manage complex dependencies or to store data that needs to exist outside the UI composition lifecycle.

This concept is also being implemented in our migration to Jetpack Compose for the SalamChat app, an engaging and feature-rich messaging app. You can see our application on salamchat.id.

This article is part of a story series about modern android development. Stay tuned for the next article!

--

--