Advanced Component Usage - Xlopec/Tea-bag GitHub Wiki
Make Component Shareable
Previously we made a great job making our app's initializer code non-blocking, but what about our requirement to make counter
's state changes visible across the app?
To do so, all we need to do is save reference to our Component instance and provide it to our subscribers, no magic here:
fun main() = runBlocking {
val component = Component(
initializer = ::initializer,
resolver = ::track,
updater = ::add,
scope = this,
)
launch {
// subscriber 1
component(+1, +2, -3).collect(::display)
}
launch {
// subscriber 2
component(+1).collect(::display)
}
}
If we need to intercept snapshots, we can attach Interceptor to Component instance using with extension:
fun main() = runBlocking {
val component = Component(
initializer = ::initializer,
resolver = ::track,
updater = ::add,
scope = this,
) with { snapshot ->
// we can log snapshots, invoke suspendable functions, etc.
}
component(+1, +2, -3).collect(::display)
}
There is often the case when subscriber doesn't need the whole Snapshot, all he needs is to receive, let's say, state updates. In such situations toStatesFlow
extension might help. There are more to***Flow
extensions that might be handy.
"Cold" Computations
Always remember that computations made inside the Component are "cold"! It means that no computations will be made unless there is at least one subscriber present. This sample won't output anything to console:
fun main() {
runBlocking {
val component = Component(
initializer = ::initializer,
resolver = ::track,
updater = ::add,
scope = this,
)
component(+1, +2, -3)
}
}
Sharing Options
Component uses shareIn operator internally to control computation upstream sharing. Thus, if we want to adjust sharing behavior, we can provide custom ShareOptions instance to Component builder:
fun main() = runBlocking {
val component = Component(
initializer = ::initializer,
resolver = ::track,
updater = ::add,
scope = this,
// share Component using Eagerly strategy
shareOptions = ShareOptions(started = SharingStarted.Eagerly, replay = 1U),
)
launch {
// subscriber 1
component(+1, +2, -3).collect(::display)
}
launch {
// subscriber 2
component(+1).collect(::display)
}
}
Error Handling
Any exceptions that occur in Updater, Resolver or Initializer are handled by the coroutine scope that we pass to Component builder:
@Test
fun `when collecting component given initializer throws exception then it is handled by coroutine scope`() {
val scope = TestScope(UnconfinedTestDispatcher(name = "Failing host scope"))
val expectedException = RuntimeException("hello")
val component = Component(
Env<String, Nothing, Nothing>(
initializer = { throw expectedException },
resolver = { snapshot, _ -> throw ComponentException("Unexpected snapshot $snapshot") },
updater = { _, s -> s },
scope = scope
)
)
val job = scope.launch { component("").collect() }
assertTrue(job.isCancelled)
val th = job.getCancellationException().cause
assertTrue("Cancellation cause $th") {
th is RuntimeException && th.message == expectedException.message
}
assertTrue(!scope.isActive)
}