DEV Community

Ayush Bansal
Ayush Bansal

Posted on

Visualizing CompositionLocal in the Composition Tree

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]
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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?
}
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

4. Shadowing with Nested Providers

@Composable
fun App() {
    CompositionLocalProvider(LocalTheme provides Dark) {
        Screen()
        CompositionLocalProvider(LocalTheme provides Light) {
            AnotherScreen()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

*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
Enter fullscreen mode Exit fullscreen mode

6. Visual: Multiple CompositionLocals

CompositionLocalProvider(
    LocalTheme provides Dark,
    LocalUser provides currentUser,
    LocalAnalytics provides analyticsService
) {
    Content()
}
Enter fullscreen mode Exit fullscreen mode
                    [App]
                      │
    ┌─────────────────────────────────────────────────────┐
    │  Provider                                           │
    │  locals: {                                          │
    │      LocalTheme     → Dark                          │
    │      LocalUser      → User(name="John")             │
    │      LocalAnalytics → AnalyticsServiceImpl          │
    │  }                                                  │
    └─────────────────────────────────────────────────────┘
                      │
                  [Content]
                      │
              ┌───────┴───────┐
              │               │
          [Header]        [Body]
              │               │
          [Avatar]        [Posts]

   LocalUser.current     LocalTheme.current
   = User("John")        = Dark
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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. │
└──────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Top comments (0)