Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom theming #77

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Comment on lines +14 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we define every button color? not doing it could mean that the usage of this button is expanded without defining the colours for it and thus ending with a weirdly themed state

) {
Text(
text = text,
style = AppTheme.typography.body,
color = AppTheme.colors.buttonText
)
}
}

@Composable
@PreviewLightDark
private fun TemplateButtonPreview() {
AppTheme {
TemplateButton("Button", {})
}
}
11 changes: 11 additions & 0 deletions core/ui/src/main/kotlin/nl/q42/template/ui/theme/AppColorTokens.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nl.q42.template.ui.theme

import androidx.compose.ui.graphics.Color

interface AppColorTokens {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would name this AppColorScheme (TemplateColorScheme?) to avoid using token in the word. token is too vague for what this is (for me).

val buttonText: Color
val accent: Color
val textPrimary: Color
val surface: Color
val error: Color
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions core/ui/src/main/kotlin/nl/q42/template/ui/theme/Dimens.kt
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +6 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we only want this for the shapes, should we move it there instead of leaving it public here? it might be nice to force the usage of the AppTheme.shapes instead of creating new shapes using the corner here

}
90 changes: 43 additions & 47 deletions core/ui/src/main/kotlin/nl/q42/template/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<AppColorTokens> {
// 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
Comment on lines 42 to 45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

MaterialTheme(
Copy link
Collaborator Author

@ninovanhooff ninovanhooff Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert: We still need the MaterialTheme wrapper for system Composables like AlertDialog

colorScheme = colorScheme,
typography = Typography,
CompositionLocalProvider(
LocalAppTypography provides typography,
LocalAppColorTokens provides if (darkTheme) AppColorTokensDark else AppColorTokensLight,
LocalAppShapes provides shapes,
content = content
)
Comment on lines -75 to 54
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would expand this a little bit to also provide some crucial elements used thought the app (namely ripples & indicators):

    CompositionLocalProvider(
        LocalAppColorTokens provides if (darkTheme) AppColorTokensDark else AppColorTokensLight,
        LocalAppTypography provides typography,
        LocalAppShapes provides shapes,
        /** configures the ripple for material components */
        LocalRippleConfiguration provides AppRippleConfiguration,
        /** needed for non-material components to have a material ripple. eg [Modifier.clickable] */
        LocalIndication provides AppRipple,
        /** merges the platform style with our type, @see [ProvideTextStyle] for more context */
        LocalTextStyle provides LocalTextStyle.current.merge(typography.body),
        content = content
    )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially also a default LocalContentColor so that at least the base text/content color is defined. it could avoid a lot of issues where a dev forgets to manually specify the color on any component.

}

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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also define a cornerRadiusSmall?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move the shapes & type to its own files similar to colors?

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 {
Expand Down
29 changes: 29 additions & 0 deletions core/ui/src/main/kotlin/nl/q42/template/ui/theme/Type.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}