Let me break this down with visual diagrams showing how data flows through the tree.
1. The Composition Tree Structure
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]
}
2. How CompositionLocal Attaches Data
Think of each node having 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)
3. The Lookup Mechanism (Walking Up the Tree)
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: 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"
4. Shadowing with Nested Providers
@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
5. Internal Data Structure (Simplified)
Compose uses a Slot Table - think of it as a flat array that mirrors the tree:
SLOT TABLE (simplified view):
─────────────────────────────
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
*slot table is a topic in itself, don't be too hard on yourself to understand this
Lookup algorithm:
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
6. Visual: Multiple CompositionLocals
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 LocalTheme.current
= User("John") = Dark
7. The "Recomposition Scope" Aspect
WHEN 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 Mental Model
┌──────────────────────────────────────────────────────────────┐
│ 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. │
└──────────────────────────────────────────────────────────────┘
Top comments (0)