Closed Bug 1855320 Opened 2 years ago Closed 9 months ago

[Meta] Upstream Fenix's theming to Android Components

Categories

(Firefox for Android :: Design System and Theming, enhancement, P3)

All
Android
enhancement

Tracking

()

RESOLVED FIXED

People

(Reporter: 007, Assigned: 007)

References

Details

(Keywords: meta, Whiteboard: [techfundamentals])

Design System Upstream to Android Components

Introduction

The theming systems on Fenix have been baking since 2021, and it'd benefit the UI in Android Components, Focus, and potentially other apps at Mozilla if we could share the color tokens, typography tokens, and top-level components we've been implementing as part of the Acorn design system.

Motivation

In a world where Composable components are reusable, themes are extensible, and colors and typography are , it becomes all the more important to broaden their accessibility through our code levels and . To that end, the FirefoxTheme wrapper and all of our components in Fenix should be migrated to Android Components. All existing Android apps under the Firefox Android team and future Mozilla Android apps would then have access to a robust theming and component system. Several goals are highlighted below, but it would be essential for the theming to be as plug-and-play as possible, similar to the MaterialTheme wrapper Google provides where apps can opt-in to as much or as little of the system when writing UI code.

Ultimately, we want to bolster our relationship with Design and make all future UI implementations at the Fenix, Focus, and AC level as seamless as possible. By having a centralized theming system, we can provide ourselves (and our community) with a one-stop-shop of the Android implementation of the Acorn design system with robust, customizable themes and components.

Goals

  • The theming system is entirely self-contained in its own module.
  • The theming system is extensible (i.e. the existing FirefoxTheme can live on top of it).
  • The color tokens are now usable in AC and Focus.
  • The color palettes are still extensible (i.e. any number of themes/color palettes could be theoretically slotted-in).
  • The typography tokens are now usable in AC and Focus.
  • The top-level components are usable in AC and Focus.
  • The components would be refactored to have theme defaults from the Acorn system, but would be extensible.
  • Any further enhancements to the design system are available to all consumers (e.g. grid system).
  • Minimal refactoring within Fenix.

Implementation from a high-level

Usage

Currently we can do the following in Fenix:

FirefoxTheme {
   ...
   color = FirefoxTheme.colors.layer1,
}

The idea would be to now be able to do:

AcornTheme {
   ...
   color = AcornTheme.colors.layer1,
}

within AC, Fenix, and Focus, and be able to extend off of AcornTheme as needed/desired.

Extensibility

Colors

  • The theme wrapper would, by default, have a light and dark palette implemented.
  • Firefox would override this and pass down light, dark, and private palettes.
  • Focus would provide its own custom color palette.

Typography

Similar to colors, there would be defaults provided via AcornTheme, but this could be overridden.

App-specific global singletons

  • Apps can create singletons that fulfill a unique use case for that app.
  • This allows the consumers of AcornTheme to subscribe to whatever they want and add anything further they need.
    • Maybe Focus gets a unique shape system, or we prototype a grid-sizing system in FirefoxTheme before upstreaming it to AcornTheme.

In practice

Theme set-up

class AcornColors(
    layer1: Color,
    ...
)


object AcornTheme {
    val colors: AcornColors
        @Composable  
        get() = LocalAcornColors.current

    val colors: AcornTypography
        @Composable
        get() = LocalAcornTypography.current  
}

internal val LocalColors = staticCompositionLocalOf { lightColorPalette }

internal val LocalTypography = staticCompositionLocalOf { Typography() }

val lightColorPalette = AcornColors(...)

val darkColorPalette = AcornColors(...)

@Composable  
fun AcornTheme(  
    colors: AcornColors = AcornTheme.colors,
    typography: AcornTypography = AcornTheme.typography,
    content: @Composable () -> Unit,  
) {  
    // This is taken from FirefoxTheme and MaterialTheme
    val rememberedColors = remember {  colors.copy()  }.apply { updateColorsFrom(colors) }
    CompositionLocalProvider(
        LocalAcornColors provides rememberColors
        LocalTypography provides typography
    ) {
        MaterialTheme(  
            content = content,  
        )  
    }  
}

Firefox

object FirefoxTheme {
    val colors: AcornColors
        @Composable  
        get() = AcornTheme.colors

    val colors: AcornTypography
        @Composable
        get() = AcornTheme.Typography
}

private val privateColorPalette = darkColorPalette.copy(...)

@Composable  
fun FirefoxTheme(  
    theme: Theme = Theme.getTheme(),  
    content: @Composable () -> Unit,  
) {  
    val colors = when (theme) {  
      Theme.Light -> AcornTheme.lightColorPalette  
      Theme.Dark -> AcornTheme.darkColorPalette  
      Theme.Private -> FirefoxTheme.privateColorPalette  
    }
  
    AcornTheme(
        colors = colors,
        content = content,
    )
}

@Composable
fun SomeComposable() {
    FirefoxTheme {
       Text(
           text = "Hello world",
           color = FirefoxTheme.colors.textPrimary,
       )
    }
}

Focus

In this example, the FocusTheme singleton doesn't need to be made unless we explicitly wanted the theme calls to be FocusTheme.colors instead of AcornTheme.colors.

object FocusTheme {
    val colors: AcornColors
        @Composable
        get() = AcornTheme.colors

    val colors: AcornTypography
        @Composable
        get() = AcornTheme.Typography
}

private val focusColorPalette = AcornColorPalette(...) // palette from scratch

@Composable  
fun FocusTheme(
    content: @Composable () -> Unit,  
) {  
    AcornTheme(
        colors = focusColorPalette,
        content = content,
    )
}

@Composable
fun SomeComposable() {
    FocusTheme {
       Text(
           text = "Hello world",
           color = FocusTheme.colors.textPrimary,
       )
    }
}
``
Component: General → Design System and Theming
Priority: P2 → P3

Plan of attack

Module creation

Stand up modules in AC for:

  • Components
  • Color tokens
  • Typography
  • Acorn (this will be the megazord module for theming)

Token migration

Colors

  1. Create AcornColors class
  2. Duplicate the color tokens
  3. Duplicate the light and dark color palettes

Typography

  1. Create AcornTypography class with the typography tokens
  2. Create the default typography (make a secondary constructor with the optionals being the existing text styles)

Acorn theme setup

Theme singleton

Create the AcornTheme object with colors and typography getters (as outlined in the example above)

Theme wrapper

Create the AcornTheme composable wrapper (as outlined in the example above)

Fenix theme transition

FirefoxTheme singleton

Have the singleton serve as a wrapper layer for calling into AcornTheme (as outlined in the example above). This will make it so there is little refactoring on the Fenix side for all of the call sites of FirefoxTheme.colors and FirefoxTheme.typography.

FirefoxTheme wrapper

  1. Change privateColorPalette to copy darkColorPalette from AcornTheme.
  2. Make no changes to the theme parameter.
  3. Inside the when, plug in the dark and light color palettes from AcornTheme.
  4. Call AcornTheme instead of MaterialTheme.

Component migration

  • Move over one component at a time
  • Refactor the colors of each component so they are passed-in as a function dependency.
    • If one color, just simply insert the color parameter with the AcornTheme value as the default.
    • If 2+ colors, create a COMPONENTColors data class as the dependency.
      • Similar to MessageCardColors, but just use a secondary constructor instead of a companion object, if possible.
    • The Button composable should be refactored to use this pattern.
  • Ensure the previews are still working.
  • Fix any usages.

Clean-up

  1. Remove FenixTypography
  2. Remove light and dark color palettes from FirefoxTheme
  3. Remove FirefoxColors
Depends on: 1902373

Marking this meta as closed. Thanks for handling this, GL!

Status: NEW → RESOLVED
Closed: 9 months ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.