Skip to content

Commit c206970

Browse files
Lunkov_A@utkonos.ruLunkov_A@utkonos.ru
authored andcommitted
coroutines support
1 parent c4ab99d commit c206970

File tree

9 files changed

+178
-35
lines changed

9 files changed

+178
-35
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
**UI-generator is a framework that allows you to intuitively and quickly create UI** using the principle of reusable components. This principle is the most modern and effective in the field of UI development, and it underlies such frameworks as React and Flutter.
22

33
---
4-
**UI-generator is similar in functionality to [Jetpack Compose](https://developer.android.com/jetpack/compose)** and provides all its main features. But unlike the Jetpack Compose, UI-generator is fully compatible with the components of the Android support library - Fragments and Views, so you do not have to rewrite all your code to implement this framework. UI-generator works on annotation processing and generates code on top of Fragment and View classes.
4+
**UI-generator is similar in functionality to [Jetpack Compose](https://developer.android.com/jetpack/compose)** and provides all its main features. But unlike the Jetpack Compose, UI-generator is fully available now and is compatible with the components of the Android support library - Fragments and Views, so you do not have to rewrite all your code to implement this framework. UI-generator works on annotation processing and generates code on top of Fragment and View classes.
55

66
## Installation
77

@@ -148,6 +148,8 @@ property3 = true
148148

149149
Data binding is performed at one time for all Views by replacing the old bound ViewModel with a new one. And this does not make the binding algorithm more complicated than using LiveData and ObservableFields, since all native data binding adapters and generated ones are not executed if the new value is equal to the old one.
150150

151+
You can manually initiate data binding by calling `onStateChanged` function in ViewModel.
152+
151153
**Note:** two-way data binding also works - changes in the view will change your state property
152154

153155
### 3. Functional rendering
@@ -200,4 +202,27 @@ class MyPlainViewModel : ComponentViewModel() {
200202
```
201203
A ViewModel with a shared property is marked with `SharedViewModel` annotation, and the shared property is declared by the `observable` or `state` delegate. Then, in the observing ViewModel, in the initial block, using `isMutableBy` method, it is indicated which property values will be duplicated to your property (your property must be a var).
202204

205+
### 5. Coroutine support
206+
207+
Suppose that before you display some data, you need to load it first. Here's how you do it:
208+
```kotlin
209+
var greeting: String? by state(async {
210+
delay(2000)
211+
"Hello world!"
212+
})
213+
```
214+
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.
215+
216+
You can also observe the loading state of your data. For example, in order to show the progress bar during loading:
217+
```xml
218+
<ProgressBar
219+
isVisible="@{viewModel.greetingIsInitializing}"
220+
android:layout_width="wrap_content"
221+
android:layout_height="wrap_content" />
222+
```
223+
```kotlin
224+
// After `isInitializing` becomes `false`, the data binding will be called and the ProgressBar will be hidden.
225+
val greetingIsInitializing: Boolean get() = ::greeting.isInitializing
226+
```
227+
203228
***For detailed examples see module `app`.***

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ dependencies {
4848
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4949
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
5050
implementation 'androidx.core:core-ktx:1.3.0'
51+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
52+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
5153
testImplementation 'junit:junit:4.12'
5254
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
5355
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

app/src/main/java/ru/impression/ui_generator_example/Ext.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.view.View
44
import android.view.animation.AlphaAnimation
55
import android.view.animation.Animation
66
import android.view.animation.TranslateAnimation
7+
import androidx.core.view.isVisible
8+
import androidx.databinding.BindingAdapter
79

810
fun View.fadeIn(duration: Long, callback: (() -> Unit)? = null) {
911
startAnimation(AlphaAnimation(0f, 1f).apply {
@@ -83,4 +85,13 @@ fun View.translateRight(duration: Long, callback: (() -> Unit)? = null) {
8385
}
8486
)
8587
})
88+
}
89+
90+
object Binders {
91+
92+
@JvmStatic
93+
@BindingAdapter("isVisible")
94+
fun setIsVisible(view: View, value: Boolean) {
95+
view.isVisible = value
96+
}
8697
}
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package ru.impression.ui_generator_example.fragment
22

3-
import android.view.View.INVISIBLE
4-
import android.view.View.VISIBLE
53
import android.widget.Toast
64
import androidx.fragment.app.Fragment
5+
import kotlinx.coroutines.async
6+
import kotlinx.coroutines.delay
77
import ru.impression.ui_generator_annotations.MakeComponent
88
import ru.impression.ui_generator_annotations.Prop
99
import ru.impression.ui_generator_base.ComponentScheme
10-
import ru.impression.ui_generator_base.ComponentViewModel
11-
import ru.impression.ui_generator_example.context
10+
import ru.impression.ui_generator_base.CoroutineViewModel
11+
import ru.impression.ui_generator_base.isInitializing
1212
import ru.impression.ui_generator_example.databinding.MainFragmentBinding
1313
import ru.impression.ui_generator_example.view.AnimatedText
1414
import ru.impression.ui_generator_example.view.TextEditorViewModel
@@ -17,9 +17,15 @@ import kotlin.random.nextInt
1717

1818
@MakeComponent
1919
class MainFragment :
20-
ComponentScheme<Fragment, MainFragmentViewModel>({ MainFragmentBinding::class })
20+
ComponentScheme<Fragment, MainFragmentViewModel>({ viewModel ->
21+
viewModel.toastMessage?.let {
22+
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
23+
viewModel.toastMessage = null
24+
}
25+
MainFragmentBinding::class
26+
})
2127

22-
class MainFragmentViewModel : ComponentViewModel() {
28+
class MainFragmentViewModel : CoroutineViewModel() {
2329

2430
@Prop
2531
var welcomeText by state<String?>(null)
@@ -28,18 +34,30 @@ class MainFragmentViewModel : ComponentViewModel() {
2834
::welcomeText.isMutableBy(TextEditorViewModel::customWelcomeText)
2935
}
3036

31-
var welcomeTextVisibility by state(VISIBLE)
37+
var welcomeTextIsVisible by state(true)
3238

33-
var textAnimation by state<AnimatedText.Animation?>(null) {
34-
Toast.makeText(context, "Current animation in ${it?.name}", Toast.LENGTH_SHORT).show()
39+
fun toggleVisibility() {
40+
welcomeTextIsVisible = !welcomeTextIsVisible
3541
}
3642

37-
fun toggleVisibility() {
38-
welcomeTextVisibility = if (welcomeTextVisibility == VISIBLE) INVISIBLE else VISIBLE
43+
44+
var textAnimation by state<AnimatedText.Animation?>(null) {
45+
toastMessage = "Current animation in ${it?.name}"
3946
}
4047

4148
fun animate() {
4249
textAnimation =
4350
AnimatedText.Animation.values()[Random.nextInt(AnimatedText.Animation.values().indices)]
4451
}
52+
53+
54+
var currentTime by state(async {
55+
delay(2000)
56+
System.currentTimeMillis().toString()
57+
})
58+
59+
val currentTimeIsInitializing get() = ::currentTime.isInitializing
60+
61+
62+
var toastMessage by state<String?>(null)
4563
}

app/src/main/res/layout/main_fragment.xml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<layout>
2+
<layout xmlns:tools="http://schemas.android.com/tools">
33

44
<data>
55

@@ -19,10 +19,10 @@
1919
android:orientation="vertical">
2020

2121
<androidx.appcompat.widget.AppCompatTextView
22+
isVisible="@{viewModel.welcomeTextIsVisible}"
2223
android:layout_width="wrap_content"
2324
android:layout_height="wrap_content"
24-
android:text="@{viewModel.welcomeText}"
25-
android:visibility="@{viewModel.welcomeTextVisibility}" />
25+
android:text="@{viewModel.welcomeText}" />
2626

2727
<Button
2828
android:layout_width="wrap_content"
@@ -47,6 +47,31 @@
4747
android:onClick="@{() -> viewModel.animate()}"
4848
android:text="Animate" />
4949

50+
<TextView
51+
android:layout_width="wrap_content"
52+
android:layout_height="wrap_content"
53+
android:layout_marginTop="32dp"
54+
android:text="Current time is:" />
55+
56+
<FrameLayout
57+
android:layout_width="wrap_content"
58+
android:layout_height="wrap_content">
59+
60+
<TextView
61+
android:layout_width="wrap_content"
62+
android:layout_height="wrap_content"
63+
android:layout_gravity="center"
64+
android:text="@{viewModel.currentTime}"
65+
tools:text="12345"/>
66+
67+
<ProgressBar
68+
isVisible="@{viewModel.currentTimeIsInitializing}"
69+
android:layout_width="wrap_content"
70+
android:layout_height="wrap_content"
71+
android:layout_gravity="center" />
72+
73+
</FrameLayout>
74+
5075
</LinearLayout>
5176

5277
</layout>

ui-generator-base/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,7 @@ dependencies {
4646
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4747
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
4848
implementation 'androidx.core:core-ktx:1.3.0'
49+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
50+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
4951
api "org.jetbrains.kotlin:kotlin-reflect:1.3.72"
5052
}

ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
3535
immediatelyBindChanges: Boolean = false,
3636
onChanged: ((T) -> Unit)? = null
3737
): ReadWriteProperty<ComponentViewModel, T> =
38-
ObservableImpl(initialValue, onChanged) { property, value: T ->
39-
onStateChanged(immediatelyBindChanges)
40-
callOnPropertyChangedListeners(property, value)
41-
}
38+
ObservableImpl(this, initialValue, immediatelyBindChanges, onChanged)
4239

4340
@CallSuper
4441
open fun onStateChanged(immediatelyBindChanges: Boolean = false) {
@@ -49,9 +46,7 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
4946
initialValue: T,
5047
onChanged: ((T) -> Unit)? = null
5148
): ReadWriteProperty<ComponentViewModel, T> =
52-
ObservableImpl(initialValue, onChanged) { property, value ->
53-
callOnPropertyChangedListeners(property, value)
54-
}
49+
ObservableImpl(this, initialValue, null, onChanged)
5550

5651
protected inline fun <reified VM : ComponentViewModel, T> KProperty<T>.isMutableBy(
5752
vararg properties: KMutableProperty1<VM, T>
@@ -82,7 +77,10 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
8277
private fun callOnStateChangedListener(immediately: Boolean) {
8378
onStateChangedListener?.let {
8479
handler.removeCallbacks(it)
85-
if (immediately) it.run() else handler.post(it)
80+
if (immediately && Thread.currentThread() === Looper.getMainLooper().thread)
81+
it.run()
82+
else
83+
handler.post(it)
8684
} ?: run { hasMissedStateChange = true }
8785
}
8886

@@ -101,7 +99,7 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
10199
owner.lifecycle.addObserver(this)
102100
}
103101

104-
private fun callOnPropertyChangedListeners(property: KMutableProperty<*>, value: Any?) {
102+
internal fun callOnPropertyChangedListeners(property: KMutableProperty<*>, value: Any?) {
105103
onPropertyChangedListeners.values.forEach { it(property, value) }
106104
}
107105

@@ -131,24 +129,25 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
131129

132130
public override fun onCleared() = Unit
133131

134-
internal class ObservableImpl<T>(
132+
open class ObservableImpl<T>(
133+
private val parent: ComponentViewModel,
135134
initialValue: T,
136-
private val onChanged: ((T) -> Unit)? = null,
137-
private val notifyPropertyChanged: (property: KMutableProperty<*>, value: T) -> Unit
135+
private val immediatelyBindChanges: Boolean?,
136+
private val onChanged: ((T) -> Unit)?
138137
) : ReadWriteProperty<ComponentViewModel, T> {
139138

140139
@Volatile
141140
private var value = initialValue
142141

143-
override fun getValue(thisRef: ComponentViewModel, property: KProperty<*>) =
144-
synchronized(this) { value }
142+
@Synchronized
143+
override fun getValue(thisRef: ComponentViewModel, property: KProperty<*>) = value
145144

145+
@Synchronized
146146
override fun setValue(thisRef: ComponentViewModel, property: KProperty<*>, value: T) {
147-
synchronized(this) {
148-
this.value = value
149-
notifyPropertyChanged(property as KMutableProperty<*>, value)
150-
onChanged?.invoke(value)
151-
}
147+
this.value = value
148+
immediatelyBindChanges?.let { parent.onStateChanged(it) }
149+
parent.callOnPropertyChangedListeners(property as KMutableProperty<*>, value)
150+
onChanged?.invoke(value)
152151
}
153152
}
154153
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package ru.impression.ui_generator_base
2+
3+
import androidx.annotation.CallSuper
4+
import kotlinx.coroutines.*
5+
import kotlin.properties.ReadWriteProperty
6+
import kotlin.reflect.KMutableProperty1
7+
import kotlin.reflect.jvm.isAccessible
8+
9+
abstract class CoroutineViewModel : ComponentViewModel(),
10+
CoroutineScope by CoroutineScope(Dispatchers.IO) {
11+
12+
protected fun <T> state(
13+
initialValue: Deferred<T>,
14+
immediatelyBindChanges: Boolean = false,
15+
onChanged: ((T?) -> Unit)? = null
16+
): ReadWriteProperty<ComponentViewModel, T?> =
17+
CoroutineObservableImpl(this, initialValue, immediatelyBindChanges, onChanged)
18+
19+
protected fun <T> observable(
20+
initialValue: Deferred<T>,
21+
onChanged: ((T?) -> Unit)? = null
22+
): ReadWriteProperty<ComponentViewModel, T?> =
23+
CoroutineObservableImpl(this, initialValue, null, onChanged)
24+
25+
internal class CoroutineObservableImpl<T>(
26+
parent: CoroutineViewModel,
27+
initialValue: Deferred<T>,
28+
immediatelyBindChanges: Boolean?,
29+
onChanged: ((T?) -> Unit)?
30+
) : ObservableImpl<T?>(parent, null, immediatelyBindChanges, onChanged) {
31+
32+
@Volatile
33+
internal var isInitializing = true
34+
35+
init {
36+
parent.launch {
37+
val result = initialValue.await()
38+
(parent::class.members.firstOrNull {
39+
it.isAccessible = true
40+
it is KMutableProperty1<*, *> && (it as KMutableProperty1<CoroutineViewModel, *>)
41+
.getDelegate(parent) === this@CoroutineObservableImpl
42+
} as KMutableProperty1<CoroutineViewModel, T>?)
43+
?.set(parent, result)
44+
isInitializing = false
45+
immediatelyBindChanges?.let { parent.onStateChanged(it) }
46+
}
47+
}
48+
}
49+
50+
@CallSuper
51+
override fun onCleared() {
52+
cancel()
53+
}
54+
}

ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import kotlin.properties.ReadWriteProperty
1717
import kotlin.reflect.*
1818
import kotlin.reflect.full.createInstance
1919
import kotlin.reflect.full.findAnnotation
20+
import kotlin.reflect.jvm.isAccessible
2021

2122
val View.activity: AppCompatActivity?
2223
get() {
@@ -73,4 +74,10 @@ fun KMutableProperty<*>.set(receiver: Any?, value: Any?) {
7374
is KMutableProperty0<*> -> (this as KMutableProperty0<Any?>).set(value)
7475
is KMutableProperty1<*, *> -> (this as KMutableProperty1<Any?, Any?>).set(receiver, value)
7576
}
76-
}
77+
}
78+
79+
val KMutableProperty0<*>.isInitializing: Boolean
80+
get() {
81+
isAccessible = true
82+
return (getDelegate() as? CoroutineViewModel.CoroutineObservableImpl<*>)?.isInitializing == true
83+
}

0 commit comments

Comments
 (0)