For developers: Kotlin
Chameleon is a collection of reusable tools and components that are divided into multiple packages. They can be used together or separately depending on your team’s needs.
chameleon-kotlin
// chameleon-native-publish insert-chameleon-version:
chameleonVersion = "6.3.0"
// chameleon-native-publish insert-chameleon-version:end
For our design system, Chameleon, we've created an implementation specifically for Kotlin to make it easy for you to create beautiful and polished user interfaces. Whether you're using Jetpack Compose or Xml, our design system provides you with all the colors, fonts, and other atoms you need to bring your UI to life.
The goal is to allow you to build an app that can efficiently switch its theme context. This goes beyond just changing between light and dark color scheme: it also allows you to theme parts of your screen. Everything can be subthemed. Subthemes can even be loaded dynamically, for example for a/b testing.
More info on how to use it with Jetpack Compose with ready made components can be found in the chameleon-kotlin/chameleoncomponents module. Here we illustrate how to use the tokens in a multi branded app and single branded app and how to load dynamic subthemes.
Modules
build-logic
(acts as single source of truth for managing the build logic for all modules in chameleon-kotlinchameleon
holds all models without values that are used in other modules to fill in with token values. This is the base of the entire system)chameleon.theme.template
not a module but used by codegen-kotlin to generate build.gradle.kts for each theme/brandchameleonAll
wrapper that contains all themes and subthemes. It manages logic to work with all the themes/brands at once.chameleoncomponents
collection of all the Chameleon components for KotlinchameleonThemes
- holds all tokens per theme/brand with their respective values for that themechameleon.theme.<#x>
- supplies an bootstrap function to fill the chameleon module models with values.demoAllThemes
- Demo app that shows how to use the Chameleon design system. It has a handy build-in theme switcher that you can use to see the Chameleon components in all their glory for each theme/brand.preview
- holds all previews and snapshot tests for the Chameleon components chameleon theming info per theme in a convenient package that can be consumed by gradle. This package contains the theme tokens.
Structure
chameleon-kotlin
contains libraries that can be used in a production environment and in a development environment. In the development environment you can use the chameleonAll
library that contains all the themes and subthemes. In a production environment you can use the chameleon
library that contains only the tokens of the theme you want to use, but not its values. The values are provided by a theme libary az, gva, ...
and wl
= white label.
The white label theme is important if you are building a component library. The tokens that are filled in in white label exist across themes. But a theme may or may not contain more tokens than there are in white label. In a component setup it is therefore unwise to use tokens other than those that are filled in in wl.
App
- Integrate published libraries in your project using gradle on `Package Registry with the required version.
- Add both
Chameleon
withChameleonAll
(if you need all themes) orChameleon<#theme#>
(if you need a certain theme) - Do an import
ChameleonTheme<#theme#>
in code that is specific for your target
Tip: A demo with tokens in components is used over here
demoAllThemes
Subthemes
You can choose to use the library with compose or with a regular view setup. Just remember: if it is impossible to use compose you can still use this package, but you will have to do the reloading of the views on a subtheme change yourself.
With Compose
Warning: This is a work in progress. None of the code is ready for production, but we can speed up this process if you can provide feedback.
As a start, you should be familiar with the compose way of handling theming. Please follow the guide [Custom design systems in Compose]
In concept the theme we provide is a TokenContainer
= TC
, the token container can be filled with values that are relevant for the theme
. This is done at startup of the application. The chameleon
contains all the tokens in the token container, but not its values.
So to put the values in there in a compose app, 3 steps are needed:
- Add a dependency on
chameleon
- Add a dependency on
chameleonAll
for a themeswitcher orwl
or whatever theme you want to use. - Boostrap your font and bootstrap your theme or themes in a place where it the registration will be called once. For example in your Application class.
- Provide the current theme context/setup to the rest of the app by wrapping it in
ChameleonThemed
composable. - To access the values, use
TC.kt
which when used inside a composable function will provide the correct values.
By doing step 3 , you can boostrap the needed theme/themes.
Do this in a place that get's called once. Preferably in application class
Each theme package and the chameleonAll
package has an bootstrap
and a bootstrapFontFamilyTypefaceRegistry
function to register the tokens
and register the fonts for the required theme/themes.
//import for all themes
import be.mediahuis.chameleonall.generated.bootstrap
import be.mediahuis.chameleonall.generated.bootstrapFontFamilyTypefaceRegistry
//import single theme -- e.g. yc
import be.mediahuis.chameleon.theme.yc.bootstrap
import be.mediahuis.chameleon.theme.yc.bootstrapFontFamilyTypefaceRegistry
//In app class
TC.bootstrapFontFamilyTypefaceRegistry = ::bootstrapFontFamilyTypefaceRegistry
//New way (V6)
TC.bootstrap = ::bootstrap
TC.bootstrap!!()
By doing step 4, you give any @Composable
function access to the TokenContainer
with it's groups and tokens for the subtheme you want to use. This includes default
,sport
,accent
for dark
and light
!
In your activity all @Composable
functions should be wrapped in the ChameleonThemed
composable in order for TC.kt
to work.
The main activity of your app should load a composable that is set up with theme and subtheme savable.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
If you do the following in your top level composable the token values for your current configuration will be available for all nested composables.
In the example we did our setup in a toplevel composable but you can add a ChameleonThemed
anywhere you want and you can even nest different ChameleonThemed
composables.
@Composable
fun TopLevelCompose() {
val theme by rememberSaveable { mutableStateOf(ChameleonTheme.Wl) }
val subTheme by rememberSaveable { mutableStateOf(SubTheme.Embedded(SubThemeName.Default)) }
val colorScheme = if(isSystemInDarkTheme()) ColorScheme.DARK else ColorScheme.LIGHT
val styleContext = StyleContext(
subTheme = subTheme,
colorScheme = colorScheme
)
ChameleonThemed(
theme = theme,
styleContext = styleContext
) {
// Here you can use Chameleon Components
// Or use tokens directly via TC and it's token groups
}
}
Locally scoped data
ChameleonThemed
works under the hood with a locally scoped data that is provided by a CompositionLocalProvider.
You have following data at your disposal.
ChameleonThemed {
val theme = LocalTheme.current
val subTheme = LocalSubTheme.current
val colorScheme = LocalColorScheme.current
val windowSizeClass = LocalWindowSizeClass.current
val richContent = LocalRichContentParsing.current
val availableThemes = LocalAvailableThemes.current
val availableSubThemes = LocalAvailableSubThemes.current
//Their are also convenience methods on TC that do the same
val theme = TC.theme
val subTheme = TC.subTheme
val colorScheme = TC.colorScheme
val windowSizeClass = TC.windowSizeClass
val richContent = TC.richContentParsingEnabled
val availableThemes = TC.availableThemes
val availableSubThemes = TC.availableSubThemes
}
SubTheming
To have a subTheme set on part of the screen use the ChameleonThemed
composable again.
val styleContext = StyleContext(
subTheme = SubTheme.Embedded(SubThemeName.SponsoredContent),
colorScheme = ColorScheme.Light,
windowSizeClass = WindowSizeClass.Normal
)
/**Also Support for strings with different formats for SubThemeName
* e.g. sponsored content SubTheme
* Formats:
* - sponsoredContent
* - SponsoredContent
* - sponsored.content
* - sponsored-content
* */
val styleContextWithString = StyleContext(
subTheme = SubTheme.get("sponsoredContent"),
colorScheme = ColorScheme.Light,
windowSizeClass = WindowSizeClass.Normal
)
ChameleonThemed(
theme = Chameleon.Wl,
styleContext = styleContext
) {
Text(
text = "SponsoredContent title for ${TC.theme}",
color = TC.colors.semantic.paragraphAltSmDefaultTypography.composeColor,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
)
}
Tip: This is done in
ChameleonThemed
and also take a look atTC.kt
Theme Switcher app
The theme switcher app should depend on chameleonAll
that contains all the themes and subthemes.
You can make a mutableState that holds the current selected Theme. Every time you change this state ChameleonThemed will use it and update all components in it.
@Composable
fun ThemeSwitcherApp() {
var currentTheme by remember { mutableStateOf(ChameleonTheme.Wl) }
// somewhere else the theme get\'s changed by user input?
currentTheme = selectedTheme
ChameleonThemed(
theme = currentTheme
){
}
}
Without Compose
Use the registedSubThemes
map to get the tokens for the group you want for the subtheme you want to use.
/** The registeredSubThemes holds all registered themes and their respective subthemes.
You can retrieve all the groups and their tokens by doing the following.*/
// Retrieve colors group for the current theme and subtheme
registeredSubThemes[theme]?.get(SubThemeStore.Key.defaultLight)?.colors?.group
// Retrieve border group for the current theme and subtheme
registeredSubThemes[theme]?.get(SubThemeStore.Key.defaultLight)?.border?.group
val subthemeKeyFromString =
SubThemeStore.Key.get(
theme = theme,
subTheme = SubTheme.get("sponsored-content"),
colorScheme = ColorScheme.LIGHT,
allSubThemes = allSubThemes,
)
registeredSubThemes[theme]?.get(subthemeKeyFromString)?.border?.group
RichContent
We provide a way to use RichContent
for the following components : ChCaption
, ChParagraph
, ChListItem
, ChHeading
.
RichContent is enabled by default and can be disabled by using the RichContentProvider
composable.
It works in a similar way as ChameleonThemed
but in only manages 1 LocalCompositionProvider that enabled/disables rich content for all Components down the Composition stack.
RichContent has the following supported tags :
<b>
: Bold<i>
: Italic<u>
: Underline<s>
: Strikethrough<a href="">
: Links{{image-token}}
- will add composable containing an icon if found in assets that is scaled to match CustomTypography.fontSize
RichContentProvider(richContentEnabled) {
// Chameleon components
ChParagraph()
ChCaption()
}
Improved Chameleon logging
We added improved logging for our Chameleon packages. This is managed by the ChameleonLogger
object.
It also possible for clients to add custom actions to be done when the ChameleonLogger logs a message.
This can be done by doing the following in a place that only gets triggered once in your app lifecycle like the application class.
//Step 1 create CustomLogAction
class DemoAction : CustomLogAction {
override fun execute(message: String?) {
Log.d("This is a custom action", "dieter")
}
}
//Step 2 add CustomLogAction to ChameleonLogger
ChameleonLogger.setCustomActions(
listOf(DemoAction())
)
Building the chameleon project
# not always needed but if you start from scratch
pmpm install
pnpm run build
pnpm run codegen
snapshot tests
Snapshot tests run using cashapp/paparazzi and can be run against local png files for now committed to the repo. The snapshot tests are ran against the local chameleon-kotlin
tokens package.
## append -u to update snapshots
pnpm run test #-u
Tests always run against the local chameleon-kotlin
tokens package. If you want to run against a published version you need to change the version in libs.version.toml.
# runs tests agains published version of chameleon-kotlin as in versions.toml
./gradlew verifyPaparazziRelease
task chameleon-test
verify snapshots
./gradlew verifyPaparazziRelease
update snapshots
./gradlew recordPaparazziRelease