feat(ui): complete ABox app experience redesign #10

Merged
KremeCN merged 40 commits from feat/ui-ux-redesign into main 2026-08-27 23:25:08 +08:00
13 changed files with 521 additions and 33 deletions
Showing only changes of commit 4055c53362 - Show all commits
@@ -5,10 +5,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest import com.android.tools.screenshot.PreviewTest
import com.flagship.abox.manager.designsystem.ABoxTheme import com.flagship.abox.manager.designsystem.ABoxTheme
import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.UiPreferences
@PreviewTest @PreviewTest
@Preview @Preview
@Composable @Composable
fun AppSmokePreview() { fun AppSmokePreview() {
ABoxTheme(darkTheme = false) { Text("ABox Manager") } ABoxTheme(
preferences = UiPreferences(
renderer = DesignRenderer.MATERIAL3,
themeMode = AppThemeMode.LIGHT,
dynamicColorEnabled = false,
),
) {
Text("ABox Manager")
}
} }
@@ -32,11 +32,14 @@ kotlin {
} }
dependencies { dependencies {
implementation(project(":android:core:preferences"))
implementation(platform(libs.compose.bom)) implementation(platform(libs.compose.bom))
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended) implementation(libs.compose.material.icons.extended)
implementation(libs.miuix.ui.android)
debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.tooling)
screenshotTestImplementation(libs.compose.screenshot.test) screenshotTestImplementation(libs.compose.screenshot.test)
testImplementation(libs.junit)
} }
@@ -2,10 +2,11 @@ package com.flagship.abox.manager.designsystem
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp import com.flagship.abox.manager.designsystem.tokens.ABoxShapeTokens
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeDefaults
val ABoxShapes = Shapes( fun ABoxShapes(tokens: ABoxShapeTokens = ABoxThemeDefaults.material3Shapes) = Shapes(
small = RoundedCornerShape(8.dp), small = RoundedCornerShape(tokens.smallCornerRadius),
medium = RoundedCornerShape(16.dp), medium = RoundedCornerShape(tokens.mediumCornerRadius),
large = RoundedCornerShape(24.dp), large = RoundedCornerShape(tokens.largeCornerRadius),
) )
@@ -1,41 +1,158 @@
package com.flagship.abox.manager.designsystem package com.flagship.abox.manager.designsystem
import androidx.compose.foundation.isSystemInDarkTheme import android.os.Build
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.flagship.abox.manager.designsystem.renderer.ABoxRendererAdapters
import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.designsystem.tokens.ABoxColorTokens
import com.flagship.abox.manager.designsystem.tokens.ABoxSpacing
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeDefaults
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeResolution
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeResolver
import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.UiPreferences
private val LightColorScheme = lightColorScheme( val LocalABoxColors = staticCompositionLocalOf { ABoxThemeDefaults.lightColors }
primary = Color(0xFF006780), val LocalABoxSpacing = staticCompositionLocalOf { ABoxSpacing() }
onPrimary = Color.White, val LocalABoxTypography = staticCompositionLocalOf { ABoxTypography }
primaryContainer = Color(0xFFB8EAFF), val LocalABoxShapes = staticCompositionLocalOf { ABoxThemeDefaults.material3Shapes }
onPrimaryContainer = Color(0xFF001F29), val LocalABoxRenderer = staticCompositionLocalOf { DesignRenderer.MATERIAL3 }
secondary = Color(0xFF4D616A), val LocalABoxReducedMotion = staticCompositionLocalOf { false }
surface = Color(0xFFF8FAFC),
background = Color(0xFFF8FAFC),
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF5DD5F8),
onPrimary = Color(0xFF003544),
primaryContainer = Color(0xFF004D61),
onPrimaryContainer = Color(0xFFB8EAFF),
secondary = Color(0xFFB4CBD4),
surface = Color(0xFF101416),
background = Color(0xFF101416),
)
@Composable @Composable
fun ABoxTheme( fun ABoxTheme(
darkTheme: Boolean = isSystemInDarkTheme(), preferences: UiPreferences,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
MaterialTheme( val systemInDarkTheme = androidx.compose.foundation.isSystemInDarkTheme()
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme, val isDarkTheme = when (preferences.themeMode) {
typography = ABoxTypography, AppThemeMode.SYSTEM -> systemInDarkTheme
shapes = ABoxShapes, AppThemeMode.LIGHT -> false
AppThemeMode.DARK -> true
}
val context = LocalContext.current
val dynamicTokens = if (preferences.dynamicColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val dynamicColorScheme = if (isDarkTheme) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
dynamicColorScheme.toABoxColorTokens()
} else {
null
}
val resolution = ABoxThemeResolver.resolve(
preferences = preferences,
systemInDarkTheme = systemInDarkTheme,
sdkInt = Build.VERSION.SDK_INT,
dynamicColorTokens = dynamicTokens,
)
val renderer = ABoxRendererAdapters.forRenderer(resolution.renderer)
CompositionLocalProvider(
LocalABoxColors provides resolution.colorTokens,
LocalABoxSpacing provides ABoxSpacing(),
LocalABoxTypography provides ABoxTypography,
LocalABoxShapes provides resolution.shapeTokens,
LocalABoxRenderer provides resolution.renderer,
LocalABoxReducedMotion provides false,
) {
renderer.Render(
resolution = resolution,
content = content,
)
}
}
@Composable
fun ABoxTheme(
darkTheme: Boolean = androidx.compose.foundation.isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
ABoxTheme(
preferences = UiPreferences(
themeMode = if (darkTheme) AppThemeMode.DARK else AppThemeMode.LIGHT,
dynamicColorEnabled = false,
),
content = content, content = content,
) )
} }
internal fun materialColorScheme(resolution: ABoxThemeResolution): ColorScheme {
val colors = resolution.colorTokens
val onPrimary = if (resolution.isDarkTheme) Color(0xFF003544) else Color.White
val onSurface = if (resolution.isDarkTheme) Color(0xFFE0E3E5) else Color(0xFF191C1E)
val onSurfaceVariant = if (resolution.isDarkTheme) Color(0xFFC0C8CC) else Color(0xFF40484C)
val primaryContainer = colors.primary.copy(alpha = if (resolution.isDarkTheme) 0.35f else 0.20f)
val secondaryContainer = colors.surfaceContainer
return if (resolution.isDarkTheme) {
darkColorScheme(
primary = colors.primary,
onPrimary = onPrimary,
primaryContainer = primaryContainer,
onPrimaryContainer = colors.primary,
secondary = colors.pending,
onSecondary = Color(0xFF3F2E00),
secondaryContainer = secondaryContainer,
onSecondaryContainer = onSurface,
tertiary = colors.success,
onTertiary = Color(0xFF00391E),
surface = colors.surface,
onSurface = onSurface,
surfaceVariant = colors.surfaceContainer,
onSurfaceVariant = onSurfaceVariant,
background = colors.surface,
onBackground = onSurface,
outline = colors.outline,
error = colors.error,
onError = Color(0xFF690005),
)
} else {
lightColorScheme(
primary = colors.primary,
onPrimary = onPrimary,
primaryContainer = primaryContainer,
onPrimaryContainer = Color(0xFF001F29),
secondary = colors.pending,
onSecondary = Color.White,
secondaryContainer = secondaryContainer,
onSecondaryContainer = onSurface,
tertiary = colors.success,
onTertiary = Color.White,
surface = colors.surface,
onSurface = onSurface,
surfaceVariant = colors.surfaceContainer,
onSurfaceVariant = onSurfaceVariant,
background = colors.surface,
onBackground = onSurface,
outline = colors.outline,
error = colors.error,
onError = Color.White,
)
}
}
private fun ColorScheme.toABoxColorTokens() = ABoxColorTokens(
primary = primary,
surface = surface,
surfaceContainer = surfaceContainer,
outline = outline,
success = tertiary,
warning = secondary,
error = error,
offline = outline,
pending = secondary,
deviceSocket = primary,
deviceSensor = tertiary,
deviceInfrared = secondary,
)
@@ -2,4 +2,5 @@ package com.flagship.abox.manager.designsystem
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
/** Default typography supplied through [LocalABoxTypography]. */
val ABoxTypography = Typography() val ABoxTypography = Typography()
@@ -0,0 +1,20 @@
package com.flagship.abox.manager.designsystem.renderer
import androidx.compose.runtime.Composable
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeResolution
import com.flagship.abox.manager.preferences.DesignRenderer
internal interface ABoxRendererAdapter {
@Composable
fun Render(
resolution: ABoxThemeResolution,
content: @Composable () -> Unit,
)
}
internal object ABoxRendererAdapters {
fun forRenderer(renderer: DesignRenderer): ABoxRendererAdapter = when (renderer) {
DesignRenderer.MATERIAL3 -> Material3RendererAdapter
DesignRenderer.MIUIX -> MiuixRendererAdapter
}
}
@@ -0,0 +1,23 @@
package com.flagship.abox.manager.designsystem.renderer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.flagship.abox.manager.designsystem.ABoxShapes
import com.flagship.abox.manager.designsystem.ABoxTypography
import com.flagship.abox.manager.designsystem.materialColorScheme
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeResolution
object Material3RendererAdapter : ABoxRendererAdapter {
@Composable
override fun Render(
resolution: ABoxThemeResolution,
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = materialColorScheme(resolution),
typography = ABoxTypography,
shapes = ABoxShapes(resolution.shapeTokens),
content = content,
)
}
}
@@ -0,0 +1,25 @@
package com.flagship.abox.manager.designsystem.renderer
import androidx.compose.runtime.Composable
import com.flagship.abox.manager.designsystem.tokens.ABoxThemeResolution
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.darkColorScheme
import top.yukonga.miuix.kmp.theme.lightColorScheme
internal object MiuixRendererAdapter : ABoxRendererAdapter {
@Composable
override fun Render(
resolution: ABoxThemeResolution,
content: @Composable () -> Unit,
) {
val colors = if (resolution.isDarkTheme) darkColorScheme() else lightColorScheme()
MiuixTheme(
colors = colors,
) {
Material3RendererAdapter.Render(
resolution = resolution,
content = content,
)
}
}
}
@@ -0,0 +1,139 @@
package com.flagship.abox.manager.designsystem.tokens
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.UiPreferences
@Immutable
data class ABoxColorTokens(
val primary: Color,
val surface: Color,
val surfaceContainer: Color,
val outline: Color,
val success: Color,
val warning: Color,
val error: Color,
val offline: Color,
val pending: Color,
val deviceSocket: Color,
val deviceSensor: Color,
val deviceInfrared: Color,
)
@Immutable
data class ABoxSpacing(
val dp4: Dp = 4.dp,
val dp8: Dp = 8.dp,
val dp12: Dp = 12.dp,
val dp16: Dp = 16.dp,
val dp20: Dp = 20.dp,
val dp24: Dp = 24.dp,
val dp32: Dp = 32.dp,
val minimumTouchTarget: Dp = 48.dp,
)
@Immutable
data class ABoxComponentState(
val enabled: Boolean = true,
val pending: Boolean = false,
val offline: Boolean = false,
val dangerous: Boolean = false,
)
@Immutable
data class ABoxShapeTokens(
val smallCornerRadius: Dp,
val mediumCornerRadius: Dp,
val largeCornerRadius: Dp,
)
@Immutable
data class ABoxThemeResolution(
val isDarkTheme: Boolean,
val colorTokens: ABoxColorTokens,
val shapeTokens: ABoxShapeTokens,
val blurEnabled: Boolean,
val renderer: DesignRenderer,
)
object ABoxThemeDefaults {
val lightColors = ABoxColorTokens(
primary = Color(0xFF006780),
surface = Color(0xFFF8FAFC),
surfaceContainer = Color(0xFFE2E8F0),
outline = Color(0xFF70797D),
success = Color(0xFF1B7F4B),
warning = Color(0xFF8A5800),
error = Color(0xFFBA1A1A),
offline = Color(0xFF5F6B72),
pending = Color(0xFF8A5800),
deviceSocket = Color(0xFF006E90),
deviceSensor = Color(0xFF476B23),
deviceInfrared = Color(0xFF77536F),
)
val darkColors = ABoxColorTokens(
primary = Color(0xFF5DD5F8),
surface = Color(0xFF101416),
surfaceContainer = Color(0xFF1F292D),
outline = Color(0xFF8A969B),
success = Color(0xFF7EDFA7),
warning = Color(0xFFFFB95B),
error = Color(0xFFFFB4AB),
offline = Color(0xFFBFC9CD),
pending = Color(0xFFFFB95B),
deviceSocket = Color(0xFF5DD5F8),
deviceSensor = Color(0xFFAFD67C),
deviceInfrared = Color(0xFFE6B5D6),
)
val material3Shapes = ABoxShapeTokens(
smallCornerRadius = 8.dp,
mediumCornerRadius = 16.dp,
largeCornerRadius = 24.dp,
)
val miuixShapes = ABoxShapeTokens(
smallCornerRadius = 12.dp,
mediumCornerRadius = 20.dp,
largeCornerRadius = 28.dp,
)
}
object ABoxThemeResolver {
fun resolve(
preferences: UiPreferences,
systemInDarkTheme: Boolean,
sdkInt: Int,
dynamicColorTokens: ABoxColorTokens?,
): ABoxThemeResolution {
val isDarkTheme = when (preferences.themeMode) {
AppThemeMode.SYSTEM -> systemInDarkTheme
AppThemeMode.LIGHT -> false
AppThemeMode.DARK -> true
}
val brandTokens = if (isDarkTheme) {
ABoxThemeDefaults.darkColors
} else {
ABoxThemeDefaults.lightColors
}
val colorTokens = if (preferences.dynamicColorEnabled && sdkInt >= 31 && dynamicColorTokens != null) {
dynamicColorTokens
} else {
brandTokens
}
val shapeTokens = when (preferences.renderer) {
DesignRenderer.MATERIAL3 -> ABoxThemeDefaults.material3Shapes
DesignRenderer.MIUIX -> ABoxThemeDefaults.miuixShapes
}
return ABoxThemeResolution(
isDarkTheme = isDarkTheme,
colorTokens = colorTokens,
shapeTokens = shapeTokens,
blurEnabled = preferences.blurEnabled && preferences.renderer == DesignRenderer.MIUIX,
renderer = preferences.renderer,
)
}
}
@@ -4,10 +4,21 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest import com.android.tools.screenshot.PreviewTest
import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.UiPreferences
@PreviewTest @PreviewTest
@Preview @Preview
@Composable @Composable
fun DesignSystemSmokePreview() { fun DesignSystemSmokePreview() {
ABoxTheme(darkTheme = false) { Text("截图基线") } ABoxTheme(
preferences = UiPreferences(
renderer = DesignRenderer.MATERIAL3,
themeMode = AppThemeMode.LIGHT,
dynamicColorEnabled = false,
),
) {
Text("截图基线")
}
} }
@@ -0,0 +1,19 @@
package com.flagship.abox.manager.designsystem.renderer
import com.flagship.abox.manager.preferences.DesignRenderer
import org.junit.Assert.assertSame
import org.junit.Test
class ABoxRendererAdapterTest {
@Test
fun selectsMaterial3AndMiuixAdaptersFromRendererPreference() {
assertSame(
Material3RendererAdapter,
ABoxRendererAdapters.forRenderer(DesignRenderer.MATERIAL3),
)
assertSame(
MiuixRendererAdapter,
ABoxRendererAdapters.forRenderer(DesignRenderer.MIUIX),
)
}
}
@@ -0,0 +1,116 @@
package com.flagship.abox.manager.designsystem.tokens
import androidx.compose.ui.graphics.Color
import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.UiPreferences
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Test
class ABoxThemeResolverTest {
@Test
fun systemLightAndDarkResolveDeterministically() {
val light = ABoxThemeResolver.resolve(
preferences = UiPreferences(themeMode = AppThemeMode.SYSTEM),
systemInDarkTheme = false,
sdkInt = 30,
dynamicColorTokens = null,
)
val dark = ABoxThemeResolver.resolve(
preferences = UiPreferences(themeMode = AppThemeMode.SYSTEM),
systemInDarkTheme = true,
sdkInt = 30,
dynamicColorTokens = null,
)
assertFalse(light.isDarkTheme)
assertEquals(ABoxThemeDefaults.lightColors, light.colorTokens)
assertEquals(ABoxThemeDefaults.darkColors, dark.colorTokens)
}
@Test
fun dynamicColorDisabledUsesBrandTokens() {
val resolution = ABoxThemeResolver.resolve(
preferences = UiPreferences(
themeMode = AppThemeMode.LIGHT,
dynamicColorEnabled = false,
),
systemInDarkTheme = false,
sdkInt = 31,
dynamicColorTokens = dynamicTokens,
)
assertEquals(ABoxThemeDefaults.lightColors, resolution.colorTokens)
}
@Test
fun pre31DynamicColorFallsBackToBrandTokens() {
val resolution = ABoxThemeResolver.resolve(
preferences = UiPreferences(
themeMode = AppThemeMode.DARK,
dynamicColorEnabled = true,
),
systemInDarkTheme = false,
sdkInt = 30,
dynamicColorTokens = dynamicTokens,
)
assertEquals(ABoxThemeDefaults.darkColors, resolution.colorTokens)
}
@Test
fun blurDisabledResolvesOpaqueSurface() {
val resolution = ABoxThemeResolver.resolve(
preferences = UiPreferences(blurEnabled = false),
systemInDarkTheme = false,
sdkInt = 31,
dynamicColorTokens = null,
)
assertFalse(resolution.blurEnabled)
assertEquals(1f, resolution.colorTokens.surface.alpha)
assertEquals(1f, resolution.colorTokens.surfaceContainer.alpha)
}
@Test
fun rendererChangesShapeButNotSemanticStateRoles() {
val material = ABoxThemeResolver.resolve(
preferences = UiPreferences(renderer = DesignRenderer.MATERIAL3),
systemInDarkTheme = false,
sdkInt = 31,
dynamicColorTokens = null,
)
val miuix = ABoxThemeResolver.resolve(
preferences = UiPreferences(renderer = DesignRenderer.MIUIX),
systemInDarkTheme = false,
sdkInt = 31,
dynamicColorTokens = null,
)
assertEquals(material.colorTokens.success, miuix.colorTokens.success)
assertEquals(material.colorTokens.warning, miuix.colorTokens.warning)
assertEquals(material.colorTokens.error, miuix.colorTokens.error)
assertEquals(material.colorTokens.offline, miuix.colorTokens.offline)
assertEquals(material.colorTokens.pending, miuix.colorTokens.pending)
assertNotEquals(material.shapeTokens, miuix.shapeTokens)
}
private companion object {
val dynamicTokens = ABoxColorTokens(
primary = Color(0xFF111111),
surface = Color(0xFF222222),
surfaceContainer = Color(0xFF333333),
outline = Color(0xFF444444),
success = Color(0xFF555555),
warning = Color(0xFF666666),
error = Color(0xFF777777),
offline = Color(0xFF888888),
pending = Color(0xFF999999),
deviceSocket = Color(0xFFAAAAAA),
deviceSensor = Color(0xFFBBBBBB),
deviceInfrared = Color(0xFFCCCCCC),
)
}
}
+2
View File
@@ -15,6 +15,7 @@ adaptive = "1.3.0"
adaptiveNavigationSuite = "1.4.0" adaptiveNavigationSuite = "1.4.0"
window = "1.5.1" window = "1.5.1"
composeScreenshot = "0.0.1-alpha16" composeScreenshot = "0.0.1-alpha16"
miuix = "0.9.3"
junit = "4.13.2" junit = "4.13.2"
androidxJunit = "1.2.1" androidxJunit = "1.2.1"
espresso = "3.6.1" espresso = "3.6.1"
@@ -40,6 +41,7 @@ compose-material-icons-extended = { module = "androidx.compose.material:material
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
compose-screenshot-test = { module = "com.android.tools.screenshot:screenshot-validation-api", version.ref = "composeScreenshot" } compose-screenshot-test = { module = "com.android.tools.screenshot:screenshot-validation-api", version.ref = "composeScreenshot" }
miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.ref = "miuix" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }