From c7f3151312868b4be8332f5df3d12f053efe2889 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Thu, 25 Jul 2019 03:00:44 +0530 Subject: [PATCH 01/24] added flow setup --- .../main/java/com/ragdroid/api/MarvelApi.kt | 31 +++-- buildSrc/src/main/java/Dependencies.kt | 10 +- data/build.gradle | 15 ++- .../com/ragdroid/data/MainRepositoryImpl.kt | 35 +++-- .../com/ragdroid/data/MainRepositorySpec.kt | 8 +- presentation/build.gradle | 21 +++ .../mvi/characters/CharactersFragment.kt | 123 ++++++++++++++++++ .../mvi/characters/CharactersViewModel.kt | 16 +++ .../ragdroid/mvi/dagger/MainActivityModule.kt | 7 +- .../com/ragdroid/mvi/main/MainFragment.kt | 2 + .../mvi/viewmodel/MainFragmentViewModel.kt | 6 +- .../viewmodel/MainFragmentViewModelTest.kt | 6 +- 12 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt create mode 100644 presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt diff --git a/api/src/main/java/com/ragdroid/api/MarvelApi.kt b/api/src/main/java/com/ragdroid/api/MarvelApi.kt index 5fecf67..0ad2393 100644 --- a/api/src/main/java/com/ragdroid/api/MarvelApi.kt +++ b/api/src/main/java/com/ragdroid/api/MarvelApi.kt @@ -23,17 +23,30 @@ const val CHARACTER_ID = "characterId" interface MarvelApi { @GET("characters") - fun getCharacters(@Query(API_KEY) publicKey: String, - @Query(HASH) md5Digest: String, - @Query(TIMESTAMP) timestamp: Long, - @Query(OFFSET) offset: Int?, - @Query(LIMIT) limit: Int?): Single>> + suspend fun getCharacters(@Query(API_KEY) publicKey: String, + @Query(HASH) md5Digest: String, + @Query(TIMESTAMP) timestamp: Long, + @Query(OFFSET) offset: Int?, + @Query(LIMIT) limit: Int?): TDataWrapper> @GET("characters/{characterId}") - fun getCharacter(@Path(CHARACTER_ID) characterId: Long, - @Query(API_KEY) publicKey: String, - @Query(HASH) md5Digest: String, - @Query(TIMESTAMP) timestamp: Long): Single>> + suspend fun getCharacter(@Path(CHARACTER_ID) characterId: Long, + @Query(API_KEY) publicKey: String, + @Query(HASH) md5Digest: String, + @Query(TIMESTAMP) timestamp: Long): TDataWrapper> + + @GET("characters") + fun getCharactersSingle(@Query(API_KEY) publicKey: String, + @Query(HASH) md5Digest: String, + @Query(TIMESTAMP) timestamp: Long, + @Query(OFFSET) offset: Int?, + @Query(LIMIT) limit: Int?): Single>> + + @GET("characters/{characterId}") + fun getCharacterSingle(@Path(CHARACTER_ID) characterId: Long, + @Query(API_KEY) publicKey: String, + @Query(HASH) md5Digest: String, + @Query(TIMESTAMP) timestamp: Long): Single>> /** * Retrieve list of comics by character Id diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 9ac32cb..e4462fb 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -1,10 +1,11 @@ object Versions { - val kotlin_version = "1.3.11" + val kotlin_version = "1.3.30" + val kotlinx_coroutines = "1.3.0-RC" val compileSdk = 28 val minSdk = 21 - val retrofitVersion = "2.3.0" + val retrofitVersion = "2.6.0" val support_library = "27.1.1" val material = "1.0.0" val okhttpVersion = "3.9.1" @@ -29,6 +30,7 @@ object Versions { val reclaim = "1.2.2" val rxBindings = "2.1.1" val appCompat = "1.0.2" + val android_ktx = "1.2.0-alpha02" // test libraries val archTesting = "2.0.1" @@ -42,7 +44,9 @@ object Versions { } object Deps { - val kotlin_jdk = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin_version}" + val kotlin_jdk = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin_version}" + val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinx_coroutines}" + val androidKtx = "androidx.core:core-ktx:${Versions.android_ktx}" val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraint_layout}" val timber = "com.jakewharton.timber:timber:${Versions.timberVersion}" val reclaim = "com.github.fueled:reclaim:${Versions.reclaim}" diff --git a/data/build.gradle b/data/build.gradle index 9e13466..69a72e4 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -15,22 +15,23 @@ sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 dependencies { + implementation Deps.coroutinesAndroid implementation Deps.kotlin_jdk - compile project(':api') + implementation project(':api') - compile Deps.timber + implementation Deps.timber implementation Deps.rx - compile Deps.dagger - compile Deps.daggerAndroid - compile Deps.daggerAndroidSupport + implementation Deps.dagger + implementation Deps.daggerAndroid + implementation Deps.daggerAndroidSupport kapt Deps.daggerCompiler kapt Deps.daggerAndroidProcessor - testCompile TestDeps.kluent + testImplementation TestDeps.kluent testImplementation TestDeps.spek testImplementation TestDeps.spekJunitPlatformEngine - testCompile TestDeps.mockitoKotlin + testImplementation TestDeps.mockitoKotlin testImplementation TestDeps.mockito } diff --git a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt index 246d994..54131a1 100644 --- a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt +++ b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt @@ -8,21 +8,37 @@ import com.ragdroid.data.entity.AppConfig import com.ragdroid.data.entity.CharacterMapper import com.ragdroid.data.entity.CharacterMarvel import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import javax.inject.Inject /** * Created by garimajain on 18/11/17. */ -class MainRepositoryImpl -@Inject -constructor( +class MainRepositoryImpl @Inject constructor( private val marvelApi: MarvelApi, private val characterMapper: CharacterMapper, private val config: AppConfig, private val helpers: Helpers): MainRepository { + override suspend fun fetchCharacters(): Flow> = flow { + val timeStamp = System.currentTimeMillis() + val charactersWrapper = marvelApi.getCharacters( + config.publicKey, + helpers.buildMD5Digest("" + timeStamp + config.privateKey + + config.publicKey), + timeStamp, + 0, + 50) + val characters = charactersWrapper.data.results + .map { + characterMapper.map(it) + }.toList() + emit(characters) + + } - override fun fetchCharacters(): Single> { + override fun fetchCharactersSingle(): Single> { val timeStamp = System.currentTimeMillis() return charactersApiSingle(timeStamp) .map { dataWrapper: TDataWrapper> -> @@ -33,7 +49,7 @@ constructor( } } - override fun fetchCharacter(id: Long): Single { + override fun fetchCharacterSingle(id: Long): Single { val timeStamp = System.currentTimeMillis() return characterApiSingle(id, timeStamp) .map { dataWrapper: TDataWrapper> -> @@ -47,7 +63,7 @@ constructor( } private fun characterApiSingle(id: Long, timeStamp: Long): Single>> { - return marvelApi.getCharacter( + return marvelApi.getCharacterSingle( id, config.publicKey, helpers.buildMD5Digest("" + timeStamp + config.privateKey @@ -56,7 +72,7 @@ constructor( ) } private fun charactersApiSingle(timeStamp: Long): Single>> { - return marvelApi.getCharacters( + return marvelApi.getCharactersSingle( config.publicKey, helpers.buildMD5Digest("" + timeStamp + config.privateKey + config.publicKey), @@ -70,6 +86,7 @@ constructor( interface MainRepository { - fun fetchCharacters(): Single> - fun fetchCharacter(id: Long): Single + fun fetchCharactersSingle(): Single> + suspend fun fetchCharacters(): Flow> + fun fetchCharacterSingle(id: Long): Single } diff --git a/data/src/test/java/com/ragdroid/data/MainRepositorySpec.kt b/data/src/test/java/com/ragdroid/data/MainRepositorySpec.kt index 6b037be..1f010af 100644 --- a/data/src/test/java/com/ragdroid/data/MainRepositorySpec.kt +++ b/data/src/test/java/com/ragdroid/data/MainRepositorySpec.kt @@ -41,10 +41,10 @@ object MainRepositorySpec: Spek({ } on("fetching from repository") { - `when`(mockApi.getCharacters(anyString(), anyString(), anyLong(), anyInt(), anyInt())) + `when`(mockApi.getCharactersSingle(anyString(), anyString(), anyLong(), anyInt(), anyInt())) .thenReturn(Single.just(getFakeMarvelCharacters())) val testObserver = TestObserver>() - repository.fetchCharacters() + repository.fetchCharactersSingle() .subscribe(testObserver) it("should receive 1 value") { @@ -61,10 +61,10 @@ object MainRepositorySpec: Spek({ on("fetching from repository gives error") { val error = mock(Throwable::class) - `when`(mockApi.getCharacters(anyString(), anyString(), anyLong(), anyInt(), anyInt())) + `when`(mockApi.getCharactersSingle(anyString(), anyString(), anyLong(), anyInt(), anyInt())) .thenReturn(Single.error(error)) val testObserver = TestObserver>() - repository.fetchCharacters() + repository.fetchCharactersSingle() .subscribe(testObserver) it("should error") { testObserver.assertError(error) diff --git a/presentation/build.gradle b/presentation/build.gradle index e4640ce..357d846 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -5,6 +5,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +androidExtensions { + experimental = true +} + + android { compileSdkVersion Versions.compileSdk defaultConfig { @@ -33,6 +38,20 @@ android { dataBinding { enabled true } + sourceSets { + androidTest.java.srcDirs += 'src/androidTest/kotlin' + debug.java.srcDirs += 'src/debug/kotlin' + main.java.srcDirs += 'src/main/kotlin' + release.java.srcDirs += 'src/release/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + packagingOptions { + exclude("META-INF/*.kotlin_module") + } } @@ -40,6 +59,8 @@ dependencies { api project(':mvvmi') api project(':data') implementation Deps.kotlin_jdk + implementation Deps.coroutinesAndroid + implementation Deps.androidKtx implementation Deps.constraintLayout diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt new file mode 100644 index 0000000..bf3d3f6 --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -0,0 +1,123 @@ +package com.ragdroid.mvi.characters + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.fueled.reclaim.ItemPresenterProvider +import com.fueled.reclaim.ItemsViewAdapter +import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding2.support.v4.widget.refreshes +import com.ragdroid.mvi.R +import com.ragdroid.mvi.databinding.FragmentMainBinding +import com.ragdroid.mvi.helpers.BindFragment +import com.ragdroid.mvi.items.CharacterItem +import com.ragdroid.mvi.main.MainAction +import com.ragdroid.mvi.main.MainNavigation +import com.ragdroid.mvi.main.MainViewState +import com.ragdroid.mvi.models.CharacterItemPresenter +import com.ragdroid.mvi.viewmodel.MainFragmentViewModel +import com.ragdroid.mvvmi.core.MviView +import com.ragdroid.mvvmi.core.NavigationState +import dagger.android.support.DaggerFragment +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.processors.PublishProcessor +import timber.log.Timber +import javax.inject.Inject + +/** + * A placeholder fragment containing a simple view. + */ +class MainFragment : DaggerFragment(), + ItemPresenterProvider, + CharacterItemPresenter { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + //delegate the binding initialization to BindFragment delegate + private val binding: FragmentMainBinding by BindFragment(R.layout.fragment_main) + private val adapter: ItemsViewAdapter by lazy(LazyThreadSafetyMode.NONE) { + ItemsViewAdapter(context) + } + private val descriptionClickProcessor: PublishProcessor = PublishProcessor.create() + + lateinit var viewModel: MainFragmentViewModel + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val manager = LinearLayoutManager(context) + val decoration = DividerItemDecoration(context, RecyclerView.VERTICAL) + manager.orientation = RecyclerView.VERTICAL + binding.listView.layoutManager = manager + binding.listView.adapter = adapter + binding.listView.addItemDecoration(decoration) + setupViewModel() + } + + private fun setupViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) + } + + + override fun onCharacterDescriptionClicked(itemId: Long) { + descriptionClickProcessor.onNext(MainAction.LoadDescription(itemId)) + } + + override fun getItemPresenter(): CharacterItemPresenter { + return this + } + + + private fun pullToRefreshIntent(): Flowable = + binding.refreshLayout.refreshes().toFlowable(BackpressureStrategy.DROP).map { MainAction.PullToRefresh } + + private fun loadingIntent(): Flowable = Flowable.just(MainAction.LoadData) + + private fun loadDescription(): Flowable { + return descriptionClickProcessor + } + + fun render(state: MainViewState) { + Timber.d("got state $state") + binding.model = state + when { + state.pullToRefreshError != null -> return + + state.loadingError != null -> { + adapter.clearAllRecyclerItems() + return + } + + else -> { + val characterModelList = + state.characters.map { + CharacterItem(it, this) + } + adapter.replaceItems(characterModelList, true) + } + } + } + + + fun navigate(navigationState: NavigationState) { + when (navigationState) { + is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT) + else -> {//do nothing + } + } + } + +} diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt new file mode 100644 index 0000000..e896539 --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -0,0 +1,16 @@ +package com.ragdroid.mvi.characters + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.ragdroid.data.MainRepository +import com.ragdroid.mvi.main.MainViewState +import javax.inject.Inject + +class CharactersViewModel @Inject constructor( + private val mainRepository: MainRepository +) { + + fun stateLiveData(): LiveData = stateLiveData + private val stateLiveData = MutableLiveData() + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt index 0a43631..98a7e55 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt @@ -1,5 +1,6 @@ package com.ragdroid.mvi.dagger +import com.ragdroid.mvi.characters.CharactersFragment import com.ragdroid.mvi.main.MainFragment import dagger.Module import dagger.android.ContributesAndroidInjector @@ -10,7 +11,11 @@ import dagger.android.ContributesAndroidInjector @Module abstract class MainActivityModule { - @ContributesAndroidInjector() + @ContributesAndroidInjector @FragmentScope internal abstract fun mainActivityFragment(): MainFragment + + @ContributesAndroidInjector + @FragmentScope + internal abstract fun charactersFragmeent(): CharactersFragment } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 158dd09..6c3dce5 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration @@ -69,6 +70,7 @@ class MainFragment : DaggerFragment(), private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) + viewModel.stateLiveData().observe(lifecycleOwner, Observer { render(it) }) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 8397722..05839d6 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -33,7 +33,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return loadDescriptionActionStream .observeOn(Schedulers.io()) .flatMap { action -> - repository.fetchCharacter(action.characterId).toFlowable() + repository.fetchCharacterSingle(action.characterId).toFlowable() .delay(2000, TimeUnit.MILLISECONDS, Schedulers.computation()) .map { item -> MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult @@ -50,7 +50,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return pullToRefreshActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - repository.fetchCharacters().toFlowable() + repository.fetchCharactersSingle().toFlowable() .map { items -> MainResult.PullToRefreshComplete(items) as MainResult } .startWith(MainResult.PullToRefreshing) .onErrorReturn { error -> MainResult.PullToRefreshError(error) } @@ -61,7 +61,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return loadDataActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - repository.fetchCharacters().toFlowable() + repository.fetchCharactersSingle().toFlowable() .map { states -> MainResult.LoadingComplete(states) as MainResult } .startWith(MainResult.Loading) .onErrorReturn(MainResult::LoadingError) diff --git a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt index 4a942ea..2512375 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt @@ -10,8 +10,6 @@ import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.main.MainAction import io.reactivex.Flowable import io.reactivex.Single -import io.reactivex.schedulers.TestScheduler -import junit.framework.Assert.assertNotNull import org.junit.Before import org.junit.Test import java.util.concurrent.TimeUnit @@ -24,8 +22,8 @@ class MainFragmentViewModelTest: BaseUnitTest() { } val mainRepository: MainRepository = mock() { - on { fetchCharacters() } doReturn Single.just(TestDataFactory.mockCharacters) - on { fetchCharacter(1234) } doReturn Single.just(TestDataFactory.marcelCharacter1) + on { fetchCharactersSingle() } doReturn Single.just(TestDataFactory.mockCharacters) + on { fetchCharacterSingle(1234) } doReturn Single.just(TestDataFactory.marcelCharacter1) } private lateinit var viewmodel: MainFragmentViewModel From c8a0228904ed7e95b0135eb6c3e7f134554d42f8 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Mon, 29 Jul 2019 03:20:40 +0530 Subject: [PATCH 02/24] mvi flow --- api/build.gradle | 12 ++-- buildSrc/src/main/java/Dependencies.kt | 9 ++- data/build.gradle | 2 +- .../com/ragdroid/data/MainRepositoryImpl.kt | 7 +- presentation/build.gradle | 5 ++ .../mvi/characters/CharactersFragment.kt | 44 ++++++++---- .../mvi/characters/CharactersViewModel.kt | 71 ++++++++++++++++++- .../ragdroid/mvi/dagger/MainActivityModule.kt | 2 +- .../ragdroid/mvi/dagger/ViewModelFactory.kt | 2 +- .../ragdroid/mvi/dagger/ViewModelModule.kt | 6 ++ .../ragdroid/mvi/helpers/DispatchProvider.kt | 18 +++++ .../com/ragdroid/mvi/main/MainActivity.kt | 4 +- 12 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt diff --git a/api/build.gradle b/api/build.gradle index a80757d..735868b 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -7,19 +7,19 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { implementation Deps.kotlin_jdk implementation Deps.retrofit - compile Deps.retrofitMoshiConverter + api Deps.retrofitMoshiConverter implementation Deps.okhttp - compile Deps.loggingInterceptor - compile project(':mocks') + api Deps.loggingInterceptor + implementation project(':mocks') compile(TestDeps.mockWebServer) { exclude group: 'org.hamcrest', module: 'hamcrest-core' } implementation Deps.retrofitRxJavaAdapter - compile Deps.dagger - compile Deps.daggerAndroid - compile Deps.daggerAndroidSupport + api Deps.dagger + api Deps.daggerAndroid + api Deps.daggerAndroidSupport kapt Deps.daggerCompiler kapt Deps.daggerAndroidProcessor diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index e4462fb..23910b4 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -11,8 +11,8 @@ object Versions { val okhttpVersion = "3.9.1" val constraint_layout = "2.0.0-alpha3" val navigationVersion = "1.0.0-rc01" - val lifecycleEx = "2.0.0" - val lifecycleRx = "2.0.0" + val lifecycleEx = "2.2.0-alpha02" + val lifecycleRx = "2.2.0-alpha02" val lifecycleSavedState = "1.0.0-alpha02" val lifecycleCompiler = "2.0.0" val arch_comp = "1.1.1" @@ -31,6 +31,8 @@ object Versions { val rxBindings = "2.1.1" val appCompat = "1.0.2" val android_ktx = "1.2.0-alpha02" + val coroutine_binding = "1.0.0-RC2" + val flowExtensions = "0.0.2" // test libraries val archTesting = "2.0.1" @@ -54,6 +56,7 @@ object Deps { val lifecycleRx = "androidx.lifecycle:lifecycle-reactivestreams:${Versions.lifecycleRx}" val lifecycleSavedState = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.lifecycleSavedState}" val lifecycleCompiler = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycleRx}" + val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycleEx}" val navigationFragment = "android.arch.navigation:navigation-fragment-ktx:${Versions.navigationVersion}" val navigationUIKtx = "android.arch.navigation:navigation-ui-ktx:${Versions.navigationVersion}" val pagingRuntime = "androidx.paging:paging-runtime:${Versions.arch_comp_paging}" @@ -76,6 +79,8 @@ object Deps { val retrofitRxJavaAdapter = "com.squareup.retrofit2:adapter-rxjava2:${Versions.retrofitVersion}" val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttpVersion}" val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.okhttpVersion}" + val coroutineBindingSwipeRefresh = "com.github.satoshun.coroutinebinding:coroutinebinding-swiperefreshlayout:${Versions.coroutine_binding}" + val flowExtensions = "com.github.akarnokd:kotlin-flow-extensions:${Versions.flowExtensions}" } object TestDeps { diff --git a/data/build.gradle b/data/build.gradle index 69a72e4..af7d995 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -17,7 +17,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { implementation Deps.coroutinesAndroid implementation Deps.kotlin_jdk - implementation project(':api') + api project(':api') implementation Deps.timber implementation Deps.rx diff --git a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt index 54131a1..f6de622 100644 --- a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt +++ b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt @@ -21,7 +21,7 @@ class MainRepositoryImpl @Inject constructor( private val config: AppConfig, private val helpers: Helpers): MainRepository { - override suspend fun fetchCharacters(): Flow> = flow { + override suspend fun fetchCharacters(): List { val timeStamp = System.currentTimeMillis() val charactersWrapper = marvelApi.getCharacters( config.publicKey, @@ -34,8 +34,7 @@ class MainRepositoryImpl @Inject constructor( .map { characterMapper.map(it) }.toList() - emit(characters) - + return characters } override fun fetchCharactersSingle(): Single> { @@ -87,6 +86,6 @@ class MainRepositoryImpl @Inject constructor( interface MainRepository { fun fetchCharactersSingle(): Single> - suspend fun fetchCharacters(): Flow> + suspend fun fetchCharacters(): List fun fetchCharacterSingle(id: Long): Single } diff --git a/presentation/build.gradle b/presentation/build.gradle index 357d846..df50455 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -58,6 +58,7 @@ android { dependencies { api project(':mvvmi') api project(':data') + implementation Deps.kotlin_jdk implementation Deps.coroutinesAndroid implementation Deps.androidKtx @@ -96,6 +97,10 @@ dependencies { implementation Deps.rxBindingRvKt implementation Deps.rxBindingSupportV4 + implementation Deps.coroutineBindingSwipeRefresh + implementation Deps.flowExtensions + implementation Deps.lifecycleViewModelKtx + testImplementation TestDeps.junit testImplementation TestDeps.mockitoKotlin testImplementation TestDeps.architectureCore diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index bf3d3f6..cecdc0c 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration @@ -12,8 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.fueled.reclaim.ItemPresenterProvider import com.fueled.reclaim.ItemsViewAdapter +import com.github.satoshun.coroutinebinding.androidx.swiperefreshlayout.widget.refreshes import com.google.android.material.snackbar.Snackbar -import com.jakewharton.rxbinding2.support.v4.widget.refreshes import com.ragdroid.mvi.R import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment @@ -23,21 +23,26 @@ import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvi.models.CharacterItemPresenter import com.ragdroid.mvi.viewmodel.MainFragmentViewModel -import com.ragdroid.mvvmi.core.MviView import com.ragdroid.mvvmi.core.NavigationState import dagger.android.support.DaggerFragment +import hu.akarnokd.kotlin.flow.PublishSubject +import hu.akarnokd.kotlin.flow.concatWith import io.reactivex.BackpressureStrategy -import io.reactivex.Flowable -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.CoroutineContext /** * A placeholder fragment containing a simple view. */ -class MainFragment : DaggerFragment(), +class CharactersFragment : DaggerFragment(), ItemPresenterProvider, - CharacterItemPresenter { + CharacterItemPresenter, CoroutineScope { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -47,9 +52,10 @@ class MainFragment : DaggerFragment(), private val adapter: ItemsViewAdapter by lazy(LazyThreadSafetyMode.NONE) { ItemsViewAdapter(context) } - private val descriptionClickProcessor: PublishProcessor = PublishProcessor.create() + private val descriptionClickProcessor: PublishSubject = PublishSubject() + override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.IO - lateinit var viewModel: MainFragmentViewModel + lateinit var viewModel: CharactersViewModel override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -68,12 +74,16 @@ class MainFragment : DaggerFragment(), } private fun setupViewModel() { - viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) + viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) + viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) + viewModel.processActions(loadingIntent().concatWith(pullToRefreshIntent().concatWith(loadDescription()))) } override fun onCharacterDescriptionClicked(itemId: Long) { - descriptionClickProcessor.onNext(MainAction.LoadDescription(itemId)) + launch { + descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) + } } override fun getItemPresenter(): CharacterItemPresenter { @@ -81,12 +91,12 @@ class MainFragment : DaggerFragment(), } - private fun pullToRefreshIntent(): Flowable = - binding.refreshLayout.refreshes().toFlowable(BackpressureStrategy.DROP).map { MainAction.PullToRefresh } + private fun pullToRefreshIntent(): Flow = + binding.refreshLayout.refreshes().consumeAsFlow().map { MainAction.PullToRefresh } - private fun loadingIntent(): Flowable = Flowable.just(MainAction.LoadData) + private fun loadingIntent(): Flow = flow { emit(MainAction.LoadData) } - private fun loadDescription(): Flowable { + private fun loadDescription(): Flow { return descriptionClickProcessor } @@ -120,4 +130,8 @@ class MainFragment : DaggerFragment(), } } + override fun onDestroyView() { + super.onDestroyView() + coroutineContext.cancel() + } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index e896539..4ac3d4e 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -2,15 +2,82 @@ package com.ragdroid.mvi.characters import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository +import com.ragdroid.mvi.base.ResourceProvider +import com.ragdroid.mvi.main.MainAction +import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState +import hu.akarnokd.kotlin.flow.concatWith +import hu.akarnokd.kotlin.flow.publish +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import java.lang.Exception import javax.inject.Inject class CharactersViewModel @Inject constructor( - private val mainRepository: MainRepository -) { + private val mainRepository: MainRepository, + private val resourceProvider: ResourceProvider +): ViewModel() { + + var broadcastChannel = ConflatedBroadcastChannel() + var actionsFlow = broadcastChannel.openSubscription().consumeAsFlow() fun stateLiveData(): LiveData = stateLiveData private val stateLiveData = MutableLiveData() + fun processActions(actions: Flow) { + viewModelScope.launch { + actionsFlow.concatWith(actions) + .onEach { + Timber.v("onAction $it") + } + .flatMapMerge { actionToResultFlow(it) } + .onEach { + Timber.v("onResult $it") + } + .scan(MainViewState.init()) { state, result: MainResult -> reduce(state, result) } + .onEach { + Timber.v(it.toString()) + stateLiveData.postValue(it) + } + .onStart { Timber.d("subscribed to states") } + .collect { + stateLiveData.postValue(it) + } + } + } + + private fun reduce(state: MainViewState, result: MainResult): MainViewState { + return state.reduce(result, resourceProvider) + } + + private fun actionToResultFlow(action: MainAction): Flow { + return when(action) { + is MainAction.PullToRefresh -> flow { + emit(MainResult.PullToRefreshing) + try { + val characters = mainRepository.fetchCharacters() + emit(MainResult.PullToRefreshComplete(characters)) + } catch (exception: Exception) { + emit(MainResult.PullToRefreshError(exception)) + } + } + is MainAction.LoadData -> flow { + emit(MainResult.Loading) + try { + val characters = mainRepository.fetchCharacters() + emit(MainResult.LoadingComplete(characters)) + } catch (exception: Exception) { + emit(MainResult.LoadingError(exception)) + } + } + else -> flow { } + } + } + } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt index 98a7e55..fdad335 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/MainActivityModule.kt @@ -17,5 +17,5 @@ abstract class MainActivityModule { @ContributesAndroidInjector @FragmentScope - internal abstract fun charactersFragmeent(): CharactersFragment + internal abstract fun charactersFragment(): CharactersFragment } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt index e979ff4..91f8fa5 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt @@ -13,7 +13,7 @@ import kotlin.reflect.KClass */ @Singleton -class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>): ViewModelProvider.Factory { +class ViewModelFactory @Inject constructor(val viewModels: MutableMap, Provider>): ViewModelProvider.Factory { override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T } diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelModule.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelModule.kt index 4155429..91b6096 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelModule.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelModule.kt @@ -2,6 +2,7 @@ package com.ragdroid.mvi.dagger import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.ragdroid.mvi.characters.CharactersViewModel import com.ragdroid.mvi.viewmodel.MainFragmentViewModel import dagger.Binds import dagger.Module @@ -18,4 +19,9 @@ abstract class ViewModelModule { @ViewModelKey(MainFragmentViewModel::class) internal abstract fun mainViewModel(viewModel: MainFragmentViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(CharactersViewModel::class) + internal abstract fun charactersViewModel(viewModel: CharactersViewModel): ViewModel + } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt new file mode 100644 index 0000000..02cc9bd --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt @@ -0,0 +1,18 @@ +package com.ragdroid.mvi.helpers + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +interface DispatchProvider { + fun main(): CoroutineDispatcher + fun io(): CoroutineDispatcher + fun computation(): CoroutineDispatcher +} + +class DispatchProviderImpl: DispatchProvider { + override fun main(): CoroutineDispatcher = Dispatchers.Main + + override fun io(): CoroutineDispatcher = Dispatchers.IO + + override fun computation(): CoroutineDispatcher = Dispatchers.Default +} \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index f7783b9..8925599 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.lifecycle.ViewModelProviders import com.google.android.material.snackbar.Snackbar import com.ragdroid.mvi.R +import com.ragdroid.mvi.characters.CharactersFragment import com.ragdroid.mvi.databinding.ActivityMainBinding import com.ragdroid.mvi.helpers.BindActivity import com.ragdroid.mvi.viewmodel.MainViewModel @@ -32,7 +33,8 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager val fragmentTransaction = fragmentManager.beginTransaction() - val fraggy = MainFragment() +// val fraggy = MainFragment() + val fraggy = CharactersFragment() fragmentTransaction.replace(R.id.fragment_container, fraggy) fragmentTransaction.commit() } From 51d5a2945206508323b192d6d92acd649d83d76c Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Tue, 30 Jul 2019 00:24:42 +0530 Subject: [PATCH 03/24] flow working --- build.gradle | 3 +- buildSrc/src/main/java/Dependencies.kt | 2 +- .../mvi/characters/CharactersFragment.kt | 4 ++- .../mvi/characters/CharactersViewModel.kt | 3 +- .../ragdroid/mvi/dagger/ViewModelFactory.kt | 10 ++---- .../com/ragdroid/mvi/dagger/ViewModelKey.java | 22 +++++++++++++ .../java/com/ragdroid/mvi/helpers/FlowEx.kt | 29 +++++++++++++++++ .../com/ragdroid/mvi/main/MainActivity.kt | 5 ++- .../com/ragdroid/mvi/main/MainFragment.kt | 1 + .../src/main/res/layout/activity_main.xml | 17 ++-------- .../src/main/res/layout/fragment_main.xml | 31 +++++++++++++++---- presentation/src/main/res/values/strings.xml | 2 ++ 12 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelKey.java create mode 100644 presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt diff --git a/build.gradle b/build.gradle index 6a96e91..8f43303 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.21' repositories { mavenCentral() google() @@ -11,7 +10,7 @@ buildscript { classpath 'com.android.tools.build:gradle:3.4.0-rc01' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_version}" classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_version}" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 23910b4..5f22e9c 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -17,7 +17,7 @@ object Versions { val lifecycleCompiler = "2.0.0" val arch_comp = "1.1.1" val arch_comp_paging= "2.1.0" - val dagger = "2.20" + val dagger = "2.24" val ktlint = "0.10.0" val leakcanary = "1.6.2" val rx = "2.1.6" diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index cecdc0c..bc66838 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -17,6 +17,7 @@ import com.google.android.material.snackbar.Snackbar import com.ragdroid.mvi.R import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment +import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.items.CharacterItem import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation @@ -64,6 +65,7 @@ class CharactersFragment : DaggerFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.toolbar.setTitle(R.string.title_flow) val manager = LinearLayoutManager(context) val decoration = DividerItemDecoration(context, RecyclerView.VERTICAL) manager.orientation = RecyclerView.VERTICAL @@ -76,7 +78,7 @@ class CharactersFragment : DaggerFragment(), private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) - viewModel.processActions(loadingIntent().concatWith(pullToRefreshIntent().concatWith(loadDescription()))) + viewModel.processActions(loadingIntent().merge(pullToRefreshIntent(), loadDescription())) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 4ac3d4e..f2cbac5 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository import com.ragdroid.mvi.base.ResourceProvider +import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState @@ -32,7 +33,7 @@ class CharactersViewModel @Inject constructor( fun processActions(actions: Flow) { viewModelScope.launch { - actionsFlow.concatWith(actions) + actionsFlow.merge(actions) .onEach { Timber.v("onAction $it") } diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt index 91f8fa5..6fba1ff 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelFactory.kt @@ -12,12 +12,6 @@ import kotlin.reflect.KClass * Thanks to https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455 */ -@Singleton -class ViewModelFactory @Inject constructor(val viewModels: MutableMap, Provider>): ViewModelProvider.Factory { +class ViewModelFactory @Inject constructor(private val viewModels: Map, @JvmSuppressWildcards Provider>): ViewModelProvider.Factory { override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T -} - -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) -@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) -@MapKey -internal annotation class ViewModelKey(val value: KClass) \ No newline at end of file +} \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelKey.java b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelKey.java new file mode 100644 index 0000000..b86528a --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/ViewModelKey.java @@ -0,0 +1,22 @@ +package com.ragdroid.mvi.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import androidx.lifecycle.ViewModel; +import dagger.MapKey; + +/** + * Workaround in Java due to Dagger/Kotlin not playing well together as of now + * https://github.com/google/dagger/issues/1478 + */ +@MapKey +@Documented +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ViewModelKey { + Class value(); +} diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt new file mode 100644 index 0000000..5db9364 --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt @@ -0,0 +1,29 @@ +package com.ragdroid.mvi.helpers + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +fun Flow.mergeWith(other: Flow): Flow = channelFlow { + // collect from one coroutine and send it + launch { + collect { send(it) } + } + // collect and send from this coroutine, too, concurrently + other.collect { send(it) } +} + +fun Flow.merge(vararg other: Flow): Flow = channelFlow { + // collect from one coroutine and send it + launch { + collect { send(it) } + } + // collect and send from this coroutine, too, concurrently + other.forEach { + it.collect { + send(it) + } + } +} + diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index 8925599..d53138e 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -19,7 +19,6 @@ class MainActivity : DaggerAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setSupportActionBar(binding.toolbar) viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) openFragment() @@ -33,8 +32,8 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager val fragmentTransaction = fragmentManager.beginTransaction() -// val fraggy = MainFragment() - val fraggy = CharactersFragment() + val fraggy = MainFragment() +// val fraggy = CharactersFragment() fragmentTransaction.replace(R.id.fragment_container, fraggy) fragmentTransaction.commit() } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 6c3dce5..3d72c55 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -56,6 +56,7 @@ class MainFragment : DaggerFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.toolbar.setTitle(R.string.title_rx) val manager = LinearLayoutManager(context) val decoration = DividerItemDecoration(context, RecyclerView.VERTICAL) manager.orientation = RecyclerView.VERTICAL diff --git a/presentation/src/main/res/layout/activity_main.xml b/presentation/src/main/res/layout/activity_main.xml index dec425a..6d580a8 100644 --- a/presentation/src/main/res/layout/activity_main.xml +++ b/presentation/src/main/res/layout/activity_main.xml @@ -8,21 +8,10 @@ android:layout_height="match_parent" tools:context="com.ragdroid.mvi.main.MainActivity"> - - - - - - - + android:layout_height="match_parent" + layout="@layout/content_main"/> + xmlns:tools="http://schemas.android.com/tools" + xmlns:bind="http://schemas.android.com/apk/res-auto" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + android:layout_height="wrap_content"/> ]]> Description + Flow Characters + Rx Characters From 72a2f087e22374ace1a2c92d237154b24e6321c9 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Wed, 31 Jul 2019 01:17:37 +0530 Subject: [PATCH 04/24] description api call --- .../com/ragdroid/data/MainRepositoryImpl.kt | 16 +++++ .../mvi/characters/CharactersFragment.kt | 9 ++- .../mvi/characters/CharactersViewModel.kt | 67 +++++++++++++------ .../com/ragdroid/mvi/main/MainActivity.kt | 4 +- .../com/ragdroid/mvi/main/MainFragment.kt | 6 +- 5 files changed, 68 insertions(+), 34 deletions(-) diff --git a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt index f6de622..4390195 100644 --- a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt +++ b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt @@ -81,11 +81,27 @@ class MainRepositoryImpl @Inject constructor( ) } + override suspend fun fetchCharacter(id: Long): CharacterMarvel { + val timeStamp = System.currentTimeMillis() + val characterWrapper = marvelApi.getCharacter( + id, + config.publicKey, + helpers.buildMD5Digest("" + timeStamp + config.privateKey + + config.publicKey), timeStamp) + + return characterWrapper.data.results + .map { + characterMapper.map(it) + }.getOrNull(0) ?: throw IllegalAccessException("Character for id $id not found") + } + + } interface MainRepository { fun fetchCharactersSingle(): Single> suspend fun fetchCharacters(): List + suspend fun fetchCharacter(id: Long): CharacterMarvel fun fetchCharacterSingle(id: Long): Single } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index bc66838..c359a23 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -54,7 +54,7 @@ class CharactersFragment : DaggerFragment(), ItemsViewAdapter(context) } private val descriptionClickProcessor: PublishSubject = PublishSubject() - override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.IO + override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main lateinit var viewModel: CharactersViewModel @@ -78,14 +78,13 @@ class CharactersFragment : DaggerFragment(), private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) + viewModel.navigationLiveData().observe(viewLifecycleOwner, Observer { navigate(it) }) viewModel.processActions(loadingIntent().merge(pullToRefreshIntent(), loadDescription())) } override fun onCharacterDescriptionClicked(itemId: Long) { - launch { - descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) - } + viewModel.onAction(MainAction.LoadDescription(itemId)) } override fun getItemPresenter(): CharacterItemPresenter { @@ -126,7 +125,7 @@ class CharactersFragment : DaggerFragment(), fun navigate(navigationState: NavigationState) { when (navigationState) { - is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT) + is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT).show() else -> {//do nothing } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index f2cbac5..9a230dd 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -5,11 +5,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository +import com.ragdroid.data.entity.CharacterMarvel import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.main.MainAction +import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState +import com.ragdroid.mvvmi.core.NavigationState import hu.akarnokd.kotlin.flow.concatWith import hu.akarnokd.kotlin.flow.publish import kotlinx.coroutines.channels.ConflatedBroadcastChannel @@ -25,12 +28,19 @@ class CharactersViewModel @Inject constructor( private val resourceProvider: ResourceProvider ): ViewModel() { + fun onAction(action: MainAction) = broadcastChannel.offer(action) + var broadcastChannel = ConflatedBroadcastChannel() var actionsFlow = broadcastChannel.openSubscription().consumeAsFlow() fun stateLiveData(): LiveData = stateLiveData private val stateLiveData = MutableLiveData() + fun navigationLiveData(): LiveData = navigationLiveData + private val navigationLiveData = MutableLiveData() + + fun navigate(navigationState: MainNavigation) = navigationLiveData.postValue(navigationState) + fun processActions(actions: Flow) { viewModelScope.launch { actionsFlow.merge(actions) @@ -43,11 +53,12 @@ class CharactersViewModel @Inject constructor( } .scan(MainViewState.init()) { state, result: MainResult -> reduce(state, result) } .onEach { - Timber.v(it.toString()) + Timber.v("onState $it") stateLiveData.postValue(it) } .onStart { Timber.d("subscribed to states") } .collect { + Timber.v("onState $it") stateLiveData.postValue(it) } } @@ -58,27 +69,39 @@ class CharactersViewModel @Inject constructor( } private fun actionToResultFlow(action: MainAction): Flow { - return when(action) { - is MainAction.PullToRefresh -> flow { - emit(MainResult.PullToRefreshing) - try { - val characters = mainRepository.fetchCharacters() - emit(MainResult.PullToRefreshComplete(characters)) - } catch (exception: Exception) { - emit(MainResult.PullToRefreshError(exception)) - } - } - is MainAction.LoadData -> flow { - emit(MainResult.Loading) - try { - val characters = mainRepository.fetchCharacters() - emit(MainResult.LoadingComplete(characters)) - } catch (exception: Exception) { - emit(MainResult.LoadingError(exception)) - } - } - else -> flow { } - } + return when(action) { + is MainAction.PullToRefresh -> flow { + emit(MainResult.PullToRefreshing) + try { + val characters = mainRepository.fetchCharacters() + emit(MainResult.PullToRefreshComplete(characters)) + } catch (exception: Exception) { + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.PullToRefreshError(exception)) + } + } + is MainAction.LoadData -> flow { + emit(MainResult.Loading) + try { + val characters = mainRepository.fetchCharacters() + emit(MainResult.LoadingComplete(characters)) + } catch (exception: Exception) { + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError(exception)) + } + } + is MainAction.LoadDescription -> flow { + emit(mainRepository.fetchCharacter(action.characterId)) } + .delayEach(2000) + .map { item -> + MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult + } + .onStart { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) } + .catch { + Timber.e(it) + MainResult.DescriptionResult.DescriptionError(action.characterId, it) + } + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index d53138e..c15101c 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -32,8 +32,8 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager val fragmentTransaction = fragmentManager.beginTransaction() - val fraggy = MainFragment() -// val fraggy = CharactersFragment() +// val fraggy = MainFragment() + val fraggy = CharactersFragment() fragmentTransaction.replace(R.id.fragment_container, fraggy) fragmentTransaction.commit() } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 3d72c55..5ab10e9 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -71,7 +71,6 @@ class MainFragment : DaggerFragment(), private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) - viewModel.stateLiveData().observe(lifecycleOwner, Observer { render(it) }) } @@ -97,11 +96,8 @@ class MainFragment : DaggerFragment(), Timber.d("got state $state") binding.model = state when { - state.pullToRefreshError != null -> return - state.loadingError != null -> { adapter.clearAllRecyclerItems() - return } else -> { @@ -120,7 +116,7 @@ class MainFragment : DaggerFragment(), override fun navigate(navigationState: NavigationState) { when (navigationState) { - is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT) + is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT).show() else -> {//do nothing } } From 9c3359fbfd2fca77a739a1a8378cd49b87759480 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Wed, 31 Jul 2019 01:34:00 +0530 Subject: [PATCH 05/24] finishing touches --- .../mvi/characters/CharactersViewModel.kt | 5 +++- .../com/ragdroid/mvi/main/MainFragment.kt | 26 ++++++++----------- .../com/ragdroid/mvi/main/MainViewState.kt | 4 ++- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 9a230dd..4b22199 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -76,6 +76,7 @@ class CharactersViewModel @Inject constructor( val characters = mainRepository.fetchCharacters() emit(MainResult.PullToRefreshComplete(characters)) } catch (exception: Exception) { + Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) emit(MainResult.PullToRefreshError(exception)) } @@ -86,6 +87,7 @@ class CharactersViewModel @Inject constructor( val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) } catch (exception: Exception) { + Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) emit(MainResult.LoadingError(exception)) } @@ -98,8 +100,9 @@ class CharactersViewModel @Inject constructor( } .onStart { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) } .catch { + navigate(MainNavigation.Snackbar(it.message ?: "Unknown Error")) Timber.e(it) - MainResult.DescriptionResult.DescriptionError(action.characterId, it) + emit(MainResult.DescriptionResult.DescriptionError(action.characterId, it)) } } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 5ab10e9..927fd41 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -84,7 +84,7 @@ class MainFragment : DaggerFragment(), private fun pullToRefreshIntent(): Flowable = - binding.refreshLayout.refreshes().toFlowable(BackpressureStrategy.DROP).map { MainAction.PullToRefresh } + binding.refreshLayout.refreshes().toFlowable(BackpressureStrategy.DROP).map { MainAction.PullToRefresh } private fun loadingIntent(): Flowable = Flowable.just(MainAction.LoadData) @@ -95,30 +95,26 @@ class MainFragment : DaggerFragment(), override fun render(state: MainViewState) { Timber.d("got state $state") binding.model = state - when { - state.loadingError != null -> { - adapter.clearAllRecyclerItems() - } - - else -> { - val characterModelList = - state.characters.map { - CharacterItem(it, this) - } - adapter.replaceItems(characterModelList, true) - } + if (state.loadingError != null) { + adapter.clearAllRecyclerItems() } + + val characterModelList = + state.characters.map { + CharacterItem(it, this) + } + adapter.replaceItems(characterModelList, true) } override val lifecycleOwner: LifecycleOwner - get() = this + get() = this override fun navigate(navigationState: NavigationState) { when (navigationState) { is MainNavigation.Snackbar -> Snackbar.make(binding.root, navigationState.message, Snackbar.LENGTH_SHORT).show() else -> {//do nothing - } + } } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt index c4c5e93..55b176e 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt @@ -55,8 +55,10 @@ data class MainViewState( pullToRefreshError = null) is MainResult.PullToRefreshError -> copy( pullToRefreshing = false, + loadingError = null, pullToRefreshError = result.throwable) is MainResult.PullToRefreshComplete -> copy( + loadingError = null, pullToRefreshing = false, pullToRefreshError = null, characters = reduceCharactersList(characters, result.characters, resources)) @@ -70,7 +72,7 @@ data class MainViewState( val newCharactersList = characters.slice(0 until previousItemStateIndex) .plus(listOf(newItemState)) .plus(characters.slice(previousItemStateIndex + 1 until characters.size)) - copy(characters = newCharactersList) + copy(characters = newCharactersList, loadingError = null, pullToRefreshError = null) } } From 2716d6957697e0ecad61eecdc79cc7213d38b9ab Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Wed, 31 Jul 2019 02:32:01 +0530 Subject: [PATCH 06/24] added viewmodel test --- buildSrc/src/main/java/Dependencies.kt | 5 +- presentation/build.gradle | 1 + .../mvi/characters/CharactersViewModel.kt | 10 +++- .../java/com/ragdroid/mvi/dagger/AppModule.kt | 5 ++ .../ragdroid/mvi/helpers/DispatchProvider.kt | 3 +- .../mvi/characters/CharactersViewModelTest.kt | 59 +++++++++++++++++++ .../ragdroid/mvi/viewmodel/BaseUnitTest.kt | 23 ++++++++ .../viewmodel/MainFragmentViewModelTest.kt | 7 +-- 8 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 5f22e9c..3387321 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -38,7 +38,7 @@ object Versions { val archTesting = "2.0.1" val kluent = "1.4" val spek = "1.1.5" - val mockitoKotlin = "1.5.0" + val mockitoKotlin = "2.1.0" val mockito = "2.8.9" val testRunner = "1.1.1" val espresso = "3.1.0" @@ -96,6 +96,7 @@ object TestDeps { val kluent = "org.amshove.kluent:kluent:${Versions.kluent}" val spek = "org.jetbrains.spek:spek-api:${Versions.spek}" val spekJunitPlatformEngine = "org.jetbrains.spek:spek-junit-platform-engine:${Versions.spek}" - val mockitoKotlin = "com.nhaarman:mockito-kotlin-kt1.1:${Versions.mockitoKotlin}" + val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.mockitoKotlin}" val mockito = "org.mockito:mockito-core:${Versions.mockito}" + val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinx_coroutines}" } \ No newline at end of file diff --git a/presentation/build.gradle b/presentation/build.gradle index df50455..437b0a4 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -114,6 +114,7 @@ dependencies { debugImplementation TestDeps.leakCanary releaseImplementation TestDeps.leakCanaryNoOp + testImplementation TestDeps.coroutinesTest implementation(TestDeps.crashlytics) { transitive = true diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 4b22199..8c756b1 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository import com.ragdroid.data.entity.CharacterMarvel import com.ragdroid.mvi.base.ResourceProvider +import com.ragdroid.mvi.helpers.DispatchProvider import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation @@ -15,6 +16,8 @@ import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvvmi.core.NavigationState import hu.akarnokd.kotlin.flow.concatWith import hu.akarnokd.kotlin.flow.publish +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* @@ -22,11 +25,14 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.lang.Exception import javax.inject.Inject +import kotlin.coroutines.CoroutineContext class CharactersViewModel @Inject constructor( private val mainRepository: MainRepository, - private val resourceProvider: ResourceProvider -): ViewModel() { + private val resourceProvider: ResourceProvider, + dispatchProvider: DispatchProvider +): ViewModel(), CoroutineScope { + override val coroutineContext: CoroutineContext = SupervisorJob() + dispatchProvider.io() fun onAction(action: MainAction) = broadcastChannel.offer(action) diff --git a/presentation/src/main/java/com/ragdroid/mvi/dagger/AppModule.kt b/presentation/src/main/java/com/ragdroid/mvi/dagger/AppModule.kt index e78da36..7d7ecc4 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/dagger/AppModule.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/dagger/AppModule.kt @@ -9,6 +9,8 @@ import com.ragdroid.mvi.MarvelApplication import com.ragdroid.mvi.base.AppResourceProvider import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.AndroidHelpers +import com.ragdroid.mvi.helpers.DispatchProvider +import com.ragdroid.mvi.helpers.DispatchProviderImpl import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -37,4 +39,7 @@ class AppModule { fun provideResourceProvider(application: MarvelApplication): ResourceProvider = AppResourceProvider(application.applicationContext) + @Provides + fun provideDispatchers(dispatchProviderImpl: DispatchProviderImpl): DispatchProvider = dispatchProviderImpl + } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt index 02cc9bd..fb14504 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/DispatchProvider.kt @@ -2,6 +2,7 @@ package com.ragdroid.mvi.helpers import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import javax.inject.Inject interface DispatchProvider { fun main(): CoroutineDispatcher @@ -9,7 +10,7 @@ interface DispatchProvider { fun computation(): CoroutineDispatcher } -class DispatchProviderImpl: DispatchProvider { +class DispatchProviderImpl @Inject constructor(): DispatchProvider { override fun main(): CoroutineDispatcher = Dispatchers.Main override fun io(): CoroutineDispatcher = Dispatchers.IO diff --git a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt new file mode 100644 index 0000000..a15b56b --- /dev/null +++ b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt @@ -0,0 +1,59 @@ +package com.ragdroid.mvi.characters + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.ragdroid.data.MainRepository +import com.ragdroid.mvi.TestDataFactory +import com.ragdroid.mvi.base.ResourceProvider +import com.ragdroid.mvi.main.MainAction +import com.ragdroid.mvi.viewmodel.BaseUnitTest +import com.ragdroid.mvi.viewmodel.blockingObserve +import io.reactivex.Single +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Test +import java.util.concurrent.TimeUnit + +class CharactersViewModelTest: BaseUnitTest() { + + val resourceProvider: ResourceProvider = mock() { + on { getString(any()) } doReturn "Description" + } + + val mainRepository: MainRepository = mock() { + onBlocking { fetchCharacters() } doReturn TestDataFactory.mockCharacters + onBlocking { fetchCharacter(1234) } doReturn TestDataFactory.marcelCharacter1 + } + + + + private lateinit var viewmodel: CharactersViewModel + + + @Before + fun setUp() { + super.setup() + viewmodel = CharactersViewModel(mainRepository, resourceProvider, testDispatcherProvider) + } + + @Test + fun testLoadData() = runBlocking { + + val loadDataFlow = flow { emit(MainAction.LoadData) } + + viewmodel.processActions(loadDataFlow) + + val state = viewmodel.stateLiveData().blockingObserve() + + println(state) + + assertTrue(state!!.characters.isNotEmpty()) + + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/BaseUnitTest.kt b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/BaseUnitTest.kt index cda0387..3a7efd5 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/BaseUnitTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/BaseUnitTest.kt @@ -1,11 +1,17 @@ package com.ragdroid.mvi.viewmodel import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.ragdroid.mvi.helpers.DispatchProvider import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.plugins.RxJavaPlugins import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.TestScheduler +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.setMain import org.junit.AfterClass +import org.junit.Before import org.junit.BeforeClass import org.junit.Rule @@ -16,6 +22,23 @@ open class BaseUnitTest { var instantExecutorRule = InstantTaskExecutorRule() + var testDispatcher = TestCoroutineDispatcher() + + var testDispatcherProvider = object: DispatchProvider { + + override fun main(): CoroutineDispatcher = testDispatcher + + override fun io(): CoroutineDispatcher = testDispatcher + + override fun computation(): CoroutineDispatcher = testDispatcher + + } + + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + companion object { @JvmStatic val testScheduler = TestScheduler() diff --git a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt index 2512375..cb13190 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt @@ -1,10 +1,9 @@ package com.ragdroid.mvi.viewmodel -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock import com.ragdroid.data.MainRepository -import com.ragdroid.data.base.SchedulerProvider import com.ragdroid.mvi.TestDataFactory import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.main.MainAction From bbb97c21de06c5fb300efc743b58b34361afdce5 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sat, 3 Aug 2019 16:57:18 +0530 Subject: [PATCH 07/24] fix publish subject --- .../ragdroid/mvi/characters/CharactersFragment.kt | 4 +++- .../ragdroid/mvi/characters/CharactersViewModel.kt | 5 ++++- .../main/java/com/ragdroid/mvi/helpers/FlowEx.kt | 13 ++++++++++--- .../ragdroid/mvi/viewmodel/MainFragmentViewModel.kt | 11 +++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index c359a23..08cda1a 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -84,7 +84,9 @@ class CharactersFragment : DaggerFragment(), override fun onCharacterDescriptionClicked(itemId: Long) { - viewModel.onAction(MainAction.LoadDescription(itemId)) + launch { + descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) + } } override fun getItemPresenter(): CharacterItemPresenter { diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 8c756b1..00725de 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -9,6 +9,7 @@ import com.ragdroid.data.entity.CharacterMarvel import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.DispatchProvider import com.ragdroid.mvi.helpers.merge +import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult @@ -17,6 +18,7 @@ import com.ragdroid.mvvmi.core.NavigationState import hu.akarnokd.kotlin.flow.concatWith import hu.akarnokd.kotlin.flow.publish import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ReceiveChannel @@ -27,6 +29,7 @@ import java.lang.Exception import javax.inject.Inject import kotlin.coroutines.CoroutineContext +@ExperimentalCoroutinesApi class CharactersViewModel @Inject constructor( private val mainRepository: MainRepository, private val resourceProvider: ResourceProvider, @@ -49,7 +52,7 @@ class CharactersViewModel @Inject constructor( fun processActions(actions: Flow) { viewModelScope.launch { - actionsFlow.merge(actions) + actionsFlow.mergeWith(actions) .onEach { Timber.v("onAction $it") } diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt index 5db9364..24a9cc0 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt @@ -14,16 +14,23 @@ fun Flow.mergeWith(other: Flow): Flow = channelFlow { other.collect { send(it) } } -fun Flow.merge(vararg other: Flow): Flow = channelFlow { +fun Flow.merge(other: Flow, + other2: Flow): Flow = channelFlow { // collect from one coroutine and send it launch { collect { send(it) } } // collect and send from this coroutine, too, concurrently - other.forEach { - it.collect { + launch { + other.collect { + send(it) + } + } + launch { + other2.collect { send(it) } } } + diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 05839d6..7100197 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class MainFragmentViewModel @Inject constructor(private val resourceProvider: ResourceProvider, - private val repository: MainRepository): + private val mainRepository: MainRepository): MviViewModel(MainViewState.init()) { override fun actionsToResultTransformer(actions: Flowable): Flowable = @@ -33,7 +33,8 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return loadDescriptionActionStream .observeOn(Schedulers.io()) .flatMap { action -> - repository.fetchCharacterSingle(action.characterId).toFlowable() + + mainRepository.fetchCharacterSingle(action.characterId).toFlowable() .delay(2000, TimeUnit.MILLISECONDS, Schedulers.computation()) .map { item -> MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult @@ -50,7 +51,9 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return pullToRefreshActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - repository.fetchCharactersSingle().toFlowable() + + + mainRepository.fetchCharactersSingle().toFlowable() .map { items -> MainResult.PullToRefreshComplete(items) as MainResult } .startWith(MainResult.PullToRefreshing) .onErrorReturn { error -> MainResult.PullToRefreshError(error) } @@ -61,7 +64,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return loadDataActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - repository.fetchCharactersSingle().toFlowable() + mainRepository.fetchCharactersSingle().toFlowable() .map { states -> MainResult.LoadingComplete(states) as MainResult } .startWith(MainResult.Loading) .onErrorReturn(MainResult::LoadingError) From 6826d43fbcd443088ce1702a7c8ea6ee7f91ba9c Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 13:49:53 +0530 Subject: [PATCH 08/24] test dispatcher changes --- .../mvi/characters/CharactersViewModel.kt | 47 +++++++------------ .../mvi/characters/CharactersViewModelTest.kt | 9 ++-- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 00725de..9d404d6 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -5,37 +5,24 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository -import com.ragdroid.data.entity.CharacterMarvel import com.ragdroid.mvi.base.ResourceProvider -import com.ragdroid.mvi.helpers.DispatchProvider -import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState -import com.ragdroid.mvvmi.core.NavigationState -import hu.akarnokd.kotlin.flow.concatWith -import hu.akarnokd.kotlin.flow.publish -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ConflatedBroadcastChannel -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.lang.Exception import javax.inject.Inject -import kotlin.coroutines.CoroutineContext @ExperimentalCoroutinesApi class CharactersViewModel @Inject constructor( private val mainRepository: MainRepository, - private val resourceProvider: ResourceProvider, - dispatchProvider: DispatchProvider -): ViewModel(), CoroutineScope { - override val coroutineContext: CoroutineContext = SupervisorJob() + dispatchProvider.io() + private val resourceProvider: ResourceProvider +): ViewModel() { fun onAction(action: MainAction) = broadcastChannel.offer(action) @@ -81,25 +68,23 @@ class CharactersViewModel @Inject constructor( return when(action) { is MainAction.PullToRefresh -> flow { emit(MainResult.PullToRefreshing) - try { - val characters = mainRepository.fetchCharacters() - emit(MainResult.PullToRefreshComplete(characters)) - } catch (exception: Exception) { - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.PullToRefreshError(exception)) - } + val characters = mainRepository.fetchCharacters() + emit(MainResult.PullToRefreshComplete(characters)) + }.catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.PullToRefreshError(exception)) } is MainAction.LoadData -> flow { emit(MainResult.Loading) - try { - val characters = mainRepository.fetchCharacters() - emit(MainResult.LoadingComplete(characters)) - } catch (exception: Exception) { - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError(exception)) - } + val characters = mainRepository.fetchCharacters() + emit(MainResult.LoadingComplete(characters)) + } +// .delayEach(1000) + .catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError(exception)) } is MainAction.LoadDescription -> flow { emit(mainRepository.fetchCharacter(action.characterId)) } diff --git a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt index a15b56b..0a6abc6 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt @@ -9,16 +9,11 @@ import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.viewmodel.BaseUnitTest import com.ragdroid.mvi.viewmodel.blockingObserve -import io.reactivex.Single import junit.framework.Assert.assertTrue -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.setMain import org.junit.Before import org.junit.Test -import java.util.concurrent.TimeUnit class CharactersViewModelTest: BaseUnitTest() { @@ -39,7 +34,7 @@ class CharactersViewModelTest: BaseUnitTest() { @Before fun setUp() { super.setup() - viewmodel = CharactersViewModel(mainRepository, resourceProvider, testDispatcherProvider) + viewmodel = CharactersViewModel(mainRepository, resourceProvider) } @Test @@ -49,6 +44,8 @@ class CharactersViewModelTest: BaseUnitTest() { viewmodel.processActions(loadDataFlow) +// testDispatcher.advanceTimeBy(2000) + val state = viewmodel.stateLiveData().blockingObserve() println(state) From c36f1ab7f24297869f473faa564aa357149bcd60 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 14:07:56 +0530 Subject: [PATCH 09/24] rx coroutines interop --- buildSrc/src/main/java/Dependencies.kt | 1 + data/build.gradle | 1 + presentation/build.gradle | 1 + .../com/ragdroid/mvi/characters/CharactersViewModel.kt | 3 ++- .../com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt | 7 +++++-- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 3387321..e7b8ce1 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -48,6 +48,7 @@ object Versions { object Deps { val kotlin_jdk = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin_version}" val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinx_coroutines}" + val coroutinesRxInterop = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:${Versions.kotlinx_coroutines}" val androidKtx = "androidx.core:core-ktx:${Versions.android_ktx}" val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraint_layout}" val timber = "com.jakewharton.timber:timber:${Versions.timberVersion}" diff --git a/data/build.gradle b/data/build.gradle index af7d995..d11b79c 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -16,6 +16,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { implementation Deps.coroutinesAndroid + implementation Deps.coroutinesRxInterop implementation Deps.kotlin_jdk api project(':api') diff --git a/presentation/build.gradle b/presentation/build.gradle index 437b0a4..b2a1e28 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -61,6 +61,7 @@ dependencies { implementation Deps.kotlin_jdk implementation Deps.coroutinesAndroid + implementation Deps.coroutinesRxInterop implementation Deps.androidKtx implementation Deps.constraintLayout diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 9d404d6..4a930aa 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.await import timber.log.Timber import javax.inject.Inject @@ -68,7 +69,7 @@ class CharactersViewModel @Inject constructor( return when(action) { is MainAction.PullToRefresh -> flow { emit(MainResult.PullToRefreshing) - val characters = mainRepository.fetchCharacters() + val characters = mainRepository.fetchCharactersSingle().await() emit(MainResult.PullToRefreshComplete(characters)) }.catch { exception -> Timber.e(exception) diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 7100197..5256655 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -8,6 +8,8 @@ import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvvmi.core.MviViewModel import io.reactivex.Flowable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.rx2.rxFlowable +import kotlinx.coroutines.rx2.rxSingle import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -52,8 +54,9 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re .observeOn(Schedulers.io()) .flatMap { ignored -> - - mainRepository.fetchCharactersSingle().toFlowable() + rxSingle { + mainRepository.fetchCharacters() + }.toFlowable() .map { items -> MainResult.PullToRefreshComplete(items) as MainResult } .startWith(MainResult.PullToRefreshing) .onErrorReturn { error -> MainResult.PullToRefreshError(error) } From 1ff085564828d6db8eb4df5488f7049766f015f5 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 14:27:34 +0530 Subject: [PATCH 10/24] clean up --- .../mvi/characters/CharactersFragment.kt | 11 ++++----- .../mvi/characters/CharactersViewModel.kt | 21 +++++++++++----- .../mvi/characters/CharactersViewModelTest.kt | 24 ++++++++++++++++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 08cda1a..04f260d 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -23,12 +23,9 @@ import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvi.models.CharacterItemPresenter -import com.ragdroid.mvi.viewmodel.MainFragmentViewModel import com.ragdroid.mvvmi.core.NavigationState import dagger.android.support.DaggerFragment import hu.akarnokd.kotlin.flow.PublishSubject -import hu.akarnokd.kotlin.flow.concatWith -import io.reactivex.BackpressureStrategy import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.consumeAsFlow @@ -72,14 +69,16 @@ class CharactersFragment : DaggerFragment(), binding.listView.layoutManager = manager binding.listView.adapter = adapter binding.listView.addItemDecoration(decoration) - setupViewModel() + setupViewModel(savedInstanceState) } - private fun setupViewModel() { + private fun setupViewModel(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) viewModel.navigationLiveData().observe(viewLifecycleOwner, Observer { navigate(it) }) - viewModel.processActions(loadingIntent().merge(pullToRefreshIntent(), loadDescription())) + if (savedInstanceState == null) { + viewModel.processActions(loadingIntent().merge(pullToRefreshIntent(), loadDescription())) + } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 4a930aa..d618f35 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -6,24 +6,28 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository import com.ragdroid.mvi.base.ResourceProvider +import com.ragdroid.mvi.helpers.DispatchProvider import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import kotlinx.coroutines.rx2.await import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.CoroutineContext @ExperimentalCoroutinesApi class CharactersViewModel @Inject constructor( private val mainRepository: MainRepository, - private val resourceProvider: ResourceProvider -): ViewModel() { + private val resourceProvider: ResourceProvider, + private val dispatchProvider: DispatchProvider +): ViewModel(), CoroutineScope { + override val coroutineContext: CoroutineContext + get() = SupervisorJob().plus(dispatchProvider.main()).plus(dispatchProvider.io()) fun onAction(action: MainAction) = broadcastChannel.offer(action) @@ -39,7 +43,7 @@ class CharactersViewModel @Inject constructor( fun navigate(navigationState: MainNavigation) = navigationLiveData.postValue(navigationState) fun processActions(actions: Flow) { - viewModelScope.launch { + launch { actionsFlow.mergeWith(actions) .onEach { Timber.v("onAction $it") @@ -71,7 +75,9 @@ class CharactersViewModel @Inject constructor( emit(MainResult.PullToRefreshing) val characters = mainRepository.fetchCharactersSingle().await() emit(MainResult.PullToRefreshComplete(characters)) - }.catch { exception -> + } + .flowOn(dispatchProvider.io()) + .catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) emit(MainResult.PullToRefreshError(exception)) @@ -102,4 +108,7 @@ class CharactersViewModel @Inject constructor( } } + override fun onCleared() { + coroutineContext.cancelChildren() + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt index 0a6abc6..d3255b8 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt @@ -9,8 +9,10 @@ import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.viewmodel.BaseUnitTest import com.ragdroid.mvi.viewmodel.blockingObserve +import io.reactivex.Single import junit.framework.Assert.assertTrue import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test @@ -23,6 +25,7 @@ class CharactersViewModelTest: BaseUnitTest() { val mainRepository: MainRepository = mock() { onBlocking { fetchCharacters() } doReturn TestDataFactory.mockCharacters + onBlocking { fetchCharactersSingle() } doReturn Single.just(TestDataFactory.mockCharacters) onBlocking { fetchCharacter(1234) } doReturn TestDataFactory.marcelCharacter1 } @@ -34,13 +37,13 @@ class CharactersViewModelTest: BaseUnitTest() { @Before fun setUp() { super.setup() - viewmodel = CharactersViewModel(mainRepository, resourceProvider) + viewmodel = CharactersViewModel(mainRepository, resourceProvider, testDispatcherProvider) } @Test - fun testLoadData() = runBlocking { + fun testLoadData() { - val loadDataFlow = flow { emit(MainAction.LoadData) } + val loadDataFlow = flowOf(MainAction.LoadData) viewmodel.processActions(loadDataFlow) @@ -53,4 +56,19 @@ class CharactersViewModelTest: BaseUnitTest() { assertTrue(state!!.characters.isNotEmpty()) } + + @Test + fun testPullToRefresh() { + + val loadDataFlow = flowOf(MainAction.PullToRefresh) + + viewmodel.processActions(loadDataFlow) + + val state = viewmodel.stateLiveData().blockingObserve() + + println(state) + + assertTrue(state!!.characters.isNotEmpty()) + + } } \ No newline at end of file From 18dbb3e14bb28acec154aaef0b6fb4ed5ad149b1 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 15:47:08 +0530 Subject: [PATCH 11/24] streams --- .../mvi/characters/CharactersViewModel.kt | 76 ++++++++++++------- .../java/com/ragdroid/mvi/helpers/FlowEx.kt | 47 +++++++++++- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index d618f35..44bf1b1 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -3,15 +3,17 @@ package com.ragdroid.mvi.characters import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.DispatchProvider +import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.helpers.mergeWith +import com.ragdroid.mvi.helpers.ofType import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState +import hu.akarnokd.kotlin.flow.publish import kotlinx.coroutines.* import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* @@ -48,7 +50,9 @@ class CharactersViewModel @Inject constructor( .onEach { Timber.v("onAction $it") } - .flatMapMerge { actionToResultFlow(it) } + .publish { + actionToResultTransformer(it) + } .onEach { Timber.v("onResult $it") } @@ -65,47 +69,65 @@ class CharactersViewModel @Inject constructor( } } - private fun reduce(state: MainViewState, result: MainResult): MainViewState { - return state.reduce(result, resourceProvider) + private fun actionToResultTransformer(actionsFlow: Flow): Flow { + return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) + .merge(loadDescriptionResult(actionsFlow.ofType(MainAction.LoadDescription::class.java)), + pullToRefreshResult(actionsFlow.ofType(MainAction.PullToRefresh::class.java))) + } + + private fun loadDescriptionResult(actionsFlow: Flow): Flow = flow { + actionsFlow.flatMapMerge { action -> + flow { + emit(mainRepository.fetchCharacter(action.characterId)) } + .delayEach(2000) + .map { item -> + MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult + } + .onStart { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) } + .catch { + navigate(MainNavigation.Snackbar(it.message ?: "Unknown Error")) + Timber.e(it) + emit(MainResult.DescriptionResult.DescriptionError(action.characterId, it)) + } + } } - private fun actionToResultFlow(action: MainAction): Flow { - return when(action) { - is MainAction.PullToRefresh -> flow { + private fun pullToRefreshResult(actionsFlow: Flow): Flow = flow { + actionsFlow.flatMapMerge { + flow { emit(MainResult.PullToRefreshing) val characters = mainRepository.fetchCharactersSingle().await() emit(MainResult.PullToRefreshComplete(characters)) } .flowOn(dispatchProvider.io()) .catch { exception -> - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.PullToRefreshError(exception)) - } - is MainAction.LoadData -> flow { + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.PullToRefreshError(exception)) + } + } + } + + private fun loadingResult(actionsFlow: Flow): Flow = flow { + actionsFlow.flatMapMerge { + flow { emit(MainResult.Loading) val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) } // .delayEach(1000) .catch { exception -> - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError(exception)) - } - is MainAction.LoadDescription -> flow { - emit(mainRepository.fetchCharacter(action.characterId)) } - .delayEach(2000) - .map { item -> - MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult - } - .onStart { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) } - .catch { - navigate(MainNavigation.Snackbar(it.message ?: "Unknown Error")) - Timber.e(it) - emit(MainResult.DescriptionResult.DescriptionError(action.characterId, it)) + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + } } + + } + + + private fun reduce(state: MainViewState, result: MainResult): MainViewState { + return state.reduce(result, resourceProvider) } override fun onCleared() { diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt index 24a9cc0..1464803 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt @@ -1,8 +1,10 @@ package com.ragdroid.mvi.helpers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collect +import io.reactivex.Flowable +import io.reactivex.functions.Predicate +import io.reactivex.internal.functions.Functions +import io.reactivex.internal.functions.ObjectHelper +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch fun Flow.mergeWith(other: Flow): Flow = channelFlow { @@ -33,4 +35,43 @@ fun Flow.merge(other: Flow, } } +fun Flow.merge3(other: Flow, + other2: Flow, + other3: Flow): Flow = channelFlow { + // collect from one coroutine and send it + launch { + collect { send(it) } + } + // collect and send from this coroutine, too, concurrently + launch { + other.collect { + send(it) + } + } + launch { + other2.collect { + send(it) + } + } + launch { + other3.collect { + send(it) + } + } +} + +fun Flow.ofType(clazz: Class): Flow { + ObjectHelper.requireNonNull(clazz, "clazz is null") + return filter { + clazz.isInstance(it) + }.cast(clazz) +} + +fun Flow.cast(clazz: Class): Flow { + ObjectHelper.requireNonNull(clazz, "clazz is null") + return map { + clazz.cast(it) + } +} + From 6d5eca88f3f2d34860fd6dd08654c4e74579a79a Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 16:55:58 +0530 Subject: [PATCH 12/24] add some docs --- .../java/com/ragdroid/mvi/characters/CharactersFragment.kt | 1 + .../java/com/ragdroid/mvi/characters/CharactersViewModel.kt | 5 +++++ .../java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 04f260d..a4ab410 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -50,6 +50,7 @@ class CharactersFragment : DaggerFragment(), private val adapter: ItemsViewAdapter by lazy(LazyThreadSafetyMode.NONE) { ItemsViewAdapter(context) } + //we can also use a ConflatedBroadcastChannel here private val descriptionClickProcessor: PublishSubject = PublishSubject() override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 44bf1b1..61d73e4 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -33,6 +33,7 @@ class CharactersViewModel @Inject constructor( fun onAction(action: MainAction) = broadcastChannel.offer(action) + //we can also use kotlin-flow-extensions `PublishSubject` here var broadcastChannel = ConflatedBroadcastChannel() var actionsFlow = broadcastChannel.openSubscription().consumeAsFlow() @@ -96,6 +97,9 @@ class CharactersViewModel @Inject constructor( actionsFlow.flatMapMerge { flow { emit(MainResult.PullToRefreshing) + //This is done only to demonstrate RxJava-Coroutine interop + //We should use available coroutine API instead : + //val characters = mainRepository.fetchCharacters() val characters = mainRepository.fetchCharactersSingle().await() emit(MainResult.PullToRefreshComplete(characters)) } @@ -115,6 +119,7 @@ class CharactersViewModel @Inject constructor( val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) } + //uncommeent this to see unit test behavior with delays // .delayEach(1000) .catch { exception -> Timber.e(exception) diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 5256655..19e7cf9 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -53,7 +53,9 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return pullToRefreshActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - + //This is done only to demonstrate RxJava-Coroutine interop + //We should use available Rx API instead : + //val characters = mainRepository.fetchCharactersSingle() rxSingle { mainRepository.fetchCharacters() }.toFlowable() From 268e0a7f73940b8b3e2c3f2379dbfb6b3e2badc0 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 4 Aug 2019 17:45:47 +0530 Subject: [PATCH 13/24] streams and non-streams solution --- .../mvi/characters/CharactersViewModel.kt | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 61d73e4..696385b 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* import kotlinx.coroutines.rx2.await import timber.log.Timber +import java.lang.Exception import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -28,6 +29,7 @@ class CharactersViewModel @Inject constructor( private val resourceProvider: ResourceProvider, private val dispatchProvider: DispatchProvider ): ViewModel(), CoroutineScope { + override val coroutineContext: CoroutineContext get() = SupervisorJob().plus(dispatchProvider.main()).plus(dispatchProvider.io()) @@ -51,8 +53,13 @@ class CharactersViewModel @Inject constructor( .onEach { Timber.v("onAction $it") } - .publish { - actionToResultTransformer(it) + //to demonstrate streams +// .publish { +// actionToResultTransformer(it) +// } + //to demonstrate non-streams solution + .flatMapMerge { + actionToResultFlow(it) } .onEach { Timber.v("onResult $it") @@ -70,6 +77,49 @@ class CharactersViewModel @Inject constructor( } } + private fun actionToResultFlow(action: MainAction): Flow = flow { + when(action) { + is MainAction.PullToRefresh -> { + try { + emit(MainResult.PullToRefreshing) + val characters = mainRepository.fetchCharactersSingle().await() + emit(MainResult.PullToRefreshComplete(characters)) + } catch (exception: Exception) { + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.PullToRefreshError(exception)) + } + } + is MainAction.LoadData -> { + try { + emit(MainResult.Loading) + val characters = mainRepository.fetchCharacters() + emit(MainResult.LoadingComplete(characters)) + } catch (exception: Exception) { + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError(exception)) + } + } + is MainAction.LoadDescription -> { + try { + emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) + val character = mainRepository.fetchCharacter(action.characterId) + delay(2000L) + emit(MainResult.DescriptionResult.DescriptionLoadComplete(character.id, character.description)) + } catch (exception : Exception) { + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + Timber.e(exception) + emit(MainResult.DescriptionResult.DescriptionError(action.characterId, exception)) + } + } + } + }.catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError(exception)) + } + private fun actionToResultTransformer(actionsFlow: Flow): Flow { return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(actionsFlow.ofType(MainAction.LoadDescription::class.java)), From 33610ca6074b1f64c71e73b87c7fb0c21c0992ce Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 11 Aug 2019 02:37:22 +0530 Subject: [PATCH 14/24] ui changes --- buildSrc/src/main/java/Dependencies.kt | 2 + mvvmi/src/main/res/values/strings.xml | 1 - presentation/build.gradle | 1 + .../mvi/characters/CharactersFragment.kt | 6 +- .../mvi/helpers/SpaceItemDecoration.kt | 25 +++ .../java/com/ragdroid/mvi/items/LoaderItem.kt | 30 ---- .../com/ragdroid/mvi/main/MainActivity.kt | 4 +- .../com/ragdroid/mvi/main/MainFragment.kt | 7 +- .../src/main/res/layout/activity_main.xml | 1 + .../src/main/res/layout/fragment_main.xml | 8 +- .../src/main/res/layout/item_character.xml | 145 +++++++++--------- presentation/src/main/res/values/dimens.xml | 2 +- 12 files changed, 121 insertions(+), 111 deletions(-) create mode 100644 presentation/src/main/java/com/ragdroid/mvi/helpers/SpaceItemDecoration.kt delete mode 100644 presentation/src/main/java/com/ragdroid/mvi/items/LoaderItem.kt diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index e7b8ce1..562112d 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -33,6 +33,7 @@ object Versions { val android_ktx = "1.2.0-alpha02" val coroutine_binding = "1.0.0-RC2" val flowExtensions = "0.0.2" + val wasabeef = "3.0.0" // test libraries val archTesting = "2.0.1" @@ -82,6 +83,7 @@ object Deps { val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.okhttpVersion}" val coroutineBindingSwipeRefresh = "com.github.satoshun.coroutinebinding:coroutinebinding-swiperefreshlayout:${Versions.coroutine_binding}" val flowExtensions = "com.github.akarnokd:kotlin-flow-extensions:${Versions.flowExtensions}" + val recyclerViewAnimations = "jp.wasabeef:recyclerview-animators:${Versions.wasabeef}" } object TestDeps { diff --git a/mvvmi/src/main/res/values/strings.xml b/mvvmi/src/main/res/values/strings.xml index 77aa064..8542005 100644 --- a/mvvmi/src/main/res/values/strings.xml +++ b/mvvmi/src/main/res/values/strings.xml @@ -1,3 +1,2 @@ - mvvmi diff --git a/presentation/build.gradle b/presentation/build.gradle index b2a1e28..168d386 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -97,6 +97,7 @@ dependencies { implementation Deps.rxBindingKt implementation Deps.rxBindingRvKt implementation Deps.rxBindingSupportV4 + implementation Deps.recyclerViewAnimations implementation Deps.coroutineBindingSwipeRefresh implementation Deps.flowExtensions diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index a4ab410..9afcac1 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.fueled.reclaim.ItemPresenterProvider @@ -17,6 +16,7 @@ import com.google.android.material.snackbar.Snackbar import com.ragdroid.mvi.R import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment +import com.ragdroid.mvi.helpers.SpaceItemDecoration import com.ragdroid.mvi.helpers.merge import com.ragdroid.mvi.items.CharacterItem import com.ragdroid.mvi.main.MainAction @@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.CoroutineContext +import jp.wasabeef.recyclerview.animators.FadeInAnimator /** * A placeholder fragment containing a simple view. @@ -65,10 +66,11 @@ class CharactersFragment : DaggerFragment(), super.onViewCreated(view, savedInstanceState) binding.toolbar.setTitle(R.string.title_flow) val manager = LinearLayoutManager(context) - val decoration = DividerItemDecoration(context, RecyclerView.VERTICAL) + val decoration = SpaceItemDecoration(resources.getDimensionPixelOffset(R.dimen.keyline_1), false, false, true, false) manager.orientation = RecyclerView.VERTICAL binding.listView.layoutManager = manager binding.listView.adapter = adapter + binding.listView.itemAnimator = FadeInAnimator() binding.listView.addItemDecoration(decoration) setupViewModel(savedInstanceState) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/SpaceItemDecoration.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/SpaceItemDecoration.kt new file mode 100644 index 0000000..d6058f5 --- /dev/null +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/SpaceItemDecoration.kt @@ -0,0 +1,25 @@ +package com.ragdroid.mvi.helpers + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class SpaceItemDecoration( + private val space: Int, + private val shouldShowLeft: Boolean = true, + private val shouldShowRight: Boolean = true, + private val shouldShowTop: Boolean = true, + private val shouldShowBottom: Boolean = true +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + if (shouldShowLeft) + outRect.left = space + if (shouldShowRight) + outRect.right = space + if (shouldShowBottom) + outRect.bottom = space + if (shouldShowTop) + outRect.top = space + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/items/LoaderItem.kt b/presentation/src/main/java/com/ragdroid/mvi/items/LoaderItem.kt deleted file mode 100644 index afee87c..0000000 --- a/presentation/src/main/java/com/ragdroid/mvi/items/LoaderItem.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.ragdroid.mvi.items - -import androidx.databinding.DataBindingUtil -import android.view.View -import com.fueled.reclaim.BaseItem -import com.fueled.reclaim.BaseViewHolder -import com.ragdroid.mvi.R -import com.ragdroid.mvi.databinding.ItemCharacterBinding -import com.ragdroid.mvi.models.CharacterItemState - -/** - * Created by garimajain on 22/11/17. - */ -class LoaderItem(val state: CharacterItemState) : - BaseItem(state, null) { - - override fun updateItemViews() { - viewHolder.binding?.state = state - } - - override fun onCreateViewHolder(view: View): ViewHolder = ViewHolder(view) - - override fun getType(): Enum> = ItemTypes.CHARACTER_MARVEL - - override fun getLayoutId(): Int = R.layout.item_character - - class ViewHolder(view: View): BaseViewHolder(view) { - val binding: ItemCharacterBinding? = DataBindingUtil.bind(view) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index c15101c..d53138e 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -32,8 +32,8 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager val fragmentTransaction = fragmentManager.beginTransaction() -// val fraggy = MainFragment() - val fraggy = CharactersFragment() + val fraggy = MainFragment() +// val fraggy = CharactersFragment() fragmentTransaction.replace(R.id.fragment_container, fraggy) fragmentTransaction.commit() } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 927fd41..1645065 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -5,10 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.ragdroid.mvvmi.core.NavigationState @@ -19,6 +17,7 @@ import com.jakewharton.rxbinding2.support.v4.widget.refreshes import com.ragdroid.mvi.R import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment +import com.ragdroid.mvi.helpers.SpaceItemDecoration import com.ragdroid.mvi.items.CharacterItem import com.ragdroid.mvi.models.CharacterItemPresenter import com.ragdroid.mvi.viewmodel.MainFragmentViewModel @@ -27,6 +26,7 @@ import dagger.android.support.DaggerFragment import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.processors.PublishProcessor +import jp.wasabeef.recyclerview.animators.FadeInAnimator import timber.log.Timber import javax.inject.Inject @@ -58,10 +58,11 @@ class MainFragment : DaggerFragment(), super.onViewCreated(view, savedInstanceState) binding.toolbar.setTitle(R.string.title_rx) val manager = LinearLayoutManager(context) - val decoration = DividerItemDecoration(context, RecyclerView.VERTICAL) + val decoration = SpaceItemDecoration(resources.getDimensionPixelOffset(R.dimen.keyline_1), false, false, true, false) manager.orientation = RecyclerView.VERTICAL binding.listView.layoutManager = manager binding.listView.adapter = adapter + binding.listView.itemAnimator = FadeInAnimator() binding.listView.addItemDecoration(decoration) setupViewModel() super.onMviViewCreated(savedInstanceState) diff --git a/presentation/src/main/res/layout/activity_main.xml b/presentation/src/main/res/layout/activity_main.xml index 6d580a8..b643949 100644 --- a/presentation/src/main/res/layout/activity_main.xml +++ b/presentation/src/main/res/layout/activity_main.xml @@ -15,6 +15,7 @@ @@ -22,7 +22,7 @@ bind:layout_constraintStart_toStartOf="parent" bind:layout_constraintEnd_toEndOf="parent" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="?actionBarSize" android:theme="@style/AppTheme.AppBarOverlay"> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/apk/res-auto"> @@ -15,78 +15,85 @@ type="com.ragdroid.mvi.models.CharacterItemPresenter"/> - - + + - + - + - + - + - - + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/dimens.xml b/presentation/src/main/res/values/dimens.xml index d80e9fc..99a309f 100644 --- a/presentation/src/main/res/values/dimens.xml +++ b/presentation/src/main/res/values/dimens.xml @@ -35,6 +35,6 @@ 10sp 100dp 48dp - 100dp + 60dp From 33e9cfb71fb94d4c3931d5923b656bc5e4e17597 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 11 Aug 2019 03:28:20 +0530 Subject: [PATCH 15/24] remove loading from state --- .../mvi/characters/CharactersFragment.kt | 21 ++++----------- .../com/ragdroid/mvi/main/MainFragment.kt | 3 --- .../com/ragdroid/mvi/main/MainViewState.kt | 26 +++++-------------- .../mvi/viewmodel/MainFragmentViewModel.kt | 13 ++++++++-- presentation/src/main/res/values/strings.xml | 2 +- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 9afcac1..74ca921 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -108,22 +108,11 @@ class CharactersFragment : DaggerFragment(), fun render(state: MainViewState) { Timber.d("got state $state") binding.model = state - when { - state.pullToRefreshError != null -> return - - state.loadingError != null -> { - adapter.clearAllRecyclerItems() - return - } - - else -> { - val characterModelList = - state.characters.map { - CharacterItem(it, this) - } - adapter.replaceItems(characterModelList, true) - } - } + val characterModelList = + state.characters.map { + CharacterItem(it, this) + } + adapter.replaceItems(characterModelList, true) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 1645065..aa34c6e 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -96,9 +96,6 @@ class MainFragment : DaggerFragment(), override fun render(state: MainViewState) { Timber.d("got state $state") binding.model = state - if (state.loadingError != null) { - adapter.clearAllRecyclerItems() - } val characterModelList = state.characters.map { diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt index 55b176e..ae3ea9a 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt @@ -16,17 +16,13 @@ import com.ragdroid.mvvmi.core.NavigationState data class MainViewState( val loading: Boolean, val characters: List, - val loadingError: Throwable?, - val pullToRefreshing: Boolean, - val pullToRefreshError: Throwable?): MviState { + val pullToRefreshing: Boolean): MviState { companion object Factory { fun init() = MainViewState( loading = true, characters = emptyList(), - loadingError = null, - pullToRefreshing = false, - pullToRefreshError = null + pullToRefreshing = false ) } @@ -35,32 +31,24 @@ data class MainViewState( val characters = characters return when (result) { is MainResult.Loading -> copy( - loading = true, - loadingError = null) + loading = true) is MainResult.LoadingError -> copy( - loading = false, - loadingError = result.throwable) + loading = false) is MainResult.LoadingComplete -> { val characterStates = reduceCharactersList(characters, result.characters, resources) copy( loading = false, - loadingError = null, characters = characterStates) } is MainResult.PullToRefreshing -> copy( loading = false, - pullToRefreshing = true, - pullToRefreshError = null) + pullToRefreshing = true) is MainResult.PullToRefreshError -> copy( - pullToRefreshing = false, - loadingError = null, - pullToRefreshError = result.throwable) + pullToRefreshing = false) is MainResult.PullToRefreshComplete -> copy( - loadingError = null, pullToRefreshing = false, - pullToRefreshError = null, characters = reduceCharactersList(characters, result.characters, resources)) is MainResult.DescriptionResult -> { @@ -72,7 +60,7 @@ data class MainViewState( val newCharactersList = characters.slice(0 until previousItemStateIndex) .plus(listOf(newItemState)) .plus(characters.slice(previousItemStateIndex + 1 until characters.size)) - copy(characters = newCharactersList, loadingError = null, pullToRefreshError = null) + copy(characters = newCharactersList) } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 19e7cf9..e329334 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -3,6 +3,7 @@ package com.ragdroid.mvi.viewmodel import com.ragdroid.data.MainRepository import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.main.MainAction +import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvvmi.core.MviViewModel @@ -44,6 +45,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re .startWith(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) .onErrorReturn { error -> Timber.e(error) + navigate(MainNavigation.Snackbar(error.message ?: "Unknown Error")) MainResult.DescriptionResult.DescriptionError(action.characterId, error) } } @@ -61,7 +63,10 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re }.toFlowable() .map { items -> MainResult.PullToRefreshComplete(items) as MainResult } .startWith(MainResult.PullToRefreshing) - .onErrorReturn { error -> MainResult.PullToRefreshError(error) } + .onErrorReturn { error -> + navigate(MainNavigation.Snackbar(error.message ?: "Unknown Error")) + MainResult.PullToRefreshError(error) + } } } @@ -72,7 +77,11 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re mainRepository.fetchCharactersSingle().toFlowable() .map { states -> MainResult.LoadingComplete(states) as MainResult } .startWith(MainResult.Loading) - .onErrorReturn(MainResult::LoadingError) + .onErrorReturn { + error -> + navigate(MainNavigation.Snackbar(error.message ?: "Unknown Error")) + MainResult.LoadingError(error) + } } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 04a717e..e176499 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -6,5 +6,5 @@ Description Flow Characters - Rx Characters + Marvel Characters From d8ee88c70551e9fba828fd33e59b1a4d6876cfac Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sun, 11 Aug 2019 16:49:38 +0530 Subject: [PATCH 16/24] clean up --- .../mvi/characters/CharactersFragment.kt | 23 ++++++++++++------- .../mvi/characters/CharactersViewModel.kt | 4 ++-- .../com/ragdroid/mvi/main/MainFragment.kt | 7 ++++-- .../com/ragdroid/mvi/main/MainViewState.kt | 10 ++++---- .../mvi/viewmodel/MainFragmentViewModel.kt | 7 +++--- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 74ca921..21e84b0 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -51,8 +51,7 @@ class CharactersFragment : DaggerFragment(), private val adapter: ItemsViewAdapter by lazy(LazyThreadSafetyMode.NONE) { ItemsViewAdapter(context) } - //we can also use a ConflatedBroadcastChannel here - private val descriptionClickProcessor: PublishSubject = PublishSubject() + override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main lateinit var viewModel: CharactersViewModel @@ -85,11 +84,7 @@ class CharactersFragment : DaggerFragment(), } - override fun onCharacterDescriptionClicked(itemId: Long) { - launch { - descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) - } - } + override fun getItemPresenter(): CharacterItemPresenter { return this @@ -97,10 +92,22 @@ class CharactersFragment : DaggerFragment(), private fun pullToRefreshIntent(): Flow = - binding.refreshLayout.refreshes().consumeAsFlow().map { MainAction.PullToRefresh } + binding.refreshLayout.refreshes() + .consumeAsFlow() + .map { MainAction.PullToRefresh } private fun loadingIntent(): Flow = flow { emit(MainAction.LoadData) } + + //we can also use a ConflatedBroadcastChannel here + private val descriptionClickProcessor = PublishSubject() + + override fun onCharacterDescriptionClicked(itemId: Long) { + launch { + descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) + } + } + private fun loadDescription(): Flow { return descriptionClickProcessor } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 696385b..3e7ea94 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -157,7 +157,7 @@ class CharactersViewModel @Inject constructor( .catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.PullToRefreshError(exception)) + emit(MainResult.PullToRefreshError) } } } @@ -174,7 +174,7 @@ class CharactersViewModel @Inject constructor( .catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - + emit(MainResult.LoadingError) } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index aa34c6e..44ef65c 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -45,7 +45,6 @@ class MainFragment : DaggerFragment(), private val adapter: ItemsViewAdapter by lazy(LazyThreadSafetyMode.NONE) { ItemsViewAdapter(context) } - private val descriptionClickProcessor: PublishProcessor = PublishProcessor.create() override lateinit var viewModel: MainFragmentViewModel @@ -74,6 +73,7 @@ class MainFragment : DaggerFragment(), viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) } + private val descriptionClickProcessor = PublishProcessor.create() override fun onCharacterDescriptionClicked(itemId: Long) { descriptionClickProcessor.onNext(MainAction.LoadDescription(itemId)) @@ -85,7 +85,10 @@ class MainFragment : DaggerFragment(), private fun pullToRefreshIntent(): Flowable = - binding.refreshLayout.refreshes().toFlowable(BackpressureStrategy.DROP).map { MainAction.PullToRefresh } + binding.refreshLayout.refreshes() + .toFlowable(BackpressureStrategy.DROP) + .map { MainAction.PullToRefresh } + private fun loadingIntent(): Flowable = Flowable.just(MainAction.LoadData) diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt index ae3ea9a..4445b5f 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt @@ -94,17 +94,19 @@ sealed class MainAction: MviAction { sealed class MainResult: MviResult { object Loading: MainResult() - data class LoadingError(val throwable: Throwable): MainResult() + object LoadingError: MainResult() data class LoadingComplete(val characters: List): MainResult() object PullToRefreshing: MainResult() - data class PullToRefreshError(val throwable: Throwable): MainResult() + object PullToRefreshError: MainResult() data class PullToRefreshComplete(val characters: List): MainResult() sealed class DescriptionResult(val characterId: Long) : MainResult() { data class DescriptionLoading(private val id: Long) : DescriptionResult(id) - data class DescriptionError(private val id: Long, val throwable: Throwable): DescriptionResult(id) - data class DescriptionLoadComplete(private val id: Long, val description: String): DescriptionResult(id) + data class DescriptionError(private val id: Long, + val throwable: Throwable): DescriptionResult(id) + data class DescriptionLoadComplete(private val id: Long, + val description: String): DescriptionResult(id) } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index e329334..201b938 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -65,12 +65,13 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re .startWith(MainResult.PullToRefreshing) .onErrorReturn { error -> navigate(MainNavigation.Snackbar(error.message ?: "Unknown Error")) - MainResult.PullToRefreshError(error) + MainResult.PullToRefreshError } } } - private fun loadingResult(loadDataActionStream: Flowable): Flowable { + private fun loadingResult(loadDataActionStream: Flowable) + : Flowable { return loadDataActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> @@ -80,7 +81,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re .onErrorReturn { error -> navigate(MainNavigation.Snackbar(error.message ?: "Unknown Error")) - MainResult.LoadingError(error) + MainResult.LoadingError } } } From d1b00ddf1a2518d242ce5dd6f07ddcc7a17aa92b Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Mon, 12 Aug 2019 16:37:39 +0530 Subject: [PATCH 17/24] minor changes --- .../ragdroid/mvi/characters/CharactersViewModel.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 3e7ea94..0a4cfa9 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -3,6 +3,7 @@ package com.ragdroid.mvi.characters import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.DispatchProvider @@ -48,7 +49,7 @@ class CharactersViewModel @Inject constructor( fun navigate(navigationState: MainNavigation) = navigationLiveData.postValue(navigationState) fun processActions(actions: Flow) { - launch { + viewModelScope.launch { actionsFlow.mergeWith(actions) .onEach { Timber.v("onAction $it") @@ -87,7 +88,7 @@ class CharactersViewModel @Inject constructor( } catch (exception: Exception) { Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.PullToRefreshError(exception)) + emit(MainResult.PullToRefreshError) } } is MainAction.LoadData -> { @@ -98,7 +99,7 @@ class CharactersViewModel @Inject constructor( } catch (exception: Exception) { Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError(exception)) + emit(MainResult.LoadingError) } } is MainAction.LoadDescription -> { @@ -117,7 +118,7 @@ class CharactersViewModel @Inject constructor( }.catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError(exception)) + emit(MainResult.LoadingError) } private fun actionToResultTransformer(actionsFlow: Flow): Flow { @@ -170,7 +171,7 @@ class CharactersViewModel @Inject constructor( emit(MainResult.LoadingComplete(characters)) } //uncommeent this to see unit test behavior with delays -// .delayEach(1000) + .delayEach(1000) .catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) From d4cc4459fd7c1d0c0e9e5196c4a27906fe2d9179 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Tue, 13 Aug 2019 01:03:12 +0530 Subject: [PATCH 18/24] mino changes --- .../ragdroid/mvi/characters/CharactersFragment.kt | 7 ++----- .../ragdroid/mvi/characters/CharactersViewModel.kt | 13 +------------ .../main/java/com/ragdroid/mvi/main/MainActivity.kt | 4 ++-- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 21e84b0..bd2cc8b 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -27,14 +27,11 @@ import com.ragdroid.mvvmi.core.NavigationState import dagger.android.support.DaggerFragment import hu.akarnokd.kotlin.flow.PublishSubject import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.CoroutineContext import jp.wasabeef.recyclerview.animators.FadeInAnimator +import kotlinx.coroutines.flow.* /** * A placeholder fragment containing a simple view. @@ -96,7 +93,7 @@ class CharactersFragment : DaggerFragment(), .consumeAsFlow() .map { MainAction.PullToRefresh } - private fun loadingIntent(): Flow = flow { emit(MainAction.LoadData) } + private fun loadingIntent(): Flow = flowOf(MainAction.LoadData) //we can also use a ConflatedBroadcastChannel here diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 0a4cfa9..48a88b2 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -29,10 +29,7 @@ class CharactersViewModel @Inject constructor( private val mainRepository: MainRepository, private val resourceProvider: ResourceProvider, private val dispatchProvider: DispatchProvider -): ViewModel(), CoroutineScope { - - override val coroutineContext: CoroutineContext - get() = SupervisorJob().plus(dispatchProvider.main()).plus(dispatchProvider.io()) +): ViewModel() { fun onAction(action: MainAction) = broadcastChannel.offer(action) @@ -66,10 +63,6 @@ class CharactersViewModel @Inject constructor( Timber.v("onResult $it") } .scan(MainViewState.init()) { state, result: MainResult -> reduce(state, result) } - .onEach { - Timber.v("onState $it") - stateLiveData.postValue(it) - } .onStart { Timber.d("subscribed to states") } .collect { Timber.v("onState $it") @@ -185,8 +178,4 @@ class CharactersViewModel @Inject constructor( private fun reduce(state: MainViewState, result: MainResult): MainViewState { return state.reduce(result, resourceProvider) } - - override fun onCleared() { - coroutineContext.cancelChildren() - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index d53138e..c15101c 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -32,8 +32,8 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager val fragmentTransaction = fragmentManager.beginTransaction() - val fraggy = MainFragment() -// val fraggy = CharactersFragment() +// val fraggy = MainFragment() + val fraggy = CharactersFragment() fragmentTransaction.replace(R.id.fragment_container, fraggy) fragmentTransaction.commit() } From 9880e55db6e6492ac46a406c89045eb004d69ef1 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Tue, 13 Aug 2019 23:49:14 +0530 Subject: [PATCH 19/24] depict state differently --- .../mvi/characters/CharactersFragment.kt | 10 +-- .../mvi/characters/CharactersViewModel.kt | 11 +-- .../com/ragdroid/mvi/main/MainFragment.kt | 4 +- .../com/ragdroid/mvi/main/MainViewState.kt | 72 +++++++++---------- .../src/main/res/layout/fragment_main.xml | 13 ++-- 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index bd2cc8b..b6b8bec 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -18,6 +18,7 @@ import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment import com.ragdroid.mvi.helpers.SpaceItemDecoration import com.ragdroid.mvi.helpers.merge +import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.items.CharacterItem import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation @@ -76,7 +77,7 @@ class CharactersFragment : DaggerFragment(), viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) viewModel.navigationLiveData().observe(viewLifecycleOwner, Observer { navigate(it) }) if (savedInstanceState == null) { - viewModel.processActions(loadingIntent().merge(pullToRefreshIntent(), loadDescription())) + viewModel.processActions(loadingIntent().mergeWith(pullToRefreshIntent())) } } @@ -100,9 +101,7 @@ class CharactersFragment : DaggerFragment(), private val descriptionClickProcessor = PublishSubject() override fun onCharacterDescriptionClicked(itemId: Long) { - launch { - descriptionClickProcessor.emit(MainAction.LoadDescription(itemId)) - } + viewModel.onAction(MainAction.LoadDescription(itemId)) } private fun loadDescription(): Flow { @@ -111,7 +110,8 @@ class CharactersFragment : DaggerFragment(), fun render(state: MainViewState) { Timber.d("got state $state") - binding.model = state + binding.refreshing = state.loadingState == MainViewState.LoadingState.PullToRefreshing + binding.loading = state.loadingState == MainViewState.LoadingState.Loading val characterModelList = state.characters.map { CharacterItem(it, this) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 48a88b2..3b0b277 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -108,11 +108,12 @@ class CharactersViewModel @Inject constructor( } } } - }.catch { exception -> - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError) - } + }.flowOn(Dispatchers.IO) + .catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError) + } private fun actionToResultTransformer(actionsFlow: Flow): Flow { return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 44ef65c..86999b7 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -98,8 +98,8 @@ class MainFragment : DaggerFragment(), override fun render(state: MainViewState) { Timber.d("got state $state") - binding.model = state - + binding.refreshing = state.loadingState == MainViewState.LoadingState.PullToRefreshing + binding.loading = state.loadingState == MainViewState.LoadingState.Loading val characterModelList = state.characters.map { CharacterItem(it, this) diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt index 4445b5f..3e38675 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainViewState.kt @@ -9,60 +9,60 @@ import com.ragdroid.mvvmi.core.MviResult import com.ragdroid.mvvmi.core.MviState import com.ragdroid.mvvmi.core.NavigationState -/** - * State of the MainView - * Created by garimajain on 22/11/17. - */ -data class MainViewState( - val loading: Boolean, - val characters: List, - val pullToRefreshing: Boolean): MviState { +data class MainViewState(val characters: List, + val emptyState: EmptyState, + val loadingState: LoadingState): MviState { + + sealed class EmptyState { + object None: EmptyState() + object NoData: EmptyState() + object NoInternet: EmptyState() + } + + sealed class LoadingState { + object None: LoadingState() + object Loading: LoadingState() + object PullToRefreshing: LoadingState() + } companion object Factory { - fun init() = MainViewState( - loading = true, - characters = emptyList(), - pullToRefreshing = false - ) + fun init() = MainViewState(emptyList(), EmptyState.NoData, LoadingState.None) } fun reduce(result: MainResult, resources: ResourceProvider): MainViewState { - val characters = characters return when (result) { - is MainResult.Loading -> copy( - loading = true) - is MainResult.LoadingError -> copy( - loading = false) + is MainResult.Loading -> copy(loadingState = LoadingState.Loading) + is MainResult.LoadingError -> copy(loadingState = LoadingState.None, + emptyState = EmptyState.NoData) is MainResult.LoadingComplete -> { - val characterStates = reduceCharactersList(characters, result.characters, resources) - copy( - loading = false, - characters = characterStates) + val characterStates = reduceCharactersList(null, result.characters, resources) + copy(characterStates, loadingState = LoadingState.None, + emptyState = EmptyState.None) } - is MainResult.PullToRefreshing -> copy( - loading = false, - pullToRefreshing = true) - is MainResult.PullToRefreshError -> copy( - pullToRefreshing = false) - is MainResult.PullToRefreshComplete -> copy( - pullToRefreshing = false, - characters = reduceCharactersList(characters, result.characters, resources)) + is MainResult.PullToRefreshing -> { + copy(loadingState = LoadingState.PullToRefreshing) + } + is MainResult.PullToRefreshError -> copy(loadingState = LoadingState.None) + is MainResult.PullToRefreshComplete -> + copy(characters = reduceCharactersList(characters, result.characters, resources), + loadingState = LoadingState.None, + emptyState = EmptyState.None) is MainResult.DescriptionResult -> { + val previousCharacters = characters val previousItemState = findItemWithId(result.characterId) - val previousItemStateIndex = characters.indexOf(previousItemState) + val previousItemStateIndex = previousCharacters.indexOf(previousItemState) val newItemState = previousItemState.reduce(resources, result) - val newCharactersList = characters.slice(0 until previousItemStateIndex) + val newCharactersList = previousCharacters.slice(0 until previousItemStateIndex) .plus(listOf(newItemState)) - .plus(characters.slice(previousItemStateIndex + 1 until characters.size)) + .plus(previousCharacters.slice(previousItemStateIndex + 1 until previousCharacters.size)) copy(characters = newCharactersList) } - } } @@ -71,8 +71,7 @@ data class MainViewState( characters.find { it.characterId == characterId }!! - - private fun reduceCharactersList(previousStateList: List, + private fun reduceCharactersList(previousStateList: List?, resultList: List, resources: ResourceProvider): List { fun initialState(result: CharacterMarvel) = @@ -85,6 +84,7 @@ data class MainViewState( } + sealed class MainAction: MviAction { object PullToRefresh: MainAction() object LoadData: MainAction() diff --git a/presentation/src/main/res/layout/fragment_main.xml b/presentation/src/main/res/layout/fragment_main.xml index 490860f..60520b3 100644 --- a/presentation/src/main/res/layout/fragment_main.xml +++ b/presentation/src/main/res/layout/fragment_main.xml @@ -6,8 +6,11 @@ + name="loading" + type="Boolean"/> + + bind:refreshing="@{refreshing}"> Date: Thu, 22 Aug 2019 00:18:03 -0400 Subject: [PATCH 20/24] review comments --- buildSrc/src/main/java/Dependencies.kt | 1 + presentation/build.gradle | 1 + .../mvi/characters/CharactersFragment.kt | 11 +++------- .../mvi/characters/CharactersViewModel.kt | 20 +++++++++++-------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 562112d..58a6ac7 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -59,6 +59,7 @@ object Deps { val lifecycleSavedState = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.lifecycleSavedState}" val lifecycleCompiler = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycleRx}" val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycleEx}" + val lifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycleEx}" val navigationFragment = "android.arch.navigation:navigation-fragment-ktx:${Versions.navigationVersion}" val navigationUIKtx = "android.arch.navigation:navigation-ui-ktx:${Versions.navigationVersion}" val pagingRuntime = "androidx.paging:paging-runtime:${Versions.arch_comp_paging}" diff --git a/presentation/build.gradle b/presentation/build.gradle index 168d386..8d01545 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -102,6 +102,7 @@ dependencies { implementation Deps.coroutineBindingSwipeRefresh implementation Deps.flowExtensions implementation Deps.lifecycleViewModelKtx + implementation Deps.lifecycleRuntimeKtx testImplementation TestDeps.junit testImplementation TestDeps.mockitoKotlin diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index b6b8bec..7adb930 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.* */ class CharactersFragment : DaggerFragment(), ItemPresenterProvider, - CharacterItemPresenter, CoroutineScope { + CharacterItemPresenter { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -50,7 +50,6 @@ class CharactersFragment : DaggerFragment(), ItemsViewAdapter(context) } - override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main lateinit var viewModel: CharactersViewModel @@ -74,8 +73,8 @@ class CharactersFragment : DaggerFragment(), private fun setupViewModel(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) - viewModel.stateLiveData().observe(viewLifecycleOwner, Observer { render(it) }) - viewModel.navigationLiveData().observe(viewLifecycleOwner, Observer { navigate(it) }) + viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { render(it) }) + viewModel.navigationLiveData.observe(viewLifecycleOwner, Observer { navigate(it) }) if (savedInstanceState == null) { viewModel.processActions(loadingIntent().mergeWith(pullToRefreshIntent())) } @@ -128,8 +127,4 @@ class CharactersFragment : DaggerFragment(), } } - override fun onDestroyView() { - super.onDestroyView() - coroutineContext.cancel() - } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 3b0b277..809fc06 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -34,16 +34,19 @@ class CharactersViewModel @Inject constructor( fun onAction(action: MainAction) = broadcastChannel.offer(action) //we can also use kotlin-flow-extensions `PublishSubject` here - var broadcastChannel = ConflatedBroadcastChannel() - var actionsFlow = broadcastChannel.openSubscription().consumeAsFlow() + private var broadcastChannel = ConflatedBroadcastChannel() + var actionsFlow = broadcastChannel.asFlow() - fun stateLiveData(): LiveData = stateLiveData - private val stateLiveData = MutableLiveData() + val stateLiveData: LiveData + get() = _stateLiveData - fun navigationLiveData(): LiveData = navigationLiveData - private val navigationLiveData = MutableLiveData() + private val _stateLiveData = MutableLiveData() - fun navigate(navigationState: MainNavigation) = navigationLiveData.postValue(navigationState) + val navigationLiveData: LiveData + get() = _navigationLiveData + private val _navigationLiveData = MutableLiveData() + + fun navigate(navigationState: MainNavigation) = _navigationLiveData.postValue(navigationState) fun processActions(actions: Flow) { viewModelScope.launch { @@ -64,9 +67,10 @@ class CharactersViewModel @Inject constructor( } .scan(MainViewState.init()) { state, result: MainResult -> reduce(state, result) } .onStart { Timber.d("subscribed to states") } + .flowOn(dispatchProvider.computation()) .collect { Timber.v("onState $it") - stateLiveData.postValue(it) + _stateLiveData.postValue(it) } } } From 14b911178fc92a3cb4ca3fd7c1f252d89c763a96 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Fri, 23 Aug 2019 00:22:22 -0400 Subject: [PATCH 21/24] addressed reviews --- .../com/ragdroid/data/MainRepositoryImpl.kt | 15 +-- presentation/build.gradle | 2 - .../mvi/characters/CharactersFragment.kt | 32 +----- .../mvi/characters/CharactersViewModel.kt | 97 ++++++++----------- .../java/com/ragdroid/mvi/helpers/FlowEx.kt | 6 -- .../mvi/viewmodel/MainFragmentViewModel.kt | 7 +- 6 files changed, 58 insertions(+), 101 deletions(-) diff --git a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt index 4390195..aa73f78 100644 --- a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt +++ b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt @@ -21,7 +21,7 @@ class MainRepositoryImpl @Inject constructor( private val config: AppConfig, private val helpers: Helpers): MainRepository { - override suspend fun fetchCharacters(): List { + override suspend fun fetchCharacters(): Flow> = flow { val timeStamp = System.currentTimeMillis() val charactersWrapper = marvelApi.getCharacters( config.publicKey, @@ -34,7 +34,7 @@ class MainRepositoryImpl @Inject constructor( .map { characterMapper.map(it) }.toList() - return characters + emit(characters) } override fun fetchCharactersSingle(): Single> { @@ -70,6 +70,7 @@ class MainRepositoryImpl @Inject constructor( timeStamp ) } + private fun charactersApiSingle(timeStamp: Long): Single>> { return marvelApi.getCharactersSingle( config.publicKey, @@ -81,7 +82,7 @@ class MainRepositoryImpl @Inject constructor( ) } - override suspend fun fetchCharacter(id: Long): CharacterMarvel { + override suspend fun fetchCharacter(id: Long): Flow = flow { val timeStamp = System.currentTimeMillis() val characterWrapper = marvelApi.getCharacter( id, @@ -89,19 +90,21 @@ class MainRepositoryImpl @Inject constructor( helpers.buildMD5Digest("" + timeStamp + config.privateKey + config.publicKey), timeStamp) - return characterWrapper.data.results + val results = characterWrapper.data.results .map { characterMapper.map(it) }.getOrNull(0) ?: throw IllegalAccessException("Character for id $id not found") + emit(results) } + } interface MainRepository { fun fetchCharactersSingle(): Single> - suspend fun fetchCharacters(): List - suspend fun fetchCharacter(id: Long): CharacterMarvel + suspend fun fetchCharacters(): Flow> + suspend fun fetchCharacter(id: Long): Flow fun fetchCharacterSingle(id: Long): Single } diff --git a/presentation/build.gradle b/presentation/build.gradle index 8d01545..d940c53 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -99,8 +99,6 @@ dependencies { implementation Deps.rxBindingSupportV4 implementation Deps.recyclerViewAnimations - implementation Deps.coroutineBindingSwipeRefresh - implementation Deps.flowExtensions implementation Deps.lifecycleViewModelKtx implementation Deps.lifecycleRuntimeKtx diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 7adb930..19edeaf 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -11,14 +11,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.fueled.reclaim.ItemPresenterProvider import com.fueled.reclaim.ItemsViewAdapter -import com.github.satoshun.coroutinebinding.androidx.swiperefreshlayout.widget.refreshes import com.google.android.material.snackbar.Snackbar import com.ragdroid.mvi.R import com.ragdroid.mvi.databinding.FragmentMainBinding import com.ragdroid.mvi.helpers.BindFragment import com.ragdroid.mvi.helpers.SpaceItemDecoration -import com.ragdroid.mvi.helpers.merge -import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.items.CharacterItem import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation @@ -26,13 +23,9 @@ import com.ragdroid.mvi.main.MainViewState import com.ragdroid.mvi.models.CharacterItemPresenter import com.ragdroid.mvvmi.core.NavigationState import dagger.android.support.DaggerFragment -import hu.akarnokd.kotlin.flow.PublishSubject -import kotlinx.coroutines.* import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import jp.wasabeef.recyclerview.animators.FadeInAnimator -import kotlinx.coroutines.flow.* /** * A placeholder fragment containing a simple view. @@ -68,15 +61,16 @@ class CharactersFragment : DaggerFragment(), binding.listView.adapter = adapter binding.listView.itemAnimator = FadeInAnimator() binding.listView.addItemDecoration(decoration) - setupViewModel(savedInstanceState) + setupViewModel() } - private fun setupViewModel(savedInstanceState: Bundle?) { + private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { render(it) }) viewModel.navigationLiveData.observe(viewLifecycleOwner, Observer { navigate(it) }) - if (savedInstanceState == null) { - viewModel.processActions(loadingIntent().mergeWith(pullToRefreshIntent())) + viewModel.onAction(MainAction.LoadData) + binding.refreshLayout.setOnRefreshListener { + viewModel.onAction(MainAction.PullToRefresh) } } @@ -87,26 +81,10 @@ class CharactersFragment : DaggerFragment(), return this } - - private fun pullToRefreshIntent(): Flow = - binding.refreshLayout.refreshes() - .consumeAsFlow() - .map { MainAction.PullToRefresh } - - private fun loadingIntent(): Flow = flowOf(MainAction.LoadData) - - - //we can also use a ConflatedBroadcastChannel here - private val descriptionClickProcessor = PublishSubject() - override fun onCharacterDescriptionClicked(itemId: Long) { viewModel.onAction(MainAction.LoadDescription(itemId)) } - private fun loadDescription(): Flow { - return descriptionClickProcessor - } - fun render(state: MainViewState) { Timber.d("got state $state") binding.refreshing = state.loadingState == MainViewState.LoadingState.PullToRefreshing diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 809fc06..4baf1ef 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -31,6 +31,10 @@ class CharactersViewModel @Inject constructor( private val dispatchProvider: DispatchProvider ): ViewModel() { + init { + processActions() + } + fun onAction(action: MainAction) = broadcastChannel.offer(action) //we can also use kotlin-flow-extensions `PublishSubject` here @@ -43,25 +47,19 @@ class CharactersViewModel @Inject constructor( private val _stateLiveData = MutableLiveData() val navigationLiveData: LiveData - get() = _navigationLiveData + get() = _navigationLiveData private val _navigationLiveData = MutableLiveData() fun navigate(navigationState: MainNavigation) = _navigationLiveData.postValue(navigationState) - fun processActions(actions: Flow) { + private fun processActions() { viewModelScope.launch { - actionsFlow.mergeWith(actions) - .onEach { - Timber.v("onAction $it") - } - //to demonstrate streams -// .publish { -// actionToResultTransformer(it) -// } + + actionToResultTransformer(actionsFlow) //to demonstrate non-streams solution - .flatMapMerge { - actionToResultFlow(it) - } +// .flatMapMerge { +// actionToResultFlow(it) +// } .onEach { Timber.v("onResult $it") } @@ -80,7 +78,7 @@ class CharactersViewModel @Inject constructor( is MainAction.PullToRefresh -> { try { emit(MainResult.PullToRefreshing) - val characters = mainRepository.fetchCharactersSingle().await() + val characters = mainRepository.fetchCharacters().first() emit(MainResult.PullToRefreshComplete(characters)) } catch (exception: Exception) { Timber.e(exception) @@ -91,7 +89,7 @@ class CharactersViewModel @Inject constructor( is MainAction.LoadData -> { try { emit(MainResult.Loading) - val characters = mainRepository.fetchCharacters() + val characters = mainRepository.fetchCharacters().first() emit(MainResult.LoadingComplete(characters)) } catch (exception: Exception) { Timber.e(exception) @@ -102,7 +100,7 @@ class CharactersViewModel @Inject constructor( is MainAction.LoadDescription -> { try { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) - val character = mainRepository.fetchCharacter(action.characterId) + val character = mainRepository.fetchCharacter(action.characterId).first() delay(2000L) emit(MainResult.DescriptionResult.DescriptionLoadComplete(character.id, character.description)) } catch (exception : Exception) { @@ -120,15 +118,14 @@ class CharactersViewModel @Inject constructor( } private fun actionToResultTransformer(actionsFlow: Flow): Flow { - return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) + return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) .merge(loadDescriptionResult(actionsFlow.ofType(MainAction.LoadDescription::class.java)), pullToRefreshResult(actionsFlow.ofType(MainAction.PullToRefresh::class.java))) } - private fun loadDescriptionResult(actionsFlow: Flow): Flow = flow { - actionsFlow.flatMapMerge { action -> - flow { - emit(mainRepository.fetchCharacter(action.characterId)) } + private fun loadDescriptionResult(actionsFlow: Flow): Flow { + return actionsFlow.flatMapMerge { action -> + mainRepository.fetchCharacter(action.characterId) .delayEach(2000) .map { item -> MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult @@ -142,42 +139,34 @@ class CharactersViewModel @Inject constructor( } } - private fun pullToRefreshResult(actionsFlow: Flow): Flow = flow { - actionsFlow.flatMapMerge { - flow { - emit(MainResult.PullToRefreshing) - //This is done only to demonstrate RxJava-Coroutine interop - //We should use available coroutine API instead : - //val characters = mainRepository.fetchCharacters() - val characters = mainRepository.fetchCharactersSingle().await() - emit(MainResult.PullToRefreshComplete(characters)) + private fun pullToRefreshResult(actionsFlow: Flow): Flow = + actionsFlow.flatMapMerge { + mainRepository.fetchCharacters() + .map { + MainResult.PullToRefreshComplete(it) as MainResult + } + .onStart { emit(MainResult.PullToRefreshing) } + .flowOn(dispatchProvider.io()) + .catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.PullToRefreshError) + } } - .flowOn(dispatchProvider.io()) - .catch { exception -> - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.PullToRefreshError) - } - } - } - private fun loadingResult(actionsFlow: Flow): Flow = flow { - actionsFlow.flatMapMerge { - flow { - emit(MainResult.Loading) - val characters = mainRepository.fetchCharacters() - emit(MainResult.LoadingComplete(characters)) + private fun loadingResult(actionsFlow: Flow): Flow = + actionsFlow.flatMapMerge { + mainRepository.fetchCharacters() + .map{ MainResult.LoadingComplete(it) as MainResult} + .onStart { emit(MainResult.Loading) } + //uncommeent this to see unit test behavior with delays + .delayEach(1000) + .catch { exception -> + Timber.e(exception) + navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + emit(MainResult.LoadingError) + } } - //uncommeent this to see unit test behavior with delays - .delayEach(1000) - .catch { exception -> - Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) - emit(MainResult.LoadingError) - } - } - - } private fun reduce(state: MainViewState, result: MainResult): MainViewState { diff --git a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt index 1464803..0020b5d 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/helpers/FlowEx.kt @@ -1,9 +1,5 @@ package com.ragdroid.mvi.helpers -import io.reactivex.Flowable -import io.reactivex.functions.Predicate -import io.reactivex.internal.functions.Functions -import io.reactivex.internal.functions.ObjectHelper import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -61,14 +57,12 @@ fun Flow.merge3(other: Flow, } fun Flow.ofType(clazz: Class): Flow { - ObjectHelper.requireNonNull(clazz, "clazz is null") return filter { clazz.isInstance(it) }.cast(clazz) } fun Flow.cast(clazz: Class): Flow { - ObjectHelper.requireNonNull(clazz, "clazz is null") return map { clazz.cast(it) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index 201b938..a323853 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -55,12 +55,7 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re return pullToRefreshActionStream .observeOn(Schedulers.io()) .flatMap { ignored -> - //This is done only to demonstrate RxJava-Coroutine interop - //We should use available Rx API instead : - //val characters = mainRepository.fetchCharactersSingle() - rxSingle { - mainRepository.fetchCharacters() - }.toFlowable() + mainRepository.fetchCharactersSingle().toFlowable() .map { items -> MainResult.PullToRefreshComplete(items) as MainResult } .startWith(MainResult.PullToRefreshing) .onErrorReturn { error -> From 2fdb71e1c53bd57822cb69c5d7e54950144b9d69 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Sat, 24 Aug 2019 01:09:10 -0400 Subject: [PATCH 22/24] finalize demo --- buildSrc/src/main/java/Dependencies.kt | 2 +- .../com/ragdroid/data/MainRepositoryImpl.kt | 14 +++-- .../java/com/ragdroid/mvvmi/core/MviView.kt | 3 - .../com/ragdroid/mvvmi/core/MviViewModel.kt | 11 ++-- .../mvi/characters/CharactersFragment.kt | 9 ++- .../mvi/characters/CharactersViewModel.kt | 60 +++++++++---------- .../com/ragdroid/mvi/main/MainActivity.kt | 12 ++-- .../com/ragdroid/mvi/main/MainFragment.kt | 14 ++--- .../mvi/viewmodel/MainFragmentViewModel.kt | 12 ++-- .../mvi/characters/CharactersViewModelTest.kt | 16 +++-- .../viewmodel/MainFragmentViewModelTest.kt | 2 +- 11 files changed, 77 insertions(+), 78 deletions(-) diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 58a6ac7..0ecb171 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -1,7 +1,7 @@ object Versions { val kotlin_version = "1.3.30" - val kotlinx_coroutines = "1.3.0-RC" + val kotlinx_coroutines = "1.3.0" val compileSdk = 28 val minSdk = 21 diff --git a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt index aa73f78..c23cd59 100644 --- a/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt +++ b/data/src/main/java/com/ragdroid/data/MainRepositoryImpl.kt @@ -21,7 +21,7 @@ class MainRepositoryImpl @Inject constructor( private val config: AppConfig, private val helpers: Helpers): MainRepository { - override suspend fun fetchCharacters(): Flow> = flow { + override suspend fun fetchCharacters(): List { val timeStamp = System.currentTimeMillis() val charactersWrapper = marvelApi.getCharacters( config.publicKey, @@ -34,7 +34,7 @@ class MainRepositoryImpl @Inject constructor( .map { characterMapper.map(it) }.toList() - emit(characters) + return characters } override fun fetchCharactersSingle(): Single> { @@ -82,7 +82,7 @@ class MainRepositoryImpl @Inject constructor( ) } - override suspend fun fetchCharacter(id: Long): Flow = flow { + override suspend fun fetchCharacter(id: Long): CharacterMarvel { val timeStamp = System.currentTimeMillis() val characterWrapper = marvelApi.getCharacter( id, @@ -94,7 +94,7 @@ class MainRepositoryImpl @Inject constructor( .map { characterMapper.map(it) }.getOrNull(0) ?: throw IllegalAccessException("Character for id $id not found") - emit(results) + return results } @@ -104,7 +104,9 @@ class MainRepositoryImpl @Inject constructor( interface MainRepository { fun fetchCharactersSingle(): Single> - suspend fun fetchCharacters(): Flow> - suspend fun fetchCharacter(id: Long): Flow fun fetchCharacterSingle(id: Long): Single + + + suspend fun fetchCharacters(): List + suspend fun fetchCharacter(id: Long): CharacterMarvel } diff --git a/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviView.kt b/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviView.kt index 2d04f60..b982522 100644 --- a/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviView.kt +++ b/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviView.kt @@ -58,9 +58,6 @@ interface MviView { fun onMviViewCreated(savedInstanceState: Bundle?) { subscribeToViewState() subscribeToNavigationState() - if (savedInstanceState == null) { - viewModel.processActions(provideActions()) - } } /** diff --git a/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviViewModel.kt b/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviViewModel.kt index 12714e1..4923b91 100644 --- a/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviViewModel.kt +++ b/mvvmi/src/main/java/com/ragdroid/mvvmi/core/MviViewModel.kt @@ -20,7 +20,7 @@ import timber.log.Timber abstract class MviViewModel(initialState: State) : ViewModel() { private val stateProcessor: PublishProcessor = PublishProcessor.create() - protected val actionsProcessor: PublishProcessor = PublishProcessor.create() + private val actionsProcessor: PublishProcessor = PublishProcessor.create() protected var currentState: State = initialState private set @@ -55,12 +55,9 @@ abstract class MviViewModel): Flowable @CallSuper - fun processActions(actions: Flowable) { - actionsProcessor.mergeWith(actions) - .doOnNext { - Timber.v("onAction $it") - } - .compose(actionToResultTransformer) + fun processActions() { + + actionsToResultTransformer(actionsProcessor) .doOnNext { Timber.v("onResult $it") } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt index 19edeaf..1cbe3e6 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersFragment.kt @@ -61,14 +61,17 @@ class CharactersFragment : DaggerFragment(), binding.listView.adapter = adapter binding.listView.itemAnimator = FadeInAnimator() binding.listView.addItemDecoration(decoration) - setupViewModel() + setupViewModel(savedInstanceState) } - private fun setupViewModel() { + private fun setupViewModel(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(CharactersViewModel::class.java) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { render(it) }) viewModel.navigationLiveData.observe(viewLifecycleOwner, Observer { navigate(it) }) - viewModel.onAction(MainAction.LoadData) + if (savedInstanceState == null) { + viewModel.processActions() + viewModel.onAction(MainAction.LoadData) + } binding.refreshLayout.setOnRefreshListener { viewModel.onAction(MainAction.PullToRefresh) } diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 4baf1ef..56a654c 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -5,24 +5,21 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ragdroid.data.MainRepository +import com.ragdroid.data.entity.CharacterMarvel import com.ragdroid.mvi.base.ResourceProvider import com.ragdroid.mvi.helpers.DispatchProvider import com.ragdroid.mvi.helpers.merge -import com.ragdroid.mvi.helpers.mergeWith import com.ragdroid.mvi.helpers.ofType import com.ragdroid.mvi.main.MainAction import com.ragdroid.mvi.main.MainNavigation import com.ragdroid.mvi.main.MainResult import com.ragdroid.mvi.main.MainViewState -import hu.akarnokd.kotlin.flow.publish import kotlinx.coroutines.* import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.rx2.await import timber.log.Timber import java.lang.Exception import javax.inject.Inject -import kotlin.coroutines.CoroutineContext @ExperimentalCoroutinesApi class CharactersViewModel @Inject constructor( @@ -31,10 +28,6 @@ class CharactersViewModel @Inject constructor( private val dispatchProvider: DispatchProvider ): ViewModel() { - init { - processActions() - } - fun onAction(action: MainAction) = broadcastChannel.offer(action) //we can also use kotlin-flow-extensions `PublishSubject` here @@ -52,11 +45,12 @@ class CharactersViewModel @Inject constructor( fun navigate(navigationState: MainNavigation) = _navigationLiveData.postValue(navigationState) - private fun processActions() { + fun processActions() { viewModelScope.launch { actionToResultTransformer(actionsFlow) - //to demonstrate non-streams solution +// actionsFlow +// //to demonstrate non-streams solution // .flatMapMerge { // actionToResultFlow(it) // } @@ -78,7 +72,7 @@ class CharactersViewModel @Inject constructor( is MainAction.PullToRefresh -> { try { emit(MainResult.PullToRefreshing) - val characters = mainRepository.fetchCharacters().first() + val characters = mainRepository.fetchCharacters() emit(MainResult.PullToRefreshComplete(characters)) } catch (exception: Exception) { Timber.e(exception) @@ -89,7 +83,7 @@ class CharactersViewModel @Inject constructor( is MainAction.LoadData -> { try { emit(MainResult.Loading) - val characters = mainRepository.fetchCharacters().first() + val characters = mainRepository.fetchCharacters() emit(MainResult.LoadingComplete(characters)) } catch (exception: Exception) { Timber.e(exception) @@ -100,7 +94,7 @@ class CharactersViewModel @Inject constructor( is MainAction.LoadDescription -> { try { emit(MainResult.DescriptionResult.DescriptionLoading(action.characterId)) - val character = mainRepository.fetchCharacter(action.characterId).first() + val character = mainRepository.fetchCharacter(action.characterId) delay(2000L) emit(MainResult.DescriptionResult.DescriptionLoadComplete(character.id, character.description)) } catch (exception : Exception) { @@ -118,15 +112,19 @@ class CharactersViewModel @Inject constructor( } private fun actionToResultTransformer(actionsFlow: Flow): Flow { - return loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) - .merge(loadDescriptionResult(actionsFlow.ofType(MainAction.LoadDescription::class.java)), - pullToRefreshResult(actionsFlow.ofType(MainAction.PullToRefresh::class.java))) + return actionsFlow.flatMapMerge { + loadingResult(actionsFlow.ofType(MainAction.LoadData::class.java)) + .merge(loadDescriptionResult(actionsFlow.ofType(MainAction.LoadDescription::class.java)), + pullToRefreshResult(actionsFlow.ofType(MainAction.PullToRefresh::class.java))) + } } private fun loadDescriptionResult(actionsFlow: Flow): Flow { return actionsFlow.flatMapMerge { action -> - mainRepository.fetchCharacter(action.characterId) - .delayEach(2000) + flow { + mainRepository.fetchCharacter(action.characterId) + } + .onEach { delay(2000) } .map { item -> MainResult.DescriptionResult.DescriptionLoadComplete(item.id, item.description) as MainResult } @@ -141,26 +139,27 @@ class CharactersViewModel @Inject constructor( private fun pullToRefreshResult(actionsFlow: Flow): Flow = actionsFlow.flatMapMerge { - mainRepository.fetchCharacters() - .map { - MainResult.PullToRefreshComplete(it) as MainResult - } - .onStart { emit(MainResult.PullToRefreshing) } - .flowOn(dispatchProvider.io()) + flow { + emit(MainResult.PullToRefreshing) + val characters = mainRepository.fetchCharacters() + emit(MainResult.PullToRefreshComplete(characters)) + } .catch { exception -> Timber.e(exception) - navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) + navigate(MainNavigation.Snackbar(exception.message + ?: "Unknown Error")) emit(MainResult.PullToRefreshError) } } private fun loadingResult(actionsFlow: Flow): Flow = actionsFlow.flatMapMerge { - mainRepository.fetchCharacters() - .map{ MainResult.LoadingComplete(it) as MainResult} - .onStart { emit(MainResult.Loading) } - //uncommeent this to see unit test behavior with delays - .delayEach(1000) + flow { + emit(MainResult.Loading) + val characters = mainRepository.fetchCharacters() + emit(MainResult.LoadingComplete(characters)) + } + .onEach { delay(1000) } .catch { exception -> Timber.e(exception) navigate(MainNavigation.Snackbar(exception.message ?: "Unknown Error")) @@ -172,4 +171,5 @@ class CharactersViewModel @Inject constructor( private fun reduce(state: MainViewState, result: MainResult): MainViewState { return state.reduce(result, resourceProvider) } + } \ No newline at end of file diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt index c15101c..30444d6 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainActivity.kt @@ -31,11 +31,15 @@ class MainActivity : DaggerAppCompatActivity() { private fun openFragment() { val fragmentManager = supportFragmentManager - val fragmentTransaction = fragmentManager.beginTransaction() + + val fragment = supportFragmentManager.findFragmentByTag("fraggy") + if (fragment == null) { + val fragmentTransaction = fragmentManager.beginTransaction() // val fraggy = MainFragment() - val fraggy = CharactersFragment() - fragmentTransaction.replace(R.id.fragment_container, fraggy) - fragmentTransaction.commit() + val fraggy = CharactersFragment() + fragmentTransaction.replace(R.id.fragment_container, fraggy, "fraggy") + fragmentTransaction.commit() + } } } diff --git a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt index 86999b7..03f814a 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/main/MainFragment.kt @@ -67,16 +67,18 @@ class MainFragment : DaggerFragment(), super.onMviViewCreated(savedInstanceState) } - override fun provideActions() = Flowable.merge(loadingIntent(), pullToRefreshIntent(), loadDescription()) - private fun setupViewModel() { viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) + binding.refreshLayout.setOnRefreshListener { + viewModel.onAction(MainAction.PullToRefresh) + } + viewModel.onAction(MainAction.LoadData) } private val descriptionClickProcessor = PublishProcessor.create() override fun onCharacterDescriptionClicked(itemId: Long) { - descriptionClickProcessor.onNext(MainAction.LoadDescription(itemId)) + viewModel.onAction(MainAction.LoadDescription(itemId)) } override fun getItemPresenter(): CharacterItemPresenter { @@ -84,12 +86,6 @@ class MainFragment : DaggerFragment(), } - private fun pullToRefreshIntent(): Flowable = - binding.refreshLayout.refreshes() - .toFlowable(BackpressureStrategy.DROP) - .map { MainAction.PullToRefresh } - - private fun loadingIntent(): Flowable = Flowable.just(MainAction.LoadData) private fun loadDescription(): Flowable { diff --git a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt index a323853..a1bec71 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModel.kt @@ -19,13 +19,15 @@ class MainFragmentViewModel @Inject constructor(private val resourceProvider: Re private val mainRepository: MainRepository): MviViewModel(MainViewState.init()) { + init { + processActions() + } + override fun actionsToResultTransformer(actions: Flowable): Flowable = - actions.publish { shared -> - Flowable.merge(loadingResult(shared.ofType(MainAction.LoadData::class.java)), - loadDescriptionResult(shared.ofType(MainAction.LoadDescription::class.java)), - pullToRefreshResult(shared.ofType(MainAction.PullToRefresh::class.java))) + Flowable.merge(loadingResult(actions.ofType(MainAction.LoadData::class.java)), + loadDescriptionResult(actions.ofType(MainAction.LoadDescription::class.java)), + pullToRefreshResult(actions.ofType(MainAction.PullToRefresh::class.java))) - } override fun reduce(previousState: MainViewState, result: MainResult): MainViewState { return previousState.reduce(result, resourceProvider) diff --git a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt index d3255b8..8e181d9 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/characters/CharactersViewModelTest.kt @@ -25,7 +25,7 @@ class CharactersViewModelTest: BaseUnitTest() { val mainRepository: MainRepository = mock() { onBlocking { fetchCharacters() } doReturn TestDataFactory.mockCharacters - onBlocking { fetchCharactersSingle() } doReturn Single.just(TestDataFactory.mockCharacters) + on { fetchCharactersSingle() } doReturn Single.just(TestDataFactory.mockCharacters) onBlocking { fetchCharacter(1234) } doReturn TestDataFactory.marcelCharacter1 } @@ -38,18 +38,18 @@ class CharactersViewModelTest: BaseUnitTest() { fun setUp() { super.setup() viewmodel = CharactersViewModel(mainRepository, resourceProvider, testDispatcherProvider) + viewmodel.processActions() } @Test fun testLoadData() { - val loadDataFlow = flowOf(MainAction.LoadData) - viewmodel.processActions(loadDataFlow) + viewmodel.onAction(MainAction.LoadData) -// testDispatcher.advanceTimeBy(2000) + testDispatcher.advanceTimeBy(2000L) - val state = viewmodel.stateLiveData().blockingObserve() + val state = viewmodel.stateLiveData.blockingObserve() println(state) @@ -60,11 +60,9 @@ class CharactersViewModelTest: BaseUnitTest() { @Test fun testPullToRefresh() { - val loadDataFlow = flowOf(MainAction.PullToRefresh) + viewmodel.onAction(MainAction.PullToRefresh) - viewmodel.processActions(loadDataFlow) - - val state = viewmodel.stateLiveData().blockingObserve() + val state = viewmodel.stateLiveData.blockingObserve() println(state) diff --git a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt index cb13190..a6440d3 100644 --- a/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt +++ b/presentation/src/test/java/com/ragdroid/mvi/viewmodel/MainFragmentViewModelTest.kt @@ -38,7 +38,7 @@ class MainFragmentViewModelTest: BaseUnitTest() { val subscriber = viewmodel.stateFlowable().test() - viewmodel.processActions(Flowable.just(MainAction.LoadData)) + viewmodel.onAction(MainAction.LoadData) testScheduler.triggerActions() testScheduler.advanceTimeBy(3000, TimeUnit.MILLISECONDS) From 2b13d3d08f3bb066d0b72c7911a2b994eb5e9cb0 Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Thu, 24 Oct 2019 00:06:32 +0100 Subject: [PATCH 23/24] fix description --- .../java/com/ragdroid/mvi/characters/CharactersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt index 56a654c..1178da5 100644 --- a/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt +++ b/presentation/src/main/java/com/ragdroid/mvi/characters/CharactersViewModel.kt @@ -122,7 +122,7 @@ class CharactersViewModel @Inject constructor( private fun loadDescriptionResult(actionsFlow: Flow): Flow { return actionsFlow.flatMapMerge { action -> flow { - mainRepository.fetchCharacter(action.characterId) + emit(mainRepository.fetchCharacter(action.characterId)) } .onEach { delay(2000) } .map { item -> From ef2ff0badcd9abedb36884552ba8930fc1c206fc Mon Sep 17 00:00:00 2001 From: Garima Jain Date: Thu, 24 Oct 2019 00:10:34 +0100 Subject: [PATCH 24/24] update co routines --- build.gradle | 2 +- buildSrc/src/main/java/Dependencies.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 8f43303..280474c 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0-rc01' + classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_version}" classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin_version}" diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 0ecb171..ec8cb0f 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -1,7 +1,7 @@ object Versions { val kotlin_version = "1.3.30" - val kotlinx_coroutines = "1.3.0" + val kotlinx_coroutines = "1.3.2" val compileSdk = 28 val minSdk = 21 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cc7f20e..75ec518 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Nov 10 18:33:46 IST 2017 +#Thu Oct 24 04:38:58 IST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip