diff --git a/README.MD b/README.MD index 309046b..1325300 100644 --- a/README.MD +++ b/README.MD @@ -45,8 +45,6 @@ note on changes: - Switch build files to KTS, also build some custom plugins. - Use lifecycle events from Google (we use our own now) - Use preview light dark from google (we use our own now) -- Optionally: Setup proper Typography in AppTheme, using custom (non-material) Typography keys, - because that is what we have in 90%+ of our projects. - Optionally: Add compose events from VM to View (https://github.com/leonard-palm/compose-state-events) and showcase with snackbar or toast. - Optionally: Switch from retrofit to ktor (because Ktor is multiplatform) diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/compose/composables/widgets/TemplateButton.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/compose/composables/widgets/TemplateButton.kt new file mode 100644 index 0000000..fae0c7e --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/compose/composables/widgets/TemplateButton.kt @@ -0,0 +1,33 @@ +package nl.q42.template.ui.compose.composables.widgets + +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import nl.q42.template.ui.theme.AppTheme +import nl.q42.template.ui.theme.PreviewLightDark + +@Composable +fun TemplateButton(text: String, onClick: () -> Unit) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = AppTheme.colors.accent, + contentColor = AppTheme.colors.buttonText + ) + ) { + Text( + text = text, + style = AppTheme.typography.body, + color = AppTheme.colors.buttonText + ) + } +} + +@Composable +@PreviewLightDark +private fun TemplateButtonPreview() { + AppTheme { + TemplateButton("Button", {}) + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokens.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokens.kt new file mode 100644 index 0000000..246a9dd --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokens.kt @@ -0,0 +1,11 @@ +package nl.q42.template.ui.theme + +import androidx.compose.ui.graphics.Color + +interface AppColorTokens { + val buttonText: Color + val accent: Color + val textPrimary: Color + val surface: Color + val error: Color +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensDark.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensDark.kt new file mode 100644 index 0000000..c5cb782 --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensDark.kt @@ -0,0 +1,11 @@ +package nl.q42.template.ui.theme + +import androidx.compose.ui.graphics.Color + +object AppColorTokensDark: AppColorTokens { + override val buttonText: Color = White + override val accent: Color = PurpleGrey80 + override val textPrimary = White + override val surface = White + override val error = Pink80 +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensLight.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensLight.kt new file mode 100644 index 0000000..24a3862 --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokensLight.kt @@ -0,0 +1,11 @@ +package nl.q42.template.ui.theme + +import androidx.compose.ui.graphics.Color + +object AppColorTokensLight : AppColorTokens { + override val buttonText: Color = White + override val accent: Color = Purple40 + override val textPrimary = Black + override val surface: Color = White + override val error: Color = Red80 +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Color.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Colors.kt similarity index 91% rename from core/ui/src/main/kotlin/nl/q42/template/ui/theme/Color.kt rename to core/ui/src/main/kotlin/nl/q42/template/ui/theme/Colors.kt index 3750eeb..d36935c 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Color.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Colors.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.Color val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) +val Red80 = Color(0xFFE57373) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Dimens.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Dimens.kt new file mode 100644 index 0000000..d4d76a1 --- /dev/null +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Dimens.kt @@ -0,0 +1,10 @@ +package nl.q42.template.ui.theme + +import androidx.compose.ui.unit.dp + +object Dimens { + object Containers { + val cornerRadius = 8.dp + val cornerRadiusLarge = 16.dp + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Theme.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Theme.kt index 7435580..a21a7ff 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Theme.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Theme.kt @@ -2,19 +2,17 @@ package nl.q42.template.ui.theme import android.annotation.SuppressLint import android.app.Activity -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat @@ -23,64 +21,62 @@ import androidx.core.view.WindowCompat * README file of our project. */ -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, - background = Black, -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - background = White - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) +private val LocalAppTypography = staticCompositionLocalOf { AppTypography() } +private val LocalAppColorTokens = staticCompositionLocalOf { + // Dummy default, will be replaced for the actual tokens by the Provider + AppColorTokensLight +} +private val LocalAppShapes = staticCompositionLocalOf { AppShapes() } @Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + typography: AppTypography = AppTheme.typography, + colors: AppColorTokens = AppTheme.colors, + shapes: AppShapes = AppTheme.shapes, + content: @Composable () -> Unit ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() + window.statusBarColor = colors.accent.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme } } - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, + CompositionLocalProvider( + LocalAppTypography provides typography, + LocalAppColorTokens provides if (darkTheme) AppColorTokensDark else AppColorTokensLight, + LocalAppShapes provides shapes, content = content ) } +object AppTheme { + val typography: AppTypography + @Composable + @ReadOnlyComposable + get() = LocalAppTypography.current + val colors: AppColorTokens + @Composable + @ReadOnlyComposable + get() = LocalAppColorTokens.current + val shapes: AppShapes + @Composable + @ReadOnlyComposable + get() = LocalAppShapes.current +} + +@Immutable +data class AppShapes( + val small: Shape = RoundedCornerShape(Dimens.Containers.cornerRadius), + val medium: Shape = RoundedCornerShape(Dimens.Containers.cornerRadius), + val large: Shape = RoundedCornerShape(Dimens.Containers.cornerRadiusLarge) +) + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PreviewAppTheme(content: @Composable () -> Unit) { AppTheme { diff --git a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Type.kt b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Type.kt index ea9a95a..f1d728c 100644 --- a/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Type.kt +++ b/core/ui/src/main/kotlin/nl/q42/template/ui/theme/Type.kt @@ -1,6 +1,7 @@ package nl.q42.template.ui.theme import androidx.compose.material3.Typography +import androidx.compose.runtime.Immutable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -31,4 +32,32 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ +) + +val defaultFontFamily = FontFamily.Default + +@Immutable +data class AppTypography ( + // The names of these styles are shared with your team's design system + // So it is easy to communicate with designers about what text style to use + + val body: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ), + val h1: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + val label: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) ) \ No newline at end of file diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt index b113920..0b0c92e 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt @@ -10,8 +10,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import nl.q42.template.home.main.presentation.HomeViewState +import nl.q42.template.ui.compose.composables.widgets.TemplateButton import nl.q42.template.ui.compose.get import nl.q42.template.ui.presentation.toViewStateString +import nl.q42.template.ui.theme.AppTheme import nl.q42.template.ui.theme.PreviewAppTheme import nl.q42.template.ui.theme.PreviewLightDark @@ -36,21 +38,22 @@ internal fun HomeContent( viewState.userEmailTitle?.get()?.let { Text(text = it) } if (viewState.isLoading) CircularProgressIndicator() - if (viewState.showError) Text(text = "Error") + if (viewState.showError) Text( + text = "Error", + style = AppTheme.typography.body, + color = AppTheme.colors.error + ) - Button(onClick = onLoadClicked) { - Text("Refresh") - } + TemplateButton("Refresh", onLoadClicked) - Button(onClick = onOpenSecondScreenClicked) { - Text("Open second screen") - } - Button(onClick = onOpenOnboardingClicked) { - Text("Open onboarding") - } + TemplateButton("Open second screen", onOpenSecondScreenClicked) + + TemplateButton("Open Onboarding", onOpenOnboardingClicked) } } + + @PreviewLightDark @Composable private fun HomeContentErrorPreview() { diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt b/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt index d114769..bf3f638 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/second/ui/HomeSecondScreen.kt @@ -16,6 +16,8 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import nl.q42.template.home.second.presentation.HomeSecondViewModel import nl.q42.template.navigation.viewmodel.InitNavigator +import nl.q42.template.ui.compose.composables.widgets.TemplateButton +import nl.q42.template.ui.theme.AppTheme @Destination @Composable @@ -40,10 +42,8 @@ fun HomeSecondScreen( verticalArrangement = Arrangement.Center, ) { - Text(viewState.title, style = MaterialTheme.typography.titleMedium) + Text(viewState.title, style = AppTheme.typography.h1, color = AppTheme.colors.textPrimary) - Button(onClick = viewModel::onBackClicked) { - Text("Close") - } + TemplateButton("Close", viewModel::onBackClicked) } }