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

Adds ThemedColorProvider to resources module. #277

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
17 changes: 17 additions & 0 deletions resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ You can treat any `String` as a key to a `Color` value.
By default, this loads the Color in the `Color Assets` (iOS) or `color.xml` (Android) files declared in the main project scope.
A `ColorLoader` class is provided to overwrite this behaviour.

### ThemedColor
`ThemedColorProvider` can be used to obtain a `Color` that has two different values depending on the theme the system is running.
It is aware of Dark and Light themes, and it's bound to it automatically.
To define such a color, the following syntax can be used:
```
val background by themeColor(light="#FFFFFF", dark="#000000")
```
Platforms will be able to use this Color in their code, knowing that it comes automatically in the correct contrast depending on the theme.
If it's still needed to refer to the single Light and Dark colors, it can be done in the following way:
```
val backgroundColorProvider = themeColor(light="#FFFFFF", dark="#000000")
val background by backgroundColorProvider
val darkBackground = backgroundColorProvider.darkColor
val lightBackground = backgroundColorProvider.lightColor

```

### Image
The `Image` class is associated with `UIImage` (iOS) and `Drawable` (Android).
You can treat any `String` as a key to a `Image` value.
Expand Down
32 changes: 32 additions & 0 deletions resources/src/androidLibMain/kotlin/ThemedColorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2021 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.splendo.kaluga.resources

class AndroidThemedColorProvider(light: String, dark: String): ThemedColorProvider() {

/** The dark theme version of this color, already loaded */
override val darkColor = colorFrom(dark)!!

/** The light theme version of this color, already loaded */
override val lightColor = colorFrom(light)!!

override val color get() = if (isInDarkMode) darkColor else lightColor
}

actual fun themeColor(light: String, dark: String): ThemedColorProvider
= AndroidThemedColorProvider(light, dark)
44 changes: 44 additions & 0 deletions resources/src/commonMain/kotlin/ThemedColorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2021 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/
@file:JvmName("CommonThemedColorProvider")
package com.splendo.kaluga.resources

import kotlin.jvm.JvmName
import kotlin.reflect.KProperty

/** Multiplatform Color Provider that is aware of a Dark and Light mode */
abstract class ThemedColorProvider {

/** light version of this color */
abstract val lightColor: Color

/** dark version of this color */
abstract val darkColor: Color
Comment on lines +27 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rename these 2 properties just light and dark, is already known they refer to color. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thought about it, but light and dark are also the name of the params which are strings.
I preferred not to rename those to lightHexValue or ligthValue, but to rename these instead.
Because I prefer to call it with val somename by themeColor(light = "#fff", dark = "#000"): very simple
Also, the third and more important variable is called color, so darkColor and lightColor seem consistent to me.

Good point, though


/** main color, automatically changing based on theme */
abstract val color: Color
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this API might be improved a bit, if color property type would be Flow. That would explicitly indicate its dynamic nature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see your point, and this feature can be implemented with a Flow.
But, thinking about the use case, I am not sure that is the best way to go.
This provider is meant to unify and simplify the usage of cross-platform color resources, when it comes to Dark/Light mode. Which is quite different in the way that each color on iOS has 2 versions, but not on Android (where a set of colors has 2 versions, not the color itself)
iOS has no need for a Flow, cause the changing of colors on UI is something that happens by using colorWithDynamicProvider . I don't see it as something you'd need to collect multiple times.

Also on the android side, I don't see a use case where is important to observe this color change, if not to trigger recomposition.
But even then, is not the change of color that should be observed but rather the Theme change.
So simplicity is preferred in this case.
In some projects using Kaluga, the common color resource file is currently made of getters or lazy initializations.

FYI:
With this idea, on Compose, there is no need to implement Material Colors: is currently intended for colors that we can use directly from a definition in common code. (a Compose Color can be easily created from a Kaluga Color).
Maybe it matters more to indicate the dynamic nature of these assets when referenced on the ViewModels.

Copy link
Contributor

Choose a reason for hiding this comment

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

But even then, is not the change of color that should be observed but rather the Theme change.

I absolutely agree, but I don't see a shared implementation of Flow<Theme> either. Moreover, in Compose UI it won't really work: the re-composition will indeed be triggered, but due to the skipping if the inputs haven't changed, it might not affect the places, where colors are being read (it really depends on the implementation).

}

/** This extension allows to get the hexValue of the provided color with the "by" syntax */
inline operator fun ThemedColorProvider.getValue(thisRef: Any?, property: KProperty<*>): Color = color

/**
* One single color represented in 2 themes Dark or Light.
* @param light color when presented in Light mode: formatted as hexadecimal string
* @param dark color when presented in Dark mode: formatted as hexadecimal string
*/
expect fun themeColor(light: String, dark: String): ThemedColorProvider
Copy link
Contributor

Choose a reason for hiding this comment

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

There is an isInDarkMode method on the kaluga level. So I dont see the point of making this platform specific. It should just be:

fun themeColor(light: Color, dark: Color): Color {
if (isInDarkMode) dark else light
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if you want the dynamic behaviour on Android on a named level, you can just make a values_night folder and put the dark color there.

Copy link
Contributor

Choose a reason for hiding this comment

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

The main reason that simple if statement doesn't reflect change in real time on iOS (including SwiftUI Previews) so UIColor should be created on other way using special constructor to have dynamic colors.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, look at the implementation. iOS provides a color that is both light and dark, whereas this does not exist on Android. See https://splendo.slack.com/archives/CN1T4SM5Y/p1618294377023400

Copy link
Contributor

@avdyushin avdyushin Apr 29, 2021

Choose a reason for hiding this comment

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

Actually good point regarding values-night worth to try on Android and just returning same color name to be handled by Android itself?

Copy link
Collaborator

Choose a reason for hiding this comment

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

putting it in values-night defies the point of what this method tries to do, defining colors once in common code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually good point regarding values-night worth to try on Android and just returning same color name to be handled by Android itself?

Yes, this is worth looking into and too bad I didn't think about it in the first place.
I was doubting that values-night is the folder used in case of dark mode (earlier versions on Android would use this folder for other modes), but indeed it is.

I will make some more experiments when I have time

@Tijl I agree that the preferred usage of this class is indeed by using the strings directly.
Although the problem is trying to solve is also to unify the usage of colors on platform without breaking the dark/light switch.
In that sense, I think is worth looking into the values-night solution while still using these ColorProviders, we would only need to add 1 extension to accepts colors directly. It will still be valuable the fact that we define the difference in the constructor.
I think this is useful for project where the best/only solution to branded colors is with scripts that generates the XML files in the correct places.
In general, we should consider how to support projects that already have their color definitions in xml and don't need to change that, while they do need to add support fro dark/light easily
(imho)

Copy link

Choose a reason for hiding this comment

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

Can you stop tagging me please

39 changes: 39 additions & 0 deletions resources/src/iosMain/kotlin/ThemedColorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2021 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.splendo.kaluga.resources


import platform.UIKit.UIColor
import platform.UIKit.UIUserInterfaceStyle
import platform.UIKit.colorWithDynamicProvider

class IOSThemedColorProvider(light: String, dark: String): ThemedColorProvider() {

override val lightColor = colorFrom(light)!!
override val darkColor = colorFrom(dark)!!

override val color = Color(UIColor.colorWithDynamicProvider {
when (it?.userInterfaceStyle) {
UIUserInterfaceStyle.UIUserInterfaceStyleDark -> darkColor
else -> lightColor
}.uiColor
})
}

actual fun themeColor(light: String, dark: String): ThemedColorProvider
= IOSThemedColorProvider(light, dark)
32 changes: 32 additions & 0 deletions resources/src/jsMain/kotlin/ThemedColorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2021 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.splendo.kaluga.resources

class JsThemedColorProvider(light: String, dark: String): ThemedColorProvider() {

/** The dark theme version of this color, already loaded */
override val darkColor = colorFrom(dark)!!

/** The light theme version of this color, already loaded */
override val lightColor = colorFrom(light)!!

override val color get() = if (isInDarkMode) darkColor else lightColor
}

actual fun themeColor(light: String, dark: String): ThemedColorProvider
= JsThemedColorProvider(light, dark)
32 changes: 32 additions & 0 deletions resources/src/jvmMain/kotlin/ThemedColorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2021 Splendo Consulting B.V. The Netherlands

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.splendo.kaluga.resources

class JvmThemedColorProvider(light: String, dark: String): ThemedColorProvider() {

/** The dark theme version of this color, already loaded */
override val darkColor = colorFrom(dark)!!

/** The light theme version of this color, already loaded */
override val lightColor = colorFrom(light)!!

override val color get() = if (isInDarkMode) darkColor else lightColor
}

actual fun themeColor(light: String, dark: String): ThemedColorProvider
= JvmThemedColorProvider(light, dark)