Skip to content

Commit

Permalink
coroutine fixes and property reload
Browse files Browse the repository at this point in the history
  • Loading branch information
Lunkov_A@utkonos.ru authored and Lunkov_A@utkonos.ru committed Jul 15, 2020
1 parent 089f556 commit fab1964
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 63 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,23 +203,31 @@ A ViewModel with a shared property is marked with `SharedViewModel` annotation,

Suppose that before you display some data, you need to load it first. Here's how you do it:
```kotlin
var greeting: String? by state(async {
var greeting: String? by state({
delay(2000)
"Hello world!"
})
```
All you need to do is inherit your model from `CoroutineViewModel`. It implements `CoroutineScope` in which your `async` block is executed. You can also execute all your other coroutines in this scope. Scope is canceled when `onCleared` is called.
All you need to do is inherit your model from `CoroutineViewModel`. It implements `CoroutineScope` in which your suspend lambda is executed. You can also execute all your other coroutines in this scope. Scope is canceled when `onCleared` is called.

You can also observe the loading state of your data. For example, in order to show the progress bar during loading:
```xml
<ProgressBar
isVisible="@{viewModel.greetingIsInitializing}"
isVisible="@{viewModel.greetingIsLoading}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
```kotlin
// After `isInitializing` becomes `false`, the data binding will be called and the ProgressBar will be hidden.
val greetingIsInitializing: Boolean get() = ::greeting.isInitializing
// After `isLoading` becomes `false`, the data binding will be called and the ProgressBar will be hidden.
val greetingIsLoading: Boolean get() = ::greeting.isLoading
```

And also you can reload your data:
```kotlin
fun reloadGreeting() {
::greeting.reload()
}
```
The suspend lambda will be called again and `isLoading` will become `true`. After that, the data binding will be called and the ProgressBar wil be shown again at loading time.

***For detailed examples see module `app`.***
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import ru.impression.ui_generator_annotations.MakeComponent
import ru.impression.ui_generator_annotations.Prop
import ru.impression.ui_generator_base.ComponentScheme
import ru.impression.ui_generator_base.CoroutineViewModel
import ru.impression.ui_generator_base.isInitializing
import ru.impression.ui_generator_base.isLoading
import ru.impression.ui_generator_base.reload
import ru.impression.ui_generator_example.databinding.MainFragmentBinding
import ru.impression.ui_generator_example.view.AnimatedText
import ru.impression.ui_generator_example.view.TextEditorViewModel
Expand Down Expand Up @@ -51,12 +52,16 @@ class MainFragmentViewModel : CoroutineViewModel() {
}


var currentTime by state(async {
delay(2000)
var currentTime by state({
delay(3000)
System.currentTimeMillis().toString()
})

val currentTimeIsInitializing get() = ::currentTime.isInitializing
val currentTimeIsLoading get() = ::currentTime.isLoading

fun reloadCurrentTime() {
::currentTime.reload()
}


var toastMessage by state<String?>(null)
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/res/layout/main_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,22 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@{viewModel.currentTime}"
tools:text="12345"/>
tools:text="12345" />

<ProgressBar
isVisible="@{viewModel.currentTimeIsInitializing}"
isVisible="@{viewModel.currentTimeIsLoading}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

</FrameLayout>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.reloadCurrentTime()}"
android:text="reload" />

</LinearLayout>

</layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.impression.ui_generator_base

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext

interface ClearableCoroutineScope : CoroutineScope {
fun clear()
}

class ClearableCoroutineScopeImpl(coroutineContext: CoroutineContext) :
ClearableCoroutineScope {

override var coroutineContext: CoroutineContext = coroutineContext + Job()

override fun clear() {
coroutineContext[Job]?.cancel()
?.also { coroutineContext = coroutineContext.minusKey(Job) + Job() }
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
package ru.impression.ui_generator_base

import androidx.annotation.CallSuper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlin.properties.ReadWriteProperty

abstract class CoroutineViewModel : ComponentViewModel(),
CoroutineScope by CoroutineScope(Dispatchers.IO) {
ClearableCoroutineScope by ClearableCoroutineScopeImpl(Dispatchers.IO) {

protected fun <T> state(
initialValue: Deferred<T>,
getInitialValue: suspend () -> T,
immediatelyBindChanges: Boolean = false,
onChanged: ((T?) -> Unit)? = null
): ReadWriteProperty<CoroutineViewModel, T?> =
ObservableImpl(this, null, initialValue, immediatelyBindChanges, onChanged)
ObservableImpl(this, null, getInitialValue, immediatelyBindChanges, onChanged)

protected fun <T> observable(
initialValue: Deferred<T>,
getInitialValue: suspend () -> T,
onChanged: ((T?) -> Unit)? = null
): ReadWriteProperty<CoroutineViewModel, T?> =
ObservableImpl(this, null, initialValue, null, onChanged)

ObservableImpl(this, null, getInitialValue, null, onChanged)

@CallSuper
override fun onCleared() {
cancel()
clear()
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
package ru.impression.ui_generator_base

import android.content.ContextWrapper
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import ru.impression.ui_generator_annotations.SharedViewModel
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.*
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.isAccessible

val View.activity: AppCompatActivity?
Expand Down Expand Up @@ -79,8 +70,13 @@ fun KMutableProperty<*>.set(receiver: Any?, value: Any?) {
}
}

val KMutableProperty0<*>.isInitializing: Boolean
val KMutableProperty0<*>.isLoading: Boolean
get() {
isAccessible = true
return (getDelegate() as? ObservableImpl<*, *>)?.isInitializing == true
}
return (getDelegate() as? ObservableImpl<*, *>)?.isLoading == true
}

fun KMutableProperty0<*>.reload() {
isAccessible = true
(getDelegate() as? ObservableImpl<*, *>)?.load(true)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ru.impression.ui_generator_base

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KMutableProperty
Expand All @@ -10,58 +10,59 @@ import kotlin.reflect.KProperty
import kotlin.reflect.jvm.isAccessible

open class ObservableImpl<R : Any, T>(
parent: R,
val parent: R,
initialValue: T,
initialValueDeferred: Deferred<T>?,
val getInitialValue: (suspend () -> T)?,
val immediatelyBindChanges: Boolean?,
val onChanged: ((T) -> Unit)?
) : ReadWriteProperty<R, T> {

open val viewModel = parent as? ComponentViewModel
val viewModel = parent as? ComponentViewModel

@Volatile
var property: KMutableProperty<*>? = null
var value = initialValue

@Volatile
var value = initialValue
var isLoading = false

@Volatile
var isInitializing = initialValueDeferred != null
var loadJob: Job? = null

init {
initialValueDeferred?.let { deferred ->
(parent as? CoroutineScope)?.launch {
val result = deferred.await()
onInitialValueLoaded(result)
isInitializing = false
(parent::class.members.firstOrNull {
it.isAccessible = true
it is KMutableProperty1<*, *> && (it as KMutableProperty1<R, *>)
.getDelegate(parent) === this@ObservableImpl
} as KMutableProperty1<R, T>?)?.set(parent, result)
}
}
load(false)
}

open fun onInitialValueLoaded(value: T) = Unit
fun load(notifyStateChangedBeforeLoading: Boolean) {
getInitialValue ?: return
if (parent !is CoroutineScope) return
loadJob?.cancel()
isLoading = true
if (notifyStateChangedBeforeLoading) notifyStateChanged()
loadJob = parent.launch {
val result = getInitialValue.invoke()
isLoading = false
(parent::class.members.firstOrNull {
it.isAccessible = true
it is KMutableProperty1<*, *> && (it as KMutableProperty1<R, *>)
.getDelegate(parent) === this@ObservableImpl
} as KMutableProperty1<R, T>?)?.set(parent, result)
loadJob = null
}
}

@Synchronized
override fun getValue(thisRef: R, property: KProperty<*>): T {
this.property ?: run { this.property = property as? KMutableProperty<*> }
return value
}
override fun getValue(thisRef: R, property: KProperty<*>) = value

@Synchronized
override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
this.property ?: run { this.property = property as? KMutableProperty<*> }
this.value = value
notifyListeners(value)
notifyStateChanged()
(property as? KMutableProperty<*>)
?.let { viewModel?.callOnPropertyChangedListeners(it, value) }
onChanged?.invoke(value)
}

open fun notifyListeners(value: T) {
open fun notifyStateChanged() {
immediatelyBindChanges?.let { viewModel?.onStateChanged(it) }
property?.let { viewModel?.callOnPropertyChangedListeners(it, value) }
viewModel?.callOnPropertyChangedListeners(property as KMutableProperty<*>, value)
onChanged?.invoke(value)
}
}

0 comments on commit fab1964

Please sign in to comment.