Skip to content

Commit

Permalink
Merge pull request badoo#112 from ShikaSD/modelwatcher-sealed-class-s…
Browse files Browse the repository at this point in the history
…upport

Update model watcher dsl to support sealed classes
  • Loading branch information
zsoltk authored Oct 8, 2019
2 parents fa8421d + 2d57dcb commit 6911b75
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 49 deletions.
46 changes: 46 additions & 0 deletions documentation/extras/modelwatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,52 @@ val watcher = modelWatcher<ViewModel> {
}
```

Models based on the sealed classes are supported with `type` and `objectType` functions.
```kotlin
sealed class Model {
data class A(val list: List<String>): Model()
object B : Model()
}

val watcher = modelWatcher<Model> {
type<A> {
watch(Model.A::list) { }
}

objectType<B> { modelB ->

}
}
```

!!! warning
Subsequent definitions of the same type will override previous ones.

If sealed class has a common property defined in the base class, its changes can be observed as well.
In the example below, `Model::list` selector is triggered when the property is changed independently on model type.
```kotlin
sealed class Model {
abstract val list: List<String>

data class A(val list: List<String>): Model()
object B : Model() {
override val list: List<String> = emptyList()
}
}

val watcher = modelWatcher<Model> {
type<A> {
watch(Model.A::list) {
// Property of Model.A only
}
}

watch(Model::list) {
// Common property
}
}
```

The watcher also provides an optional DSL to add more clarity to the definitions:
```kotlin
val watcher = modelWatcher<ViewModel> {
Expand Down
93 changes: 55 additions & 38 deletions mvicore-diff/src/main/java/com/badoo/mvicore/ModelWatcher.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package com.badoo.mvicore

class ModelWatcher<Model> private constructor(
private val watchers: List<Watcher<Model, Any?>>
class ModelWatcher<Model : Any> private constructor(
private val watchers: List<Watcher<Model, Any?>>,
private val childWatchers: Map<Class<out Model>, ModelWatcher<out Model>>
) {
private var model: Model? = null

operator fun invoke(newModel: Model) {
triggerChildren(newModel)

triggerSelf(newModel)
model = newModel
}

private fun triggerSelf(newModel: Model) {
val oldModel = model
watchers.forEach { element ->
val getter = element.accessor
Expand All @@ -14,8 +22,26 @@ class ModelWatcher<Model> private constructor(
element.callback(new)
}
}
}

model = newModel
private fun triggerChildren(newModel: Model) {
val recordedClass = childWatchers.keys.firstOrNull { it.isInstance(newModel) }
val targetWatcher = childWatchers[recordedClass] as? ModelWatcher<Model>
targetWatcher?.invoke(newModel)
clearNotMatchedChildren(selectedChild = targetWatcher)
}

private fun clearNotMatchedChildren(selectedChild: ModelWatcher<Model>?) {
childWatchers.values.forEach {
if (it !== selectedChild) {
it.clear()
}
}
}

fun clear() {
model = null
childWatchers.values.forEach { it.clear() }
}

private class Watcher<Model, Field>(
Expand All @@ -24,12 +50,15 @@ class ModelWatcher<Model> private constructor(
val diff: DiffStrategy<Field>
)

class Builder<Model> @PublishedApi internal constructor() {
@ModelWatcherDsl
class Builder<Model : Any> @PublishedApi internal constructor() : BuilderBase<Model>, WatchDsl<Model> {
private val watchers = mutableListOf<Watcher<Model, Any?>>()
@PublishedApi
internal val childWatchers = hashMapOf<Class<out Model>, ModelWatcher<out Model>>()

fun <Field> watch(
override fun <Field> watch(
accessor: (Model) -> Field,
diff: DiffStrategy<Field> = byValue(),
diff: DiffStrategy<Field>,
callback: (Field) -> Unit
) {
watchers += Watcher(
Expand All @@ -39,42 +68,30 @@ class ModelWatcher<Model> private constructor(
) as Watcher<Model, Any?>
}

@PublishedApi
internal fun build(): ModelWatcher<Model> =
ModelWatcher(watchers)

/*
* Syntactic sugar around watch (scoped inside the builder)
*/

operator fun <Field> ((Model) -> Field).invoke(callback: (Field) -> Unit) {
watch(this, callback = callback)
inline fun <reified SubModel : Model> type(block: ModelWatcher.Builder<SubModel>.() -> Unit) {
val childWatcher = modelWatcher(block)
childWatchers[SubModel::class.java] = childWatcher
}

infix fun <Field> ((Model) -> Field).using(pair: Pair<DiffStrategy<Field>, (Field) -> Unit>) {
watch(this, pair.first, pair.second)
inline fun <reified SubModel : Model> objectType(noinline block: (SubModel) -> Unit) {
type<SubModel> {
watch({ it }, byRef(), block)
}
}

operator fun <Field> (DiffStrategy<Field>).invoke(callback: (Field) -> Unit) =
this to callback

infix fun <Field1, Field2> ((Model) -> Field1).or(f: (Model) -> Field2): DiffStrategy<Model> =
{ old, new -> this(old) != this(new) || f(old) != f(new) }

infix fun <Field1, Field2> ((Model) -> Field1).and(f: (Model) -> Field2): DiffStrategy<Model> =
{ old, new -> this(old) != this(new) && f(old) != f(new) }

operator fun DiffStrategy<Model>.invoke(callback: (Model) -> Unit) {
watch(
accessor = { it },
diff = this,
callback = callback
)
}
@PublishedApi
internal fun build(): ModelWatcher<Model> =
ModelWatcher(watchers, childWatchers)
}
}

inline fun <Model> modelWatcher(init: ModelWatcher.Builder<Model>.() -> Unit): ModelWatcher<Model> =
ModelWatcher.Builder<Model>()
.apply(init)
.build()
@DslMarker
annotation class ModelWatcherDsl

internal interface BuilderBase<Model> {
fun <Field> watch(
accessor: (Model) -> Field,
diff: DiffStrategy<Field> = byValue(),
callback: (Field) -> Unit
)
}
36 changes: 36 additions & 0 deletions mvicore-diff/src/main/java/com/badoo/mvicore/ModelWatcherDsl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.badoo.mvicore

inline fun <Model : Any> modelWatcher(init: ModelWatcher.Builder<Model>.() -> Unit): ModelWatcher<Model> =
ModelWatcher.Builder<Model>()
.apply(init)
.build()

internal interface WatchDsl<Model> : BuilderBase<Model> {
/*
* Syntactic sugar around watch (scoped inside the builder)
*/
operator fun <Field> ((Model) -> Field).invoke(callback: (Field) -> Unit) {
watch(this, callback = callback)
}

infix fun <Field> ((Model) -> Field).using(pair: Pair<DiffStrategy<Field>, (Field) -> Unit>) {
watch(this, pair.first, pair.second)
}

operator fun <Field> (DiffStrategy<Field>).invoke(callback: (Field) -> Unit) =
this to callback

infix fun <Field1, Field2> ((Model) -> Field1).or(f: (Model) -> Field2): DiffStrategy<Model> =
{ old, new -> this(old) != this(new) || f(old) != f(new) }

infix fun <Field1, Field2> ((Model) -> Field1).and(f: (Model) -> Field2): DiffStrategy<Model> =
{ old, new -> this(old) != this(new) && f(old) != f(new) }

operator fun DiffStrategy<Model>.invoke(callback: (Model) -> Unit) {
watch(
accessor = { it },
diff = this,
callback = callback
)
}
}
4 changes: 2 additions & 2 deletions mvicore-diff/src/test/java/com/badoo/mvicore/HelperTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import kotlin.test.assertEquals
class HelperTest {
@Test
fun `by value strategy compares by value`() {
val results = testWatcher<List<String>>(
val results = testWatcher<List<String>, Model>(
listOf(
Model(list = listOf("")),
Model(list = listOf(""))
Expand All @@ -24,7 +24,7 @@ class HelperTest {

@Test
fun `by ref strategy compares by reference`() {
val results = testWatcher<List<String>>(
val results = testWatcher<List<String>, Model>(
listOf(
Model(list = listOf("")),
Model(list = listOf(""))
Expand Down
32 changes: 24 additions & 8 deletions mvicore-diff/src/test/java/com/badoo/mvicore/ModelWatcherTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ModelWatcherTest {

@Test
fun `invokes callback when field changes`() {
val results = testWatcher<Int>(
val results = testWatcher<Int, Model>(
listOf(
Model(int = 0),
Model(int = 1)
Expand All @@ -25,7 +25,7 @@ class ModelWatcherTest {

@Test
fun `does not invoke callback when field does not change`() {
val results = testWatcher<Int>(
val results = testWatcher<Int, Model>(
listOf(
Model(int = 0),
Model(int = 0)
Expand All @@ -41,7 +41,7 @@ class ModelWatcherTest {

@Test
fun `emits nullable fields on start`() {
val results = testWatcher<Boolean?>(
val results = testWatcher<Boolean?, Model>(
listOf(
Model(nullable = null)
)
Expand All @@ -56,7 +56,7 @@ class ModelWatcherTest {

@Test
fun `by default compares by value`() {
val results = testWatcher<List<String>>(
val results = testWatcher<List<String>, Model>(
listOf(
Model(list = listOf("")),
Model(list = listOf(""))
Expand All @@ -72,7 +72,7 @@ class ModelWatcherTest {

@Test
fun `invokes callback using dsl`() {
val results = testWatcher<Int>(
val results = testWatcher<Int, Model>(
listOf(
Model(int = 0), Model(int = 1)
)
Expand All @@ -87,7 +87,7 @@ class ModelWatcherTest {

@Test
fun `invokes callback using dsl with diffStrategy`() {
val results = testWatcher<List<String>>(
val results = testWatcher<List<String>, Model>(
listOf(
Model(list = listOf("")),
Model(list = listOf(""))
Expand All @@ -104,7 +104,7 @@ class ModelWatcherTest {

@Test
fun `invokes callback with combined diffStrategy using "or"`() {
val results = testWatcher<Model>(
val results = testWatcher<Model, Model>(
listOf(
Model(list = listOf(""), int = 1),
Model(list = listOf(""), int = 1, nullable = false),
Expand All @@ -121,7 +121,7 @@ class ModelWatcherTest {

@Test
fun `invokes callback with combined diffStrategy using "and"`() {
val results = testWatcher<Model>(
val results = testWatcher<Model, Model>(
listOf(
Model(list = listOf(""), int = 1),
Model(int = 1),
Expand All @@ -135,4 +135,20 @@ class ModelWatcherTest {

assertEquals(2, results.size)
}

@Test
fun `invokes callback after clear`() {
val results = mutableListOf<List<String>>()
val watcher = modelWatcher<Model> {
Model::list {
results += it
}
}

watcher.invoke(Model(list = listOf("")))
watcher.clear()
watcher.invoke(Model(list = listOf("")))

assertEquals(2, results.size)
}
}
Loading

0 comments on commit 6911b75

Please sign in to comment.