Skip to main content

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

  1. build-logic (acts as single source of truth for managing the build logic for all modules in chameleon-kotlin
  2. chameleon 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)
  3. chameleon.theme.template not a module but used by codegen-kotlin to generate build.gradle.kts for each theme/brand
  4. chameleonAll wrapper that contains all themes and subthemes. It manages logic to work with all the themes/brands at once.
  5. chameleoncomponents collection of all the Chameleon components for Kotlin
  6. chameleonThemes - holds all tokens per theme/brand with their respective values for that theme
  7. chameleon.theme.<#x> - supplies an bootstrap function to fill the chameleon module models with values.
  8. 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.
  9. 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

  1. Integrate published libraries in your project using gradle on `Package Registry with the required version.
  2. Add both Chameleon with ChameleonAll(if you need all themes) or Chameleon<#theme#>(if you need a certain theme)
  3. 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:

  1. Add a dependency on chameleon
  2. Add a dependency on chameleonAll for a themeswitcher or wl or whatever theme you want to use.
  3. 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.
  4. Provide the current theme context/setup to the rest of the app by wrapping it in ChameleonThemed composable.
  5. 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 at TC.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