Skip to content

Commit

Permalink
coroutines support
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 11, 2020
1 parent c4ab99d commit c206970
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 35 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
**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.

---
**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.
**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.

## Installation

Expand Down Expand Up @@ -148,6 +148,8 @@ property3 = true

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.

You can manually initiate data binding by calling `onStateChanged` function in ViewModel.

**Note:** two-way data binding also works - changes in the view will change your state property

### 3. Functional rendering
Expand Down Expand Up @@ -200,4 +202,27 @@ class MyPlainViewModel : ComponentViewModel() {
```
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).

### 5. Coroutine support

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 {
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.

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}"
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
```

***For detailed examples see module `app`.***
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.core:core-ktx:1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/ru/impression/ui_generator_example/Ext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.TranslateAnimation
import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter

fun View.fadeIn(duration: Long, callback: (() -> Unit)? = null) {
startAnimation(AlphaAnimation(0f, 1f).apply {
Expand Down Expand Up @@ -83,4 +85,13 @@ fun View.translateRight(duration: Long, callback: (() -> Unit)? = null) {
}
)
})
}

object Binders {

@JvmStatic
@BindingAdapter("isVisible")
fun setIsVisible(view: View, value: Boolean) {
view.isVisible = value
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package ru.impression.ui_generator_example.fragment

import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.fragment.app.Fragment
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
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.ComponentViewModel
import ru.impression.ui_generator_example.context
import ru.impression.ui_generator_base.CoroutineViewModel
import ru.impression.ui_generator_base.isInitializing
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 All @@ -17,9 +17,15 @@ import kotlin.random.nextInt

@MakeComponent
class MainFragment :
ComponentScheme<Fragment, MainFragmentViewModel>({ MainFragmentBinding::class })
ComponentScheme<Fragment, MainFragmentViewModel>({ viewModel ->
viewModel.toastMessage?.let {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
viewModel.toastMessage = null
}
MainFragmentBinding::class
})

class MainFragmentViewModel : ComponentViewModel() {
class MainFragmentViewModel : CoroutineViewModel() {

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

var welcomeTextVisibility by state(VISIBLE)
var welcomeTextIsVisible by state(true)

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

fun toggleVisibility() {
welcomeTextVisibility = if (welcomeTextVisibility == VISIBLE) INVISIBLE else VISIBLE

var textAnimation by state<AnimatedText.Animation?>(null) {
toastMessage = "Current animation in ${it?.name}"
}

fun animate() {
textAnimation =
AnimatedText.Animation.values()[Random.nextInt(AnimatedText.Animation.values().indices)]
}


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

val currentTimeIsInitializing get() = ::currentTime.isInitializing


var toastMessage by state<String?>(null)
}
31 changes: 28 additions & 3 deletions app/src/main/res/layout/main_fragment.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<layout xmlns:tools="http://schemas.android.com/tools">

<data>

Expand All @@ -19,10 +19,10 @@
android:orientation="vertical">

<androidx.appcompat.widget.AppCompatTextView
isVisible="@{viewModel.welcomeTextIsVisible}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.welcomeText}"
android:visibility="@{viewModel.welcomeTextVisibility}" />
android:text="@{viewModel.welcomeText}" />

<Button
android:layout_width="wrap_content"
Expand All @@ -47,6 +47,31 @@
android:onClick="@{() -> viewModel.animate()}"
android:text="Animate" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Current time is:" />

<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@{viewModel.currentTime}"
tools:text="12345"/>

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

</FrameLayout>

</LinearLayout>

</layout>
2 changes: 2 additions & 0 deletions ui-generator-base/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.core:core-ktx:1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
api "org.jetbrains.kotlin:kotlin-reflect:1.3.72"
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
immediatelyBindChanges: Boolean = false,
onChanged: ((T) -> Unit)? = null
): ReadWriteProperty<ComponentViewModel, T> =
ObservableImpl(initialValue, onChanged) { property, value: T ->
onStateChanged(immediatelyBindChanges)
callOnPropertyChangedListeners(property, value)
}
ObservableImpl(this, initialValue, immediatelyBindChanges, onChanged)

@CallSuper
open fun onStateChanged(immediatelyBindChanges: Boolean = false) {
Expand All @@ -49,9 +46,7 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
initialValue: T,
onChanged: ((T) -> Unit)? = null
): ReadWriteProperty<ComponentViewModel, T> =
ObservableImpl(initialValue, onChanged) { property, value ->
callOnPropertyChangedListeners(property, value)
}
ObservableImpl(this, initialValue, null, onChanged)

protected inline fun <reified VM : ComponentViewModel, T> KProperty<T>.isMutableBy(
vararg properties: KMutableProperty1<VM, T>
Expand Down Expand Up @@ -82,7 +77,10 @@ abstract class ComponentViewModel : ViewModel(), LifecycleEventObserver {
private fun callOnStateChangedListener(immediately: Boolean) {
onStateChangedListener?.let {
handler.removeCallbacks(it)
if (immediately) it.run() else handler.post(it)
if (immediately && Thread.currentThread() === Looper.getMainLooper().thread)
it.run()
else
handler.post(it)
} ?: run { hasMissedStateChange = true }
}

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

private fun callOnPropertyChangedListeners(property: KMutableProperty<*>, value: Any?) {
internal fun callOnPropertyChangedListeners(property: KMutableProperty<*>, value: Any?) {
onPropertyChangedListeners.values.forEach { it(property, value) }
}

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

public override fun onCleared() = Unit

internal class ObservableImpl<T>(
open class ObservableImpl<T>(
private val parent: ComponentViewModel,
initialValue: T,
private val onChanged: ((T) -> Unit)? = null,
private val notifyPropertyChanged: (property: KMutableProperty<*>, value: T) -> Unit
private val immediatelyBindChanges: Boolean?,
private val onChanged: ((T) -> Unit)?
) : ReadWriteProperty<ComponentViewModel, T> {

@Volatile
private var value = initialValue

override fun getValue(thisRef: ComponentViewModel, property: KProperty<*>) =
synchronized(this) { value }
@Synchronized
override fun getValue(thisRef: ComponentViewModel, property: KProperty<*>) = value

@Synchronized
override fun setValue(thisRef: ComponentViewModel, property: KProperty<*>, value: T) {
synchronized(this) {
this.value = value
notifyPropertyChanged(property as KMutableProperty<*>, value)
onChanged?.invoke(value)
}
this.value = value
immediatelyBindChanges?.let { parent.onStateChanged(it) }
parent.callOnPropertyChangedListeners(property as KMutableProperty<*>, value)
onChanged?.invoke(value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ru.impression.ui_generator_base

import androidx.annotation.CallSuper
import kotlinx.coroutines.*
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.jvm.isAccessible

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

protected fun <T> state(
initialValue: Deferred<T>,
immediatelyBindChanges: Boolean = false,
onChanged: ((T?) -> Unit)? = null
): ReadWriteProperty<ComponentViewModel, T?> =
CoroutineObservableImpl(this, initialValue, immediatelyBindChanges, onChanged)

protected fun <T> observable(
initialValue: Deferred<T>,
onChanged: ((T?) -> Unit)? = null
): ReadWriteProperty<ComponentViewModel, T?> =
CoroutineObservableImpl(this, initialValue, null, onChanged)

internal class CoroutineObservableImpl<T>(
parent: CoroutineViewModel,
initialValue: Deferred<T>,
immediatelyBindChanges: Boolean?,
onChanged: ((T?) -> Unit)?
) : ObservableImpl<T?>(parent, null, immediatelyBindChanges, onChanged) {

@Volatile
internal var isInitializing = true

init {
parent.launch {
val result = initialValue.await()
(parent::class.members.firstOrNull {
it.isAccessible = true
it is KMutableProperty1<*, *> && (it as KMutableProperty1<CoroutineViewModel, *>)
.getDelegate(parent) === this@CoroutineObservableImpl
} as KMutableProperty1<CoroutineViewModel, T>?)
?.set(parent, result)
isInitializing = false
immediatelyBindChanges?.let { parent.onStateChanged(it) }
}
}
}

@CallSuper
override fun onCleared() {
cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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?
get() {
Expand Down Expand Up @@ -73,4 +74,10 @@ fun KMutableProperty<*>.set(receiver: Any?, value: Any?) {
is KMutableProperty0<*> -> (this as KMutableProperty0<Any?>).set(value)
is KMutableProperty1<*, *> -> (this as KMutableProperty1<Any?, Any?>).set(receiver, value)
}
}
}

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

0 comments on commit c206970

Please sign in to comment.