You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Jetpack Compose构建可扩展UI主题系统的实现咨询

基于Jetpack Compose构建层级化主题与链式调用UI系统

一、实现层级化主题系统

核心思路是用数据类封装主题配置,通过CompositionLocal传递变体样式,让组件自动继承当前主题的颜色、排版、间距规则。

1. 定义基础主题配置结构

先把颜色、排版、间距拆成独立的数据类,再整合为完整的主题配置,同时预定义Expressive/Muted两个变体:

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.font.FontWeight

// 基础颜色板
data class ColorPalette(
    val primary: Color,
    val secondary: Color,
    val text: Color,
    val background: Color
)

// 排版规则
data class TypographyConfig(
    val buttonLabel: TextStyle,
    val bodyText: TextStyle,
    val titleText: TextStyle
)

// 间距规范
data class SpacingConfig(
    val small: Dp,
    val medium: Dp,
    val large: Dp
)

// 整合所有主题配置
data class AppThemeConfig(
    val colors: ColorPalette,
    val typography: TypographyConfig,
    val spacing: SpacingConfig
)

// 预定义Expressive/Muted变体
object ThemeVariants {
    val Expressive = AppThemeConfig(
        colors = ColorPalette(
            primary = Color(0xFF6200EE),
            secondary = Color(0xFF03DAC6),
            text = Color(0xFF121212),
            background = Color.White
        ),
        typography = TypographyConfig(
            buttonLabel = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold),
            bodyText = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal),
            titleText = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold)
        ),
        spacing = SpacingConfig(8.dp, 16.dp, 24.dp)
    )

    val Muted = AppThemeConfig(
        colors = ColorPalette(
            primary = Color(0xFFBB86FC),
            secondary = Color(0xFFCFD8DC),
            text = Color(0xFF424242),
            background = Color(0xFFF5F5F5)
        ),
        typography = TypographyConfig(
            buttonLabel = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium),
            bodyText = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Normal),
            titleText = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Medium)
        ),
        spacing = SpacingConfig(6.dp, 12.dp, 18.dp)
    )
}

2. 用CompositionLocal传递主题

Compose的CompositionLocal可以让子组件自动获取当前上下文的主题配置,无需手动传递:

import androidx.compose.runtime.staticCompositionLocalOf

// 定义主题配置的CompositionLocal,默认用Expressive变体
val LocalAppTheme = staticCompositionLocalOf { ThemeVariants.Expressive }

3. 封装主题切换组件

给每个变体写顶层Composable,内部用CompositionLocalProvider覆盖主题配置:

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Composable

@Composable
fun ExpressiveTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalAppTheme provides ThemeVariants.Expressive) {
        content()
    }
}

@Composable
fun MutedTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalAppTheme provides ThemeVariants.Muted) {
        content()
    }
}

二、实现链式调用的组件API

通过Builder模式封装组件配置,让无Compose经验的开发者可以用链式调用快速构建UI:

1. 实现XButton的链式调用

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter

// Button配置Builder类
class XButtonBuilder {
    var icon: Painter? = null
    var label: String = ""
    var onClick: () -> Unit = {}

    fun withIcon(painter: Painter) {
        icon = painter
    }

    fun withLabel(text: String) {
        label = text
    }

    fun onClick(block: () -> Unit) {
        onClick = block
    }
}

// 链式调用入口类
class LayoutWith {
    companion object {
        fun invoke() = LayoutWith()
    }
}

// 顶层函数简化入口
fun LayoutWith() = LayoutWith.invoke()

// 给LayoutWith扩展XButton方法
@Composable
fun LayoutWith.XButton(builder: XButtonBuilder.() -> Unit) {
    val theme = LocalAppTheme.current
    val buttonConfig = XButtonBuilder().apply(builder)

    Button(
        onClick = buttonConfig.onClick,
        colors = ButtonDefaults.buttonColors(
            containerColor = theme.colors.primary,
            contentColor = theme.colors.text
        ),
        modifier = Modifier.padding(theme.spacing.medium)
    ) {
        buttonConfig.icon?.let {
            Icon(painter = it, contentDescription = null)
            Spacer(modifier = Modifier.width(theme.spacing.small))
        }
        Text(
            text = buttonConfig.label,
            style = theme.typography.buttonLabel
        )
    }
}

调用示例:

// 在Composable中使用
LayoutWith().XButton {
    withIcon(painterResource(R.drawable.ic_refresh))
    withLabel("Retry")
    onClick { /* 点击逻辑 */ }
}

三、复合组件的处理

复合组件(比如ErrorScreen)直接依赖当前LocalAppTheme的配置,自动继承主题变体样式,同时支持通过Modifier覆盖局部样式:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
fun ErrorScreen(onRetry: () -> Unit) {
    val theme = LocalAppTheme.current
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(theme.spacing.large),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // 自动使用当前主题的Text样式
        Text(
            text = "Oops! Something went wrong.",
            style = theme.typography.titleText,
            color = theme.colors.text,
            modifier = Modifier.padding(bottom = theme.spacing.medium)
        )
        // 复用链式调用的XButton
        LayoutWith().XButton {
            withLabel("Retry")
            onClick(onRetry)
        }
    }
}

如果需要给复合组件加自定义样式变体,可以给它加Builder参数,或者在内部根据业务逻辑切换子组件的主题。

四、主题中添加动态值与业务逻辑

主题配置不仅可以是静态数据,还可以通过计算属性+remember缓存实现动态逻辑,比如根据系统深色模式、用户等级调整样式:

1. 带动态逻辑的主题配置

import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp

// 假设的业务枚举:用户等级
enum class UserTier { Free, Premium }

class DynamicThemeConfig(
    private val isDarkMode: Boolean,
    private val userTier: UserTier
) {
    // 根据深色模式+用户等级动态生成颜色
    val colors: ColorPalette
        get() = if (isDarkMode) {
            when(userTier) {
                UserTier.Free -> ColorPalette(
                    primary = Color(0xFFBB86FC),
                    secondary = Color(0xFF03DAC6),
                    text = Color.White,
                    background = Color(0xFF121212)
                )
                UserTier.Premium -> ColorPalette(
                    primary = Color(0xFF6200EE),
                    secondary = Color(0xFF03DAC6),
                    text = Color.White,
                    background = Color(0xFF121212)
                )
            }
        } else {
            when(userTier) {
                UserTier.Free -> ThemeVariants.Muted.colors
                UserTier.Premium -> ThemeVariants.Expressive.colors
            }
        }

    // 根据用户等级调整排版
    val typography: TypographyConfig
        get() = if (userTier == UserTier.Premium) {
            ThemeVariants.Expressive.typography.copy(
                buttonLabel = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold)
            )
        } else {
            ThemeVariants.Muted.typography
        }

    // 根据屏幕尺寸动态调整间距
    val spacing: SpacingConfig
        @Composable get() {
            val screenWidth = LocalConfiguration.current.screenWidthDp.dp
            return if (screenWidth > 600.dp) {
                SpacingConfig(12.dp, 24.dp, 32.dp)
            } else {
                SpacingConfig(8.dp, 16.dp, 24.dp)
            }
        }
}

2. 封装动态主题组件

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.foundation.isSystemInDarkTheme

@Composable
fun DynamicTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    userTier: UserTier = UserTier.Free,
    content: @Composable () -> Unit
) {
    // 缓存动态配置,避免重复计算
    val dynamicConfig = remember(isDarkMode, userTier) {
        DynamicThemeConfig(isDarkMode, userTier)
    }
    // 转换为AppThemeConfig,适配现有CompositionLocal
    val themeConfig = remember(dynamicConfig) {
        AppThemeConfig(
            colors = dynamicConfig.colors,
            typography = dynamicConfig.typography,
            spacing = dynamicConfig.spacing
        )
    }
    CompositionLocalProvider(LocalAppTheme provides themeConfig) {
        content()
    }
}

五、使用示例

在App入口处包裹主题,内部组件自动继承样式:

@Composable
fun MyApp() {
    DynamicTheme(userTier = UserTier.Premium) {
        // 页面内容
        ErrorScreen(onRetry = { /* 重试逻辑 */ })
    }
}

// 或者切换到Muted主题
@Composable
fun MutedScreen() {
    MutedTheme {
        LayoutWith().XButton {
            withLabel("Muted Button")
            onClick { /* 点击逻辑 */ }
        }
    }
}

内容的提问来源于stack exchange,提问作者user1743524

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 06:37:01