[KMP Pattern Question] I am building a design sys...
# multiplatform
j
[KMP Pattern Question] I am building a design system library that will be consumed by Android, iOS, macOS, and Web. The library will vend everything from primitives (e.g. colors/gradients/fonts/etc.) to controls (e.g. buttons/selectors/text fields/etc.). Simple enough. Here's where things got thorny: This app also needs to specialize the appearance of everything based on a
theme
, which is a global appearance setting (you can think of it sort of like light/dark mode) that is only known at runtime. In other words, this library needs to return everything from
Color.primary
to
FloatingActionButton()
(for example) based on the runtime-determined
theme
of the app. The theme doesn't change across the app lifecycle (i.e. we know right at init what the theme will be and it's a constant for the rest of that app instance's existence), which is helpful. What I'm struggling with is where this
theme
setting should live. All of the components in this design system library will need to be able to read this value to work correctly, so I can imagine having it be a global var that consumers can set on the library itself. But when I attempt to build something like this:
Copy code
interface Theme {
    companion object {
        var value: Theme = DefaultTheme()
    }
}

class DefaultTheme(...): DefaultTheme {...}
...I see the following warning in the console:
Copy code
w: Theme.kt: With old Native GC, variable in singleton without @ThreadLocal can't be changed after initialization
And I'd generally like to avoid stored mutable state with my K/N libraries if at all possible. Would love to hear peoples' thoughts on the most idiomatic/safest approach to this!
k
We ran into this problem building Kermit. I mean, you run into in a number of places in theory, but it was a core issue, and it's a core issue in any of the logging library implementations. If you want global logger access, you need to have mutable state, and across threads in K/N. that means atomic.
There were several logging libraries that used ThreadLocal on the global config, but that meant only the main thread would have logging configured, which doesn't work.
We just (like an hour ago) pushed a new version that allows for global mutable, but also static local instances to work around the problem (or let you use atomics if you don't care).
The basic issue was the same, though. Global state can't really have an externally set init if it's also immutable.
I would suggest a few things here, though. I'd say none of them are great, but ideas to consider.
UI is generally main thread. If this will never be read from a different thread, then just annotate it ThreadLocal and you're done. Maybe add a check on access to ensure main thread (throw if not).
If theme should be accessible from another thread, I'd have to think about that a bit, and it may be slightly different with old and new K/N memory model (btw, at runtime you can check with Platform.memoryModel == MemoryModel.STRICT)
❤️ 3
On the "generally like to avoid stored mutable state with my K/N libraries" problem, I tend to agree, but this is a case where you need to make a decision. I feel the same way, so much so that (as mentioned) we didn't have a global logging option, but eventually I caved on that.
So, do other thread need access? Does accessing through an atomic present concerns (performance, etc)?
You can try some weird things, like having a global atomic that you set with the default, and have a ThreadLocal initialize itself by reading from that global. Not pretty, but functional (if the state never needs to change)
j
Thanks this is super helpful!
Much appreciated
k
Had another thought. You could have, say, a global object called something like "ThemeConfig" with a public function tht lets you set the theme info, which is stored in mutable (atomic) state, then when you first access the theme companion, it reads the config from "ThemeConfig". In my tests, companion isn't initialized until first access, so it should work. Not super elegant, but still.
Would do code samples but on the phone currently :)
p
very late to the party here, but it seems to me like a thing to change could be this:
a global appearance setting … the components in this design system library will need to be able to read this value
I would have preferred a function
(Theme) -> DesignSystem
that gives you a fully-configured, immutable
DesignSystem
instance for the specified
Theme
. You’d invoke that function based on the user’s configured value at app startup, no need for any mutable state that I can see? Using DI rather than accessing a global singleton is preferable for a lot of reasons, IMO.