Is CompositionLocalProvider the only approach for custom Compose Theme? - android-jetpack-compose

I'm working on the Theme of an application which has its own design system. After reading the official documentation about custom theming I have some doubts.
Is there any reason why the different specs are provided as a CompositionLocals? I understand in some cases like colors where there is a chance we need to trigger recomposition for something like (dark/light) but for Typographies why bother providing it through CompositionLocal and not access them directly from a static variable? Having a custom Typography spec means using Material Components "won't work" anyway, right?
For context the Typography spec would be something like XXL, XL, L, M, S..(insted of h1, h2..) with a prefidined sizes (weights, etc). It could be modeled has a global object that could be accessed from everywhere instead of a class that is provided... does it make sense?

I use composition local for getting access to custom spacing anywhere as follows
Spacing.kt
data class Spacing(
val default : Dp = 0.dp,
val extraSmall: Dp = 4.dp,
val small: Dp = 8.dp,
val large: Dp = 12.dp,
val extraLarge: Dp = 16.dp,
)
val LocalSpacing = compositionLocalOf { Spacing() }
val MaterialTheme.spacing: Spacing
#Composable
#ReadOnlyComposable
get() = LocalSpacing.current
In theme.kt
#Composable
fun PokedexTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
CompositionLocalProvider(
LocalSpacing provides Spacing()
) {
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
}
Access it as:
MaterialTheme.Spacing.large

Related

Jetpack Compose - avoid unnecessary recomposition

I'm creating a custom slider control for my app but I can't avoid unnecessary recomposition without adding some ugly hacks...
CustomSlider1 is a component that recomposes all its children when the value changes; CustomSlider2 is what I came up with that does not... but the code doesn't seem right, so could anyone tell me what I'm doing wrong in CustomSlider1 and if CustomSlider2 is indeed correct?
The difference between the 2 components is basically that I read the value through a lambda and also added the Slider component inside a Scoped composable.
I'm using recomposeHighlighter to show recompositions.
Here's a gif showing how both behaves when I change its value:
Here's the code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestTheme {
Column {
var value by remember {
mutableStateOf(50f)
}
CustomSlider1("Custom Slider", value, 50f, true, { value = it }, 0f..100f, 5)
Spacer(modifier = Modifier.padding(10.dp))
CustomSlider2("Custom Slider 2", { value }, 50f, true, { value = it }, 0f..100f, 5)
}
}
}
}
}
#Composable
fun CustomSlider1(
label: String,
value: Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Slider(
value = value,
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
#Composable
fun CustomSlider2(
label: String,
value: () -> Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Scoped { //had to do this to avoid recompositions...
Slider(
value = value.invoke(),
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
}
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
#Composable
fun Scoped(content: #Composable () -> Unit) = content()
First thing you do to prevent recompositions creating Scope to create recomposition scope to limit recomposition since Column and Row are inline functions that do not create scopes.
Second thing with lambdas. In compose lambdas are unique they defer state read from composition phase of frame to layout or draw phases that's why you don't have recompositions.
Composition->Layout( measure and Layout)->Draw are the phases when a (re)composition is triggered by using lambdas you don't invoke composition phase.
For lambdas and state deferring you can check out official document or question below
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
Here, the box's background color is switching rapidly between two
colors. This state is thus changing very frequently. The composable
then reads this state in the background modifier. As a result, the box
has to recompose on every frame, since the color is changing on every
frame.
To improve this, we can use a lambda-based modifier–in this case,
drawBehind. That means the color state is only read during the draw
phase. As a result, Compose can skip the composition and layout phases
entirely–when the color changes, Compose goes straight to the draw
phase.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier
.fillMaxSize()
.drawBehind {
drawRect(color)
} )
Android Jetpack Compose - Composable Function get recompose each time Text-field value changes
For scoped composition you can check out this question or other answer linked to it

Why is MediumTopAppBar (and Large) showing two TextField in compose?

I am trying to make the title of a screen editable.
MediumTopAppBar(
title = {
val name: String? = "Some Title"
var input by remember { mutableStateOf(name ?: "") }
when (state.isEditingTitle) {
true ->
TextField(
value = input,
onValueChange = { input = it },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
callbacks.onEditTitleChange(editTitle = false, updatedTitle = input)
})
)
false -> {
Text(
modifier = Modifier.clickable { callbacks.onEditTitleChange(true, null) },
text = name ?: "(No Title)"
)
}
}
},
... more app bar parameters
}
When I click on the title Text(...) and the view gets recomposed the AppBar shows two TextFields
How do I ignore the top one and only show the one in the bottom, like the Text() is only shown in the bottom?
(Fyi: the two TextInputs have their own remembered state and calls the callback with their own respective value)
Bonus question: How do I handle the remembered state "input" so that it resets every time the onDone keyboard action is triggered? Instead of val name: String? = "Some Title" it would of course be something in the line of val name: String? = state.stateModel.title
I found out why it does this, but I have no idea how to solve it (except for just making my own views and placing it close by)
It's easy to see when looking at the function for the MediumTopBar
// androidx.compose.material3.AppBar.kt
#ExperimentalMaterial3Api
#Composable
fun MediumTopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable () -> Unit = {},
actions: #Composable RowScope.() -> Unit = {},
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TwoRowsTopAppBar(
modifier = modifier,
title = title,
titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarMediumTokens.HeadlineFont),
smallTitleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
titleBottomPadding = MediumTitleBottomPadding,
smallTitle = title, // <- this thing, right here
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
windowInsets = windowInsets,
maxHeight = TopAppBarMediumTokens.ContainerHeight,
pinnedHeight = TopAppBarSmallTokens.ContainerHeight,
scrollBehavior = scrollBehavior
)
}
There's some internal state shenanigans going on, probably checking for a Text being shown in the 2nd TopAppBarLayout (more digging required to find that), but not for any other view.
TwoRowsTopAppBar and TopAppBarLayout are not public, and can't be used directly.
This is explains why, but it would be interesting to see how to solve it (still using Medium or Large -TopAppBar)
it is stupid thing devs overlooked and should be warned against, at least. The answer is do not give default colors to your Typography TextStyles.
private val BodySmall = TextStyle(
fontSize = 10.sp,
lineHeight = 12.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = Manrope,
color = Color.Black // REMOVE THIS
)
val OurTypography = Typography(
...
bodySmall = BodySmall
)

Measuring string width to properly size Text() composable

I am working on a Jetpack Compose based app which shows a simple list with 3 columns.
Things are working in principle, but what I am struggling with is to automatically determine the size of the columns.
For example, the width requirement of the date column will differ significantly depending on user's locale settings and font size.
24.12.2021 - 20:00 requires a lot less screen space than
12/14/2021 - 08:00 PM.
What I was hoping I can do is to work with a sample date, measure it up based on current locale settings and font size and then set the width for all list entries accordingly.
Something similar to this:
val d = Date( 2021,12,30 ,23,59,59) // Sample date
val t = dateFormat.format(d) + " - " + timeFormat.format(d) // Build the output string
val dateColumnWidth = measureTextWidth(t, fontSize) // This is what I need
…
LazyColumn {
…
Row {
Text(text = t, Modifier.width(dateColumnWidth.dp), fontSize = fontSize.sp))
Text(text = value)
Text(text = comment)
}
}
…
I have been on this for weeks but a function like my "measureTextWidth" above doesn't seem to exist.
Is there any way to achieve this?
You can use SubcomposeLayout like this:
#Composable
fun MeasureUnconstrainedViewWidth(
viewToMeasure: #Composable () -> Unit,
content: #Composable (measuredWidth: Dp) -> Unit,
) {
SubcomposeLayout { constraints ->
val measuredWidth = subcompose("viewToMeasure", viewToMeasure)[0]
.measure(Constraints()).width.toDp()
val contentPlaceable = subcompose("content") {
content(measuredWidth)
}[0].measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
contentPlaceable.place(0, 0)
}
}
}
Then use it in your view:
MeasureUnconstrainedViewWidth(
viewToMeasure = {
Text("your sample text")
}
) { measuredWidth ->
// use measuredWidth to create your view
}

How do I use Jepack compose to implement a drag sorted list?

I want to implement a drag sorted list, functions like drag-sort-recyclerview/gridview,but use jetpack compose.
use this library it's help-out
and this is an Example of my usage
implementation("org.burnoutcrew.composereorderable:reorderable:0.6.1")
val state = rememberReorderState()
val list=notes.toMutableList()
LazyColumn(
state = state.listState,
modifier = Modifier.reorderable(state, { a, b -> list.move(a, b) })
) {
items(list, { it.id }) { noteIndex ->
Note(
Modifier
.draggedItem(state.offsetByKey(noteIndex.id))
.detectReorderAfterLongPress(state),
note = noteIndex,
onNoteClick = onNoteClick,
onNoteCheckedChange = onNoteCheckedChange
)
}
}

Building a DspComplex ROM in Chisel

I'm attempting to build a ROM-based Window function using DSPComplex and FixedPoint types, but seem to keep running into the following error:
chisel3.core.Binding$ExpectedHardwareException: vec element 'dsptools.numbers.DspComplex#32' must be hardware, not a bare Chisel type
The source code for my attempt at this looks like the following:
class TaylorWindow(len: Int, window: Seq[FixedPoint]) extends Module {
val io = IO(new Bundle {
val d_valid_in = Input(Bool())
val sample = Input(DspComplex(FixedPoint(16.W, 8.BP), FixedPoint(16.W, 8.BP)))
val windowed_sample = Output(DspComplex(FixedPoint(24.W, 8.BP), FixedPoint(24.W, 8.BP)))
val d_valid_out = Output(Bool())
})
val win_coeff = Vec(window.map(x=>DspComplex(x, FixedPoint(0, 16.W, 8.BP))).toSeq) // ROM storing our coefficients.
io.d_valid_out := io.d_valid_in
val counter = Reg(UInt(10.W))
// Implicit reset
io.windowed_sample:= io.sample * win_coeff(counter)
when(io.d_valid_in) {
counter := counter + 1.U
}
}
println(getVerilog(new TaylorWindow(1024, fp_seq)))
I'm actually reading the coefficients in from a file (this particular window has a complex generation function that I'm doing in Python elsewhere) with the following sequence of steps
val filename = "../generated/taylor_coeffs"
val coeff_file = Source.fromFile(filename).getLines
val double_coeffs = coeff_file.map(x => x.toDouble)
val fp_coeffs = double_coeffs.map(x => FixedPoint.fromDouble(x, 16.W, 8.BP))
val fp_seq = fp_coeffs.toSeq
Does this mean the DSPComplex type isn't able to be translated to Verilog?
Commenting out the win_coeff line seems to make the whole thing generate (but clearly doesn't do what I want it to do)
I think you should try using
val win_coeff = VecInit(window.map(x=>DspComplex.wire(x, FixedPoint.fromDouble(0.0, 16.W, 8.BP))).toSeq) // ROM storing our coefficients.
which will create hardware values like you want. The Vec just creates a Vec of the type specfied

Resources