Skip to content

Commit

Permalink
Migrate from RxJava to Coroutines (#24)
Browse files Browse the repository at this point in the history
* migration to coroutines

* updated all tests

* convert list loading to regular coroutines

* removed RxJava dependency

* update documentation
  • Loading branch information
joaquim-verges authored May 28, 2020
1 parent 9f1ebe4 commit 1f15700
Show file tree
Hide file tree
Showing 51 changed files with 328 additions and 383 deletions.
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ buildscript {
min_sdk = 14
target_sdk = 28

kotlin_version = '1.3.70'
kotlin_version = '1.3.72'
coroutines_version = '1.3.7'
robolectric_version = '4.2'
mockito_kotlin_version = '1.5.0'
mockito_kotlin_version = '2.1.0'
test_core_version = '1.2.0'
test_ext_version = '1.1.1'
test_espresso_version = '3.2.0'

rxjava_version = '2.2.17'
rxandroid_version = '2.1.1'
arch_lifecycle_version = '2.2.0'
arch_lifecycle_runtime_version = '2.3.0-alpha03'
arch_lifecycle_viewmodel_version = '2.3.0-alpha03'
autodispose_version = '1.1.0'
appcompat_version = '1.1.0'
recyclerview_version = '1.1.0'
Expand All @@ -31,7 +32,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0'
Expand Down
25 changes: 14 additions & 11 deletions helium-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ Helium requires [java 8 support](https://developer.android.com/studio/write/java

#### Notes on the implementation

- Uses [RxJava](https://github.com/ReactiveX/RxJava) to handle communication between Logic and UI blocks.
- Uses [AutoDispose](https://github.com/uber/AutoDispose) to automatically dispose subscriptions, no need to worry about cleaning up or detaching anything.
- Uses [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html) and [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) to handle communication between Logic and UI blocks
- Uses [LifecycleScope](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope) and [ViewModelScope](https://developer.android.com/topic/libraries/architecture/coroutines#viewmodelscope) to automatically release resources, no need to worry about cleaning up or detaching anything
- Uses `ViewModel` from the [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/viewmodel.html) to retain logic blocks and their states across configuration changes

## A typical, real world example
Expand Down Expand Up @@ -78,7 +78,7 @@ You can also call your own constructor if you have dynamic data to pass to your

```kotlin
val id = intent.extras.getLong(DATA_ID)
val logic = getRetainedLogicBlock<MyLogic>() { MyLogic(id) }
val logic = getRetainedLogicBlock<MyLogic> { MyLogic(id) }
```


Expand All @@ -93,14 +93,17 @@ class MyLogic(private val repository: MyRepository) : LogicBlock<MyState, MyEven

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun loadData() {
repository
.getData()
.doOnSubscribe { pushState(MyState.Loading) }
.async()
.subscribe(
{ data -> pushState(MyState.DataReady(data)) },
{ error -> pushState(MyState.Error(error)) }
)
launchInBlock { // launches a coroutine scoped to this LogicBlock
try {
pushState(MyState.Loading)
val data = withContext(Dispatchers.IO) {
repository.getData()
}
pushState(MyState.DataReady(data))
} catch(error: Exception) {
pushState(MyState.Error(error))
}
}
}

override fun onUiEvent(event : MyEvent) {
Expand Down
8 changes: 5 additions & 3 deletions helium-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

api "io.reactivex.rxjava2:rxjava:$rxjava_version"
api "io.reactivex.rxjava2:rxandroid:$rxandroid_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

api "androidx.lifecycle:lifecycle-viewmodel-ktx:$arch_lifecycle_viewmodel_version"
api "androidx.lifecycle:lifecycle-runtime-ktx:$arch_lifecycle_runtime_version"
api "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"
api "com.uber.autodispose:autodispose-android-archcomponents-ktx:$autodispose_version"
}

apply from: '../maven-push.gradle'
3 changes: 1 addition & 2 deletions helium-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.joaquimverges.helium.core"/>
<manifest package="com.joaquimverges.helium.core" />
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package com.joaquimverges.helium.core
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.coroutineScope
import com.joaquimverges.helium.core.event.BlockEvent
import com.joaquimverges.helium.core.state.BlockState
import com.joaquimverges.helium.core.util.autoDispose
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

/**
* Class responsible for assembling LogicBlocks and UiBlocks together.
Expand All @@ -31,9 +34,9 @@ class AppBlock<S : BlockState, E : BlockEvent>(
* Once this is called, LogicBlock will receive events from the UIBlock, and UIBlock will receive state updates from the LogicBlock.
* This also enables the LogicBlock to receive lifecycle events, by annotating functions with @OnLifecycleEvent.
*/
fun assemble(lifecycle: Lifecycle) {
logic.observeState().autoDispose(lifecycle).subscribe { ui.render(it) }
ui.observer().autoDispose(lifecycle).subscribe { logic.processEvent(it) }
fun assemble(lifecycle: Lifecycle, coroutineScope: CoroutineScope = lifecycle.coroutineScope) {
logic.observeState().onEach { ui.render(it) }.launchIn(coroutineScope)
ui.observer().onEach { logic.processEvent(it) }.launchIn(coroutineScope)
lifecycle.addObserver(logic)

childBlocks.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.joaquimverges.helium.core
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.joaquimverges.helium.core.event.BlockEvent
import com.joaquimverges.helium.core.state.BlockState
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

/**
* A LogicBlock holds and publishes BlockState changes to a UiBlock for rendering.
Expand All @@ -24,9 +24,8 @@ import io.reactivex.subjects.PublishSubject
*/
abstract class LogicBlock<S : BlockState, E : BlockEvent> : ViewModel(), LifecycleObserver {

private val disposables: CompositeDisposable = CompositeDisposable()
private val state: BehaviorSubject<S> = BehaviorSubject.create()
private val eventDispatcher: PublishSubject<E> = PublishSubject.create()
private val state: MutableStateFlow<S?> = MutableStateFlow(null)
private val eventDispatcher: BroadcastChannel<E> = BroadcastChannel(Channel.BUFFERED)

/**
* Implement this method to react to any BlockEvent emissions from the attached UiBlock.
Expand All @@ -39,35 +38,34 @@ abstract class LogicBlock<S : BlockState, E : BlockEvent> : ViewModel(), Lifecyc
* Must have compatible [BlockEvent] for both blocks.
*/
fun propagateEventsTo(otherBlock: LogicBlock<*, E>) {
eventDispatcher.subscribe { otherBlock.processEvent(it) }.autoDispose()
eventDispatcher.asFlow().onEach { otherBlock.processEvent(it) }.launchInBlock()
}

/**
* Observe state changes from this LogicBlock
*/
fun observeState(): Observable<S> = state
fun observeState(): Flow<S> = state.filterNotNull()

/**
* Observe events received by this LogicBlock, useful for propagating events to parent LogicBlocks
*/
fun observeEvents(): Observable<E> = eventDispatcher
fun observeEvents(): Flow<E> = eventDispatcher.asFlow()

/**
* Pushes a new state, which will trigger any active subscribers
*/
fun pushState(state: S) = this.state.onNext(state)

/**
* Will automatically dispose this subscription when the block gets cleared
*/
fun Disposable.autoDispose() = disposables.add(this)

override fun onCleared() = disposables.clear()
fun pushState(state: S) {
this.state.value = state
}

// internal functions

internal fun processEvent(event: E) {
onUiEvent(event)
eventDispatcher.onNext(event)
eventDispatcher.offer(event)
}

fun <T> Flow<T>.launchInBlock() = launchIn(viewModelScope)

inline fun launchInBlock(crossinline codeBlock: suspend () -> Unit) = viewModelScope.launch { codeBlock() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import com.joaquimverges.helium.core.event.BlockEvent
import com.joaquimverges.helium.core.state.BlockState
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow

/**
* Base class for UiBlocks.
Expand All @@ -23,9 +25,9 @@ import io.reactivex.subjects.PublishSubject
* @see com.joaquimverges.helium.core.event.BlockEvent
* @see com.joaquimverges.helium.core.LogicBlock
*/
abstract class UiBlock<in S : BlockState, E : BlockEvent>(
abstract class UiBlock<in S : BlockState, E : BlockEvent> constructor(
val view: View,
private val eventsObservable: PublishSubject<E> = PublishSubject.create(),
private val eventFlow : BroadcastChannel<E> = BroadcastChannel(Channel.BUFFERED),
protected val context: Context = view.context
) {

Expand Down Expand Up @@ -58,10 +60,10 @@ abstract class UiBlock<in S : BlockState, E : BlockEvent>(
/**
* Observe the events pushed from this UiBlock
*/
fun observer(): Observable<E> = eventsObservable
open fun observer(): Flow<E> = eventFlow.asFlow()

/**
* Pushes a new BlockEvent, which will trigger active subscribers LogicBlocks
*/
fun pushEvent(event: E) = eventsObservable.onNext(event)
fun pushEvent(event: E) = eventFlow.offer(event)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.joaquimverges.helium.core.retained

import androidx.lifecycle.ViewModel
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

/**
Expand Down

This file was deleted.

3 changes: 1 addition & 2 deletions helium-navigation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.joaquimverges.helium.navigation"/>
<manifest package="com.joaquimverges.helium.navigation" />
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.joaquimverges.helium.navigation.drawer

import androidx.drawerlayout.widget.DrawerLayout
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.drawerlayout.widget.DrawerLayout
import com.joaquimverges.helium.core.UiBlock
import com.joaquimverges.helium.navigation.R

Expand All @@ -29,6 +30,22 @@ class NavDrawerUi(
(drawerContainer.layoutParams as DrawerLayout.LayoutParams).gravity = gravity
mainContainer.addView(mainContentUi.view)
drawerContainer.addView(drawerUi.view)
drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerStateChanged(newState: Int) {
}

override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}

override fun onDrawerClosed(drawerView: View) {
pushEvent(NavDrawerEvent.DrawerClosed)
}

override fun onDrawerOpened(drawerView: View) {
pushEvent(NavDrawerEvent.DrawerOpened)
}

})
}

override fun render(state: NavDrawerState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.joaquimverges.helium.core.UiBlock
import com.joaquimverges.helium.core.event.BlockEvent
import com.joaquimverges.helium.core.state.BlockState
import com.joaquimverges.helium.core.UiBlock
import com.joaquimverges.helium.navigation.R

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.joaquimverges.helium.navigation.toolbar

import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.joaquimverges.helium.navigation.toolbar.ScrollConfiguration.CollapseMode
import com.joaquimverges.helium.navigation.toolbar.ScrollConfiguration.ScrollMode

/**
* Configures the scrolling behavior for a [CollapsingToolbarUi]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import androidx.annotation.MenuRes
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import com.joaquimverges.helium.core.state.BlockState
import com.joaquimverges.helium.core.UiBlock
import com.joaquimverges.helium.core.state.BlockState
import com.joaquimverges.helium.navigation.R

/**
Expand Down
3 changes: 2 additions & 1 deletion helium-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ dependencies {
api "androidx.test.ext:junit:$test_ext_version"
api "androidx.test.ext:junit-ktx:$test_ext_version"
api "androidx.test.espresso:espresso-core:$test_espresso_version"
api "com.nhaarman:mockito-kotlin:$mockito_kotlin_version"
api "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin_version"
api "org.robolectric:robolectric:$robolectric_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
implementation project(':helium-core')
}

Expand Down
Loading

0 comments on commit 1f15700

Please sign in to comment.