Skip to content

Commit 839f20d

Browse files
authored
Flexible configuration with Deferred Resources (#11)
Add Drew Hamilton as author
1 parent 413c71f commit 839f20d

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-0
lines changed

_data/authors.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ Hari Vignesh:
1919
name : "Hari Vignesh Jayapalan"
2020
avatar : "assets/images/avatars/hari.vignesh.jpg"
2121
bio : "Senior Mobile Engineer - R&D"
22+
23+
Drew Hamilton:
24+
name : "Drew Hamilton"
25+
avatar : "assets/images/avatars/drew.jpg"
26+
bio : "Principal Mobile Engineer - R&D"
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
---
2+
title: "Android: Flexible configuration with Deferred Resources"
3+
excerpt: Deferred Resources provides flexibility beyond the standard resource-resolution approaches.
4+
tags: Mobile Android Open-source Deferred-Resources
5+
authors:
6+
- Drew Hamilton
7+
header:
8+
teaser: /assets/images/post/05_color_configuration.png
9+
teaser_alt: Deferred Resources
10+
category: Mobile
11+
---
12+
13+
Our feature libraries, which include UI, have a number of varying requirements regarding
14+
configuration by our customers. (My colleague Hari wrote about one such library's requirements
15+
[here]({% post_url 2020-08-14-android-configuration-driven-ui-from-epoxy-to-compose %}).)
16+
Resources (text, colors, images, etc.) are a common type of configuration, and can be defined in
17+
various ways—typically in code, resources, or theme attributes. We created
18+
[Deferred Resources](https://engineering.backbase.com/DeferredResources) to support such
19+
configurations, and with it our customers can consistently declare configuration properties from
20+
any of these common sources:
21+
```kotlin
22+
SomeConfiguration {
23+
textColor = DeferredColor.Constant(Color.WHITE)
24+
// or
25+
textColor = DeferredColor.Resource(R.color.splash_text)
26+
// or
27+
textColor = DeferredColor.Attribute(R.attr.colorOnSurface)
28+
}
29+
30+
// Resolved to a @ColorInt in our UI:
31+
@ColorInt val textColor = configuration.textColor.resolve(context)
32+
```
33+
34+
This covers most of our customers' use cases. Deferred Resources has been working well for us for
35+
over a year now.
36+
37+
But beyond these common use cases, Deferred Resources' design provides a lot of flexibility to
38+
provide resources in other ways. Each deferred resource type is an interface with one or more
39+
abstract functions to resolve the underlying resource. Thus, a user of the Deferred Resources
40+
library can define any resource-resolution implementation they'd like. Here are some examples of
41+
how we're taking advantage of this flexibility.
42+
43+
## Color variants
44+
45+
The Backbase design system has a concept of "color variants," where any theme color has lighter and
46+
darker alternates. These variants are defined by a computed overlay: a "lighter" variant is the
47+
base color with a 30% white overlay, while a "darker" variant is the base color with a 30% black
48+
overlay.
49+
50+
```kotlin
51+
enum class ColorVariant(
52+
@ColorInt internal val overlay: Int
53+
) {
54+
LIGHTER(overlay = Color.WHITE.withAlpha(0x4D)),
55+
DARKER(overlay = Color.BLACK.withAlpha(0x4D)),
56+
}
57+
```
58+
59+
By shipping a `DeferredVariantColor` class with our design system, we can make it very easy for our
60+
feature libraries as well as for our customers to use the same variants:
61+
62+
```kotlin
63+
/**
64+
* Convert a [DeferredColor] to a [variant] of the same color without resolving it
65+
* yet.
66+
*/
67+
public fun DeferredColor.variant(variant: ColorVariant): DeferredColor =
68+
DeferredVariantColor(this, variant)
69+
70+
// See https://github.com/drewhamilton/Poko for more on the @Poko annotation
71+
@Poko internal class DeferredVariantColor(
72+
private val base: DeferredColor,
73+
private val variant: ColorVariant
74+
) : DeferredColor {
75+
76+
/**
77+
* Using [context], resolve the base color with the variant applied.
78+
*/
79+
@ColorInt override fun resolve(context: Context): Int =
80+
ColorUtils.compositeColors(variant.foreground, base.resolve(context))
81+
82+
/**
83+
* Using [context], resolve the base color with the variant applied.
84+
*
85+
* This implementation does not support states other than the default state.
86+
*/
87+
override fun resolveToStateList(context: Context): ColorStateList =
88+
ColorStateList.valueOf(resolve(context))
89+
}
90+
```
91+
92+
With this, anyone using our design system can convert any configured color to a variant of the same
93+
color, even if the base color comes from an outside source and its value has not been resolved yet.
94+
95+
```kotlin
96+
SomeConfiguration {
97+
buttonColor = DeferredColor.Attribute(R.attr.colorPrimary)
98+
buttonRippleColor = buttonColor.variant(ColorVariant.DARKER)
99+
}
100+
```
101+
102+
## Supporting Lottie without depending on it
103+
104+
Some of our customers want to use [Lottie](https://airbnb.design/lottie/) to provide fun
105+
micro-animations to our feature libraries' UI. Other customers don't want to use Lottie, or don't
106+
want these animations at all. A custom implementation of the `DeferredDrawable` interface lets us
107+
support Lottie animations indirectly, without actually coupling our libraries
108+
to it or forcing our customers to take it on as a dependency.
109+
110+
As a standalone library, we ship a `DeferredLottieDrawable` class:
111+
112+
```kotlin
113+
interface DeferredLottieDrawable : DeferredDrawable {
114+
115+
override fun resolve(context: Context): LottieDrawable?
116+
117+
class Resource(
118+
@RawRes private val rawRes: Int,
119+
private val transformations: LottieDrawable.(Context) -> Unit = {},
120+
) : DeferredLottieDrawable {
121+
override fun resolve(context: Context): LottieDrawable? {
122+
val compositionResult =
123+
LottieCompositionFactory.fromRawResSync(context, rawRes)
124+
when (val exception = compositionResult.exception) {
125+
null -> return compositionResult.value?.asDrawable()?.apply {
126+
transformations(context)
127+
}
128+
else -> throw exception
129+
}
130+
}
131+
}
132+
133+
// Other supported types are implemented too: Constant, Asset, and Stream
134+
}
135+
136+
private fun LottieComposition.asDrawable() = LottieDrawable().apply {
137+
composition = this@asDrawable
138+
}
139+
```
140+
141+
Thanks to the base `Drawable` class and the `Animatable` interface, which are both part of the
142+
standard Android APIs, UI code that expects an animation can display this without knowing whether
143+
Lottie is involved:
144+
145+
```kotlin
146+
val paymentSuccessIndication =
147+
configuration.paymentSuccessIndication.resolve(context)
148+
imageView.setImageDrawable(paymentSuccessIndication)
149+
if (paymentSuccessIndication is Animatable) {
150+
paymentSuccessIndication.start()
151+
}
152+
```
153+
154+
Our feature library consumers can provide a `DeferredLottieDrawable` if they are using Lottie, or
155+
any other `DeferredDrawable` if they don't use Lottie:
156+
157+
```kotlin
158+
SomeConfiguration {
159+
paymentSuccessIndication = DeferredLottieDrawable.Raw(
160+
R.raw.payment_success_animation
161+
) {
162+
repeatCount = LottieDrawable.INFINITE
163+
}
164+
// or
165+
paymentSuccessIndication = DeferredDrawable.Resource(
166+
R.drawable.payment_success_icon
167+
)
168+
// or
169+
paymentSuccessIndication = SomeCustomDeferredAnimatedDrawable(customInputs)
170+
}
171+
```
172+
173+
## Remote configuration
174+
175+
We're just starting to explore another possible resource-resolution approach with Deferred
176+
Resources: resolving values from a remote server. Imagine one of our libraries has a configuration
177+
to enable a new feature:
178+
179+
```kotlin
180+
SomeConfiguration {
181+
coolNewFeatureEnabled = DeferredBoolean.Constant(false)
182+
}
183+
```
184+
185+
A factory that is hooked up to a feature flagging backend could determine in the background whether
186+
any remote configuration has changed, and surface that update when a custom `DeferredBoolean`
187+
implementation is resolved:
188+
189+
```kotlin
190+
interface RemoteConfigApi {
191+
192+
/**
193+
* Returns the boolean value defined by [key] according to this
194+
* [RemoteConfigApi]'s internal state. This may return a default value
195+
* if the remote API call has not completed.
196+
*/
197+
fun getBooleanValue(key: String): Boolean
198+
}
199+
200+
class FeatureFlagFactory(
201+
private val remoteConfigApi: RemoteConfigApi,
202+
) {
203+
fun createDeferredFeatureFlag(featureName: String): DeferredBoolean =
204+
object : DeferredBoolean {
205+
override fun resolve(context: Context): Boolean =
206+
remoteConfigApi.getBooleanValue(featureName)
207+
}
208+
}
209+
```
210+
211+
The consuming app can fetch the remote values in the background when the app is launched and use
212+
this factory to create its deferred feature flag, and the feature will be enabled depending on
213+
whatever the configuration backend has returned:
214+
215+
```kotlin
216+
val remoteConfigApi = MyRemoteConfigApi(
217+
url = "example.com",
218+
defaultValues = mapOf("coolNewFeature" to false),
219+
).also {
220+
coroutineScope.launch { it.fetchLatestValues() }
221+
}
222+
val featureFlagFactory = FeatureFlagFactory(remoteConfigApi)
223+
224+
SomeConfiguration {
225+
coolNewFeatureEnabled =
226+
featureFlagFactory.createDeferredFeatureFlag("coolNewFeature")
227+
}
228+
```
229+
230+
We're still working on this API's design, but just like our Lottie support, we aim to ship remote
231+
configuration support for our customers that want it, while not forcing it on the customers who
232+
don't.
233+
234+
---
235+
236+
All three of these utilizations of Deferred Resources have one thing in common: they decouple the
237+
specific feature in question from the consumption site—our feature libraries. With this
238+
abstraction, our feature libraries are almost limitlessly flexible while remaining uncoupled from
239+
any specialized resource-resolution approach.

assets/images/avatars/drew.jpg

2.93 MB
Loading
54.7 KB
Loading

0 commit comments

Comments
 (0)