diff --git a/build.gradle b/build.gradle index b6739aa..96e2f9f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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' diff --git a/helium-core/README.md b/helium-core/README.md index 7e2813d..c55c3d3 100644 --- a/helium-core/README.md +++ b/helium-core/README.md @@ -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 @@ -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(id) } +val logic = getRetainedLogicBlock { MyLogic(id) } ``` @@ -93,14 +93,17 @@ class MyLogic(private val repository: MyRepository) : LogicBlock 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) { diff --git a/helium-core/build.gradle b/helium-core/build.gradle index cada3b5..b76a133 100644 --- a/helium-core/build.gradle +++ b/helium-core/build.gradle @@ -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' \ No newline at end of file diff --git a/helium-core/src/main/AndroidManifest.xml b/helium-core/src/main/AndroidManifest.xml index 1163c01..14e2313 100644 --- a/helium-core/src/main/AndroidManifest.xml +++ b/helium-core/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/helium-core/src/main/java/com/joaquimverges/helium/core/AppBlock.kt b/helium-core/src/main/java/com/joaquimverges/helium/core/AppBlock.kt index bece941..6ae248d 100644 --- a/helium-core/src/main/java/com/joaquimverges/helium/core/AppBlock.kt +++ b/helium-core/src/main/java/com/joaquimverges/helium/core/AppBlock.kt @@ -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. @@ -31,9 +34,9 @@ class AppBlock( * 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 { diff --git a/helium-core/src/main/java/com/joaquimverges/helium/core/LogicBlock.kt b/helium-core/src/main/java/com/joaquimverges/helium/core/LogicBlock.kt index 7ec0337..3dc61bb 100644 --- a/helium-core/src/main/java/com/joaquimverges/helium/core/LogicBlock.kt +++ b/helium-core/src/main/java/com/joaquimverges/helium/core/LogicBlock.kt @@ -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. @@ -24,9 +24,8 @@ import io.reactivex.subjects.PublishSubject */ abstract class LogicBlock : ViewModel(), LifecycleObserver { - private val disposables: CompositeDisposable = CompositeDisposable() - private val state: BehaviorSubject = BehaviorSubject.create() - private val eventDispatcher: PublishSubject = PublishSubject.create() + private val state: MutableStateFlow = MutableStateFlow(null) + private val eventDispatcher: BroadcastChannel = BroadcastChannel(Channel.BUFFERED) /** * Implement this method to react to any BlockEvent emissions from the attached UiBlock. @@ -39,35 +38,34 @@ abstract class LogicBlock : 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 = state + fun observeState(): Flow = state.filterNotNull() /** * Observe events received by this LogicBlock, useful for propagating events to parent LogicBlocks */ - fun observeEvents(): Observable = eventDispatcher + fun observeEvents(): Flow = 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 Flow.launchInBlock() = launchIn(viewModelScope) + + inline fun launchInBlock(crossinline codeBlock: suspend () -> Unit) = viewModelScope.launch { codeBlock() } } \ No newline at end of file diff --git a/helium-core/src/main/java/com/joaquimverges/helium/core/UiBlock.kt b/helium-core/src/main/java/com/joaquimverges/helium/core/UiBlock.kt index a3df884..3089eb1 100644 --- a/helium-core/src/main/java/com/joaquimverges/helium/core/UiBlock.kt +++ b/helium-core/src/main/java/com/joaquimverges/helium/core/UiBlock.kt @@ -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. @@ -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( +abstract class UiBlock constructor( val view: View, - private val eventsObservable: PublishSubject = PublishSubject.create(), + private val eventFlow : BroadcastChannel = BroadcastChannel(Channel.BUFFERED), protected val context: Context = view.context ) { @@ -58,10 +60,10 @@ abstract class UiBlock( /** * Observe the events pushed from this UiBlock */ - fun observer(): Observable = eventsObservable + open fun observer(): Flow = 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) } diff --git a/helium-core/src/main/java/com/joaquimverges/helium/core/retained/RetainedBlocks.kt b/helium-core/src/main/java/com/joaquimverges/helium/core/retained/RetainedBlocks.kt index 8a08cc7..fd64213 100644 --- a/helium-core/src/main/java/com/joaquimverges/helium/core/retained/RetainedBlocks.kt +++ b/helium-core/src/main/java/com/joaquimverges/helium/core/retained/RetainedBlocks.kt @@ -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 /** diff --git a/helium-core/src/main/java/com/joaquimverges/helium/core/util/RxExtensions.kt b/helium-core/src/main/java/com/joaquimverges/helium/core/util/RxExtensions.kt deleted file mode 100644 index 0f323fd..0000000 --- a/helium-core/src/main/java/com/joaquimverges/helium/core/util/RxExtensions.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.joaquimverges.helium.core.util - -import android.view.View -import androidx.lifecycle.Lifecycle -import com.uber.autodispose.AutoDispose -import com.uber.autodispose.ObservableSubscribeProxy -import com.uber.autodispose.android.ViewScopeProvider -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider -import io.reactivex.* -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers - -/** - * Useful extension functions for Rx classes - */ - -fun Single.async(): Single { - return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) -} - -fun Maybe.async(): Maybe { - return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) -} - -fun Flowable.async(): Flowable { - return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) -} - -fun Observable.async(): Observable { - return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) -} - -fun Completable.async(): Completable { - return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) -} - -fun Observable.autoDispose(lifecycle: Lifecycle): ObservableSubscribeProxy { - val scope = AndroidLifecycleScopeProvider.from(lifecycle) - return `as`(AutoDispose.autoDisposable(scope)) -} - -fun Observable.autoDispose(view: View): ObservableSubscribeProxy { - val scope = ViewScopeProvider.from(view) - return `as`(AutoDispose.autoDisposable(scope)) -} diff --git a/helium-navigation/src/main/AndroidManifest.xml b/helium-navigation/src/main/AndroidManifest.xml index bd4acd4..c9aae78 100644 --- a/helium-navigation/src/main/AndroidManifest.xml +++ b/helium-navigation/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/drawer/NavDrawerUi.kt b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/drawer/NavDrawerUi.kt index 98af7f7..099cdcc 100644 --- a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/drawer/NavDrawerUi.kt +++ b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/drawer/NavDrawerUi.kt @@ -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 @@ -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) { diff --git a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/CollapsingToolbarUi.kt b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/CollapsingToolbarUi.kt index cf65da9..d25fe40 100644 --- a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/CollapsingToolbarUi.kt +++ b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/CollapsingToolbarUi.kt @@ -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 /** diff --git a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ScrollConfiguration.kt b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ScrollConfiguration.kt index 4adad9f..442e562 100644 --- a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ScrollConfiguration.kt +++ b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ScrollConfiguration.kt @@ -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] diff --git a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ToolbarUi.kt b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ToolbarUi.kt index 78dcb11..35a76b2 100644 --- a/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ToolbarUi.kt +++ b/helium-navigation/src/main/java/com/joaquimverges/helium/navigation/toolbar/ToolbarUi.kt @@ -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 /** diff --git a/helium-test/build.gradle b/helium-test/build.gradle index 56f8c90..89814a3 100644 --- a/helium-test/build.gradle +++ b/helium-test/build.gradle @@ -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') } diff --git a/helium-test/src/main/java/com/joaquimverges/helium/test/CoroutinesTestRule.kt b/helium-test/src/main/java/com/joaquimverges/helium/test/CoroutinesTestRule.kt new file mode 100644 index 0000000..45fa188 --- /dev/null +++ b/helium-test/src/main/java/com/joaquimverges/helium/test/CoroutinesTestRule.kt @@ -0,0 +1,24 @@ +package com.joaquimverges.helium.test + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +class CoroutinesTestRule( + val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() +) : TestWatcher() { + + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } +} \ No newline at end of file diff --git a/helium-test/src/main/java/com/joaquimverges/helium/test/HeliumTestCase.kt b/helium-test/src/main/java/com/joaquimverges/helium/test/HeliumTestCase.kt index 88ee72b..3e90322 100644 --- a/helium-test/src/main/java/com/joaquimverges/helium/test/HeliumTestCase.kt +++ b/helium-test/src/main/java/com/joaquimverges/helium/test/HeliumTestCase.kt @@ -5,9 +5,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.joaquimverges.helium.core.AppBlock import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.state.BlockState -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever import junit.framework.TestCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestCoroutineScope import org.junit.Rule import org.junit.runner.RunWith @@ -18,13 +20,15 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) open class HeliumTestCase : TestCase() { @get:Rule var mockRule = MockitoInitializationRule(this) - @get:Rule var rxRule = RxSchedulerRule() + @get:Rule var coroutinesTestRule = CoroutinesTestRule() + + val testCoroutineScope : CoroutineScope = TestCoroutineScope(coroutinesTestRule.testDispatcher) /** * Assemble blocks with a mocked lifecycle */ fun assemble(appBlock: AppBlock) { - appBlock.assemble(getMockLifecycle()) + appBlock.assemble(getMockLifecycle(), testCoroutineScope) } /** @@ -35,4 +39,5 @@ open class HeliumTestCase : TestCase() { whenever(currentState).thenReturn(state) } } + } \ No newline at end of file diff --git a/helium-test/src/main/java/com/joaquimverges/helium/test/RxSchedulerRule.kt b/helium-test/src/main/java/com/joaquimverges/helium/test/RxSchedulerRule.kt deleted file mode 100644 index ea708c7..0000000 --- a/helium-test/src/main/java/com/joaquimverges/helium/test/RxSchedulerRule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.joaquimverges.helium.test - -import io.reactivex.android.plugins.RxAndroidPlugins -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.Schedulers -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class RxSchedulerRule : TestRule{ - override fun apply(base: Statement?, description: Description?): Statement { - return object : Statement() { - override fun evaluate() { - RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } - RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() } - RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() } - RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() } - try { - base?.evaluate() - } finally { - RxJavaPlugins.reset() - RxAndroidPlugins.reset() - } - } - - } - } -} diff --git a/helium-test/src/main/java/com/joaquimverges/helium/test/TestLogicBLock.kt b/helium-test/src/main/java/com/joaquimverges/helium/test/TestLogicBLock.kt index 6968f6e..05afbd3 100644 --- a/helium-test/src/main/java/com/joaquimverges/helium/test/TestLogicBLock.kt +++ b/helium-test/src/main/java/com/joaquimverges/helium/test/TestLogicBLock.kt @@ -1,8 +1,9 @@ package com.joaquimverges.helium.test -import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.LogicBlock +import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.state.BlockState +import kotlinx.coroutines.flow.onEach import org.mockito.internal.matchers.apachecommons.ReflectionEquals /** @@ -14,9 +15,9 @@ class TestLogicBLock : LogicBlock() { private var lastBlockState: BlockState? = null init { - observeState().subscribe { + observeState().onEach { lastBlockState = it - }.autoDispose() + }.launchInBlock() } override fun onUiEvent(event: E) { diff --git a/helium-test/src/main/java/com/joaquimverges/helium/test/TestUiBlock.kt b/helium-test/src/main/java/com/joaquimverges/helium/test/TestUiBlock.kt index 8582b50..0a440e6 100644 --- a/helium-test/src/main/java/com/joaquimverges/helium/test/TestUiBlock.kt +++ b/helium-test/src/main/java/com/joaquimverges/helium/test/TestUiBlock.kt @@ -2,14 +2,10 @@ package com.joaquimverges.helium.test import android.content.Context import android.view.View -import androidx.lifecycle.Lifecycle +import com.joaquimverges.helium.core.UiBlock import com.joaquimverges.helium.core.event.BlockEvent -import com.joaquimverges.helium.core.LogicBlock import com.joaquimverges.helium.core.state.BlockState -import com.joaquimverges.helium.core.UiBlock -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockitokotlin2.mock import org.junit.Assert.fail import org.mockito.internal.matchers.apachecommons.ReflectionEquals diff --git a/helium-test/src/test/java/com/joaquimverges/helium/test/CoreTests.kt b/helium-test/src/test/java/com/joaquimverges/helium/test/CoreTests.kt index fa95f6e..41751ad 100644 --- a/helium-test/src/test/java/com/joaquimverges/helium/test/CoreTests.kt +++ b/helium-test/src/test/java/com/joaquimverges/helium/test/CoreTests.kt @@ -5,7 +5,6 @@ import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.core.state.BlockState import org.junit.Before import org.junit.Test -import org.mockito.MockitoAnnotations class CoreTests : HeliumTestCase() { diff --git a/helium-ui/src/main/AndroidManifest.xml b/helium-ui/src/main/AndroidManifest.xml index edce5d5..e3845cb 100644 --- a/helium-ui/src/main/AndroidManifest.xml +++ b/helium-ui/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListLogic.kt b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListLogic.kt index dda2634..8855af7 100644 --- a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListLogic.kt +++ b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListLogic.kt @@ -2,14 +2,20 @@ package com.joaquimverges.helium.ui.list import androidx.lifecycle.Lifecycle import androidx.lifecycle.OnLifecycleEvent -import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.LogicBlock -import com.joaquimverges.helium.core.util.async +import com.joaquimverges.helium.core.event.BlockEvent +import com.joaquimverges.helium.core.state.DataLoadState import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.list.repository.ListRepository -import com.joaquimverges.helium.core.state.DataLoadState import com.joaquimverges.helium.ui.util.RefreshPolicy -import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.withContext /** * A Typical List logic implementation: @@ -22,7 +28,8 @@ import io.reactivex.subjects.PublishSubject */ open class ListLogic( private val repository: ListRepository>, - private val refreshPolicy: RefreshPolicy = RefreshPolicy() + private val refreshPolicy: RefreshPolicy = RefreshPolicy(), + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : LogicBlock>, ListBlockEvent>() { sealed class PaginationEvent { @@ -30,10 +37,10 @@ open class ListLogic( data class AdditionalPageLoaded(val data: List) : PaginationEvent() } - private val paginationEvents = PublishSubject.create>() + private val paginationEvents = BroadcastChannel>(Channel.BUFFERED) init { - paginationEvents.scan>>( + paginationEvents.asFlow().scan, DataLoadState>>( DataLoadState.Init(), { prevState, paginationEvent -> when (paginationEvent) { @@ -50,9 +57,9 @@ open class ListLogic( } } }) - .subscribe { state -> + .onEach { state -> pushState(state) - }.autoDispose() + }.launchInBlock() } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @@ -63,33 +70,37 @@ open class ListLogic( } fun loadFirstPage() { - repository.getFirstPage() - .async() - .doOnSubscribe { pushState(DataLoadState.Loading()) } - .doOnSuccess { refreshPolicy.updateLastRefreshedTime() } - .subscribe( - { data -> - if (data.isNotEmpty()) { - paginationEvents.onNext(PaginationEvent.FirstPageLoaded(data)) - } else { - pushState(DataLoadState.Empty()) - } - }, - { error -> pushState(DataLoadState.Error(error)) } - ).autoDispose() + launchInBlock { + try { + pushState(DataLoadState.Loading()) + val data = withContext(dispatcher) { + repository.getFirstPage() + } + if (data.isNotEmpty()) { + paginationEvents.offer(PaginationEvent.FirstPageLoaded(data)) + } else { + pushState(DataLoadState.Empty()) + } + refreshPolicy.updateLastRefreshedTime() + } catch (error: Exception) { + pushState(DataLoadState.Error(error)) + } + } } fun paginate() { - repository.paginate() - .async() - .subscribe( - { paginatedData -> - if (paginatedData.isNotEmpty()) { - paginationEvents.onNext(PaginationEvent.AdditionalPageLoaded(paginatedData)) - } - }, - { error -> pushState(DataLoadState.Error(error)) } - ).autoDispose() + launchInBlock { + try { + val data = withContext(dispatcher) { + repository.paginate() + } + if (data?.isNotEmpty() == true) { + paginationEvents.offer(PaginationEvent.AdditionalPageLoaded(data)) + } + } catch (error: Exception) { + pushState(DataLoadState.Error(error)) + } + } } override fun onUiEvent(event: ListBlockEvent) { diff --git a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListUi.kt b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListUi.kt index 5edcaa3..c081fa5 100644 --- a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListUi.kt +++ b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/ListUi.kt @@ -12,12 +12,14 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.joaquimverges.helium.core.UiBlock import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.state.DataLoadState -import com.joaquimverges.helium.core.util.autoDispose -import com.joaquimverges.helium.core.util.onAttached import com.joaquimverges.helium.ui.R import com.joaquimverges.helium.ui.list.adapter.ListAdapter import com.joaquimverges.helium.ui.list.adapter.ListItem import com.joaquimverges.helium.ui.list.event.ListBlockEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.map import java.util.Collections.emptyList /** @@ -51,8 +53,8 @@ constructor( // optional list config layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(inflater.context), recyclerViewConfig: ((RecyclerView) -> Unit)? = null, - emptyUiBlock: UiBlock<*, E>? = null, - errorUiBlock: UiBlock<*, E>? = null, + private val emptyUiBlock: UiBlock<*, E>? = null, + private val errorUiBlock: UiBlock<*, E>? = null, swipeToRefreshEnabled: Boolean = false ) : UiBlock>, ListBlockEvent>(layoutResId, inflater, container, addToContainer) { @@ -91,26 +93,15 @@ constructor( // empty, error view emptyUiBlock?.let { emptyViewContainer.addView(it.view) } errorUiBlock?.let { errorViewContainer.addView(it.view) } + } - view.onAttached { - // adapter items events - adapter.observeItemEvents() - .autoDispose(view) - .subscribe { ev -> pushEvent(ListBlockEvent.ListItemEvent(ev)) } - // empty view events - emptyUiBlock?.let { - it.observer() - .autoDispose(view) - .subscribe { event: E -> pushEvent(ListBlockEvent.EmptyBlockEvent(event)) } - - } - // error view events - errorUiBlock?.let { - it.observer() - .autoDispose(view) - .subscribe { event: E -> pushEvent(ListBlockEvent.ErrorBlockEvent(event)) } - } - } + override fun observer(): Flow> { + val mergedFlows = mutableListOf>>() + mergedFlows.add(super.observer()) + mergedFlows.add(adapter.observeItemEvents().map { event -> ListBlockEvent.ListItemEvent(event) }) + emptyUiBlock?.let { mergedFlows.add(it.observer().map { event -> ListBlockEvent.EmptyBlockEvent(event) }) } + errorUiBlock?.let { mergedFlows.add(it.observer().map { event -> ListBlockEvent.ErrorBlockEvent(event) }) } + return mergedFlows.asFlow().flattenMerge() } override fun render(state: DataLoadState>) { diff --git a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListAdapter.kt b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListAdapter.kt index 563e941..c7f1e8c 100644 --- a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListAdapter.kt +++ b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListAdapter.kt @@ -7,8 +7,10 @@ import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.joaquimverges.helium.core.event.BlockEvent -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 /** * Convenience Adapter that renders list items in a BaseRecyclerViewItem. @@ -19,7 +21,7 @@ import io.reactivex.subjects.PublishSubject class ListAdapter>( private val inflater: LayoutInflater, private val viewHolderFactory: (LayoutInflater, ViewGroup) -> VH, - private val viewEvents: PublishSubject = PublishSubject.create() + private val viewEvents: BroadcastChannel = BroadcastChannel(Channel.BUFFERED) ) : RecyclerView.Adapter() { private val diff = AsyncListDiffer(this, object : DiffUtil.ItemCallback() { @@ -43,8 +45,8 @@ class ListAdapter>( holder.bind(getItem(position)) } - fun observeItemEvents(): Observable { - return viewEvents.hide() + fun observeItemEvents(): Flow { + return viewEvents.asFlow() } private fun getItem(position: Int): T { diff --git a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListItem.kt b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListItem.kt index ed3b2a7..bd2a833 100644 --- a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListItem.kt +++ b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/adapter/ListItem.kt @@ -1,14 +1,15 @@ package com.joaquimverges.helium.ui.list.adapter import android.content.Context -import androidx.annotation.LayoutRes -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView import com.joaquimverges.helium.core.event.BlockEvent -import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BroadcastChannel /** * Base class for RecyclerView items. @@ -34,12 +35,12 @@ abstract class ListItem(val view: View) : RecyclerView.Vie view: View = inflater.inflate(layoutResId, container, false) ) : this(view) - internal var viewEvents: PublishSubject? = null + internal var viewEvents: BroadcastChannel? = null protected var context: Context = itemView.context abstract fun bind(data: T) - fun pushEvent(event: V) = viewEvents?.onNext(event) + fun pushEvent(event: V) = viewEvents?.offer(event) /** * Convenience method to find a view by id within this recyclerViewItem diff --git a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/repository/ListRepository.kt b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/repository/ListRepository.kt index 9291081..e1a9cfb 100644 --- a/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/repository/ListRepository.kt +++ b/helium-ui/src/main/java/com/joaquimverges/helium/ui/list/repository/ListRepository.kt @@ -1,13 +1,10 @@ package com.joaquimverges.helium.ui.list.repository -import io.reactivex.Maybe -import io.reactivex.Single - /** * Interface to implement for any data provider used by a [com.joaquimverges.helium.ui.list.ListLogic]. * Here is where the actual data fetching/caching logic lives. */ interface ListRepository { - fun getFirstPage(): Single - fun paginate(): Maybe = Maybe.empty() + suspend fun getFirstPage(): T + suspend fun paginate(): T? = null } \ No newline at end of file diff --git a/helium-ui/src/test/java/com/joaquimverges/helium/ui/list/ListLogicTest.kt b/helium-ui/src/test/java/com/joaquimverges/helium/ui/list/ListLogicTest.kt index 277cb8b..8d0497a 100644 --- a/helium-ui/src/test/java/com/joaquimverges/helium/ui/list/ListLogicTest.kt +++ b/helium-ui/src/test/java/com/joaquimverges/helium/ui/list/ListLogicTest.kt @@ -1,27 +1,15 @@ package com.joaquimverges.helium.ui.list -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.joaquimverges.helium.core.assemble import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.core.state.DataLoadState import com.joaquimverges.helium.test.HeliumTestCase -import com.joaquimverges.helium.test.HeliumUiTestCase import com.joaquimverges.helium.test.TestUiBlock import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.list.repository.ListRepository import com.joaquimverges.helium.ui.util.RefreshPolicy -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.never -import com.nhaarman.mockito_kotlin.verify -import com.nhaarman.mockito_kotlin.whenever -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.android.plugins.RxAndroidPlugins -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.TestScheduler +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -37,84 +25,72 @@ class ListLogicTest : HeliumTestCase() { @Mock lateinit var repo: ListRepository> private lateinit var logic: ListLogic - private val testScheduler = TestScheduler() private val testUi = TestUiBlock>, ListBlockEvent>() - private val testData = Observable.range(0, 20).map { TestItem() }.toList().blockingGet() + private val testData = (0..20).map { TestItem() }.toList() @Before fun setup() { - RxJavaPlugins.setIoSchedulerHandler { testScheduler } - RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler } - whenever(repo.getFirstPage()).thenReturn(Single.just(testData)) - logic = ListLogic(repo, refreshPolicy) + logic = ListLogic(repo, refreshPolicy, coroutinesTestRule.testDispatcher) assemble(logic + testUi) } @Test - fun testRefreshPolicy() { + fun testRefreshPolicy() = runBlockingTest { + repo.stub { onBlocking { getFirstPage() }.doReturn(testData) } whenever(refreshPolicy.shouldRefresh()).thenReturn(false) logic.refreshIfNeeded() testUi.assertLastRendered(DataLoadState.Init()) whenever(refreshPolicy.shouldRefresh()).thenReturn(true) logic.refreshIfNeeded() - testUi.assertLastRendered(DataLoadState.Loading()) + testUi.assertLastRendered(DataLoadState.Ready(testData)) } @Test - fun testRefreshWithData() { + fun testRefreshWithData() = runBlockingTest { + repo.stub { onBlocking { getFirstPage() }.doReturn(testData) } logic.loadFirstPage() - testUi.assertLastRendered(DataLoadState.Loading>()) - testScheduler.triggerActions() - testUi.assertLastRendered(DataLoadState.Ready>(testData)) + testUi.assertLastRendered(DataLoadState.Ready(testData)) verify(refreshPolicy).updateLastRefreshedTime() } @Test - fun testRefreshWithNoData() { - whenever(repo.getFirstPage()).thenReturn(Single.just(emptyList())) + fun testRefreshWithNoData() = runBlockingTest { + repo.stub { onBlocking { getFirstPage() }.doReturn(emptyList()) } logic.loadFirstPage() - testUi.assertLastRendered(DataLoadState.Loading>()) - testScheduler.triggerActions() testUi.assertLastRendered(DataLoadState.Empty>()) verify(refreshPolicy).updateLastRefreshedTime() } @Test - fun testRefreshError() { + fun testRefreshError() = runBlockingTest { val error = RuntimeException("error") - whenever(repo.getFirstPage()).thenReturn(Single.error>(error)) + repo.stub { onBlocking { getFirstPage() }.doThrow(error) } logic.loadFirstPage() - testUi.assertLastRendered(DataLoadState.Loading>()) - testScheduler.triggerActions() testUi.assertLastRendered(DataLoadState.Error>(error)) verify(refreshPolicy, never()).updateLastRefreshedTime() } @Test - fun testPagination() { + fun testPagination() = runBlockingTest { + repo.stub { onBlocking { getFirstPage() }.doReturn(testData) } logic.loadFirstPage() - testUi.assertLastRendered(DataLoadState.Loading>()) - testScheduler.triggerActions() - testUi.assertLastRendered(DataLoadState.Ready>(testData)) + testUi.assertLastRendered(DataLoadState.Ready(testData)) verify(refreshPolicy).updateLastRefreshedTime() // paginate once val page1 = listOf(TestItem()) - whenever(repo.paginate()).thenReturn(Maybe.just(page1)) + repo.stub { onBlocking { paginate() }.doReturn(page1) } logic.paginate() - testScheduler.triggerActions() - testUi.assertLastRendered(DataLoadState.Ready>(testData + page1)) + testUi.assertLastRendered(DataLoadState.Ready(testData + page1)) // paginate twice val page2 = listOf(TestItem()) - whenever(repo.paginate()).thenReturn(Maybe.just(page2)) + repo.stub { onBlocking { paginate() }.doReturn(page2) } logic.paginate() - testScheduler.triggerActions() - testUi.assertLastRendered(DataLoadState.Ready>(testData + page1 + page2)) + testUi.assertLastRendered(DataLoadState.Ready(testData + page1 + page2)) // refresh initial logic.loadFirstPage() - testScheduler.triggerActions() - testUi.assertLastRendered(DataLoadState.Ready>(testData)) + testUi.assertLastRendered(DataLoadState.Ready(testData)) } } diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/BottomNavActivity.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/BottomNavActivity.kt index 86b8352..d889ff2 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/BottomNavActivity.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/BottomNavActivity.kt @@ -1,6 +1,5 @@ package com.joaquimverges.demoapp -import android.content.res.ColorStateList import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ListFragment.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ListFragment.kt index 261d766..1db1363 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ListFragment.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ListFragment.kt @@ -1,7 +1,6 @@ package com.joaquimverges.demoapp import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -12,7 +11,6 @@ import com.joaquimverges.helium.core.assemble import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.core.retained.getRetainedLogicBlock import com.joaquimverges.helium.ui.list.ListUi -import kotlin.math.log class ListFragment : Fragment() { diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/MainActivity.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/MainActivity.kt index d7c6245..dfe27ac 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/MainActivity.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/MainActivity.kt @@ -2,22 +2,20 @@ package com.joaquimverges.demoapp import android.content.Intent import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import com.joaquimverges.demoapp.ui.GridSpacingDecorator import com.joaquimverges.helium.core.assemble -import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.core.event.ClickEvent -import com.joaquimverges.helium.ui.list.event.ListBlockEvent +import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.ui.list.ListLogic -import com.joaquimverges.helium.ui.list.repository.ListRepository -import com.joaquimverges.helium.ui.list.card.CardListItem import com.joaquimverges.helium.ui.list.ListUi -import io.reactivex.Observable -import io.reactivex.Single +import com.joaquimverges.helium.ui.list.card.CardListItem +import com.joaquimverges.helium.ui.list.event.ListBlockEvent +import com.joaquimverges.helium.ui.list.repository.ListRepository class MainActivity : AppCompatActivity() { @@ -35,7 +33,7 @@ class MainActivity : AppCompatActivity() { class MenuRepository : ListRepository> { private fun getMenuItems() = MenuItem.values().toList() - override fun getFirstPage(): Single> = Observable.fromIterable(getMenuItems()).toList() + override suspend fun getFirstPage(): List = getMenuItems() } // Logic diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/SimpleListActivity.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/SimpleListActivity.kt index 6d26b8d..95827a7 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/SimpleListActivity.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/SimpleListActivity.kt @@ -2,12 +2,10 @@ package com.joaquimverges.demoapp import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.joaquimverges.demoapp.logic.MyDetailLogic import com.joaquimverges.demoapp.logic.MyListLogic import com.joaquimverges.demoapp.ui.MyListItem import com.joaquimverges.helium.core.assemble import com.joaquimverges.helium.core.plus -import com.joaquimverges.helium.core.retained.getRetainedLogicBlock import com.joaquimverges.helium.ui.list.ListUi class SimpleListActivity : AppCompatActivity() { diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ViewPagerActivity.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ViewPagerActivity.kt index bab0787..ad62574 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ViewPagerActivity.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ViewPagerActivity.kt @@ -1,8 +1,8 @@ package com.joaquimverges.demoapp import android.os.Bundle -import androidx.fragment.app.Fragment import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import com.joaquimverges.helium.ui.viewpager.PagerUi class ViewPagerActivity : AppCompatActivity() { diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyDetailRepository.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyDetailRepository.kt index 9d73329..757b150 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyDetailRepository.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyDetailRepository.kt @@ -1,19 +1,16 @@ package com.joaquimverges.demoapp.data import com.joaquimverges.demoapp.data.Colors.randomColor -import com.joaquimverges.helium.ui.list.repository.ListRepository -import io.reactivex.Single +import kotlinx.coroutines.delay import java.util.* -import java.util.concurrent.TimeUnit /** * @author joaquim */ class MyDetailRepository { - fun getData(): Single { - return Single - .just(randomColor(Random().nextInt()).run { MyItem(color, name.toLowerCase().replace("_", " ")) }) - .delay(1, TimeUnit.SECONDS) + suspend fun getData(): MyItem { + delay(1000) + return randomColor(Random().nextInt()).run { MyItem(color, name.toLowerCase().replace("_", " ")) } } } diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyListRepository.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyListRepository.kt index 1afd7fa..d1ef4f3 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyListRepository.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/data/MyListRepository.kt @@ -2,19 +2,16 @@ package com.joaquimverges.demoapp.data import com.joaquimverges.demoapp.data.Colors.randomColor import com.joaquimverges.helium.ui.list.repository.ListRepository -import io.reactivex.Flowable -import io.reactivex.Single -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.delay /** * @author joaquim */ class MyListRepository : ListRepository> { - override fun getFirstPage(): Single> { - return Flowable.range(0, 100) - .map { i -> randomColor(i).run { MyItem(color, name.toLowerCase().replace("_", " ")) } } - .toList() - .delay(1, TimeUnit.SECONDS) + override suspend fun getFirstPage(): List { + delay(1000) + return (0..100).toList() + .map { randomColor(it).run { MyItem(color, name.toLowerCase().replace("_", " ")) } } } } diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/logic/MyDetailLogic.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/logic/MyDetailLogic.kt index e93f35d..41107d0 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/logic/MyDetailLogic.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/logic/MyDetailLogic.kt @@ -7,7 +7,8 @@ import com.joaquimverges.demoapp.data.MyItem import com.joaquimverges.helium.core.LogicBlock import com.joaquimverges.helium.core.event.ClickEvent import com.joaquimverges.helium.core.state.DataLoadState -import com.joaquimverges.helium.core.util.async +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext /** * @author joaquim @@ -18,14 +19,17 @@ class MyDetailLogic( @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) private fun loadDetailModel() { - repository - .getData() - .async() - .doOnSubscribe { pushState(DataLoadState.Loading()) } - .subscribe( - { item -> pushState(DataLoadState.Ready(item)) }, - { error -> pushState(DataLoadState.Error(error)) } - ).autoDispose() + launchInBlock { + try { + pushState(DataLoadState.Loading()) + val data = withContext(Dispatchers.IO) { + repository.getData() + } + pushState(DataLoadState.Ready(data)) + } catch (error: Exception) { + pushState(DataLoadState.Error(error)) + } + } } override fun onUiEvent(event: ClickEvent) { diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/GridSpacingDecorator.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/GridSpacingDecorator.kt index 9276d2a..b9b89d2 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/GridSpacingDecorator.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/GridSpacingDecorator.kt @@ -1,10 +1,10 @@ package com.joaquimverges.demoapp.ui import android.graphics.Rect +import android.view.View import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import android.view.View /** * @author joaquim diff --git a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/MyDetailUi.kt b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/MyDetailUi.kt index 9810625..f79afb3 100644 --- a/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/MyDetailUi.kt +++ b/samples/demoapp/src/main/java/com/joaquimverges/demoapp/ui/MyDetailUi.kt @@ -1,18 +1,18 @@ package com.joaquimverges.demoapp.ui -import androidx.core.content.ContextCompat import android.transition.TransitionManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView +import androidx.core.content.ContextCompat import com.joaquimverges.demoapp.R import com.joaquimverges.demoapp.data.Colors import com.joaquimverges.demoapp.data.MyItem +import com.joaquimverges.helium.core.UiBlock import com.joaquimverges.helium.core.event.ClickEvent import com.joaquimverges.helium.core.state.DataLoadState -import com.joaquimverges.helium.core.UiBlock /** * @author joaquim diff --git a/samples/newsapp/build.gradle b/samples/newsapp/build.gradle index aa41ab9..fab1257 100644 --- a/samples/newsapp/build.gradle +++ b/samples/newsapp/build.gradle @@ -10,6 +10,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = "1.8" + } + defaultConfig { applicationId "com.jv.news" minSdkVersion 23 @@ -32,11 +36,10 @@ dependencies { implementation "androidx.legacy:legacy-support-v13:1.0.0" implementation "androidx.palette:palette:1.0.0" implementation "androidx.browser:browser:1.2.0" - implementation "com.squareup.retrofit2:retrofit:2.3.0" - implementation "com.squareup.retrofit2:converter-gson:2.3.0" - implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0" - implementation "com.squareup.okhttp3:logging-interceptor:3.9.1" - implementation "com.github.bumptech.glide:glide:4.7.1" + implementation "com.squareup.retrofit2:retrofit:2.7.1" + implementation "com.squareup.retrofit2:converter-gson:2.4.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.5.0" + implementation "com.github.bumptech.glide:glide:4.11.0" kapt "com.github.bumptech.glide:compiler:4.7.1" implementation "com.thoughtbot:expandablecheckrecyclerview:1.4" debugImplementation "com.squareup.leakcanary:leakcanary-android:1.5.4" diff --git a/samples/newsapp/src/main/java/com/jv/news/AppBlocks.kt b/samples/newsapp/src/main/java/com/jv/news/AppBlocks.kt index c896542..534907f 100644 --- a/samples/newsapp/src/main/java/com/jv/news/AppBlocks.kt +++ b/samples/newsapp/src/main/java/com/jv/news/AppBlocks.kt @@ -2,10 +2,10 @@ package com.jv.news import androidx.fragment.app.FragmentActivity import com.joaquimverges.helium.core.AppBlock -import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.plus import com.joaquimverges.helium.core.retained.getRetainedLogicBlock -import com.joaquimverges.helium.core.state.BlockState +import com.joaquimverges.helium.navigation.drawer.NavDrawerEvent +import com.joaquimverges.helium.navigation.drawer.NavDrawerState import com.jv.news.logic.ArticleListLogic import com.jv.news.logic.MainScreenLogic import com.jv.news.ui.ArticleListUi @@ -15,7 +15,7 @@ import com.jv.news.ui.MainScreenUi * @author joaqu */ object MainAppBlock { - fun build(activity: FragmentActivity): AppBlock { + fun build(activity: FragmentActivity): AppBlock { val logic = activity.getRetainedLogicBlock() val ui = MainScreenUi(activity.layoutInflater) activity.setContentView(ui.view) diff --git a/samples/newsapp/src/main/java/com/jv/news/MainActivity.kt b/samples/newsapp/src/main/java/com/jv/news/MainActivity.kt index cf52cca..011a7e4 100644 --- a/samples/newsapp/src/main/java/com/jv/news/MainActivity.kt +++ b/samples/newsapp/src/main/java/com/jv/news/MainActivity.kt @@ -6,12 +6,6 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.joaquimverges.helium.core.assemble -import com.joaquimverges.helium.core.plus -import com.joaquimverges.helium.core.retained.getRetainedLogicBlock -import com.jv.news.logic.ArticleListLogic -import com.jv.news.logic.MainScreenLogic -import com.jv.news.ui.ArticleListUi -import com.jv.news.ui.MainScreenUi import com.jv.news.util.VersionUtil class MainActivity : AppCompatActivity() { diff --git a/samples/newsapp/src/main/java/com/jv/news/data/ArticleRepository.kt b/samples/newsapp/src/main/java/com/jv/news/data/ArticleRepository.kt index 91813d3..2086d38 100644 --- a/samples/newsapp/src/main/java/com/jv/news/data/ArticleRepository.kt +++ b/samples/newsapp/src/main/java/com/jv/news/data/ArticleRepository.kt @@ -2,9 +2,6 @@ package com.jv.news.data import com.joaquimverges.helium.ui.list.repository.ListRepository import com.jv.news.data.model.Article -import com.jv.news.data.model.ArticleResponse -import io.reactivex.Maybe -import io.reactivex.Single import java.net.URL /** @@ -17,32 +14,28 @@ class ArticleRepository( private var page = 1 - override fun getFirstPage(): Single> { + override suspend fun getFirstPage(): List
{ page = 1 return fetch() } - override fun paginate(): Maybe> { + override suspend fun paginate(): List
? { page++ if (page >= 6) { - return Maybe.empty() + return null } - return fetch().toMaybe() + return fetch() } - private fun fetch(): Single> { + private suspend fun fetch(): List
{ val ids = sourcesRepository.getSelectedSourceIds() if (ids.isEmpty()) { - return Single.just(listOf()) + return listOf() } - return api - .getArticles(ids.joinToString(separator = ","), page = page) - .map { response: ArticleResponse -> - response.articles - .filter { it.url != null && it.urlToImage != null } - .distinctBy { it.title } - .distinctBy { URL(it.url).path } - } + return api.getArticles(ids.joinToString(separator = ","), page = page).articles + .filter { it.url != null && it.urlToImage != null } + .distinctBy { it.title } + .distinctBy { URL(it.url).path } } fun sourcesUpdatedObserver() = sourcesRepository.observer() diff --git a/samples/newsapp/src/main/java/com/jv/news/data/NewsApiServer.kt b/samples/newsapp/src/main/java/com/jv/news/data/NewsApiServer.kt index c0562f9..564f8de 100644 --- a/samples/newsapp/src/main/java/com/jv/news/data/NewsApiServer.kt +++ b/samples/newsapp/src/main/java/com/jv/news/data/NewsApiServer.kt @@ -4,11 +4,9 @@ import com.jv.news.App import com.jv.news.R import com.jv.news.data.model.ArticleResponse import com.jv.news.data.model.SourcesResponse -import io.reactivex.Single import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import retrofit2.http.Query @@ -28,13 +26,13 @@ object NewsApiServer { init { val client = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) - .addInterceptor { + .addNetworkInterceptor { val original = it.request() it.proceed( original .newBuilder() .header("X-Api-Key", API_KEY) - .method(original.method(), original.body()) + .method(original.method, original.body) .build() ) } @@ -43,7 +41,6 @@ object NewsApiServer { val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(client) .build() service = retrofit.create(NewsApiService::class.java) @@ -51,12 +48,12 @@ object NewsApiServer { interface NewsApiService { @GET(ENDPOINT_ARTICLES) - fun getArticles(@Query("sources") source: String, @Query("page") page: Int): Single + suspend fun getArticles(@Query("sources") source: String, @Query("page") page: Int): ArticleResponse @GET(ENDPOINT_ARTICLES) - fun getArticlesByInterest(@Query("q") query: String, @Query("page") page: Int, @Query("sortBy") sort: String = "relevancy"): Single + suspend fun getArticlesByInterest(@Query("q") query: String, @Query("page") page: Int, @Query("sortBy") sort: String = "relevancy"): ArticleResponse @GET(ENDPOINT_SOURCES) - fun getSources(): Single + suspend fun getSources(): SourcesResponse } } diff --git a/samples/newsapp/src/main/java/com/jv/news/data/SourcesRepository.kt b/samples/newsapp/src/main/java/com/jv/news/data/SourcesRepository.kt index 4ec2ae9..c032c47 100644 --- a/samples/newsapp/src/main/java/com/jv/news/data/SourcesRepository.kt +++ b/samples/newsapp/src/main/java/com/jv/news/data/SourcesRepository.kt @@ -6,9 +6,10 @@ import com.joaquimverges.helium.ui.list.repository.ListRepository import com.jv.news.App import com.jv.news.data.model.ArticleSource import com.jv.news.data.model.SourcesCategoryGroup -import io.reactivex.Observable -import io.reactivex.Single -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 /** * @author joaquim @@ -23,12 +24,12 @@ class SourcesRepository( } private val sources = mutableSetOf().apply { addAll(preferences.getStringSet(SELECTED_SOURCES, mutableSetOf())?.toList() ?: listOf()) } - private val sourcesSubject = PublishSubject.create>() + private val sourcesSubject = BroadcastChannel>(Channel.BUFFERED) - override fun getFirstPage(): Single> { - return api.getSources() - .map { it.sources.groupBy { source -> source.category } } - .map { it.mapNotNull { mapEntry -> mapEntry.key?.let { name -> SourcesCategoryGroup(name, mapEntry.value) { source -> isSelected(source) } } } } + override suspend fun getFirstPage(): List { + return api.getSources().sources + .groupBy { source -> source.category } + .mapNotNull { mapEntry -> mapEntry.key?.let { name -> SourcesCategoryGroup(name, mapEntry.value) { source -> isSelected(source) } } } } fun getSelectedSourceIds(): MutableSet { @@ -38,7 +39,7 @@ class SourcesRepository( fun markUnselected(source: ArticleSource) { sources.apply { remove(source.id) - sourcesSubject.onNext(this) + sourcesSubject.offer(this) } persistToDisk() } @@ -46,7 +47,7 @@ class SourcesRepository( fun markSelected(source: ArticleSource) { sources.apply { source.id?.let { add(it) } - sourcesSubject.onNext(this) + sourcesSubject.offer(this) } persistToDisk() } @@ -59,5 +60,5 @@ class SourcesRepository( return getSelectedSourceIds().contains(articleSource.id) } - fun observer(): Observable> = sourcesSubject + fun observer(): Flow> = sourcesSubject.asFlow() } \ No newline at end of file diff --git a/samples/newsapp/src/main/java/com/jv/news/logic/ArticleListLogic.kt b/samples/newsapp/src/main/java/com/jv/news/logic/ArticleListLogic.kt index fba9908..928967c 100644 --- a/samples/newsapp/src/main/java/com/jv/news/logic/ArticleListLogic.kt +++ b/samples/newsapp/src/main/java/com/jv/news/logic/ArticleListLogic.kt @@ -8,13 +8,15 @@ import androidx.core.app.ShareCompat import com.joaquimverges.helium.core.LogicBlock import com.joaquimverges.helium.navigation.toolbar.ToolbarEvent import com.joaquimverges.helium.navigation.toolbar.ToolbarLogic -import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.list.ListLogic +import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.util.RefreshPolicy import com.jv.news.data.ArticleRepository import com.jv.news.data.model.Article import com.jv.news.logic.state.ArticleListState import com.jv.news.ui.event.ArticleEvent +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.onEach import java.util.concurrent.TimeUnit /** @@ -33,30 +35,30 @@ class ArticleListLogic( init { repository .sourcesUpdatedObserver() - .subscribe { listLogic.loadFirstPage() } - .autoDispose() + .onEach { listLogic.loadFirstPage() } + .launchInBlock() // receive all list item view events in this block - listLogic.observeEvents().subscribe { + listLogic.observeEvents().onEach { when (it) { is ListBlockEvent.ListItemEvent -> onUiEvent(it.itemEvent) is ListBlockEvent.EmptyBlockEvent -> onUiEvent(it.emptyViewEvent) is ListBlockEvent.UserScrolledBottom -> listLogic.paginate() is ListBlockEvent.SwipedToRefresh -> listLogic.loadFirstPage() } - }.autoDispose() + }.launchInBlock() // when the list changes state, propagate the state up to the MainScreen // so it can close the nav drawer after 2s listLogic.observeState() - .debounce(2, TimeUnit.SECONDS) - .subscribe { pushState(ArticleListState.ArticlesLoaded) } - .autoDispose() + .debounce(2000) + .onEach { pushState(ArticleListState.ArticlesLoaded) } + .launchInBlock() - toolbarLogic.observeEvents().subscribe { + toolbarLogic.observeEvents().onEach { when (it) { is ToolbarEvent.HomeClicked -> pushState(ArticleListState.MoreSourcesRequested) } - }.autoDispose() + }.launchInBlock() } override fun onUiEvent(event: ArticleEvent) { diff --git a/samples/newsapp/src/main/java/com/jv/news/logic/MainScreenLogic.kt b/samples/newsapp/src/main/java/com/jv/news/logic/MainScreenLogic.kt index 3a96996..774815b 100644 --- a/samples/newsapp/src/main/java/com/jv/news/logic/MainScreenLogic.kt +++ b/samples/newsapp/src/main/java/com/jv/news/logic/MainScreenLogic.kt @@ -1,18 +1,18 @@ package com.jv.news.logic -import com.joaquimverges.helium.core.event.BlockEvent import com.joaquimverges.helium.core.LogicBlock -import com.joaquimverges.helium.core.state.BlockState +import com.joaquimverges.helium.navigation.drawer.NavDrawerEvent import com.joaquimverges.helium.navigation.drawer.NavDrawerState import com.jv.news.data.ArticleRepository import com.jv.news.data.SourcesRepository import com.jv.news.logic.state.ArticleListState +import kotlinx.coroutines.flow.onEach /** * @author joaquim */ -class MainScreenLogic : LogicBlock() { +class MainScreenLogic : LogicBlock() { private val sourcesRepo = SourcesRepository() private val articleRepo = ArticleRepository(sourcesRepo) @@ -21,15 +21,19 @@ class MainScreenLogic : LogicBlock() { internal val sourcesLogic = SourcesLogic(sourcesRepo) init { - articleListLogic.observeState().subscribe { state -> + articleListLogic.observeState().onEach { state -> when (state) { ArticleListState.ArticlesLoaded -> pushState(NavDrawerState.Closed) ArticleListState.MoreSourcesRequested -> pushState(NavDrawerState.Opened) } - }.autoDispose() + }.launchInBlock() } - override fun onUiEvent(event: BlockEvent) { - // no-op for now + override fun onUiEvent(event: NavDrawerEvent) { + when (event) { + NavDrawerEvent.DrawerOpened -> { + } + NavDrawerEvent.DrawerClosed -> articleListLogic.pushState(ArticleListState.ArticlesLoaded) + } } } diff --git a/samples/newsapp/src/main/java/com/jv/news/logic/SourcesLogic.kt b/samples/newsapp/src/main/java/com/jv/news/logic/SourcesLogic.kt index 5c75891..cb0231b 100644 --- a/samples/newsapp/src/main/java/com/jv/news/logic/SourcesLogic.kt +++ b/samples/newsapp/src/main/java/com/jv/news/logic/SourcesLogic.kt @@ -1,7 +1,7 @@ package com.jv.news.logic -import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.list.ListLogic +import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.ui.util.RefreshPolicy import com.jv.news.data.SourcesRepository import com.jv.news.data.model.SourcesCategoryGroup diff --git a/samples/newsapp/src/main/java/com/jv/news/ui/ArticleListUi.kt b/samples/newsapp/src/main/java/com/jv/news/ui/ArticleListUi.kt index b57f28a..4f575c7 100644 --- a/samples/newsapp/src/main/java/com/jv/news/ui/ArticleListUi.kt +++ b/samples/newsapp/src/main/java/com/jv/news/ui/ArticleListUi.kt @@ -6,9 +6,9 @@ import android.view.LayoutInflater import android.view.View import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +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.toolbar.CollapsingToolbarUi import com.joaquimverges.helium.ui.list.ListUi import com.jv.news.App.Companion.context diff --git a/samples/newsapp/src/main/java/com/jv/news/ui/MainScreenUi.kt b/samples/newsapp/src/main/java/com/jv/news/ui/MainScreenUi.kt index 229f13a..e051172 100644 --- a/samples/newsapp/src/main/java/com/jv/news/ui/MainScreenUi.kt +++ b/samples/newsapp/src/main/java/com/jv/news/ui/MainScreenUi.kt @@ -2,12 +2,14 @@ package com.jv.news.ui import android.view.LayoutInflater import android.view.ViewGroup -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.drawer.NavDrawerEvent import com.joaquimverges.helium.navigation.drawer.NavDrawerState import com.joaquimverges.helium.navigation.drawer.NavDrawerUi import com.jv.news.R +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.flowOf /** * @author joaquim @@ -20,7 +22,7 @@ class MainScreenUi( articleListUi, drawerUi ) -) : UiBlock(R.layout.activity_main, inflater) { +) : UiBlock(R.layout.activity_main, inflater) { private val mainContainer = findView(R.id.main_container) @@ -28,9 +30,11 @@ class MainScreenUi( mainContainer.addView(navDrawerUi.view) } - override fun render(state: BlockState) { - when (state) { - is NavDrawerState -> navDrawerUi.render(state) - } + override fun render(state: NavDrawerState) { + navDrawerUi.render(state) + } + + override fun observer(): Flow { + return flowOf(super.observer(), navDrawerUi.observer()).flattenMerge() } } \ No newline at end of file diff --git a/samples/newsapp/src/main/java/com/jv/news/ui/SourcesUi.kt b/samples/newsapp/src/main/java/com/jv/news/ui/SourcesUi.kt index 5e3c2b3..da004a0 100644 --- a/samples/newsapp/src/main/java/com/jv/news/ui/SourcesUi.kt +++ b/samples/newsapp/src/main/java/com/jv/news/ui/SourcesUi.kt @@ -6,8 +6,8 @@ import android.widget.ProgressBar import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.joaquimverges.helium.core.UiBlock -import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.joaquimverges.helium.core.state.DataLoadState +import com.joaquimverges.helium.ui.list.event.ListBlockEvent import com.jv.news.R import com.jv.news.data.model.ArticleSource import com.jv.news.data.model.SourcesCategoryGroup diff --git a/samples/newsapp/src/main/java/com/jv/news/ui/SpacesItemDecoration.kt b/samples/newsapp/src/main/java/com/jv/news/ui/SpacesItemDecoration.kt index f154cd2..7f55daa 100644 --- a/samples/newsapp/src/main/java/com/jv/news/ui/SpacesItemDecoration.kt +++ b/samples/newsapp/src/main/java/com/jv/news/ui/SpacesItemDecoration.kt @@ -1,10 +1,10 @@ package com.jv.news.ui import android.graphics.Rect +import android.view.View import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import android.view.View /** * @author: joaquim