Visualizing CompositionLocal in the Composition Tree
Source: Dev.to
Visualizing Data Flow Through the Compose Tree
When Compose runs your composables, it builds an internal tree structure:
Your Code: Composition Tree:
───────────── ─────────────────
@Composable
fun App() { [App]
Screen() ───────► │
} [Screen]
│
@Composable [Card]
fun Screen() { │
Card() [Text]
}
Locals Map on Each Node
Each node can have an optional locals map attached.
Without CompositionLocalProvider
─────────────────────────────────
[App] ← locals: { }
│
[Screen] ← locals: { }
│
[Card] ← locals: { }
│
[Text] ← locals: { }
With CompositionLocalProvider
──────────────────────────────
[App] ← locals: { }
│
┌────────────────────────────────────────────┐
│ CompositionLocalProvider(LocalTheme → Dark)│
└────────────────────────────────────────────┘
│
[Screen] ← locals: { LocalTheme → Dark } ✓ ATTACHED HERE
│
[Card] ← locals: { } (inherits from parent)
│
[Text] ← locals: { } (inherits from parent)
When you call LocalTheme.current, Compose walks up the tree until it finds a provider:
@Composable
fun Text() {
val theme = LocalTheme.current // How does this work?
}
Lookup Walk‑through
LOOKUP: LocalTheme.current from [Card]
──────────────────────────────────────
[App] ← 4. Still not found? Use default or throw
│
[Screen] ← 3. FOUND! LocalTheme → Dark ✓ RETURN THIS
│ (Provider attached here)
[Card] ← 2. Not here, look at parent...
│
[Text] ← 1. Start here: "I need LocalTheme"
Shadowing Example
@Composable
fun App() {
CompositionLocalProvider(LocalTheme provides Dark) {
Screen()
CompositionLocalProvider(LocalTheme provides Light) {
AnotherScreen()
}
}
}
TREE WITH SHADOWING:
────────────────────
[App]
│
┌─────────────────────────────────────┐
│ Provider(LocalTheme → Dark) │
└─────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
[Screen] ┌─────────────────────────────┐
│ │ Provider(LocalTheme → Light)│ ← SHADOWS parent
[Card] └─────────────────────────────┘
│ │
[Text] [AnotherScreen]
│
LocalTheme.current [Card]
= Dark │
[Text]
LocalTheme.current
= Light ← SHADOWED VALUE
The Slot Table (Simplified)
Think of the slot table as a flat array that mirrors the tree:
| Index | Node | Locals Map | Parent Index |
|---|---|---|---|
| 0 | App | { } | null |
| 1 | Provider | { LocalTheme → Dark } | 0 |
| 2 | Screen | { } | 1 |
| 3 | Card | { } | 2 |
| 4 | Text | { } | 3 |
| 5 | Provider | { LocalTheme → Light } | 1 |
| 6 | AnotherScr | { } | 5 |
| 7 | Card | { } | 6 |
| 8 | Text | { } | 7 |
The slot table is a deep topic—don’t feel pressured to master it right away.
Lookup Algorithm (pseudo‑code)
function lookup(key, nodeIndex):
current = nodeIndex
while (current != null):
if (current.locals.contains(key)):
return current.locals[key]
current = current.parentIndex
return key.defaultValue // or throw if no default
Providing Multiple Locals
CompositionLocalProvider(
LocalTheme provides Dark,
LocalUser provides currentUser,
LocalAnalytics provides analyticsService
) {
Content()
}
[App]
│
┌─────────────────────────────────────────────────────┐
│ Provider │
│ locals: { │
│ LocalTheme → Dark │
│ LocalUser → User(name="John") │
│ LocalAnalytics → AnalyticsServiceImpl │
│ } │
└─────────────────────────────────────────────────────┘
│
[Content]
│
┌───────┴───────┐
│ │
[Header] [Body]
│ │
[Avatar] [Posts]
LocalUser.current → User("John")
LocalTheme.current → Dark
When a Value Changes (compositionLocalOf)
Provider(LocalCounter → 1) ──changes to──► Provider(LocalCounter → 2)
│ │
[Screen] ← NOT recomposed [Screen]
│ (doesn't read it) │
[Counter] ← RECOMPOSED! ✓ [Counter] ← reads LocalCounter
│ (reads LocalCounter.current) │
[Label] ← NOT recomposed [Label]
(doesn't read it)
Only nodes that actually READ the value get recomposed!
Summary Diagram
┌──────────────────────────────────────────────────────────────┐
│ COMPOSITION TREE │
└──────────────────────────────────────────────────────────────┘
│
│ ┌─────┐ │
│ │Node │ ◄─── Each node can have a "locals" attachment │
│ └──┬──┘ │
│ │ ┌──────────────────────┐ │
│ │ │ locals: { │ │
│ │ │ Key1 → Value1 │ ◄── Provider attaches │
│ │ │ Key2 → Value2 │ key‑value pairs │
│ │ │ } │ │
│ │ └──────────────────────┘ │
│ ┌──┴──┐ │
│ │Child│ ◄─── Children inherit (lookup walks up the tree) │
│ └─────┘ │
│ │
│ KEY INSIGHT: Data is NOT copied down. │
│ It's attached at one node, and lookups walk UP to find it. │
└──────────────────────────────────────────────────────────────┘