当前位置:网站首页>What if there are too few jetpack compose theme colors? Design your own color system
What if there are too few jetpack compose theme colors? Design your own color system
2022-06-23 21:00:00 【Petterp】
introduction
JetPack Compose The official version has been out for months , In the meantime , In addition to business-related requirements , I also started Compose The landing experiment in the actual project , Because once you want to access the current project , The problems encountered are actually much larger than those needed to create a new project .
What this article wants to solve is Compose Default Material Too few theme colors , How to configure your own business color board , Or say , How to customize your own color system , And from point to depth , System analysis, related implementation methods and principles .
problem
Before we start , Let's take a look at creating a Compose project , default Material What colors does the theme provide us :
For an ordinary application , The default has basically met the development requirements , The basic theme color is enough . But then a problem arises , What if I have other theme colors ?
Traditional practice
In traditional View In the system , We usually define color in color.xml In file , When in use, it can be read directly ,getColor(R.xx) , Everyone is already familiar with this , So in Compose What about China? ?
Compose
stay Compose in ,google The color values are unified in theme Under the color.kt in , This is actually a global static variable , At first glance, it seems that there is no problem , Where is my business color , You can't expose the whole situation ?
But smart you must know , I put it in the old way color.xml Just go inside , It's not impossible , But the following problems follow :
- When switching themes , How to unify the colors ?
- stay
GoogleOf simple in ,color.xml There is often no configuration written in the , namely Google It is not recommended tocomposeUse it like this in
So what should I do , I'll go and see google Of simple, See how they solve :
simple Sure enough simple ,Google In full accordance with Material Standards for , That is, there will be no other non theme colors , What about the reality , What should we do when we develop . Then I searched the current github Some open source projects written by the big guys on , I found that they all followed Material To achieve , But obviously, this is very unrealistic ( National conditions ).
Solutions
Write as you like ( Not recommended )
Describe There are no standards , Just roll up your sleeves , Left brain thinking , Knock with your right hand , Pick up ️ Just do it , It also refers to those who work hard in the new era *️
Since the official didn't write how to solve it , Then find a way to solve it by yourself .
compose in , For data change monitoring, use MutableState , Then I customize a singleton holding class myself , Hold the existing theme configuration , Then define a business color class , And define the corresponding theme color class object , Finally, configure... According to the theme of the current single example , To determine what color board to use in the end , When changing the business topic, you only need to change the configuration field of this single instance topic . At the thought of being so simple , I'm so clever , Do as you say
Create topic enumeration
enum class ColorPallet {
// The default is two colors , Multiple can be defined according to requirements
DARK, LIGHT
}Add a single example of topic configuration
object ColorsManager {
/** Use a variable to maintain the current topic */
var pallet by mutableStateOf(ColorPallet.LIGHT)
}Add color board
/** Common colors */
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
/** Business color configuration , If you need to add other business themes , Directly define the following objects , If a color shares , Then increase the default value */
open class CkColor(val homeBackColor: Color, val homeTitleTvColor: Color = Color.Gray)
/** Business color template objects defined in advance */
private val CkDarkColor = CkColor(
homeBackColor = Color.Black
)
private val CkLightColor = CkColor(
homeBackColor = Color.White
)
/** Default color configuration , namely Md Default configuration color */
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
)Add unified call entry
In order to facilitate practical use , Let's add one MaterialTheme.ckColor The extension function of , In order to use our custom color group :
/** Expand */
val MaterialTheme.ckColor: CkColor
get() = when (ColorsManager.pallet) {
ColorPallet.DARK -> CkDarkColor
ColorPallet.LIGHT -> CkLightColor
}The final theme is as follows
@Composable
fun CkTheme(
pallet: ColorPallet = ColorsManager.pallet,
content: @Composable() () -> Unit
) {
val colors = when (pallet) {
ColorPallet.DARK -> DarkColorPalette
ColorPallet.LIGHT -> LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}design sketch
Look at the effect , Simple and crude ,[ Look at ] No problem , Is there any other way ? I still don't believe the official didn't write , Maybe I was negligent .
Custom color system ( official )
Just as I looked through the official documents , Suddenly saw such small characters , It has achieved Custom color system .
I'm really blind , I didn't see this line , With an official example , So I hurried to study ( copy ) Code .
Add color template
// Example , The right thing to do is to put color.kt Next
val Blue50 = Color(0xFFE3F2FD)
val Blue200 = Color(0xFF90CAF9)
val A900 = Color(0xFF0D47A1)
/**
* The color set of the actual theme , All colors need to be added , And use the corresponding subclass to override the color .
* Every time you change, you need to configure the color below [CkColors] in , Synchronization [CkDarkColor] And [CkLightColor]
* */
@Stable
class CkColors(
homeBackColor: Color,
homeTitleTvColor: Color
) {
var homeBackColor by mutableStateOf(homeBackColor)
private set
var homeTitleTvColor by mutableStateOf(homeTitleTvColor)
private set
fun update(colors: CkColors) {
this.homeBackColor = colors.homeBackColor
this.homeTitleTvColor = colors.homeTitleTvColor
}
fun copy() = CkColors(homeBackColor, homeTitleTvColor)
}
/** Pre defined color template objects */
private val CkDarkColors = CkColors(
homeBackColor = A900,
homeTitleTvColor = Blue50,
)
private val CkLightColors = CkColors(
homeBackColor = Blue200,
homeTitleTvColor = Color.White,
)increase xxLocalProvider
@Composable
fun ProvideLcColors(colors: CkColors, content: @Composable () -> Unit) {
val colorPalette = remember {
colors.copy()
}
colorPalette.update(colors)
CompositionLocalProvider(LocalLcColors provides colorPalette, content = content)
}increase LocalLcColors Static variables
// Create static CompositionLocal , In general, the theme will not change very frequently
private val LocalLcColors = staticCompositionLocalOf {
CkLightColors
}Add a single example of topic configuration
enum class StylePallet {
// The default is two colors , Multiple can be defined according to requirements
DARK, LIGHT
}
/* Configure the color palette extension properties for the current theme */
private val StylePallet.colors: Pair<Colors, CkColors>
get() = when (this) {
StylePallet.DARK -> DarkColorPalette to CkDarkColors
StylePallet.LIGHT -> LightColorPalette to CkLightColors
}
/** CkX-Compose Subject Manager */
object CkXTheme {
/** from CompositionLocal Take out the corresponding Local */
val colors: CkColors
@Composable
get() = LocalLcColors.current
/** Use one state Maintain the current topic configuration , The wording here depends on the specific business , If you use dark mode, the default configuration , You don't need this variable , namely app Only dark and bright colors are supported , Then you only need to read the system configuration every time . however compose It can switch themes quickly , So maintaining a variable is definitely unavoidable */
var pallet by mutableStateOf(StylePallet.LIGHT)
}Final subject code
@Composable
fun CkXTheme(
pallet: StylePallet = CkXTheme.pallet,
content: @Composable () -> Unit
) {
val (colorPalette, lcColors) = pallet.colors
ProvideLcColors(colors = lcColors) {
MaterialTheme(
colors = colorPalette,
typography = Typography,
shapes = Shapes,
content = content
)
}
}analysis
The final effect is consistent with the above , I won't go into details , Let's mainly analyze , Why? Google Write like this :
We can see that the above example mainly uses CompositionLocalProvider To save the current theme configuration , and CompositionLocalProvider And inherit from CompositionLocal , For example, we often use MaterialTheme In the theme Shapes ,typography It's all about managing .
CkColors Added to this class @Stable , It stands for Compose for , This class is a stable class , That is, each change will not trigger reorganization , The internal color field uses mustbaleStateOf packing , To trigger a reorganization when the color changes , The interior has also added update() And copy() Method , To facilitate management and one-way data changes .
In fact, if we go to see Colors class . You will find... In the above example CkColors It's exactly the same design .
So in Compose Custom theme colors in , In fact, we are
ColorsOn the basis of, I wrote a set of my own color matching .
In this case , Then why don't we inherit directly Colors To add color ? When I use it, I can force it , So you don't have to make anything by yourself CompositionLocal 了 ?
It's easy to understand , because Colors Medium copy() as well as update() Can't be rewritten , I didn't add it open , And its internal variables use internal modification set Method . The more important reason is that Do not conform to the Md The design of the , So that's why We need to customize our own color system , You can even completely customize your own theme system . The premise is that you think there is another layer of packaging in the custom theme MaterialTheme If the theme is ugly , Of course, the corresponding , You also need to consider how to solve other incidental problems .
reflection
What we said above is all about use , So have you ever thought about ? Why should the official custom design system use CompositionLocal Well ?
Maybe some new students haven't used this , For better understanding , First, let's find out CompositionLocal What is it , Not to mention its popular concept , We can explain it simply with a small example .
deconstruction
In common development scenarios , We often , Often a parameter is passed to other methods , We call it display passing .
Switch scenes , We are Compose in , We often pass parameters to composable functions , So this way is Google Academic is called : The data is in the form of parameters Flow down The entire interface tree is passed to each composable function , As shown below :
@Composable
fun A(message: String) {
Column() {
B(message)
}
}
@Composable
fun B(message: String) {
Text(text = message)
C(message)
}
@Composable
fun C(message: String) {
Text(text = message)
} In the example above 3 A composable function , among A Need to receive a message character string , And pass it on to B , and B At the same time, it needs to be passed to C , Like an infinite doll , We may feel OK at this time , But if this doll appears n The floor , But if there is more than one data ? This can be very troublesome .
So is there any other way to solve it ? stay Compose in , Officials have given the standard answer , That's it CompositionLocal :
That is to use CompositionLocal To complete composable Data sharing in the tree , also CompositionLocal With hierarchy , It can be limited to a certain composable In the subtree as the root node , Pass down by default , At the same time, one of the subtrees composable You can also apply to this CompositionLocal To cover , Then the new value will be in this composable Continue to pass down .
composableYou can combine functions , Simple understanding is to use@ComposableThe way to mark .
practice
As shown below , We use... For the above code CompositionLocal To transform :
val MessageContent = compositionLocalOf { "simple" }
@Composable
fun A(message: String) {
Column() {
// provides Equivalent to writing data
CompositionLocalProvider(MessageContent provides message) {
B()
}
}
}
@Composable
fun B() {
Text(text = MessageContent.current)
CompositionLocalProvider(MessageContent provides " Temporarily change the value ") {
C()
}
}
@Composable
fun C() {
Text(text = MessageContent.current)
} First, a named MessageContent Of CompositionLocal , The default value is "simple" , A Method to receive a message Field , And write it into MessageContent , And then in B in , We can get the method just now A Write to the CompositionLocal The data of , Instead of displaying, add fields in the method parameters .
Again , Method B You can also deal with this CompositionLocal Make changes , such C You'll get another value .
And when we use CompositionLocal.current To get data , This current The value closest to the current component will be returned , So we can also use it to do some basic implementation of implicit separation .
Expand
Corresponding , Let's talk about creating CompositionLocal The way :
compositionLocalOf: Changing the supplied value during reorganization will only cause it to be readcurrentThe content of the value is invalid .staticCompositionLocalOf: AndcompositionLocalOfDifferent ,ComposeNo trackingstaticCompositionLocalOfThe read . Changing this value will result inCompositionLocalThe wholecontentlambdaBe reorganized , Instead of just reading in a combinationcurrentValue position .
summary
We know something about it CompositionLocal The role of , Just imagine , If you don't use it , If we let ourselves implement a color system , Maybe we'll fall into our first kind of Follow one's inclinations Writing .
First , Can you use that kind of writing ? Yes, of course , But in practice, there will be many problems , For example, the change of theme will lead to and non-compliance with Compose The design of the , And if we may have some business under certain circumstances , It may always maintain a theme color , So how to solve it at this time ?
If it's a method 1, You may enter the hard coding phase at this time , That is, use complex business logic to complete ; But if it's using
CompositionLocalWell ? Will this problem still exist , Just write a new color configuration , After this logic is completed, you can re write the current topic configuration , Will there be complex logical entanglement ? That's whyCompositionLocalTo customize the color system and the configuration that can be manipulated by users in the whole theme system , Implicit , For users , You can do it without perception .
In depth analysis
It's over CompositionLocal And the actual scene , Let's think about it ,CompositionLocal How to achieve it , The so-called know it, know why . Without going deep into the source code, it is often difficult to understand the specific implementation , So the analysis of this part may be a little complicated . If you think it's obscure , Take a look first Android developer - Go into detail Jetpack Compose Realization principle , Let's understand some of the following terms , It might be simpler , Because this article is not popular compose Realization principle , So please refer to the link above .
CompositionLocal
Get down to business , Let's take a look at the source code , The corresponding comments and codes are as follows :
sealed class CompositionLocal<T> constructor(defaultFactory: () -> T) {
// The default value is
internal val defaultValueHolder = LazyValueHolder(defaultFactory)
// Write the latest data
@Composable
internal abstract fun provided(value: T): State<T>
// Return from the most recent CompositionLocalProvider Provide the value of the
@OptIn(InternalComposeApi::class)
inline val current: T
@ReadOnlyComposable
@Composable
// Go straight to the code here
get() = currentComposer.consume(this)
} We know what to use when getting data current , Then just chase here .
currentComposer.consume()
@InternalComposeApi
override fun <T> consume(key: CompositionLocal<T>): T =
// currentCompositionLocalScope() Gets the current value provided by the parent composable item CompositionLocal Range map
resolveCompositionLocal(key, currentCompositionLocalScope())This code is mainly used to parse local composable items , To get the data . Let's look inside first currentCompositionLocalScope()
currentCompositionLocalScope()
private fun currentCompositionLocalScope(): CompositionLocalMap {
// If the composable item is currently being inserted and has a data provider
if (inserting && hasProvider) {
// Remove the current... From the insert table composable Current group( It can be understood as taking... Directly index Current currentGroup)
var current = writer.parent
// If this group It's not empty
while (current > 0) {
// Take this... Out of the slot table group Of key With the current composable Of key Contrast
if (writer.groupKey(current) == compositionLocalMapKey &&
writer.groupObjectKey(current) == compositionLocalMap
) {
// Returns the CompositionLocalMap
return writer.groupAux(current) as CompositionLocalMap
}
// Dead cycle , Keep looking up , If there is no in the current group , Just keep looking up , Until you find something that matches the current
current = writer.parent(current)
}
}
// If at present composable Of slotTable The array inside is not empty
if (slotTable.groupsSize > 0) {
// Remove the current slot from the current slot composable Current graoup
var current = reader.parent
// The default value is -1, If there is , That means there are composable items
while (current > 0) {
if (reader.groupKey(current) == compositionLocalMapKey &&
reader.groupObjectKey(current) == compositionLocalMap
) {
// from providerUpdates Gets the current value from the array CompositionLocalMap, Insert see - startProviders
return providerUpdates[current]
?: reader.groupAux(current) as CompositionLocalMap
}
current = reader.parent(current)
}
}
// If not , Returns the parent composable Of Provider
return parentProvider
} Used to get the distance from the current composable Current CompositionLocalMap .
resolveCompositionLocal()
private fun <T> resolveCompositionLocal(
key: CompositionLocal<T>,
scope: CompositionLocalMap
): T = if (scope.contains(key)) {
// If the current parent CompositionLocalMap It contains the current local, Directly from map To take
scope.getValueOf(key)
} else {
// Otherwise, it means that it is currently the top layer , No father local, Use default values directly
key.defaultValueHolder.value
} With the current CompositionLocal treat as key , Then go to the nearest CompositionLocalMap Find the corresponding value , If there is a direct return , Otherwise use CompositionLocal The default value of .
summary
So when we use CompositionLocal.current When getting data , Inside is actually through currentCompositionLocalScope() Get parent CompositionLocalMap, Be careful , Why is this map Well ? Because what is obtained here is under the current parent composable function all Of CompositionLocal, So in the source code consume The current... Needs to be passed in the method parameters CompositionLocal go in , Judge what we are going to take at present local Is there anything in it , If there is , Then take directly , Otherwise use the default value .
That's the question , our CompositionLocal How is it saved by composable tree ? With this question , Let's go on .
CompositionLocalProvider
Want to know CompositionLocal How to be saved by composable tree , You must start from below .
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProviders(values)
content()
currentComposer.endProviders()
} there currentComposer What is it again? ? And why first start, after end Well ?
Let's go in currentComposer have a look .
/**
* Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by
* code generation helpers. It is highly recommended that direct calls these be avoided as the
* runtime assumes that the calls are generated by the compiler and contain only a minimum amount
* of state validation.
*/
interface Composer
internal class ComposerImpl : Composer You can see in the Composer In the definition of , Composer Is for Compose kotlin Compiler plug-ins Provided ,google It is strongly not recommended that we manually call , in other words , there start and end It's actually two marks , The compiler calls itself , Or just for the convenience of the compiler . Then we went to see startProviders()
startProviders
@InternalComposeApi
override fun startProviders(values: Array<out ProvidedValue<*>>) {
// Get the current Composable Under the CompositionLocal-Map
val parentScope = currentCompositionLocalScope()
...
val currentProviders = invokeComposableForResult(this) {
compositionLocalMapOf(values, parentScope)
}
val providers: CompositionLocalMap
val invalid: Boolean
// If the composable item is being inserted into the tree or it is called for the first time , Then for true
if (inserting) {
// Update the current CompositionLocalMap
providers = updateProviderMapGroup(parentScope, currentProviders)
invalid = false
hasProvider = true
} else {
// If the current composable item cannot be skipped ( That is, it has changed ) perhaps providers inequality , Update the current Composable Of group
if (!skipping || oldValues != currentProviders) {
providers = updateProviderMapGroup(parentScope, currentProviders)
invalid = providers != oldScope
} else {
// Otherwise, skip the current update
skipGroup()
providers = oldScope
invalid = false
}
}
// If the reorganization is invalid and no is being inserted , Update current group Of CompositionLocalMap
if (invalid && !inserting) {
providerUpdates[reader.currentGroup] = providers
}
// The current operation push Go to the stack , Then pop up again
providersInvalidStack.push(providersInvalid.asInt())
...
// take providers Data writing group, It will eventually be written to SlotTable-slots( Slot buffer ) Array
start(compositionLocalMapKey, compositionLocalMap, false, providers)
} This method is used to start the data provider , If seen compose You will know the design principle of this is actually equivalent to group A start mark for , Its internal content is mainly to obtain the information from the current composable Current CompositionLocalMap , And then use compositionLocalMapOf() Pass in the current value Update to the corresponding CompositionLocalMap And in return , And then put this map Then update to the current group in .
Accordingly, we said , This is a start mark , Naturally, there is also a termination mark , namely end, In the above source code , We can know , It is endProviders():
endProviders
override fun endProviders() {
endGroup()
endGroup()
// Stack the current operation
providersInvalid = providersInvalidStack.pop().asBool()
} Its function is to end the provider's call , As for why end two , It should prevent inconsistency caused by writing , If there is a big man with different understanding , Just share it in the comment area .
summary
When we use CompositionLocalProvider Bind data to CompositionLocal when , Its interior will save it to the distance from the current composable Current CompositionLocalMap Inside , When we want to use it later , Use CompositionLocal.current When reading data , It will find the corresponding CompositionLocalMap , And with our CompositionLocal by key, Return if present , Otherwise, use the default value to return .
In fact, this article is not a particularly difficult problem , But it is Compose A problem that will be encountered in practice , It's easy to solve this problem , But understanding the design behind it is more important , I hope that through this article , You can better understand CompositionLocal The actual scene and design concept . Of course, to understand the specific source code, you still need to know Compose The basic design of , This point is posted below the reference article Android Developers can link . In the future, I will continue to pursue Compose Problems to be solved in practical application and analysis ideas , If there is an error , I hope I can help you .
If this article helps you , Welcome to support , You come on :)
Reference link
Official documents - Use CompositionLocal Scope data locally
Android developer - Go into detail Jetpack Compose | Realization principle
Android developer - Go into detail Jetpack Compose | Optimize UI structure
边栏推荐
- Why is it invalid to assign values to offsetwidth and offsetHeight
- [golang] how to realize real-time hot update of Go program
- [SAP ABAP] call API interface instance
- How to log in to the server through the fortress machine to transfer files? What are the specific steps?
- 国内期货开户怎么开?哪家期货公司开户更安全?
- [golang] quick review guide quickreview (I) -- string
- [golang] some questions to strengthen slice
- QPS fails to go up due to frequency limitation of public network CLB bandwidth
- Is Guoyuan futures trading software formal? How to download safely?
- Can Tencent cloud disk service share data? What are the advantages of cloud disk service?
猜你喜欢
Application of JDBC in performance test

Ugeek's theory 𞓜 application and design of observable hyperfusion storage system

Yaokui tower in Fengjie, Chongqing, after its completion, will be the safety tower for Sichuan river shipping with five local scholars in the company

Use of the vs2022scanf function. An error is reported when using scanf - the return value is ignored: Solutions

JS advanced programming version 4: generator learning

Add two factor authentication, not afraid of password disclosure, let alone 123456

Syntax of SQL union query (inline, left, right, and full)
Implementing MySQL fuzzy search with node and express

重庆 奉节耀奎塔,建成后当地连中五名进士,是川江航运的安全塔

The "open source star picking program" container pulls private images from harbor, which is a necessary skill for cloud native advanced technology
随机推荐
How to handle the prompt that DNS is incorrect when adding resolution to Tencent cloud?
@@Script implementation of ishell automatic deployment
Implementing MySQL fuzzy search with node and express
Yaokui tower in Fengjie, Chongqing, after its completion, will be the safety tower for Sichuan river shipping with five local scholars in the company
What is the main content of short video audit? What is illegal?
[golang] quick review guide quickreview (VI) -- struct
重庆 奉节耀奎塔,建成后当地连中五名进士,是川江航运的安全塔
Machine learning related
Is it safe for flush to open an account online? Is the Commission high
【Golang】在Go语言的角度重新审视闭包
How to separate image processing? What should I pay attention to when separating layers?
How do I open an account? Is it safe to open an account in Guohai Securities? What do you need to bring?
December 29, 2021: the elimination rules of a subsequence are as follows: 1. In a subsequence
The element of display:none cannot get offsetwidth and offsetHeight
How to solve the problem that the ID is not displayed when easycvr edits the national standard channel?
Applet development framework recommendation
[golang] quick review guide quickreview (VII) -- Interface
Implementation of flashback query for PostgreSQL database compatible with Oracle Database
How to Net project migration to NET Core
FPGA based electromagnetic ultrasonic pulse compression detection system paper + source file