From bade7dce7b327c0a861591ade446acc8a65ea632 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Mon, 11 Oct 2021 07:45:46 +0300 Subject: [PATCH 01/47] # splash screen on compose --- app/build.gradle | 18 ++++ .../mdgd/pokemon/ui/splash/SplashFragment.kt | 85 +++++++++++++++++-- app/src/main/res/layout/fragment_splash.xml | 62 +++++++------- app/src/main/res/values/strings.xml | 1 + build.gradle | 5 +- 5 files changed, 134 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 16aeb75..2c46b7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,6 +20,14 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + kotlinOptions { + jvmTarget = "1.8" + } + + buildFeatures { + compose true + } + buildTypes { release { minifyEnabled false @@ -39,6 +47,16 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "androidx.multidex:multidex:2.0.1" + implementation "androidx.compose.runtime:runtime:$composeVersion" + implementation "androidx.compose.ui:ui:$composeVersion" + implementation "androidx.compose.foundation:foundation:$composeVersion" + implementation "androidx.compose.foundation:foundation-layout:$composeVersion" + implementation "androidx.compose.material:material:$composeVersion" + implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" + implementation "androidx.compose.ui:ui-tooling:$composeVersion" + implementation "com.google.android.material:compose-theme-adapter:$composeVersion" + + implementation "androidx.appcompat:appcompat:$compat" implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'androidx.recyclerview:recyclerview:1.2.1' diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index c78de0d..76700b4 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -1,13 +1,25 @@ package com.mdgd.pokemon.ui.splash +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import androidx.work.WorkRequest import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.PokemonsApp import com.mdgd.pokemon.R @@ -15,16 +27,30 @@ import com.mdgd.pokemon.bg.UploadWorker import com.mdgd.pokemon.ui.splash.state.SplashScreenAction import com.mdgd.pokemon.ui.splash.state.SplashScreenState -class SplashFragment : HostedFragment(), SplashContract.View { +class SplashFragment : + HostedFragment(), + SplashContract.View { override fun createModel(): SplashContract.ViewModel { - return ViewModelProvider(this, SplashViewModelFactory(PokemonsApp.instance?.appComponent!!)).get(SplashViewModel::class.java) + return ViewModelProvider( + this, SplashViewModelFactory(PokemonsApp.instance?.appComponent!!) + ).get(SplashViewModel::class.java) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { return inflater.inflate(R.layout.fragment_splash, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + view.findViewById(R.id.splash_compose_root)?.setContent { + SplashScreen() + } + } + override fun proceedToNextScreen() { if (hasHost()) { fragmentHost!!.proceedToPokemonsScreen() @@ -33,8 +59,8 @@ class SplashFragment : HostedFragment - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + - + - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f0ab85..32a08b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,5 @@ Some error happened Details: Oooops!\nThere is no pokemons for some reason + splash logo diff --git a/build.gradle b/build.gradle index 3e08e48..816012a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.4.32' // '1.5.21' ext.lifecycle_ktx = "2.3.1" ext.nav_version = "2.3.5" ext.work_version = "2.6.0" @@ -17,9 +17,10 @@ buildscript { ext.espresso = "3.4.0" ext.ktx = "1.6.0" ext.coroutins = "1.5.0" + ext.composeVersion = "1.0.3" project.ext { - min = 20 + min = 21 target = 30 compile = 30 tools = "30.0.2" From 84cefc2ca9d68c934ca1a314db10e3e710d0849b Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Mon, 11 Oct 2021 09:17:41 +0300 Subject: [PATCH 02/47] # error screen on compose + flows in cache --- .../com/mdgd/pokemon/bg/LoadPokemonsModel.kt | 16 ++--- .../mdgd/pokemon/ui/error/ErrorFragment.kt | 52 ++++++++------ .../com/mdgd/pokemon/ui/error/ErrorScreen.kt | 72 +++++++++++++++++++ .../mdgd/pokemon/ui/splash/SplashFragment.kt | 44 +++++++++--- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 26 +++++-- app/src/main/res/layout/activity_main.xml | 32 ++------- app/src/main/res/layout/fragment_splash.xml | 33 +-------- app/src/main/res/values/styles.xml | 2 +- .../com/mdgd/pokemon/models/cache/Cache.kt | 6 +- .../com/mdgd/pokemon/models/infra/Result.kt | 17 ++--- .../pokemon/models_impl/cache/CacheImpl.kt | 14 ++-- 11 files changed, 184 insertions(+), 130 deletions(-) create mode 100644 app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt diff --git a/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsModel.kt b/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsModel.kt index 192124a..fc48c31 100644 --- a/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsModel.kt @@ -8,10 +8,8 @@ import kotlinx.coroutines.* class LoadPokemonsModel(private val repo: PokemonsRepo, private val cache: Cache) : ServiceModel { private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val exceptionHandler = CoroutineExceptionHandler { _, e -> - coroutineScope.launch { - cache.putLoadingProgress(Result(e)) - cancelScope() - } + cache.putLoadingProgress(Result(e)) + coroutineScope.cancel() } override fun load() { @@ -20,13 +18,9 @@ class LoadPokemonsModel(private val repo: PokemonsRepo, private val cache: Cache repo.loadInitialPages(initialAmount) cache.putLoadingProgress(Result(initialAmount)) - cache.putLoadingProgress(Result(repo.loadPokemons(initialAmount))) - cancelScope() + val newAmount = repo.loadPokemons(initialAmount) + cache.putLoadingProgress(Result(newAmount)) + coroutineScope.cancel() } } - - private suspend fun cancelScope() { - delay(50) - coroutineScope.cancel() - } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorFragment.kt index c0804aa..16dbd04 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorFragment.kt @@ -15,6 +15,30 @@ class ErrorFragment : MessageDialog< ErrorContract.ViewModel, ErrorContract.Host>(), ErrorContract.View, DialogInterface.OnClickListener { + + companion object { + fun newInstance(title: String?, message: String?): ErrorFragment { + val b = Bundle() + b.putString(KEY_TITLE_STR, title) + b.putString(KEY_MSG_STR, message) + b.putInt(KEY_TYPE, TYPE_STR) + val errorFragment = ErrorFragment() + errorFragment.arguments = b + return errorFragment + } + + fun newInstance(title: Int, message: Int): ErrorFragment { + val b = Bundle() + b.putInt(KEY_TITLE, title) + b.putInt(KEY_MSG, message) + b.putInt(KEY_TYPE, TYPE_INT) + val errorFragment = ErrorFragment() + errorFragment.arguments = b + return errorFragment + } + } + + private var error: Throwable? = null override fun createModel(): ErrorContract.ViewModel? { @@ -38,8 +62,12 @@ class ErrorFragment : MessageDialog< builder.setMessage(message) } } else if (TYPE_STR == type) { + val message = args.getString( + KEY_MSG_STR, + "" + ) + if (error == null) "" else " " + error!!.message + builder.setTitle(args.getString(KEY_TITLE_STR, "")) - val message = args.getString(KEY_MSG_STR, "") + if (error == null) "" else " " + error!!.message builder.setMessage(message) } } @@ -58,26 +86,4 @@ class ErrorFragment : MessageDialog< fun setError(error: Throwable?) { this.error = error } - - companion object { - fun newInstance(title: String?, message: String?): ErrorFragment { - val b = Bundle() - b.putString(KEY_TITLE_STR, title) - b.putString(KEY_MSG_STR, message) - b.putInt(KEY_TYPE, TYPE_STR) - val errorFragment = ErrorFragment() - errorFragment.arguments = b - return errorFragment - } - - fun newInstance(title: Int, message: Int): ErrorFragment { - val b = Bundle() - b.putInt(KEY_TITLE, title) - b.putInt(KEY_MSG, message) - b.putInt(KEY_TYPE, TYPE_INT) - val errorFragment = ErrorFragment() - errorFragment.arguments = b - return errorFragment - } - } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt new file mode 100644 index 0000000..dbb8ae5 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt @@ -0,0 +1,72 @@ +package com.mdgd.pokemon.ui.error + +import android.content.res.Configuration +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun ErrorScreen( + trigger: MutableState, title: MutableState, message: MutableState +) { + if (trigger.value) { + MaterialTheme { + AlertDialog( + title = { + Text(text = title.value) + }, + text = { + Text(text = message.value) + }, + confirmButton = { + Button( + onClick = { + trigger.value = false + }, + ) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + onDismissRequest = { + // Dismiss the dialog when the user clicks outside the dialog or on the back + // button. If you want to disable that functionality, simply use an empty + // onCloseRequest. + trigger.value = false + }, + ) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + name = "Light Mode" +) +@Composable +fun ErrorPreviewThemeLight() { + MaterialTheme { + ErrorScreen(mutableStateOf(true), mutableStateOf("Title"), mutableStateOf("Message")) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun ErrorPreviewThemeDark() { + MaterialTheme { + ErrorScreen( + mutableStateOf(true), mutableStateOf("Title"), mutableStateOf("Message") + ) + } +} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index 76700b4..e1487d5 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -6,17 +6,22 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager @@ -24,6 +29,7 @@ import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.PokemonsApp import com.mdgd.pokemon.R import com.mdgd.pokemon.bg.UploadWorker +import com.mdgd.pokemon.ui.error.ErrorScreen import com.mdgd.pokemon.ui.splash.state.SplashScreenAction import com.mdgd.pokemon.ui.splash.state.SplashScreenState @@ -31,6 +37,10 @@ class SplashFragment : HostedFragment(), SplashContract.View { + private val errorDialogTrigger = mutableStateOf(false) + private val errorTitle = mutableStateOf("") + private val errorMessage = mutableStateOf("") + override fun createModel(): SplashContract.ViewModel { return ViewModelProvider( this, SplashViewModelFactory(PokemonsApp.instance?.appComponent!!) @@ -45,9 +55,8 @@ class SplashFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.findViewById(R.id.splash_compose_root)?.setContent { - SplashScreen() + SplashScreen(errorDialogTrigger, errorTitle, errorMessage) } } @@ -65,14 +74,26 @@ class SplashFragment : } override fun showError(error: Throwable?) { - if (hasHost()) { - fragmentHost!!.showError(error) + errorTitle.value = getString(R.string.dialog_error_title) + errorMessage.value = error?.let { + getString(R.string.dialog_error_message) + " " + error.message + } ?: kotlin.run { + getString(R.string.dialog_error_message) } + errorDialogTrigger.value = true } } @Composable -fun SplashScreen() { +fun SplashScreen( + errorTrigger: MutableState, + errorTitle: MutableState, + errorMessage: MutableState +) { + val errorDialogTrigger = remember { errorTrigger } + val errorDialogTitle = remember { errorTitle } + val errorDialogMessage = remember { errorMessage } + MaterialTheme { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -84,11 +105,14 @@ fun SplashScreen() { Image( painter = painterResource(R.drawable.logo_splash), contentDescription = stringResource(R.string.screen_splash_logo), + modifier = Modifier.weight(3f) ) Text( + style = MaterialTheme.typography.h5, text = stringResource(R.string.screen_splash_advertisement), - modifier = Modifier.padding(60.dp) + modifier = Modifier.weight(1f) ) + ErrorScreen(errorDialogTrigger, errorDialogTitle, errorDialogMessage) } } } @@ -102,7 +126,7 @@ fun SplashScreen() { @Composable fun PreviewThemeLight() { MaterialTheme { - SplashScreen() + SplashScreen(mutableStateOf(true), mutableStateOf("Title"), mutableStateOf("Message")) } } @@ -114,6 +138,6 @@ fun PreviewThemeLight() { @Composable fun PreviewThemeDark() { MaterialTheme { - SplashScreen() + SplashScreen(mutableStateOf(true), mutableStateOf("Title"), mutableStateOf("Message")) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 4efc655..3c88c8f 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -5,14 +5,20 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.models.cache.Cache +import com.mdgd.pokemon.models.infra.Result import com.mdgd.pokemon.ui.splash.state.SplashScreenAction import com.mdgd.pokemon.ui.splash.state.SplashScreenState import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -class SplashViewModel(private val cache: Cache) : MviViewModel(), SplashContract.ViewModel { +class SplashViewModel(private val cache: Cache) : + MviViewModel(), + SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> setAction(SplashScreenAction.ShowError(e)) @@ -24,12 +30,18 @@ class SplashViewModel(private val cache: Cache) : MviViewModel -> + result // Result(Throwable("Dummy")) + }.collect { + if (it.isError()) { + setAction(SplashScreenAction.ShowError(it.getError())) + } else if (it.getValue() != 0L) { + setAction(SplashScreenAction.NextScreen) + } } } setAction(SplashScreenAction.LaunchWorker) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c9481f2..e017ab7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,29 +1,9 @@ - - - - - - - - - - - - - - + app:defaultNavHost="true" + app:navGraph="@navigation/navigation_graph" /> diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index 446ed53..f113d45 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -1,35 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 30a2b63..f57685a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,8 +1,13 @@ - #6200EE - #3700B3 - #03DAC5 - #000 + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + + @color/purple_700 #737373 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index a1bc11d..0000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1ffbd64 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + From 4b6b55b9097770e2a7dc3e5776bb9c04ec65de41 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 07:43:12 +0300 Subject: [PATCH 19/47] # unused libraries removed --- app/build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 365eedd..2ae5aaf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,12 +58,7 @@ dependencies { implementation "androidx.compose.ui:ui-tooling:$composeVersion" implementation "com.google.android.material:compose-theme-adapter:$composeVersion" - implementation "androidx.appcompat:appcompat:$compat" - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' - implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.4.0' // swipe-refresh implementation "com.google.accompanist:accompanist-swiperefresh:0.19.0" @@ -72,9 +67,6 @@ dependencies { implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" - // images - implementation 'com.squareup.picasso:picasso:2.71828' - // image loading implementation 'io.coil-kt:coil-compose:1.3.0' From 3ca764a237ca9a632b83ce3238cf499ba4922693 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 09:50:57 +0300 Subject: [PATCH 20/47] # fucking hilt --- app/build.gradle | 23 ++++++-- .../mdgd/pokemon/ExampleInstrumentedTest.java | 6 +- app/src/main/AndroidManifest.xml | 8 ++- .../main/java/com/mdgd/pokemon/PokemonsApp.kt | 26 +++++---- .../pokemon/bg/PokemonsLoadingModelFactory.kt | 10 ---- .../java/com/mdgd/pokemon/bg/UploadWorker.kt | 14 ++++- .../java/com/mdgd/pokemon/ui/MainActivity.kt | 10 ++-- .../ui/pokemon/PokemonDetailsFragment.kt | 10 ++-- .../ui/pokemon/PokemonDetailsViewModel.kt | 8 ++- .../pokemon/PokemonDetailsViewModelFactory.kt | 14 ----- .../pokemon/ui/pokemons/PokemonsFragment.kt | 10 ++-- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 7 ++- .../ui/pokemons/PokemonsViewModelFactory.kt | 15 ----- .../mdgd/pokemon/ui/splash/SplashFragment.kt | 10 ++-- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 5 +- .../ui/splash/SplashViewModelFactory.kt | 14 ----- .../ui/pokemons/PokemonsViewModelTest.kt | 2 +- build.gradle | 8 ++- gradle.properties | 7 +++ .../java/com/mdgd/pokemon/models/AppModule.kt | 17 ------ .../pokemon/models}/util/DispatchersHolder.kt | 2 +- models_impl/build.gradle | 22 ++++++-- .../pokemon/models_impl/DefaultAppModule.kt | 55 +++++++++++-------- .../models_impl/repo/PokemonsRepository.kt | 5 +- .../models_impl/repo/dao/PokemonsDaoImpl.kt | 7 ++- .../repo/network/PokemonsNetwork.kt | 14 +++-- .../models_impl/util/DispatchersHolderImpl.kt | 11 ++++ .../mdgd/mvi/util/DispatchersHolderImpl.kt | 15 ----- 28 files changed, 186 insertions(+), 169 deletions(-) delete mode 100644 app/src/main/java/com/mdgd/pokemon/bg/PokemonsLoadingModelFactory.kt delete mode 100644 app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelFactory.kt delete mode 100644 app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelFactory.kt delete mode 100644 app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModelFactory.kt delete mode 100644 models/src/main/java/com/mdgd/pokemon/models/AppModule.kt rename {mvi/src/main/java/com/mdgd/mvi => models/src/main/java/com/mdgd/pokemon/models}/util/DispatchersHolder.kt (80%) create mode 100644 models_impl/src/main/java/com/mdgd/pokemon/models_impl/util/DispatchersHolderImpl.kt delete mode 100644 mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolderImpl.kt diff --git a/app/build.gradle b/app/build.gradle index 2ae5aaf..d874989 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ plugins { id 'kotlin-android' id 'kotlin-kapt' id 'androidx.navigation.safeargs.kotlin' + id 'dagger.hilt.android.plugin' } android { @@ -21,7 +22,7 @@ android { } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_1_8.toString() } buildFeatures { @@ -73,6 +74,13 @@ dependencies { // json implementation "com.google.code.gson:gson:$gson" + // hilt + implementation("com.google.dagger:hilt-android:2.38.1") + kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation("androidx.hilt:hilt-navigation-fragment:$hilt_jetpack") + kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") + implementation 'androidx.hilt:hilt-work:1.0.0' + // lifecycle implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ktx" @@ -80,15 +88,14 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" testImplementation "junit:junit:$junit" - testImplementation "org.mockito:mockito-core:3.7.0" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - testImplementation "android.arch.core:core-testing:1.1.1" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0" + testImplementation "org.mockito:mockito-core:$mockito_core" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin" + testImplementation "android.arch.core:core-testing:$testing_core" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$testing_coroutine" testImplementation "com.google.code.gson:gson:$gson" androidTestImplementation "androidx.test.ext:junit:$junit_android" androidTestImplementation "androidx.test.espresso:espresso-core:$espresso" - androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.3.0" @@ -96,3 +103,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" } + +kapt{ + correctErrorTypes true +} diff --git a/app/src/androidTest/java/com/mdgd/pokemon/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/mdgd/pokemon/ExampleInstrumentedTest.java index 9102944..98ee159 100644 --- a/app/src/androidTest/java/com/mdgd/pokemon/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/mdgd/pokemon/ExampleInstrumentedTest.java @@ -1,15 +1,15 @@ package com.mdgd.pokemon; +import static org.junit.Assert.assertEquals; + import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.*; - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 25a0d43..7b564ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -7,7 +8,7 @@ + + diff --git a/app/src/main/java/com/mdgd/pokemon/PokemonsApp.kt b/app/src/main/java/com/mdgd/pokemon/PokemonsApp.kt index 9678c3a..79237ae 100644 --- a/app/src/main/java/com/mdgd/pokemon/PokemonsApp.kt +++ b/app/src/main/java/com/mdgd/pokemon/PokemonsApp.kt @@ -1,22 +1,28 @@ package com.mdgd.pokemon +import androidx.hilt.work.HiltWorkerFactory import androidx.multidex.MultiDexApplication -import com.mdgd.pokemon.models.AppModule -import com.mdgd.pokemon.models_impl.DefaultAppModule +import androidx.work.Configuration +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject -class PokemonsApp : MultiDexApplication() { +@HiltAndroidApp +class PokemonsApp : MultiDexApplication(), Configuration.Provider { - var appComponent: AppModule? = null - private set + companion object { + var instance: PokemonsApp? = null + private set + } + + @Inject + lateinit var workerFactory: HiltWorkerFactory override fun onCreate() { super.onCreate() instance = this - appComponent = DefaultAppModule(this) } - companion object { - var instance: PokemonsApp? = null - private set - } + override fun getWorkManagerConfiguration() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() } diff --git a/app/src/main/java/com/mdgd/pokemon/bg/PokemonsLoadingModelFactory.kt b/app/src/main/java/com/mdgd/pokemon/bg/PokemonsLoadingModelFactory.kt deleted file mode 100644 index 89498a1..0000000 --- a/app/src/main/java/com/mdgd/pokemon/bg/PokemonsLoadingModelFactory.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mdgd.pokemon.bg - -import com.mdgd.pokemon.models.AppModule - -class PokemonsLoadingModelFactory(var appComponent: AppModule) { - - fun create(): ServiceModel { - return LoadPokemonsModel(appComponent.getPokemonsRepo(), appComponent.getCache()) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/bg/UploadWorker.kt b/app/src/main/java/com/mdgd/pokemon/bg/UploadWorker.kt index a539df4..6e90784 100644 --- a/app/src/main/java/com/mdgd/pokemon/bg/UploadWorker.kt +++ b/app/src/main/java/com/mdgd/pokemon/bg/UploadWorker.kt @@ -1,12 +1,20 @@ package com.mdgd.pokemon.bg import android.content.Context +import androidx.hilt.work.HiltWorker import androidx.work.Worker import androidx.work.WorkerParameters -import com.mdgd.pokemon.PokemonsApp.Companion.instance +import com.mdgd.pokemon.models.cache.Cache +import com.mdgd.pokemon.models.repo.PokemonsRepo +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject -class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) { - private val model: ServiceModel = PokemonsLoadingModelFactory(instance!!.appComponent!!).create() +@HiltWorker +class UploadWorker @AssistedInject constructor( + @Assisted context: Context, @Assisted params: WorkerParameters, + repo: PokemonsRepo, cache: Cache +) : Worker(context, params) { + private val model: ServiceModel = LoadPokemonsModel(repo, cache) override fun doWork(): Result { model.load() diff --git a/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt b/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt index bab0d35..faea836 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt @@ -10,7 +10,9 @@ import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract import com.mdgd.pokemon.ui.pokemon.PokemonDetailsFragmentArgs import com.mdgd.pokemon.ui.pokemons.PokemonsContract import com.mdgd.pokemon.ui.splash.SplashContract +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : HostActivity(), SplashContract.Host, PokemonsContract.Host, PokemonDetailsContract.Host { private var navController: NavController? = null @@ -26,14 +28,14 @@ class MainActivity : HostActivity(), SplashContract.Host, PokemonsContract.Host, override fun proceedToPokemonsScreen() { navController!!.navigate(R.id.action_splashFragment_to_pokemonsFragment, null, // doesn't work from xml... - NavOptions.Builder() - .setPopUpTo(R.id.splashFragment, true) - .build() + NavOptions.Builder() + .setPopUpTo(R.id.splashFragment, true) + .build() ) } override fun proceedToPokemonScreen(pokemonId: Long?) { navController!!.navigate(R.id.action_pokemonsFragment_to_pokemonDetailsFragment, PokemonDetailsFragmentArgs(pokemonId - ?: -1).toBundle()) + ?: -1).toBundle()) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index e42be58..c643d12 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -29,11 +29,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import coil.compose.rememberImagePainter import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment -import com.mdgd.pokemon.PokemonsApp.Companion.instance import com.mdgd.pokemon.R import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @@ -44,7 +43,9 @@ import com.mdgd.pokemon.ui.error.ErrorScreen import com.mdgd.pokemon.ui.pokemon.dto.* import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class PokemonDetailsFragment : HostedFragment< PokemonDetailsContract.View, PokemonDetailsScreenState, @@ -62,9 +63,8 @@ class PokemonDetailsFragment : HostedFragment< } override fun createModel(): PokemonDetailsContract.ViewModel { - return ViewModelProvider( - this, PokemonDetailsViewModelFactory(instance!!.appComponent!!) - ).get(PokemonDetailsViewModel::class.java) + val model: PokemonDetailsViewModel by viewModels() + return model } override fun onCreateView( diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index bdd8c90..10e59de 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -14,15 +14,19 @@ import com.mdgd.pokemon.models.repo.schemas.Type import com.mdgd.pokemon.ui.pokemon.dto.* import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.util.* +import javax.inject.Inject import kotlin.collections.ArrayList -class PokemonDetailsViewModel(private val repo: PokemonsRepo) - : MviViewModel(), PokemonDetailsContract.ViewModel { +@HiltViewModel +class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo) : + MviViewModel(), + PokemonDetailsContract.ViewModel { private val pokemonIdFlow = MutableStateFlow(-1L) private var pokemonLoadingJob: Job? = null diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelFactory.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelFactory.kt deleted file mode 100644 index d9325ab..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelFactory.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.mdgd.pokemon.models.AppModule - -class PokemonDetailsViewModelFactory(private val appComponent: AppModule) : ViewModelProvider.NewInstanceFactory() { - - override fun create(modelClass: Class): T { - return if (modelClass == PokemonDetailsViewModel::class.java) { - PokemonDetailsViewModel(appComponent.getPokemonsRepo()) as T - } else super.create(modelClass) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index 43ffe75..e04be8e 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -29,13 +29,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import coil.compose.rememberImagePainter import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment -import com.mdgd.pokemon.PokemonsApp import com.mdgd.pokemon.R import com.mdgd.pokemon.models.filters.FilterData import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema @@ -46,8 +45,10 @@ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +@AndroidEntryPoint class PokemonsFragment : HostedFragment< PokemonsContract.View, PokemonsScreenState, @@ -59,9 +60,8 @@ class PokemonsFragment : HostedFragment< private val screenState = mutableStateOf(PokemonsUiState(isLoading = true)) override fun createModel(): PokemonsContract.ViewModel { - return ViewModelProvider( - this, PokemonsViewModelFactory(PokemonsApp.instance?.appComponent!!) - ).get(PokemonsViewModel::class.java) + val model: PokemonsViewModel by viewModels() + return model } override fun onCreateView( diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index 206a903..edd6e75 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -4,19 +4,22 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel -import com.mdgd.mvi.util.DispatchersHolder import com.mdgd.pokemon.models.filters.FilterData import com.mdgd.pokemon.models.filters.StatsFilter import com.mdgd.pokemon.models.repo.PokemonsRepo import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema +import com.mdgd.pokemon.models.util.DispatchersHolder import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import javax.inject.Inject -class PokemonsViewModel( +@HiltViewModel +class PokemonsViewModel @Inject constructor( private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelFactory.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelFactory.kt deleted file mode 100644 index ad9fe7e..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelFactory.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.mdgd.mvi.util.DispatchersHolderImpl -import com.mdgd.pokemon.models.AppModule - -class PokemonsViewModelFactory(private val appComponent: AppModule) : ViewModelProvider.NewInstanceFactory() { - - override fun create(modelClass: Class): T { - return if (modelClass == PokemonsViewModel::class.java) { - PokemonsViewModel(appComponent.getPokemonsRepo(), appComponent.getFiltersFactory(), DispatchersHolderImpl()) as T - } else super.create(modelClass) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index 4bc03e4..07db1bd 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -22,12 +22,11 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment -import com.mdgd.pokemon.PokemonsApp import com.mdgd.pokemon.R import com.mdgd.pokemon.bg.UploadWorker import com.mdgd.pokemon.ui.error.DefaultErrorParams @@ -35,7 +34,9 @@ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen import com.mdgd.pokemon.ui.splash.state.SplashScreenAction import com.mdgd.pokemon.ui.splash.state.SplashScreenState +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SplashFragment : HostedFragment(), SplashContract.View { @@ -43,9 +44,8 @@ class SplashFragment : private val errorDialogTrigger = mutableStateOf(DefaultErrorParams()) override fun createModel(): SplashContract.ViewModel { - return ViewModelProvider( - this, SplashViewModelFactory(PokemonsApp.instance?.appComponent!!) - ).get(SplashViewModel::class.java) + val model: SplashViewModel by viewModels() + return model } override fun onCreateView( diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 13be347..602108b 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -8,6 +8,7 @@ import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result import com.mdgd.pokemon.ui.splash.state.SplashScreenAction import com.mdgd.pokemon.ui.splash.state.SplashScreenState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -15,8 +16,10 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import javax.inject.Inject -class SplashViewModel(private val cache: Cache) : +@HiltViewModel +class SplashViewModel @Inject constructor(private val cache: Cache) : MviViewModel(), SplashContract.ViewModel { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModelFactory.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModelFactory.kt deleted file mode 100644 index fdd1fbb..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModelFactory.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mdgd.pokemon.ui.splash - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.mdgd.pokemon.models.AppModule - -class SplashViewModelFactory(private val appComponent: AppModule) : ViewModelProvider.NewInstanceFactory() { - - override fun create(modelClass: Class): T { - return if (modelClass == SplashViewModel::class.java) { - SplashViewModel(appComponent.getCache()) as T - } else super.create(modelClass) - } -} diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt index 529d4d0..5340269 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt @@ -3,7 +3,6 @@ package com.mdgd.pokemon.ui.pokemons import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer -import com.mdgd.mvi.util.DispatchersHolder import com.mdgd.pokemon.Mocks import com.mdgd.pokemon.TestSuit import com.mdgd.pokemon.models.filters.FilterData @@ -12,6 +11,7 @@ import com.mdgd.pokemon.models.repo.PokemonsRepo import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema import com.mdgd.pokemon.models.repo.schemas.Stat +import com.mdgd.pokemon.models.util.DispatchersHolder import com.mdgd.pokemon.models_impl.filters.StatsFiltersFactory import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState diff --git a/build.gradle b/build.gradle index a425fd4..d55db72 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,12 @@ buildscript { ext.ktx = "1.6.0" ext.coroutins = "1.5.0" ext.composeVersion = "1.0.3" + ext.hilt = "2.38.1" + ext.hilt_jetpack = "1.0.0" + ext.mockito_core = "3.7.0" + ext.mockito_kotlin = "2.2.0" + ext.testing_core = "1.1.1" + ext.testing_coroutine = "1.5.0" project.ext { min = 21 @@ -35,7 +41,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" - + classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/gradle.properties b/gradle.properties index 2f26404..fe250a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,10 @@ org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.caching=true +kotlin.code.style=official +-Pkapt.use.worker.api=true +kapt.incremental.apt=true +android.enableR8.fullMode=false diff --git a/models/src/main/java/com/mdgd/pokemon/models/AppModule.kt b/models/src/main/java/com/mdgd/pokemon/models/AppModule.kt deleted file mode 100644 index 71b985c..0000000 --- a/models/src/main/java/com/mdgd/pokemon/models/AppModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mdgd.pokemon.models - -import android.content.Context -import com.mdgd.pokemon.models.cache.Cache -import com.mdgd.pokemon.models.filters.StatsFilter -import com.mdgd.pokemon.models.repo.PokemonsRepo -import com.mdgd.pokemon.models.repo.dao.PokemonsDao -import com.mdgd.pokemon.models.repo.network.Network - -interface AppModule { - fun getAppContext(): Context - fun getPokemonsNetwork(): Network - fun getPokemonsDao(): PokemonsDao - fun getPokemonsRepo(): PokemonsRepo - fun getCache(): Cache - fun getFiltersFactory(): StatsFilter; -} diff --git a/mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolder.kt b/models/src/main/java/com/mdgd/pokemon/models/util/DispatchersHolder.kt similarity index 80% rename from mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolder.kt rename to models/src/main/java/com/mdgd/pokemon/models/util/DispatchersHolder.kt index b251be0..dd523b8 100644 --- a/mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolder.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/util/DispatchersHolder.kt @@ -1,4 +1,4 @@ -package com.mdgd.mvi.util +package com.mdgd.pokemon.models.util import kotlinx.coroutines.CoroutineDispatcher diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 0ba316e..2725a72 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -2,7 +2,9 @@ plugins { id 'com.android.library' id 'kotlin-android' id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' } + android { compileSdkVersion project.compile buildToolsVersion project.tools @@ -25,6 +27,9 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { @@ -50,14 +55,23 @@ dependencies { // coroutins implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + // hilt + implementation("com.google.dagger:hilt-android:2.38.1") + kapt("com.google.dagger:hilt-android-compiler:2.38.1") + kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") + testImplementation "junit:junit:$junit" - testImplementation "org.mockito:mockito-core:3.7.0" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - testImplementation "android.arch.core:core-testing:1.1.1" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0" + testImplementation "org.mockito:mockito-core:$mockito_core" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin" + testImplementation "android.arch.core:core-testing:$testing_core" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$testing_coroutine" testImplementation "com.google.code.gson:gson:$gson" } repositories { mavenCentral() } + +kapt{ + correctErrorTypes true +} diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/DefaultAppModule.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/DefaultAppModule.kt index f1d9a6f..ed03f24 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/DefaultAppModule.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/DefaultAppModule.kt @@ -1,44 +1,55 @@ package com.mdgd.pokemon.models_impl import android.content.Context -import com.mdgd.pokemon.models.AppModule import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.filters.StatsFilter import com.mdgd.pokemon.models.repo.PokemonsRepo import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.network.Network +import com.mdgd.pokemon.models.util.DispatchersHolder import com.mdgd.pokemon.models_impl.cache.CacheImpl import com.mdgd.pokemon.models_impl.filters.StatsFiltersFactory import com.mdgd.pokemon.models_impl.repo.PokemonsRepository import com.mdgd.pokemon.models_impl.repo.cache.PokemonsCacheImpl import com.mdgd.pokemon.models_impl.repo.dao.PokemonsDaoImpl import com.mdgd.pokemon.models_impl.repo.network.PokemonsNetwork - -class DefaultAppModule(val app: Context) : AppModule { - private val cache: Cache = CacheImpl() +import com.mdgd.pokemon.models_impl.util.DispatchersHolderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DefaultAppModule { + private val cache = CacheImpl() private val pokemonCache = PokemonsCacheImpl() - override fun getAppContext(): Context { - return app - } + @Provides + @Singleton + fun providePokemonsNetwork() = PokemonsNetwork() as Network - override fun getPokemonsNetwork(): Network { - return PokemonsNetwork() - } + @Provides + @Singleton + fun providePokemonsDao(@ApplicationContext ctx: Context) = PokemonsDaoImpl(ctx) as PokemonsDao - override fun getPokemonsDao(): PokemonsDao { - return PokemonsDaoImpl(app) - } + @Provides + @Singleton + fun providePokemonsRepo(dao: PokemonsDao, network: Network) = PokemonsRepository( + dao, network, pokemonCache + ) as PokemonsRepo - override fun getPokemonsRepo(): PokemonsRepo { - return PokemonsRepository(getPokemonsDao(), getPokemonsNetwork(), pokemonCache) - } + @Provides + @Singleton + fun provideCache() = cache as Cache - override fun getCache(): Cache { - return cache - } + @Provides + @Singleton + fun provideFiltersFactory() = StatsFiltersFactory() as StatsFilter - override fun getFiltersFactory(): StatsFilter { - return StatsFiltersFactory() - } + @Provides + @Singleton + fun provideDispatchers() = DispatchersHolderImpl() as DispatchersHolder } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt index 1e4106d..ec9c052 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt @@ -5,8 +5,11 @@ import com.mdgd.pokemon.models.repo.cache.PokemonsCache import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.Network +import javax.inject.Inject -class PokemonsRepository(private val dao: PokemonsDao, private val network: Network, private val cache: PokemonsCache) : PokemonsRepo { +class PokemonsRepository @Inject constructor( + private val dao: PokemonsDao, private val network: Network, private val cache: PokemonsCache +) : PokemonsRepo { override fun getPokemons() = cache.getPokemons() diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt index 2c81e50..7f27f3d 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt @@ -5,10 +5,13 @@ import androidx.room.Room import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.schemas.PokemonDetails +import dagger.hilt.android.qualifiers.ApplicationContext import java.util.* +import javax.inject.Inject -class PokemonsDaoImpl(ctx: Context) : PokemonsDao { - private val pokemonsRoomDao: PokemonsRoomDao? = Room.databaseBuilder(ctx, AppDatabase::class.java, "PokemonsAppDB").build().pokemonsDao() +class PokemonsDaoImpl @Inject constructor(@ApplicationContext ctx: Context) : PokemonsDao { + private val pokemonsRoomDao: PokemonsRoomDao? = + Room.databaseBuilder(ctx, AppDatabase::class.java, "PokemonsAppDB").build().pokemonsDao() override suspend fun save(list: List) { pokemonsRoomDao?.save(list) diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt index e832bf4..70acd77 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt @@ -21,17 +21,21 @@ class PokemonsNetwork : Network { init { val logging = HttpLoggingInterceptor() - logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC else HttpLoggingInterceptor.Level.NONE + logging.level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BASIC + } else { + HttpLoggingInterceptor.Level.NONE + } val httpClient = OkHttpClient.Builder() httpClient.addInterceptor(logging) httpClient.readTimeout(10, TimeUnit.SECONDS) httpClient.writeTimeout(10, TimeUnit.SECONDS) httpClient.connectTimeout(10, TimeUnit.SECONDS) val retrofit = Retrofit.Builder() - .addConverterFactory(GsonConverterFactory.create()) - .client(httpClient.build()) - .baseUrl("https://pokeapi.co/api/v2/") - .build() + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient.build()) + .baseUrl("https://pokeapi.co/api/v2/") + .build() service = retrofit.create(PokemonsRetrofitApi::class.java) } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/util/DispatchersHolderImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/util/DispatchersHolderImpl.kt new file mode 100644 index 0000000..81dddfb --- /dev/null +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/util/DispatchersHolderImpl.kt @@ -0,0 +1,11 @@ +package com.mdgd.pokemon.models_impl.util + +import com.mdgd.pokemon.models.util.DispatchersHolder +import kotlinx.coroutines.Dispatchers + +class DispatchersHolderImpl : DispatchersHolder { + + override fun getMain() = Dispatchers.Main + + override fun getIO() = Dispatchers.IO +} diff --git a/mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolderImpl.kt b/mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolderImpl.kt deleted file mode 100644 index 0b6b311..0000000 --- a/mvi/src/main/java/com/mdgd/mvi/util/DispatchersHolderImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mdgd.mvi.util - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -class DispatchersHolderImpl : DispatchersHolder { - - override fun getMain(): CoroutineDispatcher { - return Dispatchers.Main - } - - override fun getIO(): CoroutineDispatcher { - return Dispatchers.IO - } -} From e39da76b2eff4a3057886e61c827dafdbf0e4299 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 09:53:55 +0300 Subject: [PATCH 21/47] # cleanup --- app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsContract.kt | 2 -- .../java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt | 2 +- .../java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt | 2 +- .../mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt | 1 + .../java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsContract.kt b/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsContract.kt index 7f6bf0f..04aa55e 100644 --- a/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/bg/LoadPokemonsContract.kt @@ -3,5 +3,3 @@ package com.mdgd.pokemon.bg interface ServiceModel { fun load() } - -interface LoadPokemonContext diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt index a91ea31..75296ea 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt @@ -15,5 +15,5 @@ class LabelPropertyData : TitlePropertyData, LabelProperty { } override val type: Int - get() = PokemonProperty.Companion.PROPERTY_LABEL + get() = PokemonProperty.PROPERTY_LABEL } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt index c3b8c52..19c6b54 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt @@ -2,5 +2,5 @@ package com.mdgd.pokemon.ui.pokemon.dto class TextPropertyData(override val text: String, override val nestingLevel: Int) : TextProperty { override val type: Int - get() = PokemonProperty.Companion.PROPERTY_TEXT + get() = PokemonProperty.PROPERTY_TEXT } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt index 8340e18..92d4600 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt @@ -4,6 +4,7 @@ import com.mdgd.mvi.states.AbstractAction import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract sealed class PokemonDetailsScreenAction : AbstractAction() { + class ActionBack : PokemonDetailsScreenAction() { override fun handle(screen: PokemonDetailsContract.View) { screen.goBack() diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt index c761213..a939cd2 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt @@ -12,7 +12,6 @@ sealed class SplashScreenAction : AbstractAction() { } } - object LaunchWorker : SplashScreenAction() { override fun handle(screen: SplashContract.View) { From f054b6b98680087e45b3051577d648c61e8e3fb8 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 09:54:51 +0300 Subject: [PATCH 22/47] # cleanup --- models/src/main/java/com/mdgd/pokemon/models/infra/Result.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/src/main/java/com/mdgd/pokemon/models/infra/Result.kt b/models/src/main/java/com/mdgd/pokemon/models/infra/Result.kt index cb9baae..3d44f81 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/infra/Result.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/infra/Result.kt @@ -21,6 +21,4 @@ class Result { fun getValue() = value!! fun getError() = error!! - - } From e028296012b9f9d2cb251443e4b95a65620159b4 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 09:55:45 +0300 Subject: [PATCH 23/47] # todos --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ee54a92..c7f691d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # Pokemons Exam task VMedia + + +TODO: error handling in image loading, animate screen changes From 16720b4731e81b944ef464ced00370b8455bb88e Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 13 Oct 2021 12:58:57 +0300 Subject: [PATCH 24/47] # error handling while loading image --- README.md | 2 +- .../pokemon/ui/pokemon/PokemonDetailsFragment.kt | 13 +++++++++++-- .../mdgd/pokemon/ui/pokemons/PokemonsFragment.kt | 12 +++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c7f691d..8f8efa1 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Exam task VMedia -TODO: error handling in image loading, animate screen changes +TODO: animate screen changes diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index c643d12..2610287 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -31,6 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.viewModels import coil.compose.rememberImagePainter +import coil.request.ImageRequest import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.R @@ -157,8 +159,15 @@ fun PokemonDetailItem(property: PokemonProperty) { contentDescription = stringResource(id = R.string.fragment_pokemon_picture), contentScale = ContentScale.Inside, modifier = Modifier.size(200.dp), - painter = rememberImagePainter(p.imageUrl), // TODO: catch error - ) + painter = rememberImagePainter( + data = p.imageUrl, + builder = { + ImageRequest.Builder(LocalContext.current) + .placeholder(R.drawable.ic_pokemon) + .error(R.drawable.ic_pokemon) + .build() + } + )) } } PokemonProperty.PROPERTY_LABEL -> { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index e04be8e..3512150 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.viewModels import coil.compose.rememberImagePainter +import coil.request.ImageRequest import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.android.material.composethemeadapter.MdcTheme @@ -232,10 +233,15 @@ fun PokemonItem(item: PokemonFullDataSchema, model: PokemonsContract.ViewModel?) Image( painter = item.pokemonSchema?.sprites?.other?.officialArtwork?.frontDefault?.let { rememberImagePainter( - data = it // TODO: catch fail - ) + data = it, + builder = { + ImageRequest.Builder(LocalContext.current) + .placeholder(R.drawable.ic_pokemon) + .error(R.drawable.ic_pokemon) + .build() + }) } ?: kotlin.run { - painterResource(R.drawable.logo_splash) + painterResource(R.drawable.ic_pokemon) }, contentDescription = stringResource(id = R.string.screen_pokemons_icon), modifier = Modifier From fe8c5e50845313697d8083049ffc45eddb5d9966 Mon Sep 17 00:00:00 2001 From: DanGdl Date: Fri, 12 Nov 2021 09:36:21 +0200 Subject: [PATCH 25/47] # mvi improvement --- .../main/java/com/mdgd/mvi/MviViewModel.kt | 2 +- .../mdgd/mvi/fragments/FragmentContract.kt | 3 ++ .../mvi/fragments/HostedDialogFragment.kt | 40 +++++++++---------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 2c8ca18..4617cf1 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -32,6 +32,6 @@ abstract class MviViewModel, A> : ViewModel(), Fragment } @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { } } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index 1479b42..dfd3457 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -1,10 +1,13 @@ package com.mdgd.mvi.fragments +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData class FragmentContract { interface ViewModel : LifecycleObserver { + fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) fun getStateObservable(): MutableLiveData fun getActionObservable(): MutableLiveData } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt index 961f60e..da3d17c 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt @@ -3,7 +3,7 @@ package com.mdgd.mvi.fragments import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment -import androidx.lifecycle.Observer +import androidx.lifecycle.* import com.mdgd.mvi.states.AbstractAction import com.mdgd.mvi.states.ScreenState import java.lang.reflect.ParameterizedType @@ -14,7 +14,7 @@ abstract class HostedDialogFragment< ACTION : AbstractAction, VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : AppCompatDialogFragment(), FragmentContract.View, Observer { + : AppCompatDialogFragment(), FragmentContract.View, Observer, LifecycleObserver { protected var model: VIEW_MODEL? = null private set @@ -44,12 +44,22 @@ abstract class HostedDialogFragment< override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setModel(createModel()) - if (model != null) { - lifecycle.addObserver(model!!) - model!!.getStateObservable().observe(this, this) - model!!.getActionObservable().observe(this, { action -> - action.visit(this as VIEW) - }) + lifecycle.addObserver(this) + model?.getStateObservable()?.observe(this, this) + model?.getActionObservable()?.observe(this, { action -> + action.visit(this as VIEW) + }) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + model?.onAny(owner, event) + + if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { + lifecycle.removeObserver(this) + // order matters + model?.getActionObservable()?.removeObservers(this) + model?.getStateObservable()?.removeObservers(this) } } @@ -57,22 +67,8 @@ abstract class HostedDialogFragment< state.visit(this as VIEW) } - override fun onDestroy() { - // order matters - if (model != null) { - model!!.getActionObservable().removeObservers(this) - model!!.getStateObservable().removeObservers(this) - lifecycle.removeObserver(model!!) - } - super.onDestroy() - } - protected abstract fun createModel(): VIEW_MODEL? - protected fun hasHost(): Boolean { - return fragmentHost != null - } - protected fun setModel(model: VIEW_MODEL?) { this.model = model } From d9d7245ac0b80a40ea7528e872440a111fb5632a Mon Sep 17 00:00:00 2001 From: DanGdl Date: Sat, 13 Nov 2021 13:39:03 +0200 Subject: [PATCH 26/47] # mvi improvement --- .../com/mdgd/mvi/fragments/HostedFragment.kt | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index e6c638c..6970744 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -2,7 +2,7 @@ package com.mdgd.mvi.fragments import android.content.Context import android.os.Bundle -import androidx.lifecycle.Observer +import androidx.lifecycle.* import androidx.navigation.fragment.NavHostFragment import com.mdgd.mvi.states.ScreenAction import com.mdgd.mvi.states.ScreenState @@ -14,7 +14,7 @@ abstract class HostedFragment< ACTION : ScreenAction, VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : NavHostFragment(), FragmentContract.View, Observer { + : NavHostFragment(), FragmentContract.View, Observer, LifecycleObserver { protected var model: VIEW_MODEL? = null private set @@ -41,29 +41,28 @@ abstract class HostedFragment< fragmentHost = null } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setModel(createModel()) - if (model != null) { - lifecycle.addObserver(model!!) - model!!.getStateObservable().observe(this, this) - model!!.getActionObservable().observe(this, { action -> - action.visit(this as VIEW) - }) - } + lifecycle.addObserver(this) + model?.getStateObservable()?.observe(this, this) + model?.getActionObservable()?.observe(this, { action -> + action.visit(this as VIEW) + }) } protected abstract fun createModel(): VIEW_MODEL - override fun onDestroy() { - // order matters - if (model != null) { - model!!.getActionObservable().removeObservers(this) - model!!.getStateObservable().removeObservers(this) - lifecycle.removeObserver(model!!) + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + model?.onAny(owner, event) + + if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { + lifecycle.removeObserver(this) + // order matters + model?.getActionObservable()?.removeObservers(this) + model?.getStateObservable()?.removeObservers(this) } - super.onDestroy() } override fun onChanged(screenState: STATE) { From 6bd93c09eff5efc559838490d5f0c9e46e13439d Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Wed, 17 Nov 2021 09:12:19 +0200 Subject: [PATCH 27/47] # libs update --- app/build.gradle | 10 +++++----- app/src/main/AndroidManifest.xml | 6 ++++-- build.gradle | 22 +++++++++++----------- models/build.gradle | 4 ++-- models_impl/build.gradle | 8 ++++---- mvi/build.gradle | 4 ++-- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d874989..997503d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,8 +65,8 @@ dependencies { implementation "com.google.accompanist:accompanist-swiperefresh:0.19.0" // navigation - implementation "androidx.navigation:navigation-fragment:$nav_version" - implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // image loading implementation 'io.coil-kt:coil-compose:1.3.0' @@ -75,8 +75,8 @@ dependencies { implementation "com.google.code.gson:gson:$gson" // hilt - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation("com.google.dagger:hilt-android:2.39.1") + kapt("com.google.dagger:hilt-android-compiler:2.39.1") implementation("androidx.hilt:hilt-navigation-fragment:$hilt_jetpack") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") implementation 'androidx.hilt:hilt-work:1.0.0' @@ -101,7 +101,7 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" } kapt{ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b564ec..0750448 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,16 +6,18 @@ - + diff --git a/build.gradle b/build.gradle index d55db72..ce786bd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,24 +1,24 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.5.31' - ext.lifecycle_ktx = "2.3.1" + ext.lifecycle_ktx = "2.4.0" ext.nav_version = "2.3.5" - ext.work_version = "2.6.0" + ext.work_version = "2.7.0" ext.room = "2.3.0" ext.room_compiler = "2.2.5" ext.compat = "1.3.1" ext.gson = "2.8.6" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" - ext.okhttp_log = "4.9.0" - ext.okhttp = "4.9.0" + ext.okhttp_log = "4.9.2" + ext.okhttp = "4.9.2" ext.junit = "4.13.2" ext.junit_android = "1.1.3" ext.espresso = "3.4.0" - ext.ktx = "1.6.0" - ext.coroutins = "1.5.0" - ext.composeVersion = "1.0.3" - ext.hilt = "2.38.1" + ext.ktx = "1.7.0" + ext.coroutines = "1.5.0" + ext.composeVersion = "1.0.5" + ext.hilt = "2.39.1" ext.hilt_jetpack = "1.0.0" ext.mockito_core = "3.7.0" ext.mockito_kotlin = "2.2.0" @@ -27,8 +27,8 @@ buildscript { project.ext { min = 21 - target = 30 - compile = 30 + target = 31 + compile = 31 tools = "30.0.2" } @@ -41,7 +41,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" - classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1") + classpath("com.google.dagger:hilt-android-gradle-plugin:2.39.1") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/models/build.gradle b/models/build.gradle index 000ec1c..39c999c 100644 --- a/models/build.gradle +++ b/models/build.gradle @@ -38,8 +38,8 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + // coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" } repositories { diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 2725a72..ed014d8 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -52,12 +52,12 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + // coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // hilt - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation("com.google.dagger:hilt-android:2.39.1") + kapt("com.google.dagger:hilt-android-compiler:2.39.1") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") testImplementation "junit:junit:$junit" diff --git a/mvi/build.gradle b/mvi/build.gradle index b34e4d0..8cc2cae 100644 --- a/mvi/build.gradle +++ b/mvi/build.gradle @@ -29,8 +29,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx" // navigation - implementation "androidx.navigation:navigation-fragment:$nav_version" - implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" From 674a21c1937e13c1d93ed0b460621cf738f8f5f3 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Mon, 13 Dec 2021 07:44:23 +0200 Subject: [PATCH 28/47] # libs update --- app/build.gradle | 7 +- .../ui/pokemon/PokemonDetailsContract.kt | 4 +- .../ui/pokemon/PokemonDetailsFragment.kt | 4 +- .../ui/pokemon/PokemonDetailsViewModel.kt | 15 ++-- ...ction.kt => PokemonDetailsScreenEffect.kt} | 6 +- .../pokemon/ui/pokemons/PokemonsContract.kt | 4 +- .../pokemon/ui/pokemons/PokemonsFragment.kt | 4 +- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 15 ++-- ...creenAction.kt => PokemonsScreenEffect.kt} | 8 +- .../mdgd/pokemon/ui/splash/SplashContract.kt | 4 +- .../mdgd/pokemon/ui/splash/SplashFragment.kt | 4 +- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 17 ++--- ...hScreenAction.kt => SplashScreenEffect.kt} | 10 +-- .../test/java/com/mdgd/pokemon/TestSuit.kt | 20 ++--- .../ui/pokemon/PokemonDetailsViewModelTest.kt | 23 +++--- ...ionTest.kt => PokemonsScreenEffectTest.kt} | 12 +-- .../ui/pokemons/PokemonsViewModelTest.kt | 75 +++++++++++-------- ...ctionTest.kt => SplashScreenEffectTest.kt} | 10 +-- .../pokemon/ui/splash/SplashViewModelTest.kt | 48 ++++++------ build.gradle | 7 +- models_impl/build.gradle | 4 +- .../main/java/com/mdgd/mvi/MviViewModel.kt | 36 ++++----- .../mdgd/mvi/fragments/FragmentContract.kt | 12 ++- .../mvi/fragments/HostedDialogFragment.kt | 75 ------------------- .../com/mdgd/mvi/fragments/HostedFragment.kt | 17 ++--- .../{AbstractAction.kt => AbstractEffect.kt} | 2 +- .../{ScreenAction.kt => ScreenEffect.kt} | 2 +- 27 files changed, 183 insertions(+), 262 deletions(-) rename app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/{PokemonDetailsScreenAction.kt => PokemonDetailsScreenEffect.kt} (60%) rename app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/{PokemonsScreenAction.kt => PokemonsScreenEffect.kt} (63%) rename app/src/main/java/com/mdgd/pokemon/ui/splash/state/{SplashScreenAction.kt => SplashScreenEffect.kt} (62%) rename app/src/test/java/com/mdgd/pokemon/ui/pokemons/{PokemonsScreenActionTest.kt => PokemonsScreenEffectTest.kt} (68%) rename app/src/test/java/com/mdgd/pokemon/ui/splash/{SplashScreenActionTest.kt => SplashScreenEffectTest.kt} (82%) delete mode 100644 mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt rename mvi/src/main/java/com/mdgd/mvi/states/{AbstractAction.kt => AbstractEffect.kt} (81%) rename mvi/src/main/java/com/mdgd/mvi/states/{ScreenAction.kt => ScreenEffect.kt} (66%) diff --git a/app/build.gradle b/app/build.gradle index 997503d..c020525 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { implementation "androidx.compose.material:material:$composeVersion" implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" implementation "androidx.compose.ui:ui-tooling:$composeVersion" - implementation "com.google.android.material:compose-theme-adapter:$composeVersion" + implementation "com.google.android.material:compose-theme-adapter:$compose_theme" implementation "androidx.appcompat:appcompat:$compat" @@ -75,8 +75,8 @@ dependencies { implementation "com.google.code.gson:gson:$gson" // hilt - implementation("com.google.dagger:hilt-android:2.39.1") - kapt("com.google.dagger:hilt-android-compiler:2.39.1") + implementation("com.google.dagger:hilt-android:2.40") + kapt("com.google.dagger:hilt-android-compiler:2.40") implementation("androidx.hilt:hilt-navigation-fragment:$hilt_jetpack") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") implementation 'androidx.hilt:hilt-work:1.0.0' @@ -96,7 +96,6 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:$junit_android" androidTestImplementation "androidx.test.espresso:espresso-core:$espresso" - androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.3.0" implementation "androidx.core:core-ktx:$ktx" diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt index 4c53ff2..d0a4f37 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt @@ -2,12 +2,12 @@ package com.mdgd.pokemon.ui.pokemon import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState class PokemonDetailsContract { interface ViewModel : - FragmentContract.ViewModel { + FragmentContract.ViewModel { fun setPokemonId(pokemonId: Long) fun onBackPressed() } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index 2610287..47fbaed 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -43,7 +43,7 @@ import com.mdgd.pokemon.models.repo.schemas.Stat_ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen import com.mdgd.pokemon.ui.pokemon.dto.* -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import dagger.hilt.android.AndroidEntryPoint @@ -51,7 +51,7 @@ import dagger.hilt.android.AndroidEntryPoint class PokemonDetailsFragment : HostedFragment< PokemonDetailsContract.View, PokemonDetailsScreenState, - PokemonDetailsScreenAction, + PokemonDetailsScreenEffect, PokemonDetailsContract.ViewModel, PokemonDetailsContract.Host>(), PokemonDetailsContract.View { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 10e59de..7c7df3b 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -1,7 +1,6 @@ package com.mdgd.pokemon.ui.pokemon import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.R @@ -12,7 +11,7 @@ import com.mdgd.pokemon.models.repo.schemas.Form import com.mdgd.pokemon.models.repo.schemas.GameIndex import com.mdgd.pokemon.models.repo.schemas.Type import com.mdgd.pokemon.ui.pokemon.dto.* -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -25,7 +24,7 @@ import kotlin.collections.ArrayList @HiltViewModel class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo) : - MviViewModel(), + MviViewModel(), PokemonDetailsContract.ViewModel { private val pokemonIdFlow = MutableStateFlow(-1L) @@ -35,16 +34,16 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo pokemonIdFlow.tryEmit(pokemonId) } - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_CREATE && pokemonLoadingJob == null) { pokemonLoadingJob = viewModelScope.launch { pokemonIdFlow .filter { it != -1L } .map { repo.getPokemonById(it) } .map { it?.let { mapToListPokemon(it) } ?: LinkedList() } - .flowOn(Dispatchers.IO) - .collect { setState(PokemonDetailsScreenState.SetData(it)) } + .flowOn(Dispatchers.IO) + .collect { setState(PokemonDetailsScreenState.SetData(it)) } } } } @@ -104,7 +103,7 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo } override fun onBackPressed() { - setAction(PokemonDetailsScreenAction.ActionBack()) + setAction(PokemonDetailsScreenEffect.EffectBack()) } override fun onCleared() { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt similarity index 60% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt index 92d4600..65300b4 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt @@ -1,11 +1,11 @@ package com.mdgd.pokemon.ui.pokemon.state -import com.mdgd.mvi.states.AbstractAction +import com.mdgd.mvi.states.AbstractEffect import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract -sealed class PokemonDetailsScreenAction : AbstractAction() { +sealed class PokemonDetailsScreenEffect : AbstractEffect() { - class ActionBack : PokemonDetailsScreenAction() { + class EffectBack : PokemonDetailsScreenEffect() { override fun handle(screen: PokemonDetailsContract.View) { screen.goBack() } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt index d3ec9a0..77f005c 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt @@ -2,11 +2,11 @@ package com.mdgd.pokemon.ui.pokemons import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState class PokemonsContract { - interface ViewModel : FragmentContract.ViewModel { + interface ViewModel : FragmentContract.ViewModel { fun reload() fun sort(filter: String) fun onItemClicked(pokemon: PokemonFullDataSchema) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index 3512150..fad13f5 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -44,7 +44,7 @@ import com.mdgd.pokemon.models.repo.schemas.Stat import com.mdgd.pokemon.models.repo.schemas.Stat_ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -53,7 +53,7 @@ import kotlinx.coroutines.launch class PokemonsFragment : HostedFragment< PokemonsContract.View, PokemonsScreenState, - PokemonsScreenAction, + PokemonsScreenEffect, PokemonsContract.ViewModel, PokemonsContract.Host>(), PokemonsContract.View { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index edd6e75..a37e01a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -1,7 +1,6 @@ package com.mdgd.pokemon.ui.pokemons import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.models.filters.FilterData @@ -9,7 +8,7 @@ import com.mdgd.pokemon.models.filters.StatsFilter import com.mdgd.pokemon.models.repo.PokemonsRepo import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.util.DispatchersHolder -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler @@ -23,19 +22,19 @@ class PokemonsViewModel @Inject constructor( private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder -) : MviViewModel(), +) : MviViewModel(), PokemonsContract.ViewModel { private var firstVisibleIndex: Int = 0 private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(PokemonsScreenAction.Error(e)) + setAction(PokemonsScreenEffect.Error(e)) } private val pageFlow = MutableStateFlow(0) private val filterFlow = MutableStateFlow(FilterData()) private var launch: Job? = null - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_CREATE && launch == null) { launch = viewModelScope.launch(exceptionHandler) { pageFlow @@ -43,7 +42,7 @@ class PokemonsViewModel @Inject constructor( .flowOn(dispatchers.getMain()) .map { page -> Pair(page, repo.getPage(page)) } .flowOn(dispatchers.getIO()) - .catch { e: Throwable -> setAction(PokemonsScreenAction.Error(e)) } + .catch { e: Throwable -> setAction(PokemonsScreenEffect.Error(e)) } .collect { pagePair: Pair> -> if (pagePair.first == 0) { setState( @@ -105,7 +104,7 @@ class PokemonsViewModel @Inject constructor( } override fun onItemClicked(pokemon: PokemonFullDataSchema) { - setAction(PokemonsScreenAction.ShowDetails(pokemon.pokemonSchema?.id)) + setAction(PokemonsScreenEffect.ShowDetails(pokemon.pokemonSchema?.id)) } override fun onScroll(firstVisibleIndex: Int, lastVisibleIndex: Int) { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt similarity index 63% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenAction.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt index e4c1013..8030ca8 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenAction.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt @@ -1,11 +1,11 @@ package com.mdgd.pokemon.ui.pokemons.state -import com.mdgd.mvi.states.AbstractAction +import com.mdgd.mvi.states.AbstractEffect import com.mdgd.pokemon.ui.pokemons.PokemonsContract -sealed class PokemonsScreenAction() : AbstractAction() { +sealed class PokemonsScreenEffect : AbstractEffect() { - class Error(val error: Throwable?) : PokemonsScreenAction() { + class Error(val error: Throwable?) : PokemonsScreenEffect() { override fun handle(screen: PokemonsContract.View) { screen.setProgressVisibility(false) @@ -13,7 +13,7 @@ sealed class PokemonsScreenAction() : AbstractAction() { } } - class ShowDetails(val id: Long?) : PokemonsScreenAction() { + class ShowDetails(val id: Long?) : PokemonsScreenEffect() { override fun handle(screen: PokemonsContract.View) { screen.setProgressVisibility(false) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt index 5a105f6..4cd631a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt @@ -1,7 +1,7 @@ package com.mdgd.pokemon.ui.splash import com.mdgd.mvi.fragments.FragmentContract -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import com.mdgd.pokemon.ui.splash.state.SplashScreenState class SplashContract { @@ -10,7 +10,7 @@ class SplashContract { } - interface ViewModel : FragmentContract.ViewModel + interface ViewModel : FragmentContract.ViewModel interface View : FragmentContract.View { fun proceedToNextScreen() diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index 07db1bd..242e2f3 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -32,13 +32,13 @@ import com.mdgd.pokemon.bg.UploadWorker import com.mdgd.pokemon.ui.error.DefaultErrorParams import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import com.mdgd.pokemon.ui.splash.state.SplashScreenState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SplashFragment : - HostedFragment(), + HostedFragment(), SplashContract.View { private val errorDialogTrigger = mutableStateOf(DefaultErrorParams()) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 602108b..37c44f1 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -1,12 +1,11 @@ package com.mdgd.pokemon.ui.splash import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import com.mdgd.pokemon.ui.splash.state.SplashScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler @@ -20,17 +19,17 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor(private val cache: Cache) : - MviViewModel(), + MviViewModel(), SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(SplashScreenAction.ShowError(e)) + setAction(SplashScreenEffect.ShowError(e)) } private var progressJob: Job? = null - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_START && progressJob == null) { progressJob = viewModelScope.launch(exceptionHandler) { flow { @@ -41,13 +40,13 @@ class SplashViewModel @Inject constructor(private val cache: Cache) : // Result(Throwable("Dummy")) }.collect { if (it.isError()) { - setAction(SplashScreenAction.ShowError(it.getError())) + setAction(SplashScreenEffect.ShowError(it.getError())) } else if (it.getValue() != 0L) { - setAction(SplashScreenAction.NextScreen) + setAction(SplashScreenEffect.NextScreen) } } } - setAction(SplashScreenAction.LaunchWorker) + setAction(SplashScreenEffect.LaunchWorker) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenEffect.kt similarity index 62% rename from app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt rename to app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenEffect.kt index a939cd2..1f414c4 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenAction.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenEffect.kt @@ -1,25 +1,25 @@ package com.mdgd.pokemon.ui.splash.state -import com.mdgd.mvi.states.AbstractAction +import com.mdgd.mvi.states.AbstractEffect import com.mdgd.pokemon.ui.splash.SplashContract -sealed class SplashScreenAction : AbstractAction() { +sealed class SplashScreenEffect : AbstractEffect() { - class ShowError(val e: Throwable) : SplashScreenAction() { + class ShowError(val e: Throwable) : SplashScreenEffect() { override fun handle(screen: SplashContract.View) { screen.showError(e) } } - object LaunchWorker : SplashScreenAction() { + object LaunchWorker : SplashScreenEffect() { override fun handle(screen: SplashContract.View) { screen.launchWorker() } } - object NextScreen : SplashScreenAction() { + object NextScreen : SplashScreenEffect() { override fun handle(screen: SplashContract.View) { screen.proceedToNextScreen() diff --git a/app/src/test/java/com/mdgd/pokemon/TestSuit.kt b/app/src/test/java/com/mdgd/pokemon/TestSuit.kt index e0201f8..4e803c1 100644 --- a/app/src/test/java/com/mdgd/pokemon/TestSuit.kt +++ b/app/src/test/java/com/mdgd/pokemon/TestSuit.kt @@ -3,27 +3,27 @@ package com.mdgd.pokemon import com.mdgd.pokemon.bg.LoadPokemonsModelTest import com.mdgd.pokemon.ui.pokemon.PokemonDetailsScreenStateTest import com.mdgd.pokemon.ui.pokemon.PokemonDetailsViewModelTest -import com.mdgd.pokemon.ui.pokemons.PokemonsScreenActionTest +import com.mdgd.pokemon.ui.pokemons.PokemonsScreenEffectTest import com.mdgd.pokemon.ui.pokemons.PokemonsScreenStateTest import com.mdgd.pokemon.ui.pokemons.PokemonsViewModelTest -import com.mdgd.pokemon.ui.splash.SplashScreenActionTest +import com.mdgd.pokemon.ui.splash.SplashScreenEffectTest import com.mdgd.pokemon.ui.splash.SplashViewModelTest import org.junit.runner.RunWith import org.junit.runners.Suite @RunWith(Suite::class) @Suite.SuiteClasses( - SplashScreenActionTest::class, - SplashViewModelTest::class, + SplashScreenEffectTest::class, + SplashViewModelTest::class, - PokemonDetailsScreenStateTest::class, - PokemonDetailsViewModelTest::class, + PokemonDetailsScreenStateTest::class, + PokemonDetailsViewModelTest::class, - PokemonsScreenStateTest::class, - PokemonsScreenActionTest::class, - PokemonsViewModelTest::class, + PokemonsScreenStateTest::class, + PokemonsScreenEffectTest::class, + PokemonsViewModelTest::class, - LoadPokemonsModelTest::class + LoadPokemonsModelTest::class ) class TestSuit { companion object { diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt index 6c6e747..898f588 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt @@ -10,7 +10,7 @@ import com.mdgd.pokemon.ui.pokemon.dto.ImagePropertyData import com.mdgd.pokemon.ui.pokemon.dto.LabelPropertyData import com.mdgd.pokemon.ui.pokemon.dto.TextPropertyData import com.mdgd.pokemon.ui.pokemon.dto.TitlePropertyData -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import com.nhaarman.mockitokotlin2.argumentCaptor import kotlinx.coroutines.Dispatchers @@ -50,8 +50,9 @@ class PokemonDetailsViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_START) model.onAny(null, Lifecycle.Event.ON_RESUME) @@ -64,7 +65,7 @@ class PokemonDetailsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -75,8 +76,9 @@ class PokemonDetailsViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_CREATE) model.setPokemonId(0) @@ -87,7 +89,7 @@ class PokemonDetailsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -99,8 +101,9 @@ class PokemonDetailsViewModelTest { val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_CREATE) @@ -201,6 +204,6 @@ class PokemonDetailsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } } diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenActionTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt similarity index 68% rename from app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenActionTest.kt rename to app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt index b093b9c..293387a 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenActionTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt @@ -1,6 +1,6 @@ package com.mdgd.pokemon.ui.pokemons -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test @@ -9,7 +9,7 @@ import org.junit.runners.JUnit4 import org.mockito.Mockito @RunWith(JUnit4::class) -class PokemonsScreenActionTest { +class PokemonsScreenEffectTest { private lateinit var view: PokemonsContract.View @Before @@ -25,14 +25,12 @@ class PokemonsScreenActionTest { fun test_ErrorState() = runBlockingTest { val error = Throwable("TestError") - val state = PokemonsScreenAction.Error(error) + val state = PokemonsScreenEffect.Error(error) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() Mockito.verify(view, Mockito.times(1)).showError(error) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() verifyNoMoreInteractions() } @@ -41,14 +39,12 @@ class PokemonsScreenActionTest { fun test_ShowDetailsState() = runBlockingTest { val pokemonId = 1L - val state = PokemonsScreenAction.ShowDetails(pokemonId) + val state = PokemonsScreenEffect.ShowDetails(pokemonId) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() Mockito.verify(view, Mockito.times(1)).proceedToNextScreen(pokemonId) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() verifyNoMoreInteractions() } diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt index 5340269..d5082aa 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt @@ -13,7 +13,7 @@ import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema import com.mdgd.pokemon.models.repo.schemas.Stat import com.mdgd.pokemon.models.util.DispatchersHolder import com.mdgd.pokemon.models_impl.filters.StatsFiltersFactory -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import com.nhaarman.mockitokotlin2.firstValue import kotlinx.coroutines.Dispatchers @@ -64,8 +64,9 @@ class PokemonsViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_START) @@ -80,7 +81,7 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -94,9 +95,10 @@ class PokemonsViewModelTest { val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_CREATE) @@ -122,7 +124,7 @@ class PokemonsViewModelTest { } for (action in actionCaptor.allValues) { when (action) { - is PokemonsScreenAction.Error -> { + is PokemonsScreenEffect.Error -> { errorCounter += 1 Assert.assertEquals(error.message, action.error?.message) } @@ -139,7 +141,7 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -154,9 +156,10 @@ class PokemonsViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) model.onItemClicked(pokemon) @@ -164,13 +167,13 @@ class PokemonsViewModelTest { Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) val capturedAction = actionCaptor.firstValue - Assert.assertTrue(capturedAction is PokemonsScreenAction.ShowDetails) - Assert.assertEquals(testId, (capturedAction as PokemonsScreenAction.ShowDetails).id) + Assert.assertTrue(capturedAction is PokemonsScreenEffect.ShowDetails) + Assert.assertEquals(testId, (capturedAction as PokemonsScreenEffect.ShowDetails).id) Mockito.verifyNoMoreInteractions(observerMock) Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -179,9 +182,10 @@ class PokemonsViewModelTest { val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) val pokemons = getPage(0) @@ -226,7 +230,7 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } private fun getPage(page: Int): List { @@ -249,9 +253,10 @@ class PokemonsViewModelTest { val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) @@ -315,12 +320,13 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test fun test_Filter_Add() = runBlocking { - Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_SPEED)) + Mockito.`when`(filtersFactory.getAvailableFilters()) + .thenReturn(listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_SPEED)) Mockito.`when`(filtersFactory.getFilters()).thenReturn(StatsFiltersFactory().getFilters()) val testFilter = FilterData.FILTER_ATTACK @@ -328,9 +334,10 @@ class PokemonsViewModelTest { val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) Mockito.`when`(repo.getPage(0)).then { @@ -414,12 +421,13 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test fun test_Filter_Remove() = runBlocking { - Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_SPEED)) + Mockito.`when`(filtersFactory.getAvailableFilters()) + .thenReturn(listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_SPEED)) Mockito.`when`(filtersFactory.getFilters()).thenReturn(StatsFiltersFactory().getFilters()) val testFilter = FilterData.FILTER_ATTACK @@ -427,9 +435,10 @@ class PokemonsViewModelTest { val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) Mockito.`when`(repo.getPage(0)).then { @@ -525,6 +534,6 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } } diff --git a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenActionTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenEffectTest.kt similarity index 82% rename from app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenActionTest.kt rename to app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenEffectTest.kt index d6456f4..712a6cb 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenActionTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashScreenEffectTest.kt @@ -1,6 +1,6 @@ package com.mdgd.pokemon.ui.splash -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import org.junit.Assert import org.junit.Before import org.junit.Test @@ -10,7 +10,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mockito @RunWith(JUnit4::class) -class SplashScreenActionTest { +class SplashScreenEffectTest { private lateinit var view: SplashContract.View @@ -25,7 +25,7 @@ class SplashScreenActionTest { @Test fun testLaunchWorkerState() { - SplashScreenAction.LaunchWorker.visit(view) + SplashScreenEffect.LaunchWorker.visit(view) Mockito.verify(view, Mockito.times(1)).launchWorker() verifyNoMoreInteractions() @@ -35,7 +35,7 @@ class SplashScreenActionTest { fun testErrorState() { val error = Throwable("TestError") val errorCaptor = ArgumentCaptor.forClass(Throwable::class.java) - SplashScreenAction.ShowError(error).visit(view) + SplashScreenEffect.ShowError(error).visit(view) Mockito.verify(view, Mockito.times(1)).showError(errorCaptor.capture()) Assert.assertEquals(error, errorCaptor.value) @@ -45,7 +45,7 @@ class SplashScreenActionTest { @Test fun testProceedToNextState() { - SplashScreenAction.NextScreen.visit(view) + SplashScreenEffect.NextScreen.visit(view) Mockito.verify(view, Mockito.times(1)).proceedToNextScreen() verifyNoMoreInteractions() diff --git a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt index 1b13d4e..0d981be 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import com.mdgd.pokemon.ui.splash.state.SplashScreenState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -47,8 +47,8 @@ class SplashViewModelTest { val stateObserverMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(stateObserverMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = Mockito.mock(Observer::class.java) as Observer + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_CREATE) model.onAny(null, Lifecycle.Event.ON_RESUME) @@ -61,7 +61,7 @@ class SplashViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(stateObserverMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -73,30 +73,30 @@ class SplashViewModelTest { val stateObserverMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(stateObserverMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_START) Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenAction.LaunchWorker) + Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) progressChanel.send(Result(error)) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) val errorState = actionCaptor.value - Assert.assertTrue(errorState is SplashScreenAction.ShowError) - Assert.assertEquals((errorState as SplashScreenAction.ShowError).e, error) + Assert.assertTrue(errorState is SplashScreenEffect.ShowError) + Assert.assertEquals((errorState as SplashScreenEffect.ShowError).e, error) Mockito.verifyNoInteractions(stateObserverMock) Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(stateObserverMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -107,29 +107,29 @@ class SplashViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_START) Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenAction.LaunchWorker) + Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) val errorState = actionCaptor.value - Assert.assertTrue(errorState is SplashScreenAction.ShowError) - Assert.assertEquals((errorState as SplashScreenAction.ShowError).e, error) + Assert.assertTrue(errorState is SplashScreenEffect.ShowError) + Assert.assertEquals((errorState as SplashScreenEffect.ShowError).e, error) Mockito.verifyNoInteractions(observerMock) Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -140,29 +140,29 @@ class SplashViewModelTest { val observerMock = Mockito.mock(Observer::class.java) as Observer model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = Mockito.mock(Observer::class.java) as Observer + val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + model.getEffectObservable().observeForever(actionObserverMock) model.onAny(null, Lifecycle.Event.ON_START) Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenAction.LaunchWorker) + Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) progressChanel.send(Result(90L)) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) val errorState = actionCaptor.value - Assert.assertTrue(errorState is SplashScreenAction.NextScreen) + Assert.assertTrue(errorState is SplashScreenEffect.NextScreen) Mockito.verifyNoInteractions(observerMock) Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } } diff --git a/build.gradle b/build.gradle index ce786bd..449947a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,10 @@ buildscript { ext.kotlin_version = '1.5.31' ext.lifecycle_ktx = "2.4.0" ext.nav_version = "2.3.5" - ext.work_version = "2.7.0" + ext.work_version = "2.7.1" ext.room = "2.3.0" ext.room_compiler = "2.2.5" - ext.compat = "1.3.1" + ext.compat = "1.4.0" ext.gson = "2.8.6" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" @@ -16,8 +16,9 @@ buildscript { ext.junit_android = "1.1.3" ext.espresso = "3.4.0" ext.ktx = "1.7.0" - ext.coroutines = "1.5.0" + ext.coroutines = "1.5.2" ext.composeVersion = "1.0.5" + ext.compose_theme = "1.1.1" ext.hilt = "2.39.1" ext.hilt_jetpack = "1.0.0" ext.mockito_core = "3.7.0" diff --git a/models_impl/build.gradle b/models_impl/build.gradle index ed014d8..ccf1661 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -56,8 +56,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // hilt - implementation("com.google.dagger:hilt-android:2.39.1") - kapt("com.google.dagger:hilt-android-compiler:2.39.1") + implementation("com.google.dagger:hilt-android:2.40") + kapt("com.google.dagger:hilt-android-compiler:2.40") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") testImplementation "junit:junit:$junit" diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 4617cf1..52e5829 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -1,37 +1,31 @@ package com.mdgd.mvi -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.mvi.states.ScreenState -abstract class MviViewModel, A> : ViewModel(), FragmentContract.ViewModel { - private val stateHolder = MutableLiveData() // TODO: use StateFlow: val uiState: StateFlow = _uiState ? - private val actionHolder = MutableLiveData() +abstract class MviViewModel, E> : ViewModel(), + FragmentContract.ViewModel { + private val stateHolder = MutableLiveData() + private val effectHolder = MutableLiveData() - override fun getStateObservable(): MutableLiveData { - return stateHolder - } + override fun getStateObservable() = stateHolder + + override fun getEffectObservable() = effectHolder protected fun setState(state: S) { - if (stateHolder.value != null) { - state.merge(stateHolder.value as S) - } + stateHolder.value?.let { state.merge(it) } stateHolder.value = state } - protected fun getState(): S? { - return stateHolder.value - } - - override fun getActionObservable(): MutableLiveData { - return actionHolder - } + protected fun getState() = stateHolder.value - protected fun setAction(action: A) { - actionHolder.value = action + protected fun setAction(action: E) { + effectHolder.value = action } - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + override fun onStateChanged(event: Lifecycle.Event) { } } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index dfd3457..66f166c 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -1,15 +1,13 @@ package com.mdgd.mvi.fragments import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.LiveData class FragmentContract { - interface ViewModel : LifecycleObserver { - fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) - fun getStateObservable(): MutableLiveData - fun getActionObservable(): MutableLiveData + interface ViewModel { + fun onStateChanged(event: Lifecycle.Event) + fun getStateObservable(): LiveData + fun getEffectObservable(): LiveData } interface View diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt deleted file mode 100644 index da3d17c..0000000 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.mdgd.mvi.fragments - -import android.content.Context -import android.os.Bundle -import androidx.appcompat.app.AppCompatDialogFragment -import androidx.lifecycle.* -import com.mdgd.mvi.states.AbstractAction -import com.mdgd.mvi.states.ScreenState -import java.lang.reflect.ParameterizedType - -abstract class HostedDialogFragment< - VIEW : FragmentContract.View, - STATE : ScreenState, - ACTION : AbstractAction, - VIEW_MODEL : FragmentContract.ViewModel, - HOST : FragmentContract.Host> - : AppCompatDialogFragment(), FragmentContract.View, Observer, LifecycleObserver { - - protected var model: VIEW_MODEL? = null - private set - - protected var fragmentHost: HOST? = null - private set - - override fun onAttach(context: Context) { - super.onAttach(context) - // keep the call back - try { - fragmentHost = context as HOST - } catch (e: Throwable) { - val hostClassName = ((javaClass.genericSuperclass as ParameterizedType) - .actualTypeArguments[1] as Class<*>).canonicalName - throw RuntimeException("Activity must implement " + hostClassName - + " to attach " + javaClass.simpleName, e) - } - } - - override fun onDetach() { - super.onDetach() - // release the call back - fragmentHost = null - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setModel(createModel()) - lifecycle.addObserver(this) - model?.getStateObservable()?.observe(this, this) - model?.getActionObservable()?.observe(this, { action -> - action.visit(this as VIEW) - }) - } - - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - model?.onAny(owner, event) - - if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { - lifecycle.removeObserver(this) - // order matters - model?.getActionObservable()?.removeObservers(this) - model?.getStateObservable()?.removeObservers(this) - } - } - - override fun onChanged(state: STATE) { - state.visit(this as VIEW) - } - - protected abstract fun createModel(): VIEW_MODEL? - - protected fun setModel(model: VIEW_MODEL?) { - this.model = model - } -} diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index 6970744..ef1954b 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -4,17 +4,17 @@ import android.content.Context import android.os.Bundle import androidx.lifecycle.* import androidx.navigation.fragment.NavHostFragment -import com.mdgd.mvi.states.ScreenAction +import com.mdgd.mvi.states.ScreenEffect import com.mdgd.mvi.states.ScreenState import java.lang.reflect.ParameterizedType abstract class HostedFragment< VIEW : FragmentContract.View, STATE : ScreenState, - ACTION : ScreenAction, - VIEW_MODEL : FragmentContract.ViewModel, + EFFECT : ScreenEffect, + VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : NavHostFragment(), FragmentContract.View, Observer, LifecycleObserver { + : NavHostFragment(), FragmentContract.View, Observer, LifecycleEventObserver { protected var model: VIEW_MODEL? = null private set @@ -46,21 +46,20 @@ abstract class HostedFragment< setModel(createModel()) lifecycle.addObserver(this) model?.getStateObservable()?.observe(this, this) - model?.getActionObservable()?.observe(this, { action -> + model?.getEffectObservable()?.observe(this, { action -> action.visit(this as VIEW) }) } protected abstract fun createModel(): VIEW_MODEL - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - model?.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + model?.onStateChanged(event) if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) // order matters - model?.getActionObservable()?.removeObservers(this) + model?.getEffectObservable()?.removeObservers(this) model?.getStateObservable()?.removeObservers(this) } } diff --git a/mvi/src/main/java/com/mdgd/mvi/states/AbstractAction.kt b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt similarity index 81% rename from mvi/src/main/java/com/mdgd/mvi/states/AbstractAction.kt rename to mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt index 15baef5..d2124dd 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/AbstractAction.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt @@ -1,6 +1,6 @@ package com.mdgd.mvi.states -abstract class AbstractAction : ScreenAction { +abstract class AbstractEffect : ScreenEffect { var isHandled = false override fun visit(screen: T) { diff --git a/mvi/src/main/java/com/mdgd/mvi/states/ScreenAction.kt b/mvi/src/main/java/com/mdgd/mvi/states/ScreenEffect.kt similarity index 66% rename from mvi/src/main/java/com/mdgd/mvi/states/ScreenAction.kt rename to mvi/src/main/java/com/mdgd/mvi/states/ScreenEffect.kt index a01d860..6ff0b30 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/ScreenAction.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/ScreenEffect.kt @@ -1,5 +1,5 @@ package com.mdgd.mvi.states -interface ScreenAction { +interface ScreenEffect { fun visit(screen: T) } From 14f16068a49bff7508de0bd12730576158130da3 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Mon, 13 Dec 2021 08:00:15 +0200 Subject: [PATCH 29/47] # libs update --- .../java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt index 8f8494c..9c0008d 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt @@ -2,5 +2,5 @@ package com.mdgd.pokemon.ui.pokemon.dto class ImagePropertyData(override val imageUrl: String) : ImageProperty { override val type: Int - get() = PokemonProperty.Companion.PROPERTY_IMAGE + get() = PokemonProperty.PROPERTY_IMAGE } From 778998cd3db0bdc69e9094d748467a66da27ac0f Mon Sep 17 00:00:00 2001 From: DanGdl Date: Tue, 22 Mar 2022 20:17:54 +0200 Subject: [PATCH 30/47] # libs update --- app/build.gradle | 18 ++++----- app/src/main/AndroidManifest.xml | 4 +- build.gradle | 37 ++++++++++--------- gradle/wrapper/gradle-wrapper.properties | 2 +- models/build.gradle | 4 +- models_impl/build.gradle | 8 ++-- mvi/build.gradle | 4 +- .../main/java/com/mdgd/mvi/MviViewModel.kt | 8 ++-- .../mdgd/mvi/fragments/FragmentContract.kt | 6 +-- .../mvi/fragments/HostedDialogFragment.kt | 18 +++++---- .../com/mdgd/mvi/fragments/HostedFragment.kt | 24 +++++++----- 11 files changed, 74 insertions(+), 59 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d874989..747126e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.1.0-alpha05" + kotlinCompilerExtensionVersion = "1.2.0-alpha05" } buildTypes { release { @@ -57,7 +57,7 @@ dependencies { implementation "androidx.compose.material:material:$composeVersion" implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" implementation "androidx.compose.ui:ui-tooling:$composeVersion" - implementation "com.google.android.material:compose-theme-adapter:$composeVersion" + implementation "com.google.android.material:compose-theme-adapter:$composeVersionTheme" implementation "androidx.appcompat:appcompat:$compat" @@ -65,8 +65,8 @@ dependencies { implementation "com.google.accompanist:accompanist-swiperefresh:0.19.0" // navigation - implementation "androidx.navigation:navigation-fragment:$nav_version" - implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // image loading implementation 'io.coil-kt:coil-compose:1.3.0' @@ -75,9 +75,9 @@ dependencies { implementation "com.google.code.gson:gson:$gson" // hilt - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") - implementation("androidx.hilt:hilt-navigation-fragment:$hilt_jetpack") + implementation "com.google.dagger:hilt-android:2.40" + kapt("com.google.dagger:hilt-android-compiler:2.40") + implementation "androidx.hilt:hilt-navigation-fragment:$hilt_jetpack" kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") implementation 'androidx.hilt:hilt-work:1.0.0' @@ -101,9 +101,9 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" } -kapt{ +kapt { correctErrorTypes true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b564ec..606753a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,9 @@ android:supportsRtl="true" android:theme="@style/Theme.Pokemons"> - + diff --git a/build.gradle b/build.gradle index d55db72..0965633 100644 --- a/build.gradle +++ b/build.gradle @@ -1,24 +1,25 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.31' - ext.lifecycle_ktx = "2.3.1" - ext.nav_version = "2.3.5" - ext.work_version = "2.6.0" - ext.room = "2.3.0" + ext.kotlin_version = '1.6.10' + ext.lifecycle_ktx = "2.4.1" + ext.nav_version = "2.4.1" + ext.work_version = "2.7.1" + ext.room = "2.4.2" ext.room_compiler = "2.2.5" - ext.compat = "1.3.1" + ext.compat = "1.4.1" ext.gson = "2.8.6" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" - ext.okhttp_log = "4.9.0" - ext.okhttp = "4.9.0" + ext.okhttp_log = "4.9.2" + ext.okhttp = "4.9.2" ext.junit = "4.13.2" ext.junit_android = "1.1.3" ext.espresso = "3.4.0" - ext.ktx = "1.6.0" - ext.coroutins = "1.5.0" - ext.composeVersion = "1.0.3" - ext.hilt = "2.38.1" + ext.ktx = "1.7.0" + ext.coroutines = "1.5.2" + ext.composeVersion = "1.1.1" + ext.composeVersionTheme = "1.1.5" + ext.hilt = "2.40" ext.hilt_jetpack = "1.0.0" ext.mockito_core = "3.7.0" ext.mockito_kotlin = "2.2.0" @@ -27,9 +28,9 @@ buildscript { project.ext { min = 21 - target = 30 - compile = 30 - tools = "30.0.2" + target = 31 + compile = 31 + tools = "30.0.3" } repositories { @@ -38,10 +39,10 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" - classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1") + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2be7f07..d955494 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip diff --git a/models/build.gradle b/models/build.gradle index 000ec1c..39c999c 100644 --- a/models/build.gradle +++ b/models/build.gradle @@ -38,8 +38,8 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + // coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" } repositories { diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 2725a72..7d1f6f2 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -52,12 +52,12 @@ dependencies { implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" + // coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // hilt - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation "com.google.dagger:hilt-android:2.40" + kapt("com.google.dagger:hilt-android-compiler:2.40") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") testImplementation "junit:junit:$junit" diff --git a/mvi/build.gradle b/mvi/build.gradle index b34e4d0..8cc2cae 100644 --- a/mvi/build.gradle +++ b/mvi/build.gradle @@ -29,8 +29,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx" // navigation - implementation "androidx.navigation:navigation-fragment:$nav_version" - implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.core:core-ktx:$ktx" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 4617cf1..4fceed9 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -1,6 +1,9 @@ package com.mdgd.mvi -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.mvi.states.ScreenState @@ -31,7 +34,6 @@ abstract class MviViewModel, A> : ViewModel(), Fragment actionHolder.value = action } - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { } } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index dfd3457..1739435 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -1,13 +1,13 @@ package com.mdgd.mvi.fragments import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData class FragmentContract { - interface ViewModel : LifecycleObserver { - fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) + interface ViewModel : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) fun getStateObservable(): MutableLiveData fun getActionObservable(): MutableLiveData } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt index da3d17c..22bf354 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt @@ -3,7 +3,10 @@ package com.mdgd.mvi.fragments import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import com.mdgd.mvi.states.AbstractAction import com.mdgd.mvi.states.ScreenState import java.lang.reflect.ParameterizedType @@ -14,7 +17,7 @@ abstract class HostedDialogFragment< ACTION : AbstractAction, VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : AppCompatDialogFragment(), FragmentContract.View, Observer, LifecycleObserver { + : AppCompatDialogFragment(), FragmentContract.View, Observer, LifecycleEventObserver { protected var model: VIEW_MODEL? = null private set @@ -46,14 +49,15 @@ abstract class HostedDialogFragment< setModel(createModel()) lifecycle.addObserver(this) model?.getStateObservable()?.observe(this, this) - model?.getActionObservable()?.observe(this, { action -> - action.visit(this as VIEW) + model?.getActionObservable()?.observe(this, object : Observer { + override fun onChanged(action: ACTION) { + action.visit(this as VIEW) + } }) } - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - model?.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + model?.onStateChanged(source, event) if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index 6970744..1e6b51c 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -2,7 +2,10 @@ package com.mdgd.mvi.fragments import android.content.Context import android.os.Bundle -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment import com.mdgd.mvi.states.ScreenAction import com.mdgd.mvi.states.ScreenState @@ -14,7 +17,7 @@ abstract class HostedFragment< ACTION : ScreenAction, VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : NavHostFragment(), FragmentContract.View, Observer, LifecycleObserver { + : NavHostFragment(), FragmentContract.View, Observer, LifecycleEventObserver { protected var model: VIEW_MODEL? = null private set @@ -30,8 +33,10 @@ abstract class HostedFragment< } catch (e: Throwable) { val hostClassName = ((javaClass.genericSuperclass as ParameterizedType) .actualTypeArguments[1] as Class<*>).canonicalName - throw RuntimeException("Activity must implement " + hostClassName - + " to attach " + this.javaClass.simpleName, e) + throw RuntimeException( + "Activity must implement $hostClassName to attach ${this.javaClass.simpleName}", + e + ) } } @@ -46,16 +51,17 @@ abstract class HostedFragment< setModel(createModel()) lifecycle.addObserver(this) model?.getStateObservable()?.observe(this, this) - model?.getActionObservable()?.observe(this, { action -> - action.visit(this as VIEW) + model?.getActionObservable()?.observe(this, object : Observer { + override fun onChanged(action: ACTION) { + action.visit(this as VIEW) + } }) } protected abstract fun createModel(): VIEW_MODEL - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - model?.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + model?.onStateChanged(source, event) if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) From ab171013b2f1449757be99803da0fbea95c7720b Mon Sep 17 00:00:00 2001 From: DanGdl Date: Tue, 22 Mar 2022 20:32:24 +0200 Subject: [PATCH 31/47] # migration fixes --- .../mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt | 9 ++++----- .../com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt | 4 ++-- .../java/com/mdgd/pokemon/ui/splash/SplashFragment.kt | 6 +++--- .../java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt | 4 ++-- .../java/com/mdgd/mvi/fragments/HostedDialogFragment.kt | 6 ++++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 10e59de..384b22d 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList @HiltViewModel class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo) : @@ -35,16 +34,16 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo pokemonIdFlow.tryEmit(pokemonId) } - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + super.onStateChanged(source, event) if (event == Lifecycle.Event.ON_CREATE && pokemonLoadingJob == null) { pokemonLoadingJob = viewModelScope.launch { pokemonIdFlow .filter { it != -1L } .map { repo.getPokemonById(it) } .map { it?.let { mapToListPokemon(it) } ?: LinkedList() } - .flowOn(Dispatchers.IO) - .collect { setState(PokemonDetailsScreenState.SetData(it)) } + .flowOn(Dispatchers.IO) + .collect { setState(PokemonDetailsScreenState.SetData(it)) } } } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index edd6e75..8dd8cfd 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -34,8 +34,8 @@ class PokemonsViewModel @Inject constructor( private val filterFlow = MutableStateFlow(FilterData()) private var launch: Job? = null - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + super.onStateChanged(source, event) if (event == Lifecycle.Event.ON_CREATE && launch == null) { launch = viewModelScope.launch(exceptionHandler) { pageFlow diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index 07db1bd..17919a6 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -41,7 +41,7 @@ class SplashFragment : HostedFragment(), SplashContract.View { - private val errorDialogTrigger = mutableStateOf(DefaultErrorParams()) + private val errorDialogTrigger = mutableStateOf(DefaultErrorParams()) override fun createModel(): SplashContract.ViewModel { val model: SplashViewModel by viewModels() @@ -81,8 +81,8 @@ class SplashFragment : } @Composable -fun SplashScreen(errorParams: MutableState) { - val errorDialogTrigger = remember { errorParams as MutableState } +fun SplashScreen(errorParams: MutableState) { + val errorDialogTrigger = remember { errorParams } MdcTheme { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 602108b..f5d079e 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -29,8 +29,8 @@ class SplashViewModel @Inject constructor(private val cache: Cache) : private var progressJob: Job? = null - public override fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { - super.onAny(owner, event) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + super.onStateChanged(source, event) if (event == Lifecycle.Event.ON_START && progressJob == null) { progressJob = viewModelScope.launch(exceptionHandler) { flow { diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt index 22bf354..e21569a 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt @@ -33,8 +33,10 @@ abstract class HostedDialogFragment< } catch (e: Throwable) { val hostClassName = ((javaClass.genericSuperclass as ParameterizedType) .actualTypeArguments[1] as Class<*>).canonicalName - throw RuntimeException("Activity must implement " + hostClassName - + " to attach " + javaClass.simpleName, e) + throw RuntimeException( + "Activity must implement $hostClassName to attach ${javaClass.simpleName}", + e + ) } } From bac456f720fdc5068cd1a8b8d3ccc26c12dacba0 Mon Sep 17 00:00:00 2001 From: DanGdl Date: Tue, 22 Mar 2022 20:49:18 +0200 Subject: [PATCH 32/47] # merge fix --- .../ui/pokemon/PokemonDetailsViewModel.kt | 11 ++++---- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 15 +++++------ .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 17 ++++++------ .../main/java/com/mdgd/mvi/MviViewModel.kt | 26 +++++++++---------- .../mdgd/mvi/fragments/FragmentContract.kt | 12 ++++----- .../com/mdgd/mvi/fragments/HostedFragment.kt | 10 +++---- 6 files changed, 42 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 384b22d..481fd2e 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -1,7 +1,6 @@ package com.mdgd.pokemon.ui.pokemon import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.R @@ -12,7 +11,7 @@ import com.mdgd.pokemon.models.repo.schemas.Form import com.mdgd.pokemon.models.repo.schemas.GameIndex import com.mdgd.pokemon.models.repo.schemas.Type import com.mdgd.pokemon.ui.pokemon.dto.* -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -24,7 +23,7 @@ import javax.inject.Inject @HiltViewModel class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo) : - MviViewModel(), + MviViewModel(), PokemonDetailsContract.ViewModel { private val pokemonIdFlow = MutableStateFlow(-1L) @@ -34,8 +33,8 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo pokemonIdFlow.tryEmit(pokemonId) } - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - super.onStateChanged(source, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_CREATE && pokemonLoadingJob == null) { pokemonLoadingJob = viewModelScope.launch { pokemonIdFlow @@ -103,7 +102,7 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo } override fun onBackPressed() { - setAction(PokemonDetailsScreenAction.ActionBack()) + setAction(PokemonDetailsScreenEffect.EffectBack()) } override fun onCleared() { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index 8dd8cfd..a37e01a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -1,7 +1,6 @@ package com.mdgd.pokemon.ui.pokemons import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.models.filters.FilterData @@ -9,7 +8,7 @@ import com.mdgd.pokemon.models.filters.StatsFilter import com.mdgd.pokemon.models.repo.PokemonsRepo import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.util.DispatchersHolder -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler @@ -23,19 +22,19 @@ class PokemonsViewModel @Inject constructor( private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder -) : MviViewModel(), +) : MviViewModel(), PokemonsContract.ViewModel { private var firstVisibleIndex: Int = 0 private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(PokemonsScreenAction.Error(e)) + setAction(PokemonsScreenEffect.Error(e)) } private val pageFlow = MutableStateFlow(0) private val filterFlow = MutableStateFlow(FilterData()) private var launch: Job? = null - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - super.onStateChanged(source, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_CREATE && launch == null) { launch = viewModelScope.launch(exceptionHandler) { pageFlow @@ -43,7 +42,7 @@ class PokemonsViewModel @Inject constructor( .flowOn(dispatchers.getMain()) .map { page -> Pair(page, repo.getPage(page)) } .flowOn(dispatchers.getIO()) - .catch { e: Throwable -> setAction(PokemonsScreenAction.Error(e)) } + .catch { e: Throwable -> setAction(PokemonsScreenEffect.Error(e)) } .collect { pagePair: Pair> -> if (pagePair.first == 0) { setState( @@ -105,7 +104,7 @@ class PokemonsViewModel @Inject constructor( } override fun onItemClicked(pokemon: PokemonFullDataSchema) { - setAction(PokemonsScreenAction.ShowDetails(pokemon.pokemonSchema?.id)) + setAction(PokemonsScreenEffect.ShowDetails(pokemon.pokemonSchema?.id)) } override fun onScroll(firstVisibleIndex: Int, lastVisibleIndex: Int) { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index f5d079e..37c44f1 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -1,12 +1,11 @@ package com.mdgd.pokemon.ui.splash import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import com.mdgd.mvi.MviViewModel import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect import com.mdgd.pokemon.ui.splash.state.SplashScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler @@ -20,17 +19,17 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor(private val cache: Cache) : - MviViewModel(), + MviViewModel(), SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(SplashScreenAction.ShowError(e)) + setAction(SplashScreenEffect.ShowError(e)) } private var progressJob: Job? = null - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - super.onStateChanged(source, event) + override fun onStateChanged(event: Lifecycle.Event) { + super.onStateChanged(event) if (event == Lifecycle.Event.ON_START && progressJob == null) { progressJob = viewModelScope.launch(exceptionHandler) { flow { @@ -41,13 +40,13 @@ class SplashViewModel @Inject constructor(private val cache: Cache) : // Result(Throwable("Dummy")) }.collect { if (it.isError()) { - setAction(SplashScreenAction.ShowError(it.getError())) + setAction(SplashScreenEffect.ShowError(it.getError())) } else if (it.getValue() != 0L) { - setAction(SplashScreenAction.NextScreen) + setAction(SplashScreenEffect.NextScreen) } } } - setAction(SplashScreenAction.LaunchWorker) + setAction(SplashScreenEffect.LaunchWorker) } } diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 887b3d0..c96c586 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -1,19 +1,21 @@ package com.mdgd.mvi import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.mvi.states.ScreenState -abstract class MviViewModel, A> : ViewModel(), FragmentContract.ViewModel { - private val stateHolder = MutableLiveData() // TODO: use StateFlow: val uiState: StateFlow = _uiState ? - private val actionHolder = MutableLiveData() +abstract class MviViewModel, E> : ViewModel(), + FragmentContract.ViewModel { + private val stateHolder = + MutableLiveData() // TODO: use StateFlow: val uiState: StateFlow = _uiState ? + private val effectHolder = MutableLiveData() - override fun getStateObservable(): MutableLiveData { - return stateHolder - } + override fun getStateObservable(): LiveData = stateHolder + + override fun getActionObservable(): LiveData = effectHolder protected fun setState(state: S) { stateHolder.value?.let { state.merge(it) } @@ -22,14 +24,10 @@ abstract class MviViewModel, A> : ViewModel(), Fragment protected fun getState() = stateHolder.value - override fun getActionObservable(): MutableLiveData { - return actionHolder - } - - protected fun setAction(action: A) { - actionHolder.value = action + protected fun setAction(effect: E) { + effectHolder.value = effect } - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + override fun onStateChanged(event: Lifecycle.Event) { } } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index 1739435..ea8f408 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -1,15 +1,13 @@ package com.mdgd.mvi.fragments import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.LiveData class FragmentContract { - interface ViewModel : LifecycleEventObserver { - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) - fun getStateObservable(): MutableLiveData - fun getActionObservable(): MutableLiveData + interface ViewModel { + fun onStateChanged(event: Lifecycle.Event) + fun getStateObservable(): LiveData + fun getActionObservable(): LiveData } interface View diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index 1e6b51c..24ee21a 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -7,14 +7,14 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment -import com.mdgd.mvi.states.ScreenAction +import com.mdgd.mvi.states.ScreenEffect import com.mdgd.mvi.states.ScreenState import java.lang.reflect.ParameterizedType abstract class HostedFragment< VIEW : FragmentContract.View, STATE : ScreenState, - ACTION : ScreenAction, + ACTION : ScreenEffect, VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> : NavHostFragment(), FragmentContract.View, Observer, LifecycleEventObserver { @@ -52,8 +52,8 @@ abstract class HostedFragment< lifecycle.addObserver(this) model?.getStateObservable()?.observe(this, this) model?.getActionObservable()?.observe(this, object : Observer { - override fun onChanged(action: ACTION) { - action.visit(this as VIEW) + override fun onChanged(effect: ACTION) { + effect.visit(this as VIEW) } }) } @@ -61,7 +61,7 @@ abstract class HostedFragment< protected abstract fun createModel(): VIEW_MODEL override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - model?.onStateChanged(source, event) + model?.onStateChanged(event) if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) From 3bcc74c61e1bc64cdb0fe25150522e05584747c3 Mon Sep 17 00:00:00 2001 From: DanGdl Date: Mon, 11 Apr 2022 19:31:17 +0300 Subject: [PATCH 33/47] # libs update --- app/build.gradle | 2 +- build.gradle | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 881df96..d8e19dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-alpha05" + kotlinCompilerExtensionVersion = "1.2.0-alpha07" } buildTypes { release { diff --git a/build.gradle b/build.gradle index 0965633..eed9e1e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,12 @@ buildscript { ext.kotlin_version = '1.6.10' ext.lifecycle_ktx = "2.4.1" - ext.nav_version = "2.4.1" + ext.nav_version = "2.4.2" ext.work_version = "2.7.1" ext.room = "2.4.2" ext.room_compiler = "2.2.5" ext.compat = "1.4.1" - ext.gson = "2.8.6" + ext.gson = "2.8.9" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" ext.okhttp_log = "4.9.2" @@ -16,15 +16,15 @@ buildscript { ext.junit_android = "1.1.3" ext.espresso = "3.4.0" ext.ktx = "1.7.0" - ext.coroutines = "1.5.2" + ext.coroutines = "1.6.0" ext.composeVersion = "1.1.1" ext.composeVersionTheme = "1.1.5" ext.hilt = "2.40" ext.hilt_jetpack = "1.0.0" - ext.mockito_core = "3.7.0" + ext.mockito_core = "4.3.1" ext.mockito_kotlin = "2.2.0" ext.testing_core = "1.1.1" - ext.testing_coroutine = "1.5.0" + ext.testing_coroutine = "1.6.0" project.ext { min = 21 @@ -39,7 +39,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt" From c68ea1381c67b2394f47c92ecd07960da3a91347 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Thu, 16 Jun 2022 08:19:56 +0300 Subject: [PATCH 34/47] # mvi update (partial states) --- .../com/mdgd/pokemon/ui/error/ErrorScreen.kt | 2 +- .../ui/pokemon/PokemonDetailsViewModel.kt | 7 +- .../state/PokemonDetailsScreenState.kt | 18 ++-- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 15 ++-- .../ui/pokemons/state/PokemonsScreenEffect.kt | 7 ++ .../ui/pokemons/state/PokemonsScreenState.kt | 88 ++++++++++--------- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 9 +- .../ui/splash/state/SplashScreenState.kt | 9 +- .../main/java/com/mdgd/mvi/MviViewModel.kt | 14 +-- .../mdgd/mvi/fragments/FragmentContract.kt | 2 +- .../com/mdgd/mvi/fragments/HostedFragment.kt | 14 ++- .../com/mdgd/mvi/states/AbstractEffect.kt | 6 +- .../java/com/mdgd/mvi/states/AbstractState.kt | 12 +++ .../java/com/mdgd/mvi/states/ScreenState.kt | 2 +- 14 files changed, 116 insertions(+), 89 deletions(-) create mode 100644 mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt index 4a49aa1..24c17af 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt @@ -35,7 +35,7 @@ fun ErrorScreen(params: MutableState) { // button. If you want to disable that functionality, simply use an empty // onCloseRequest. params.value = params.value.hide() - }, + } ) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 481fd2e..3d1f8ce 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -16,7 +16,10 @@ import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.* import javax.inject.Inject @@ -102,7 +105,7 @@ class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo } override fun onBackPressed() { - setAction(PokemonDetailsScreenEffect.EffectBack()) + setEffect(PokemonDetailsScreenEffect.EffectBack()) } override fun onCleared() { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenState.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenState.kt index c3ca2ae..e8e5aa6 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenState.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenState.kt @@ -1,18 +1,20 @@ package com.mdgd.pokemon.ui.pokemon.state -import com.mdgd.mvi.states.ScreenState +import com.mdgd.mvi.states.AbstractState import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty -sealed class PokemonDetailsScreenState : ScreenState { +open class PokemonDetailsScreenState( + val items: List +) : AbstractState() { - override fun merge(prevState: PokemonDetailsScreenState) {} + override fun visit(screen: PokemonDetailsContract.View) { + screen.setItems(items) + } + // PARTIAL STATES - class SetData(val items: List) : PokemonDetailsScreenState() { + class SetData(items: List) : PokemonDetailsScreenState(items) - override fun visit(screen: PokemonDetailsContract.View) { - screen.setItems(items) - } - } + // EOF: PARTIAL STATES } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index a37e01a..d55f578 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -27,7 +27,7 @@ class PokemonsViewModel @Inject constructor( private var firstVisibleIndex: Int = 0 private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(PokemonsScreenEffect.Error(e)) + setEffect(PokemonsScreenEffect.Error(e)) } private val pageFlow = MutableStateFlow(0) private val filterFlow = MutableStateFlow(FilterData()) @@ -42,7 +42,7 @@ class PokemonsViewModel @Inject constructor( .flowOn(dispatchers.getMain()) .map { page -> Pair(page, repo.getPage(page)) } .flowOn(dispatchers.getIO()) - .catch { e: Throwable -> setAction(PokemonsScreenEffect.Error(e)) } + .catch { e: Throwable -> setEffect(PokemonsScreenEffect.Error(e)) } .collect { pagePair: Pair> -> if (pagePair.first == 0) { setState( @@ -62,6 +62,7 @@ class PokemonsViewModel @Inject constructor( .collect { sortedList -> firstVisibleIndex = 0 setState(PokemonsScreenState.UpdateData(sortedList)) + setEffect(PokemonsScreenEffect.ScrollToStart()) } } } @@ -76,8 +77,8 @@ class PokemonsViewModel @Inject constructor( list.sortWith { pokemon1: PokemonFullDataSchema?, pokemon2: PokemonFullDataSchema? -> var compare = 0 for (filter in filters.filters) { - compare = comparators[filter]?.compare(pokemon2!!, pokemon1!!) - ?: 0 // swap, instead of multiply on -1 + // swap, instead of multiply on -1 + compare = comparators[filter]?.compare(pokemon2!!, pokemon1!!) ?: 0 if (compare != 0) { break } @@ -93,18 +94,18 @@ class PokemonsViewModel @Inject constructor( } override fun sort(filter: String) { - val filters = ArrayList(getState()?.getActiveFilters() ?: listOf()) + val filters = getState()?.activeFilters?.toMutableList() ?: mutableListOf() if (filters.contains(filter)) { filters.remove(filter) } else { filters.add(filter) } setState(PokemonsScreenState.ChangeFilterState(filters)) - filterFlow.tryEmit(FilterData(getState()?.getActiveFilters() ?: listOf())) + filterFlow.tryEmit(FilterData(filters)) } override fun onItemClicked(pokemon: PokemonFullDataSchema) { - setAction(PokemonsScreenEffect.ShowDetails(pokemon.pokemonSchema?.id)) + setEffect(PokemonsScreenEffect.ShowDetails(pokemon.pokemonSchema?.id)) } override fun onScroll(firstVisibleIndex: Int, lastVisibleIndex: Int) { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt index 8030ca8..ed2865e 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt @@ -20,4 +20,11 @@ sealed class PokemonsScreenEffect : AbstractEffect() { screen.proceedToNextScreen(id) } } + + class ScrollToStart : PokemonsScreenEffect() { + + override fun handle(screen: PokemonsContract.View) { + screen.scrollToStart() + } + } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt index 5c5f1b0..1512fce 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt @@ -1,20 +1,15 @@ package com.mdgd.pokemon.ui.pokemons.state -import com.mdgd.mvi.states.ScreenState +import com.mdgd.mvi.states.AbstractState import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.ui.pokemons.PokemonsContract -sealed class PokemonsScreenState( - private val isProgressVisible: Boolean = false, - protected val list: MutableList = mutableListOf(), - protected val availableFilters: MutableList = mutableListOf(), - protected val activeFilters: MutableList = mutableListOf()) - : ScreenState { - - fun getItems() = list.toList() - - @JvmName("getActiveFilters1") - fun getActiveFilters() = activeFilters.toList() +open class PokemonsScreenState( + protected val isProgressVisible: Boolean = false, + val list: List = listOf(), + protected val availableFilters: List = listOf(), + val activeFilters: List = listOf() +) : AbstractState() { override fun visit(screen: PokemonsContract.View) { screen.setProgressVisibility(isProgressVisible) @@ -25,52 +20,65 @@ sealed class PokemonsScreenState( } + // PARTIAL STATES + class Loading : PokemonsScreenState(true) { - override fun merge(prevState: PokemonsScreenState) { - list.addAll(prevState.list) - availableFilters.addAll(prevState.availableFilters) - activeFilters.addAll(prevState.activeFilters) + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + return PokemonsScreenState( + isProgressVisible, prevState.list, + prevState.availableFilters, prevState.activeFilters + ) } } - class SetData(list: List, availableFilters: List) - : PokemonsScreenState(false, ArrayList(list), ArrayList(availableFilters)) { + class SetData( + list: List, availableFilters: List + ) : PokemonsScreenState(false, list, availableFilters) { - override fun merge(prevState: PokemonsScreenState) { - activeFilters.addAll(prevState.activeFilters) + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + return PokemonsScreenState( + isProgressVisible, list, availableFilters, prevState.activeFilters + ) } } - class AddData(list: List) : PokemonsScreenState(false, ArrayList(list)) { + class AddData( + list: List + ) : PokemonsScreenState(false, list) { - override fun merge(prevState: PokemonsScreenState) { - list.addAll(0, prevState.list) - availableFilters.addAll(prevState.availableFilters) - activeFilters.addAll(prevState.activeFilters) + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + val items = list.toMutableList() + items.addAll(0, prevState.list) + return PokemonsScreenState( + isProgressVisible, items, prevState.availableFilters, prevState.activeFilters + ) } } - class UpdateData(items: List) - : PokemonsScreenState(false, ArrayList(items)) { + class UpdateData( + items: List + ) : PokemonsScreenState(false, items) { - override fun merge(prevState: PokemonsScreenState) { - availableFilters.addAll(prevState.availableFilters) - activeFilters.addAll(prevState.activeFilters) - } - - override fun visit(screen: PokemonsContract.View) { - super.visit(screen) - screen.scrollToStart() + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + return PokemonsScreenState( + isProgressVisible, list, prevState.availableFilters, prevState.activeFilters + ) } } - class ChangeFilterState(filters: MutableList) : - PokemonsScreenState(activeFilters = filters) { + class ChangeFilterState(activeFilters: List) : + PokemonsScreenState(activeFilters = activeFilters) { - override fun merge(prevState: PokemonsScreenState) { - availableFilters.addAll(prevState.availableFilters) - list.addAll(prevState.list) + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + return PokemonsScreenState( + isProgressVisible, + list, + prevState.availableFilters, + activeFilters + ) } } + + // EOF: PARTIAL STATES } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 37c44f1..5be92fc 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -11,7 +11,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch @@ -23,7 +22,7 @@ class SplashViewModel @Inject constructor(private val cache: Cache) : SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(SplashScreenEffect.ShowError(e)) + setEffect(SplashScreenEffect.ShowError(e)) } private var progressJob: Job? = null @@ -40,13 +39,13 @@ class SplashViewModel @Inject constructor(private val cache: Cache) : // Result(Throwable("Dummy")) }.collect { if (it.isError()) { - setAction(SplashScreenEffect.ShowError(it.getError())) + setEffect(SplashScreenEffect.ShowError(it.getError())) } else if (it.getValue() != 0L) { - setAction(SplashScreenEffect.NextScreen) + setEffect(SplashScreenEffect.NextScreen) } } } - setAction(SplashScreenEffect.LaunchWorker) + setEffect(SplashScreenEffect.LaunchWorker) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenState.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenState.kt index 793e5d0..a2ee06a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenState.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/state/SplashScreenState.kt @@ -1,11 +1,6 @@ package com.mdgd.pokemon.ui.splash.state -import com.mdgd.mvi.states.ScreenState +import com.mdgd.mvi.states.AbstractState import com.mdgd.pokemon.ui.splash.SplashContract -sealed class SplashScreenState : ScreenState { - - override fun visit(screen: SplashContract.View) {} - - override fun merge(prevState: SplashScreenState) {} -} +sealed class SplashScreenState : AbstractState() diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index c96c586..a49110b 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -1,5 +1,6 @@ package com.mdgd.mvi +import androidx.annotation.CallSuper import androidx.lifecycle.Lifecycle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -9,25 +10,26 @@ import com.mdgd.mvi.states.ScreenState abstract class MviViewModel, E> : ViewModel(), FragmentContract.ViewModel { - private val stateHolder = - MutableLiveData() // TODO: use StateFlow: val uiState: StateFlow = _uiState ? + private val stateHolder = MutableLiveData() private val effectHolder = MutableLiveData() override fun getStateObservable(): LiveData = stateHolder - override fun getActionObservable(): LiveData = effectHolder + override fun getEffectObservable(): LiveData = effectHolder protected fun setState(state: S) { - stateHolder.value?.let { state.merge(it) } - stateHolder.value = state + stateHolder.value = stateHolder.value?.let { + state.merge(it) + } ?: state } protected fun getState() = stateHolder.value - protected fun setAction(effect: E) { + protected fun setEffect(effect: E) { effectHolder.value = effect } + @CallSuper override fun onStateChanged(event: Lifecycle.Event) { } } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index ea8f408..66f166c 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -7,7 +7,7 @@ class FragmentContract { interface ViewModel { fun onStateChanged(event: Lifecycle.Event) fun getStateObservable(): LiveData - fun getActionObservable(): LiveData + fun getEffectObservable(): LiveData } interface View diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index 24ee21a..c56cbd0 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -14,8 +14,8 @@ import java.lang.reflect.ParameterizedType abstract class HostedFragment< VIEW : FragmentContract.View, STATE : ScreenState, - ACTION : ScreenEffect, - VIEW_MODEL : FragmentContract.ViewModel, + EFFECT : ScreenEffect, + VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> : NavHostFragment(), FragmentContract.View, Observer, LifecycleEventObserver { @@ -51,11 +51,7 @@ abstract class HostedFragment< setModel(createModel()) lifecycle.addObserver(this) model?.getStateObservable()?.observe(this, this) - model?.getActionObservable()?.observe(this, object : Observer { - override fun onChanged(effect: ACTION) { - effect.visit(this as VIEW) - } - }) + model?.getEffectObservable()?.observe(this) { it.visit(this@HostedFragment as VIEW) } } protected abstract fun createModel(): VIEW_MODEL @@ -66,13 +62,13 @@ abstract class HostedFragment< if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) // order matters - model?.getActionObservable()?.removeObservers(this) + model?.getEffectObservable()?.removeObservers(this) model?.getStateObservable()?.removeObservers(this) } } override fun onChanged(screenState: STATE) { - screenState.visit(this as VIEW) + screenState.visit(this@HostedFragment as VIEW) } protected fun setModel(model: VIEW_MODEL) { diff --git a/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt index d2124dd..827d6fc 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt @@ -5,10 +5,12 @@ abstract class AbstractEffect : ScreenEffect { override fun visit(screen: T) { if (!isHandled) { - handle(screen); + handle(screen) isHandled = true } } - abstract fun handle(screen: T); + open fun handle(screen: T) { + + } } \ No newline at end of file diff --git a/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt b/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt new file mode 100644 index 0000000..041dc97 --- /dev/null +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt @@ -0,0 +1,12 @@ +package com.mdgd.mvi.states + +abstract class AbstractState : ScreenState { + + override fun visit(screen: T) { + + } + + override fun merge(prevState: S): S { + return this as S + } +} \ No newline at end of file diff --git a/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt b/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt index 410df9a..4f8d687 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt @@ -3,5 +3,5 @@ package com.mdgd.mvi.states interface ScreenState { fun visit(screen: T) - fun merge(prevState: S) + fun merge(prevState: S): S } From c87b76e5d2d82f13273972c1bb13c419e1018f66 Mon Sep 17 00:00:00 2001 From: "YDT_DOM\\dan-gl" Date: Thu, 16 Jun 2022 09:07:39 +0300 Subject: [PATCH 35/47] # libs update --- app/build.gradle | 6 +++--- build.gradle | 26 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- models_impl/build.gradle | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d8e19dd..e7c9c4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-alpha07" + kotlinCompilerExtensionVersion = "1.2.0-rc01" } buildTypes { release { @@ -75,8 +75,8 @@ dependencies { implementation "com.google.code.gson:gson:$gson" // hilt - implementation "com.google.dagger:hilt-android:2.40" - kapt("com.google.dagger:hilt-android-compiler:2.40") + implementation "com.google.dagger:hilt-android:2.42" + kapt("com.google.dagger:hilt-android-compiler:2.42") implementation "androidx.hilt:hilt-navigation-fragment:$hilt_jetpack" kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") implementation 'androidx.hilt:hilt-work:1.0.0' diff --git a/build.gradle b/build.gradle index eed9e1e..979f825 100644 --- a/build.gradle +++ b/build.gradle @@ -1,25 +1,25 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.6.21' // because composeVersion 1.7.0 ext.lifecycle_ktx = "2.4.1" ext.nav_version = "2.4.2" ext.work_version = "2.7.1" ext.room = "2.4.2" ext.room_compiler = "2.2.5" - ext.compat = "1.4.1" - ext.gson = "2.8.9" + ext.compat = "1.4.2" + ext.gson = "2.9.0" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" - ext.okhttp_log = "4.9.2" - ext.okhttp = "4.9.2" + ext.okhttp_log = "4.9.3" + ext.okhttp = "4.9.3" ext.junit = "4.13.2" ext.junit_android = "1.1.3" ext.espresso = "3.4.0" - ext.ktx = "1.7.0" - ext.coroutines = "1.6.0" - ext.composeVersion = "1.1.1" - ext.composeVersionTheme = "1.1.5" - ext.hilt = "2.40" + ext.ktx = "1.8.0" + ext.coroutines = "1.6.1" + ext.composeVersion = "1.2.0-rc01" // update kotlinCompilerExtensionVersion when composeVersion updated + ext.composeVersionTheme = "1.1.11" // update kotlinCompilerExtensionVersion when composeVersionTheme updated + ext.hilt = "2.42" ext.hilt_jetpack = "1.0.0" ext.mockito_core = "4.3.1" ext.mockito_kotlin = "2.2.0" @@ -28,8 +28,8 @@ buildscript { project.ext { min = 21 - target = 31 - compile = 31 + target = 32 + compile = 32 tools = "30.0.3" } @@ -39,7 +39,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d955494..52d2165 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 7d1f6f2..f9a27d5 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -56,8 +56,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // hilt - implementation "com.google.dagger:hilt-android:2.40" - kapt("com.google.dagger:hilt-android-compiler:2.40") + implementation "com.google.dagger:hilt-android:2.42" + kapt("com.google.dagger:hilt-android-compiler:2.42") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") testImplementation "junit:junit:$junit" From 990f0721bd954917f9258bed48db150e8886d203 Mon Sep 17 00:00:00 2001 From: DanGdl Date: Thu, 16 Jun 2022 18:32:19 +0300 Subject: [PATCH 36/47] # cleanup --- ...er-Practical Task-Updated-June-2020-pdf.pdf | Bin 183610 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Android Developer-Practical Task-Updated-June-2020-pdf.pdf diff --git a/Android Developer-Practical Task-Updated-June-2020-pdf.pdf b/Android Developer-Practical Task-Updated-June-2020-pdf.pdf deleted file mode 100644 index c938e5974692c59795c06c6d82ac585199ec0fdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183610 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 From 224f4259a1b2c30f55fe915d9ddadbefd278962d Mon Sep 17 00:00:00 2001 From: Dan Gld Date: Fri, 11 Nov 2022 12:55:57 +0200 Subject: [PATCH 37/47] # libs update + room fixes --- app/build.gradle | 2 +- build.gradle | 44 +++++++++++-------- gradle/wrapper/gradle-wrapper.properties | 2 +- .../models/repo/dao/schemas/MoveSchema.kt | 2 +- .../pokemon/models/repo/schemas/Ability.kt | 2 +- .../mdgd/pokemon/models/repo/schemas/Form.kt | 2 +- .../pokemon/models/repo/schemas/GameIndex.kt | 2 +- .../mdgd/pokemon/models/repo/schemas/Move_.kt | 5 ++- .../mdgd/pokemon/models/repo/schemas/Stat.kt | 2 +- .../mdgd/pokemon/models/repo/schemas/Type.kt | 2 +- .../models/repo/schemas/VersionGroupDetail.kt | 2 +- .../models_impl/repo/dao/AppDatabase.kt | 10 +++-- .../models_impl/repo/dao/PokemonsDaoImpl.kt | 14 +++--- 13 files changed, 52 insertions(+), 39 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e7c9c4b..2791e2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-rc01" + kotlinCompilerExtensionVersion = "1.3.2" } buildTypes { release { diff --git a/build.gradle b/build.gradle index 979f825..493046e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,43 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.21' // because composeVersion 1.7.0 - ext.lifecycle_ktx = "2.4.1" - ext.nav_version = "2.4.2" + ext.kotlin_version = '1.7.20' // update according to composeVersion + ext.compat = "1.5.1" + ext.nav_version = "2.5.3" ext.work_version = "2.7.1" - ext.room = "2.4.2" + + ext.ktx = "1.9.0" + ext.coroutines = "1.6.4" + ext.lifecycle_ktx = "2.5.1" + + ext.room = "2.4.3" ext.room_compiler = "2.2.5" - ext.compat = "1.4.2" + ext.gson = "2.9.0" ext.retrofit = "2.9.0" ext.retrofit_gson = "2.9.0" - ext.okhttp_log = "4.9.3" - ext.okhttp = "4.9.3" - ext.junit = "4.13.2" - ext.junit_android = "1.1.3" - ext.espresso = "3.4.0" - ext.ktx = "1.8.0" - ext.coroutines = "1.6.1" - ext.composeVersion = "1.2.0-rc01" // update kotlinCompilerExtensionVersion when composeVersion updated - ext.composeVersionTheme = "1.1.11" // update kotlinCompilerExtensionVersion when composeVersionTheme updated + ext.okhttp_log = "4.10.0" + ext.okhttp = "4.10.0" + + ext.composeVersion = "1.3.1" + // update kotlinCompilerExtensionVersion when composeVersion updated + ext.composeVersionTheme = "1.1.21" + // update kotlinCompilerExtensionVersion when composeVersionTheme updated + ext.hilt = "2.42" ext.hilt_jetpack = "1.0.0" - ext.mockito_core = "4.3.1" + + ext.junit = "4.13.2" + ext.junit_android = "1.1.3" + ext.mockito_core = "4.8.1" ext.mockito_kotlin = "2.2.0" ext.testing_core = "1.1.1" ext.testing_coroutine = "1.6.0" + ext.espresso = "3.4.0" project.ext { min = 21 - target = 32 - compile = 32 + target = 33 + compile = 33 tools = "30.0.3" } @@ -39,7 +47,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 52d2165..481f98a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/dao/schemas/MoveSchema.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/dao/schemas/MoveSchema.kt index 5300bb6..0b9f220 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/dao/schemas/MoveSchema.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/dao/schemas/MoveSchema.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.schemas.Move_ @Entity( - tableName = "moves", indices = [Index("id")], + tableName = "moves", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], childColumns = ["pokemonId"], onDelete = ForeignKey.CASCADE diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Ability.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Ability.kt index a1278d3..881963c 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Ability.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Ability.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @Entity( - tableName = "abilities", indices = [Index("id")], + tableName = "abilities", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Form.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Form.kt index 6063af2..9174ae7 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Form.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Form.kt @@ -9,7 +9,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @Entity( - tableName = "forms", indices = [Index("id")], + tableName = "forms", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/GameIndex.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/GameIndex.kt index 922806e..14eecc6 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/GameIndex.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/GameIndex.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @Entity( - tableName = "game_indexes", indices = [Index("id")], + tableName = "game_indexes", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt index b93832a..5794f2a 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt @@ -4,11 +4,12 @@ import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName class Move_ { - @SerializedName("name") + @Expose + @SerializedName("name") var name: String? = null - @SerializedName("url") @Expose + @SerializedName("url") var url: String? = null } diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Stat.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Stat.kt index 878d026..4299067 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Stat.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Stat.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @Entity( - tableName = "stats", indices = [Index("id")], + tableName = "stats", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Type.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Type.kt index 907498a..9e769ca 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Type.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Type.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema @Entity( - tableName = "types", indices = [Index("id")], + tableName = "types", indices = [Index("id"), Index("pokemonId")], foreignKeys = [ForeignKey( entity = PokemonSchema::class, parentColumns = ["id"], diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/VersionGroupDetail.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/VersionGroupDetail.kt index 435e672..3ab77be 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/VersionGroupDetail.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/VersionGroupDetail.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName import com.mdgd.pokemon.models.repo.dao.schemas.MoveSchema @Entity( - tableName = "VersionGroupDetails", indices = [Index("id")], + tableName = "VersionGroupDetails", indices = [Index("id"), Index("moveId")], foreignKeys = [ForeignKey( entity = MoveSchema::class, parentColumns = ["id"], childColumns = ["moveId"], onDelete = ForeignKey.CASCADE diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt index 7e51158..ef04c4b 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt @@ -6,9 +6,13 @@ import com.mdgd.pokemon.models.repo.dao.schemas.MoveSchema import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema import com.mdgd.pokemon.models.repo.schemas.* -@Database(version = 1, exportSchema = false, - entities = [PokemonSchema::class, Ability::class, Form::class, GameIndex::class, - MoveSchema::class, Stat::class, Type::class, VersionGroupDetail::class]) +@Database( + version = 1, exportSchema = false, + entities = [ + PokemonSchema::class, Ability::class, Form::class, GameIndex::class, + MoveSchema::class, Stat::class, Type::class, VersionGroupDetail::class + ] +) abstract class AppDatabase : RoomDatabase() { abstract fun pokemonsDao(): PokemonsRoomDao? } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt index 7f27f3d..67f0d35 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt @@ -6,12 +6,12 @@ import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.schemas.PokemonDetails import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.* import javax.inject.Inject class PokemonsDaoImpl @Inject constructor(@ApplicationContext ctx: Context) : PokemonsDao { - private val pokemonsRoomDao: PokemonsRoomDao? = - Room.databaseBuilder(ctx, AppDatabase::class.java, "PokemonsAppDB").build().pokemonsDao() + private val pokemonsRoomDao: PokemonsRoomDao? = Room.databaseBuilder( + ctx, AppDatabase::class.java, "PokemonsAppDB" + ).build().pokemonsDao() override suspend fun save(list: List) { pokemonsRoomDao?.save(list) @@ -19,16 +19,16 @@ class PokemonsDaoImpl @Inject constructor(@ApplicationContext ctx: Context) : Po override suspend fun getPage(page: Int, pageSize: Int): List { val offset = page * pageSize - val rows = pokemonsRoomDao!!.countRows() + val rows = pokemonsRoomDao?.countRows() ?: 0 return when { rows == 0 -> ArrayList(0) - (pokemonsRoomDao.countRows() <= offset) -> throw Exception(PokemonsDao.NO_MORE_POKEMONS_MSG) - else -> pokemonsRoomDao.getPage(offset, pageSize) + (rows <= offset) -> throw Exception(PokemonsDao.NO_MORE_POKEMONS_MSG) + else -> pokemonsRoomDao?.getPage(offset, pageSize) ?: listOf() } } override suspend fun getCount(): Long { - return pokemonsRoomDao!!.countRows().toLong() + return pokemonsRoomDao?.countRows()?.toLong() ?: 0 } override suspend fun getPokemonById(pokemonId: Long): PokemonFullDataSchema? { From 0d4aa1ac90515a9f71a536c567b80c20d3815f80 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Fri, 21 Apr 2023 15:54:12 +0300 Subject: [PATCH 38/47] # mvi --- .../ui/pokemon/PokemonDetailsContract.kt | 5 +--- .../ui/pokemon/PokemonDetailsFragment.kt | 29 ++++++++++++++----- .../ui/pokemon/PokemonDetailsViewModel.kt | 13 ++++++--- .../pokemon/ui/pokemons/PokemonsContract.kt | 4 +-- .../pokemon/ui/pokemons/PokemonsFragment.kt | 27 ++++++++++++----- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 9 ++++-- .../mdgd/pokemon/ui/splash/SplashContract.kt | 4 +-- .../mdgd/pokemon/ui/splash/SplashFragment.kt | 4 +-- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 6 ++-- mvi/proguard-rules.pro | 1 - .../main/java/com/mdgd/mvi/MviViewModel.kt | 26 ++++++++--------- .../mdgd/mvi/fragments/FragmentContract.kt | 8 +++-- .../com/mdgd/mvi/fragments/HostedFragment.kt | 12 ++++---- .../com/mdgd/mvi/states/AbstractEffect.kt | 4 +-- .../java/com/mdgd/mvi/states/AbstractState.kt | 10 +++---- .../java/com/mdgd/mvi/states/ScreenState.kt | 6 ++-- 16 files changed, 94 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt index d0a4f37..e19138b 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsContract.kt @@ -2,12 +2,9 @@ package com.mdgd.pokemon.ui.pokemon import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState class PokemonDetailsContract { - interface ViewModel : - FragmentContract.ViewModel { + interface ViewModel : FragmentContract.ViewModel { fun setPokemonId(pokemonId: Long) fun onBackPressed() } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index 47fbaed..5d067b8 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -6,10 +6,21 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable @@ -42,16 +53,20 @@ import com.mdgd.pokemon.models.repo.schemas.Stat import com.mdgd.pokemon.models.repo.schemas.Stat_ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen -import com.mdgd.pokemon.ui.pokemon.dto.* -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState +import com.mdgd.pokemon.ui.pokemon.dto.ImageProperty +import com.mdgd.pokemon.ui.pokemon.dto.ImagePropertyData +import com.mdgd.pokemon.ui.pokemon.dto.LabelProperty +import com.mdgd.pokemon.ui.pokemon.dto.LabelPropertyData +import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty +import com.mdgd.pokemon.ui.pokemon.dto.TextProperty +import com.mdgd.pokemon.ui.pokemon.dto.TextPropertyData +import com.mdgd.pokemon.ui.pokemon.dto.TitleProperty +import com.mdgd.pokemon.ui.pokemon.dto.TitlePropertyData import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class PokemonDetailsFragment : HostedFragment< PokemonDetailsContract.View, - PokemonDetailsScreenState, - PokemonDetailsScreenEffect, PokemonDetailsContract.ViewModel, PokemonDetailsContract.Host>(), PokemonDetailsContract.View { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 3d1f8ce..5e459a0 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -10,7 +10,11 @@ import com.mdgd.pokemon.models.repo.schemas.Ability import com.mdgd.pokemon.models.repo.schemas.Form import com.mdgd.pokemon.models.repo.schemas.GameIndex import com.mdgd.pokemon.models.repo.schemas.Type -import com.mdgd.pokemon.ui.pokemon.dto.* +import com.mdgd.pokemon.ui.pokemon.dto.ImagePropertyData +import com.mdgd.pokemon.ui.pokemon.dto.LabelPropertyData +import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty +import com.mdgd.pokemon.ui.pokemon.dto.TextPropertyData +import com.mdgd.pokemon.ui.pokemon.dto.TitlePropertyData import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,12 +25,13 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.util.* +import java.util.LinkedList import javax.inject.Inject @HiltViewModel -class PokemonDetailsViewModel @Inject constructor(private val repo: PokemonsRepo) : - MviViewModel(), +class PokemonDetailsViewModel @Inject constructor( + private val repo: PokemonsRepo +) : MviViewModel(), PokemonDetailsContract.ViewModel { private val pokemonIdFlow = MutableStateFlow(-1L) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt index 77f005c..2e1d76d 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt @@ -2,11 +2,9 @@ package com.mdgd.pokemon.ui.pokemons import com.mdgd.mvi.fragments.FragmentContract import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState class PokemonsContract { - interface ViewModel : FragmentContract.ViewModel { + interface ViewModel : FragmentContract.ViewModel { fun reload() fun sort(filter: String) fun onItemClicked(pokemon: PokemonFullDataSchema) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index fad13f5..e582003 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -8,13 +8,30 @@ import android.view.ViewGroup import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.Card +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,16 +61,12 @@ import com.mdgd.pokemon.models.repo.schemas.Stat import com.mdgd.pokemon.models.repo.schemas.Stat_ import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @AndroidEntryPoint class PokemonsFragment : HostedFragment< PokemonsContract.View, - PokemonsScreenState, - PokemonsScreenEffect, PokemonsContract.ViewModel, PokemonsContract.Host>(), PokemonsContract.View { diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index d55f578..b52022f 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -13,7 +13,11 @@ import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,8 +26,7 @@ class PokemonsViewModel @Inject constructor( private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder -) : MviViewModel(), - PokemonsContract.ViewModel { +) : MviViewModel(), PokemonsContract.ViewModel { private var firstVisibleIndex: Int = 0 private val exceptionHandler = CoroutineExceptionHandler { _, e -> diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt index 4cd631a..834aff5 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashContract.kt @@ -1,8 +1,6 @@ package com.mdgd.pokemon.ui.splash import com.mdgd.mvi.fragments.FragmentContract -import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect -import com.mdgd.pokemon.ui.splash.state.SplashScreenState class SplashContract { companion object { @@ -10,7 +8,7 @@ class SplashContract { } - interface ViewModel : FragmentContract.ViewModel + interface ViewModel : FragmentContract.ViewModel interface View : FragmentContract.View { fun proceedToNextScreen() diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index 0b3e8c6..acbfb0a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -32,13 +32,11 @@ import com.mdgd.pokemon.bg.UploadWorker import com.mdgd.pokemon.ui.error.DefaultErrorParams import com.mdgd.pokemon.ui.error.ErrorParams import com.mdgd.pokemon.ui.error.ErrorScreen -import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect -import com.mdgd.pokemon.ui.splash.state.SplashScreenState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SplashFragment : - HostedFragment(), + HostedFragment(), SplashContract.View { private val errorDialogTrigger = mutableStateOf(DefaultErrorParams()) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index 5be92fc..be1cfd4 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -17,9 +17,9 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class SplashViewModel @Inject constructor(private val cache: Cache) : - MviViewModel(), - SplashContract.ViewModel { +class SplashViewModel @Inject constructor( + private val cache: Cache +) : MviViewModel(), SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> setEffect(SplashScreenEffect.ShowError(e)) diff --git a/mvi/proguard-rules.pro b/mvi/proguard-rules.pro index 8207eb6..6670b3d 100644 --- a/mvi/proguard-rules.pro +++ b/mvi/proguard-rules.pro @@ -19,4 +19,3 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFil -e \ No newline at end of file diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index a49110b..6634392 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -2,31 +2,29 @@ package com.mdgd.mvi import androidx.annotation.CallSuper import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mdgd.mvi.fragments.FragmentContract +import com.mdgd.mvi.states.AbstractState import com.mdgd.mvi.states.ScreenState -abstract class MviViewModel, E> : ViewModel(), - FragmentContract.ViewModel { - private val stateHolder = MutableLiveData() - private val effectHolder = MutableLiveData() +abstract class MviViewModel> : ViewModel(), + FragmentContract.ViewModel { + private val stateHolder = MutableLiveData>() + private val effectHolder = MutableLiveData>() - override fun getStateObservable(): LiveData = stateHolder + override fun getStateObservable() = stateHolder - override fun getEffectObservable(): LiveData = effectHolder + override fun getEffectObservable() = effectHolder - protected fun setState(state: S) { - stateHolder.value = stateHolder.value?.let { - state.merge(it) - } ?: state + protected fun setState(state: STATE) { + stateHolder.value = stateHolder.value?.let { state.merge(it as STATE) } ?: state } - protected fun getState() = stateHolder.value + protected fun getState() = stateHolder.value as STATE? - protected fun setEffect(effect: E) { - effectHolder.value = effect + protected fun setEffect(action: ScreenState) { + effectHolder.value = action } @CallSuper diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt index 66f166c..c1fdaed 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/FragmentContract.kt @@ -1,13 +1,15 @@ package com.mdgd.mvi.fragments import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LiveData +import com.mdgd.mvi.states.ScreenState class FragmentContract { - interface ViewModel { + interface ViewModel : LifecycleObserver { fun onStateChanged(event: Lifecycle.Event) - fun getStateObservable(): LiveData - fun getEffectObservable(): LiveData + fun getStateObservable(): LiveData> + fun getEffectObservable(): LiveData> } interface View diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index c56cbd0..e9c0f16 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -7,17 +7,15 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment -import com.mdgd.mvi.states.ScreenEffect import com.mdgd.mvi.states.ScreenState import java.lang.reflect.ParameterizedType abstract class HostedFragment< VIEW : FragmentContract.View, - STATE : ScreenState, - EFFECT : ScreenEffect, - VIEW_MODEL : FragmentContract.ViewModel, + VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : NavHostFragment(), FragmentContract.View, Observer, LifecycleEventObserver { + : NavHostFragment(), FragmentContract.View, Observer>, + LifecycleEventObserver { protected var model: VIEW_MODEL? = null private set @@ -67,8 +65,8 @@ abstract class HostedFragment< } } - override fun onChanged(screenState: STATE) { - screenState.visit(this@HostedFragment as VIEW) + override fun onChanged(value: ScreenState) { + value.visit(this@HostedFragment as VIEW) } protected fun setModel(model: VIEW_MODEL) { diff --git a/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt index 827d6fc..2174ebc 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt @@ -1,6 +1,6 @@ package com.mdgd.mvi.states -abstract class AbstractEffect : ScreenEffect { +abstract class AbstractEffect : ScreenState { var isHandled = false override fun visit(screen: T) { @@ -13,4 +13,4 @@ abstract class AbstractEffect : ScreenEffect { open fun handle(screen: T) { } -} \ No newline at end of file +} diff --git a/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt b/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt index 041dc97..f650597 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt @@ -1,12 +1,10 @@ package com.mdgd.mvi.states -abstract class AbstractState : ScreenState { +abstract class AbstractState : ScreenState { - override fun visit(screen: T) { + override fun visit(screen: V) { } - override fun merge(prevState: S): S { - return this as S - } -} \ No newline at end of file + open fun merge(prevState: S): S = this as S +} diff --git a/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt b/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt index 4f8d687..c1b5012 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/ScreenState.kt @@ -1,7 +1,5 @@ package com.mdgd.mvi.states -interface ScreenState { - fun visit(screen: T) - - fun merge(prevState: S): S +interface ScreenState { + fun visit(screen: V) } From c05bf82799e451d50cb14acbb655420cbdaba7a8 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Fri, 21 Apr 2023 15:55:21 +0300 Subject: [PATCH 39/47] # libs update --- app/build.gradle | 7 ++-- build.gradle | 26 ++++++------ models/build.gradle | 1 + .../models/repo/cache/PokemonsCache.kt | 2 +- .../mdgd/pokemon/models/repo/schemas/Move_.kt | 5 +-- models_impl/build.gradle | 4 +- .../filters/StatsFiltersFactory.kt | 18 ++++---- .../models_impl/repo/PokemonsRepository.kt | 19 ++++----- .../repo/cache/PokemonsCacheImpl.kt | 5 +-- .../models_impl/repo/dao/AppDatabase.kt | 13 +++--- .../models_impl/repo/dao/PokemonsDaoImpl.kt | 17 ++++---- .../models_impl/repo/dao/PokemonsRoomDao.kt | 5 +-- .../repo/network/PokemonsNetwork.kt | 22 +++++----- .../repo/PokemonsRepositoryTest.kt | 42 ++++++++++--------- 14 files changed, 93 insertions(+), 93 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2791e2b..233dc6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.3.2" + kotlinCompilerExtensionVersion = "1.4.6" } buildTypes { release { @@ -75,8 +75,9 @@ dependencies { implementation "com.google.code.gson:gson:$gson" // hilt - implementation "com.google.dagger:hilt-android:2.42" - kapt("com.google.dagger:hilt-android-compiler:2.42") + implementation "com.google.dagger:hilt-android:2.45" + kapt("com.google.dagger:hilt-android-compiler:2.45") + implementation "androidx.hilt:hilt-navigation-fragment:$hilt_jetpack" kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") implementation 'androidx.hilt:hilt-work:1.0.0' diff --git a/build.gradle b/build.gradle index 493046e..9fcd591 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,16 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.7.20' // update according to composeVersion - ext.compat = "1.5.1" + ext.kotlin_version = '1.8.20' // update according to composeVersion + ext.compat = "1.6.1" ext.nav_version = "2.5.3" - ext.work_version = "2.7.1" + ext.work_version = "2.8.1" - ext.ktx = "1.9.0" + ext.ktx = "1.10.0" ext.coroutines = "1.6.4" - ext.lifecycle_ktx = "2.5.1" + ext.lifecycle_ktx = "2.6.1" - ext.room = "2.4.3" - ext.room_compiler = "2.2.5" + ext.room = "2.5.1" + ext.room_compiler = "2.5.1" ext.gson = "2.9.0" ext.retrofit = "2.9.0" @@ -18,21 +18,21 @@ buildscript { ext.okhttp_log = "4.10.0" ext.okhttp = "4.10.0" - ext.composeVersion = "1.3.1" + ext.composeVersion = "1.4.2" // update kotlinCompilerExtensionVersion when composeVersion updated - ext.composeVersionTheme = "1.1.21" + ext.composeVersionTheme = "1.2.1" // update kotlinCompilerExtensionVersion when composeVersionTheme updated ext.hilt = "2.42" ext.hilt_jetpack = "1.0.0" ext.junit = "4.13.2" - ext.junit_android = "1.1.3" - ext.mockito_core = "4.8.1" + ext.junit_android = "1.1.5" + ext.mockito_core = "5.2.0" ext.mockito_kotlin = "2.2.0" ext.testing_core = "1.1.1" ext.testing_coroutine = "1.6.0" - ext.espresso = "3.4.0" + ext.espresso = "3.5.1" project.ext { min = 21 @@ -50,7 +50,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" - classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt" + classpath "com.google.dagger:hilt-android-gradle-plugin:2.45" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/models/build.gradle b/models/build.gradle index 39c999c..16f5e53 100644 --- a/models/build.gradle +++ b/models/build.gradle @@ -23,6 +23,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + namespace 'com.mdgd.pokemon.models' } dependencies { diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/cache/PokemonsCache.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/cache/PokemonsCache.kt index 914900c..61ce04f 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/cache/PokemonsCache.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/cache/PokemonsCache.kt @@ -7,4 +7,4 @@ interface PokemonsCache { fun addPokemons(list: List) fun setPokemons(list: List) fun getPokemons(): List -} \ No newline at end of file +} diff --git a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt index 5794f2a..b93832a 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/repo/schemas/Move_.kt @@ -4,12 +4,11 @@ import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName class Move_ { - - @Expose @SerializedName("name") + @Expose var name: String? = null - @Expose @SerializedName("url") + @Expose var url: String? = null } diff --git a/models_impl/build.gradle b/models_impl/build.gradle index f9a27d5..286e629 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -56,8 +56,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" // hilt - implementation "com.google.dagger:hilt-android:2.42" - kapt("com.google.dagger:hilt-android-compiler:2.42") + implementation "com.google.dagger:hilt-android:2.45" + kapt("com.google.dagger:hilt-android-compiler:2.45") kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") testImplementation "junit:junit:$junit" diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/filters/StatsFiltersFactory.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/filters/StatsFiltersFactory.kt index bcfb2ce..fa622c1 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/filters/StatsFiltersFactory.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/filters/StatsFiltersFactory.kt @@ -4,19 +4,21 @@ import com.mdgd.pokemon.models.filters.CharacteristicComparator import com.mdgd.pokemon.models.filters.FilterData import com.mdgd.pokemon.models.filters.StatsFilter import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import java.util.* class StatsFiltersFactory : StatsFilter { - override fun getAvailableFilters(): List { - return listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_DEFENCE, FilterData.FILTER_SPEED) - } + override fun getAvailableFilters(): List = listOf( + FilterData.FILTER_ATTACK, FilterData.FILTER_DEFENCE, FilterData.FILTER_SPEED + ) override fun getFilters(): Map { return object : HashMap() { init { put(FilterData.FILTER_ATTACK, object : CharacteristicComparator { - override fun compare(p1: PokemonFullDataSchema, p2: PokemonFullDataSchema): Int { + override fun compare( + p1: PokemonFullDataSchema, + p2: PokemonFullDataSchema + ): Int { return compareProperty(FilterData.FILTER_ATTACK, p1, p2) } }) @@ -37,18 +39,18 @@ class StatsFiltersFactory : StatsFilter { private fun compareProperty(property: String, p1: PokemonFullDataSchema, p2: PokemonFullDataSchema): Int { var val1 = -1 for (s in p1.stats) { - if (property == s.stat!!.name) { + if (property == s.stat?.name) { val1 = s.baseStat!! break } } var val2 = -1 for (s in p2.stats) { - if (property == s.stat!!.name) { + if (property == s.stat?.name) { val2 = s.baseStat!! break } } return val1.compareTo(val2) } -} \ No newline at end of file +} diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt index ec9c052..22b7385 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepository.kt @@ -5,25 +5,23 @@ import com.mdgd.pokemon.models.repo.cache.PokemonsCache import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.Network -import javax.inject.Inject -class PokemonsRepository @Inject constructor( - private val dao: PokemonsDao, private val network: Network, private val cache: PokemonsCache +class PokemonsRepository( + private val dao: PokemonsDao, + private val network: Network, + private val cache: PokemonsCache ) : PokemonsRepo { override fun getPokemons() = cache.getPokemons() override suspend fun getPage(page: Int): List { val list = getPageFromDao(page) - return if (list.isEmpty()) { - loadPage(page) - } else { - list - } + return list.ifEmpty { loadPage(page) } } private suspend fun loadPage(page: Int): List { - dao.save(network.loadPokemons(page, PokemonsRepo.PAGE_SIZE)) + val list = network.loadPokemons(page, PokemonsRepo.PAGE_SIZE) + dao.save(list) return getPageFromDao(page) } @@ -65,7 +63,8 @@ class PokemonsRepository @Inject constructor( } override suspend fun loadInitialPages(amount: Long) { - if (dao.getCount() < amount) { + val count = dao.getCount() + if (count < amount) { loadPokemonsInner(amount, 0) } } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/cache/PokemonsCacheImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/cache/PokemonsCacheImpl.kt index cb8c89a..f6cbe20 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/cache/PokemonsCacheImpl.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/cache/PokemonsCacheImpl.kt @@ -2,7 +2,6 @@ package com.mdgd.pokemon.models_impl.repo.cache import com.mdgd.pokemon.models.repo.cache.PokemonsCache import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import java.util.* class PokemonsCacheImpl : PokemonsCache { private val pokemons = mutableListOf() @@ -11,9 +10,7 @@ class PokemonsCacheImpl : PokemonsCache { pokemons.addAll(list) } - override fun getPokemons(): List { - return ArrayList(pokemons) - } + override fun getPokemons(): List = ArrayList(pokemons) override fun setPokemons(list: List) { pokemons.clear() diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt index ef04c4b..ec83794 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/AppDatabase.kt @@ -4,14 +4,17 @@ import androidx.room.Database import androidx.room.RoomDatabase import com.mdgd.pokemon.models.repo.dao.schemas.MoveSchema import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema -import com.mdgd.pokemon.models.repo.schemas.* +import com.mdgd.pokemon.models.repo.schemas.Ability +import com.mdgd.pokemon.models.repo.schemas.Form +import com.mdgd.pokemon.models.repo.schemas.GameIndex +import com.mdgd.pokemon.models.repo.schemas.Stat +import com.mdgd.pokemon.models.repo.schemas.Type +import com.mdgd.pokemon.models.repo.schemas.VersionGroupDetail @Database( version = 1, exportSchema = false, - entities = [ - PokemonSchema::class, Ability::class, Form::class, GameIndex::class, - MoveSchema::class, Stat::class, Type::class, VersionGroupDetail::class - ] + entities = [PokemonSchema::class, Ability::class, Form::class, GameIndex::class, + MoveSchema::class, Stat::class, Type::class, VersionGroupDetail::class] ) abstract class AppDatabase : RoomDatabase() { abstract fun pokemonsDao(): PokemonsRoomDao? diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt index 67f0d35..7d701ed 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsDaoImpl.kt @@ -5,10 +5,8 @@ import androidx.room.Room import com.mdgd.pokemon.models.repo.dao.PokemonsDao import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.schemas.PokemonDetails -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -class PokemonsDaoImpl @Inject constructor(@ApplicationContext ctx: Context) : PokemonsDao { +class PokemonsDaoImpl(ctx: Context) : PokemonsDao { private val pokemonsRoomDao: PokemonsRoomDao? = Room.databaseBuilder( ctx, AppDatabase::class.java, "PokemonsAppDB" ).build().pokemonsDao() @@ -22,16 +20,15 @@ class PokemonsDaoImpl @Inject constructor(@ApplicationContext ctx: Context) : Po val rows = pokemonsRoomDao?.countRows() ?: 0 return when { rows == 0 -> ArrayList(0) - (rows <= offset) -> throw Exception(PokemonsDao.NO_MORE_POKEMONS_MSG) + (pokemonsRoomDao?.countRows() + ?: 0) <= offset -> throw Exception(PokemonsDao.NO_MORE_POKEMONS_MSG) + else -> pokemonsRoomDao?.getPage(offset, pageSize) ?: listOf() } } - override suspend fun getCount(): Long { - return pokemonsRoomDao?.countRows()?.toLong() ?: 0 - } + override suspend fun getCount() = pokemonsRoomDao?.countRows()?.toLong() ?: 0 - override suspend fun getPokemonById(pokemonId: Long): PokemonFullDataSchema? { - return pokemonsRoomDao?.getPokemonById(pokemonId) - } + override suspend fun getPokemonById(pokemonId: Long) = + pokemonsRoomDao?.getPokemonById(pokemonId) } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsRoomDao.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsRoomDao.kt index 9f21981..cd7905c 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsRoomDao.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/dao/PokemonsRoomDao.kt @@ -8,7 +8,6 @@ import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema import com.mdgd.pokemon.models.repo.network.schemas.PokemonDetails import com.mdgd.pokemon.models.repo.schemas.* import java.util.* -import kotlin.collections.ArrayList @Dao abstract class PokemonsRoomDao { @@ -111,8 +110,8 @@ abstract class PokemonsRoomDao { fun getPage(offset: Int, pageSize: Int): List { val pokemons = getPokemonsForPage(offset, pageSize) - val schemas: List = ArrayList(mapPokemons(pokemons)) - Collections.shuffle(schemas) + val schemas: MutableList = ArrayList(mapPokemons(pokemons)) + schemas.shuffle() return schemas } diff --git a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt index 70acd77..bfb8901 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/repo/network/PokemonsNetwork.kt @@ -11,9 +11,8 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.util.* +import java.util.Vector import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList import kotlin.math.max class PokemonsNetwork : Network { @@ -21,11 +20,8 @@ class PokemonsNetwork : Network { init { val logging = HttpLoggingInterceptor() - logging.level = if (BuildConfig.DEBUG) { - HttpLoggingInterceptor.Level.BASIC - } else { - HttpLoggingInterceptor.Level.NONE - } + logging.level = + if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC else HttpLoggingInterceptor.Level.NONE val httpClient = OkHttpClient.Builder() httpClient.addInterceptor(logging) httpClient.readTimeout(10, TimeUnit.SECONDS) @@ -40,11 +36,13 @@ class PokemonsNetwork : Network { } override suspend fun loadPokemons(pokemonsCount: Long, offset: Long): List { - return mapToDetails(service.loadPage(pokemonsCount.toInt(), offset.toInt())) + val nextPage = service.loadPage(pokemonsCount.toInt(), offset.toInt()) + return mapToDetails(nextPage) } override suspend fun loadPokemons(page: Int, pageSize: Int): List { - val dataPage = service.loadPage(pageSize, max(page - 1, 0) * pageSize) + val i = max(page - 1, 0) + val dataPage = service.loadPage(pageSize, i * pageSize) return mapToDetails(dataPage) } @@ -53,10 +51,10 @@ class PokemonsNetwork : Network { } private suspend fun mapToDetails(result: PokemonsList): List { - val list = result.results?.let { - it - } ?: kotlin.run { + val list = if (result.results == null) { ArrayList() + } else { + result.results!! } val details = Vector(list.size) val channel = Channel() diff --git a/models_impl/src/test/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepositoryTest.kt b/models_impl/src/test/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepositoryTest.kt index 4875eb4..9f7e14e 100644 --- a/models_impl/src/test/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepositoryTest.kt +++ b/models_impl/src/test/java/com/mdgd/pokemon/models_impl/repo/PokemonsRepositoryTest.kt @@ -7,8 +7,7 @@ import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.models.repo.network.Network import com.mdgd.pokemon.models.repo.network.schemas.PokemonDetails import com.mdgd.pokemon.models_impl.Mocks -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Before import org.junit.Test @@ -16,7 +15,6 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mockito import java.util.* -import kotlin.collections.ArrayList @RunWith(JUnit4::class) class PokemonsRepositoryTest { @@ -53,7 +51,7 @@ class PokemonsRepositoryTest { } @Test - fun test_getPageFromDao_InitialPage() = runBlockingTest { + fun test_getPageFromDao_InitialPage() = runTest { val list = listOf(Mocks.getPokemon()) Mockito.`when`(dao.getPage(0, PokemonsRepo.PAGE_SIZE)).thenReturn(list) @@ -66,7 +64,7 @@ class PokemonsRepositoryTest { } @Test - fun test_getPageFromDao() = runBlockingTest { + fun test_getPageFromDao() = runTest { val list = listOf(Mocks.getPokemon()) Mockito.`when`(dao.getPage(1, PokemonsRepo.PAGE_SIZE)).thenReturn(list) @@ -79,17 +77,21 @@ class PokemonsRepositoryTest { } @Test - fun test_getPageFromNetwork() = runBlockingTest { + fun test_getPageFromNetwork() = runTest { val emptyList = listOf() val list = listOf(Mocks.getPokemon()) val networkList = listOf() - Mockito.`when`(dao.getPage(1, PokemonsRepo.PAGE_SIZE)).thenReturn(emptyList) - Mockito.`when`(network.loadPokemons(1, PokemonsRepo.PAGE_SIZE)).then { - launch { - Mockito.`when`(dao.getPage(1, PokemonsRepo.PAGE_SIZE)).thenReturn(list) + + val invocations = Array(1) { 0 } + Mockito.`when`(dao.getPage(1, PokemonsRepo.PAGE_SIZE)).then { + invocations[0]++ + if (invocations[0] == 1) { + emptyList + } else { + list } - networkList } + Mockito.`when`(network.loadPokemons(1, PokemonsRepo.PAGE_SIZE)).thenReturn(networkList) val pokemons = repo.getPage(1) @@ -102,7 +104,7 @@ class PokemonsRepositoryTest { } @Test - fun test_loadPokemons_AllLoaded() = runBlockingTest { + fun test_loadPokemons_AllLoaded() = runTest { val initialAmount = 10L Mockito.`when`(dao.getCount()).thenReturn(initialAmount) Mockito.`when`(network.getPokemonsCount()).thenReturn(initialAmount) @@ -116,10 +118,11 @@ class PokemonsRepositoryTest { } @Test - fun test_loadPokemons_NothingLoaded() = runBlockingTest { + fun test_loadPokemons_NothingLoaded() = runTest { val initialAmount = 10L val totalAmount = 20L - val networkList = ArrayList(Collections.nCopies(totalAmount.toInt(), Mocks.getPokemonDetails())) + val networkList = + ArrayList(Collections.nCopies(totalAmount.toInt(), Mocks.getPokemonDetails())) Mockito.`when`(dao.getCount()).thenReturn(initialAmount) Mockito.`when`(network.getPokemonsCount()).thenReturn(totalAmount) Mockito.`when`(network.loadPokemons(totalAmount, initialAmount)).thenReturn(networkList) @@ -135,9 +138,10 @@ class PokemonsRepositoryTest { } @Test - fun test_loadInitialPages_NoData() = runBlockingTest { + fun test_loadInitialPages_NoData() = runTest { val initialAmount = 10L - val networkList = ArrayList(Collections.nCopies(initialAmount.toInt(), Mocks.getPokemonDetails())) + val networkList = + ArrayList(Collections.nCopies(initialAmount.toInt(), Mocks.getPokemonDetails())) Mockito.`when`(dao.getCount()).thenReturn(0) Mockito.`when`(network.loadPokemons(initialAmount, 0)).thenReturn(networkList) @@ -150,7 +154,7 @@ class PokemonsRepositoryTest { } @Test - fun test_loadInitialPages() = runBlockingTest { + fun test_loadInitialPages() = runTest { val initialAmount = 10L Mockito.`when`(dao.getCount()).thenReturn(initialAmount) @@ -161,7 +165,7 @@ class PokemonsRepositoryTest { } @Test - fun test_getPokemonById_Cached() = runBlockingTest { + fun test_getPokemonById_Cached() = runTest { val pokemonId = 0L val pokemon = Mocks.getPokemon() pokemon.pokemonSchema?.id = pokemonId @@ -175,7 +179,7 @@ class PokemonsRepositoryTest { } @Test - fun test_getPokemonById_NotCached() = runBlockingTest { + fun test_getPokemonById_NotCached() = runTest { val pokemonId = 0L val pokemon = Mocks.getPokemon() Mockito.`when`(dao.getPokemonById(pokemonId)).thenReturn(pokemon) From 0d359a4d87494e91c159b4e957d5aa234648b160 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Fri, 21 Apr 2023 15:57:29 +0300 Subject: [PATCH 40/47] # agp migration --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- models/src/main/AndroidManifest.xml | 2 +- models_impl/build.gradle | 1 + models_impl/src/main/AndroidManifest.xml | 2 +- mvi/build.gradle | 1 + mvi/src/main/AndroidManifest.xml | 2 +- 9 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 233dc6e..8e5e9a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + namespace 'com.mdgd.pokemon' } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0750448..91303f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/build.gradle b/build.gradle index 9fcd591..15faac8 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.dagger:hilt-android-gradle-plugin:2.45" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 481f98a..6ea999a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/models/src/main/AndroidManifest.xml b/models/src/main/AndroidManifest.xml index 1731195..cc947c5 100644 --- a/models/src/main/AndroidManifest.xml +++ b/models/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 286e629..2fd7559 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -30,6 +30,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + namespace 'com.mdgd.pokemon.models_impl' } dependencies { diff --git a/models_impl/src/main/AndroidManifest.xml b/models_impl/src/main/AndroidManifest.xml index 03c44a2..cc947c5 100644 --- a/models_impl/src/main/AndroidManifest.xml +++ b/models_impl/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/mvi/build.gradle b/mvi/build.gradle index 8cc2cae..2e30b2c 100644 --- a/mvi/build.gradle +++ b/mvi/build.gradle @@ -19,6 +19,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + namespace 'com.mdgd.mvi' } dependencies { diff --git a/mvi/src/main/AndroidManifest.xml b/mvi/src/main/AndroidManifest.xml index a9bceb7..cc947c5 100644 --- a/mvi/src/main/AndroidManifest.xml +++ b/mvi/src/main/AndroidManifest.xml @@ -1 +1 @@ - + From 68987415e9d4cf069977dd6323cc22172fd2e909 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Fri, 21 Apr 2023 15:59:16 +0300 Subject: [PATCH 41/47] # agp migration to 8.0.0 --- build.gradle | 2 +- gradle.properties | 3 +++ gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 15faac8..b287640 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.dagger:hilt-android-gradle-plugin:2.45" diff --git a/gradle.properties b/gradle.properties index fe250a9..3ed8aa7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,3 +24,6 @@ kotlin.code.style=official -Pkapt.use.worker.api=true kapt.incremental.apt=true android.enableR8.fullMode=false +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6ea999a..9cb1828 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip From 1a2ca5855feecdf1839384f6ffdddc928341b96f Mon Sep 17 00:00:00 2001 From: DanDdl Date: Fri, 21 Apr 2023 16:02:21 +0300 Subject: [PATCH 42/47] # agp migration fix --- app/build.gradle | 7 +++++-- models/build.gradle | 7 +++++-- models_impl/build.gradle | 6 +++--- mvi/build.gradle | 7 +++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8e5e9a8..fee9f17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,8 +38,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() } namespace 'com.mdgd.pokemon' } diff --git a/models/build.gradle b/models/build.gradle index 16f5e53..a3a4034 100644 --- a/models/build.gradle +++ b/models/build.gradle @@ -20,8 +20,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() } namespace 'com.mdgd.pokemon.models' } diff --git a/models_impl/build.gradle b/models_impl/build.gradle index 2fd7559..e55b3e0 100644 --- a/models_impl/build.gradle +++ b/models_impl/build.gradle @@ -24,11 +24,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } namespace 'com.mdgd.pokemon.models_impl' } diff --git a/mvi/build.gradle b/mvi/build.gradle index 2e30b2c..8738b84 100644 --- a/mvi/build.gradle +++ b/mvi/build.gradle @@ -19,6 +19,13 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } namespace 'com.mdgd.mvi' } From 0d549911f4c8221a115f67e58cf46ecf1ad98845 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Sat, 22 Apr 2023 14:08:41 +0300 Subject: [PATCH 43/47] # unit tests fixed --- .../ui/pokemons/state/PokemonsScreenState.kt | 11 +- .../com/mdgd/pokemon/MainDispatcherRule.kt | 21 ++ .../mdgd/pokemon/bg/LoadPokemonsModelTest.kt | 22 +- .../pokemon/PokemonDetailsScreenStateTest.kt | 4 +- .../ui/pokemon/PokemonDetailsViewModelTest.kt | 54 ++- .../ui/pokemons/PokemonsScreenEffectTest.kt | 10 +- .../ui/pokemons/PokemonsScreenStateTest.kt | 36 +- .../ui/pokemons/PokemonsViewModelTest.kt | 352 +++++++++--------- .../pokemon/ui/splash/SplashViewModelTest.kt | 111 +++--- .../com/mdgd/pokemon/models_impl/TestSuit.kt | 11 + 10 files changed, 333 insertions(+), 299 deletions(-) create mode 100644 app/src/test/java/com/mdgd/pokemon/MainDispatcherRule.kt create mode 100644 models_impl/src/test/java/com/mdgd/pokemon/models_impl/TestSuit.kt diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt index 1512fce..ed0851a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenState.kt @@ -5,7 +5,7 @@ import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.ui.pokemons.PokemonsContract open class PokemonsScreenState( - protected val isProgressVisible: Boolean = false, + val isProgressVisible: Boolean = false, val list: List = listOf(), protected val availableFilters: List = listOf(), val activeFilters: List = listOf() @@ -21,7 +21,6 @@ open class PokemonsScreenState( // PARTIAL STATES - class Loading : PokemonsScreenState(true) { override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { @@ -67,15 +66,11 @@ open class PokemonsScreenState( } } - class ChangeFilterState(activeFilters: List) : - PokemonsScreenState(activeFilters = activeFilters) { + class ChangeFilterState(filters: List) : PokemonsScreenState(activeFilters = filters) { override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { return PokemonsScreenState( - isProgressVisible, - list, - prevState.availableFilters, - activeFilters + isProgressVisible, prevState.list, prevState.availableFilters, activeFilters ) } } diff --git a/app/src/test/java/com/mdgd/pokemon/MainDispatcherRule.kt b/app/src/test/java/com/mdgd/pokemon/MainDispatcherRule.kt new file mode 100644 index 0000000..a3dbad4 --- /dev/null +++ b/app/src/test/java/com/mdgd/pokemon/MainDispatcherRule.kt @@ -0,0 +1,21 @@ +package com.mdgd.pokemon + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +class MainDispatcherRule( + private val testDispatcher: CoroutineDispatcher = Dispatchers.Unconfined, +) : TestWatcher() { + + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} diff --git a/app/src/test/java/com/mdgd/pokemon/bg/LoadPokemonsModelTest.kt b/app/src/test/java/com/mdgd/pokemon/bg/LoadPokemonsModelTest.kt index 29706dc..df7c676 100644 --- a/app/src/test/java/com/mdgd/pokemon/bg/LoadPokemonsModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/bg/LoadPokemonsModelTest.kt @@ -1,14 +1,13 @@ package com.mdgd.pokemon.bg import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.mdgd.pokemon.MainDispatcherRule import com.mdgd.pokemon.TestSuit import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result import com.mdgd.pokemon.models.repo.PokemonsRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.test.setMain +import com.nhaarman.mockitokotlin2.argumentCaptor +import kotlinx.coroutines.runBlocking import org.junit.* import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -16,6 +15,9 @@ import org.mockito.Mockito @RunWith(JUnit4::class) class LoadPokemonsModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @get:Rule val rule = InstantTaskExecutorRule() @@ -25,27 +27,21 @@ class LoadPokemonsModelTest { @Before fun setup() { - Dispatchers.setMain(Dispatchers.Unconfined) cache = Mockito.mock(Cache::class.java) repo = Mockito.mock(PokemonsRepo::class.java) model = LoadPokemonsModel(repo, cache) } - @After - fun tearDown() { - Dispatchers.resetMain() - } - private fun verifyNoMoreInteractions() { Mockito.verifyNoMoreInteractions(cache) Mockito.verifyNoMoreInteractions(repo) } @Test - fun test_LoadPokemonsCrash() = runBlockingTest { + fun test_LoadPokemonsCrash() = runBlocking { val initialLoadingAmount = (PokemonsRepo.PAGE_SIZE * 2).toLong() val error = RuntimeException("TestError") - val resultCaptor = com.nhaarman.mockitokotlin2.argumentCaptor>() + val resultCaptor = argumentCaptor>() Mockito.`when`(repo.loadInitialPages(initialLoadingAmount)).thenThrow(error) model.load() @@ -60,7 +56,7 @@ class LoadPokemonsModelTest { } @Test - fun test_LoadPokemonsOk() = runBlockingTest { + fun test_LoadPokemonsOk() = runBlocking { val initialLoadingAmount = (PokemonsRepo.PAGE_SIZE * 2).toLong() val totalLoadingAmount = initialLoadingAmount * 2 Mockito.`when`(repo.loadPokemons(initialLoadingAmount)).thenReturn(totalLoadingAmount) diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsScreenStateTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsScreenStateTest.kt index c4ed3d8..d73ac46 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsScreenStateTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsScreenStateTest.kt @@ -3,7 +3,7 @@ package com.mdgd.pokemon.ui.pokemon import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import com.nhaarman.mockitokotlin2.argumentCaptor -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test @@ -25,7 +25,7 @@ class PokemonDetailsScreenStateTest { } @Test - fun testSetDataState() = runBlockingTest { + fun testSetDataState() = runBlocking { val detailsCaptor = argumentCaptor>() val list = ArrayList(0) PokemonDetailsScreenState.SetData(list).visit(view) diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt index 898f588..8ee1e7b 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModelTest.kt @@ -3,6 +3,8 @@ package com.mdgd.pokemon.ui.pokemon import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer +import com.mdgd.mvi.states.ScreenState +import com.mdgd.pokemon.MainDispatcherRule import com.mdgd.pokemon.Mocks import com.mdgd.pokemon.R import com.mdgd.pokemon.models.repo.PokemonsRepo @@ -10,13 +12,9 @@ import com.mdgd.pokemon.ui.pokemon.dto.ImagePropertyData import com.mdgd.pokemon.ui.pokemon.dto.LabelPropertyData import com.mdgd.pokemon.ui.pokemon.dto.TextPropertyData import com.mdgd.pokemon.ui.pokemon.dto.TitlePropertyData -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenEffect import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState import com.nhaarman.mockitokotlin2.argumentCaptor -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.runBlocking import org.junit.* import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -24,6 +22,9 @@ import org.mockito.Mockito @RunWith(JUnit4::class) class PokemonDetailsViewModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @get:Rule val rule = InstantTaskExecutorRule() private lateinit var model: PokemonDetailsViewModel @@ -31,35 +32,30 @@ class PokemonDetailsViewModelTest { @Before fun setup() { - Dispatchers.setMain(Dispatchers.Unconfined) repo = Mockito.mock(PokemonsRepo::class.java) model = PokemonDetailsViewModel(repo) } - @After - fun tearDown() { - Dispatchers.resetMain() - } - private fun verifyNoMoreInteractions() { Mockito.verifyNoMoreInteractions(repo) } @Test - fun testSetup_NotingHappened() = runBlockingTest { - val observerMock = Mockito.mock(Observer::class.java) as Observer + fun testSetup_NotingHappened() = runBlocking { + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer + Mockito.mock(Observer::class.java) as Observer> model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_START) - model.onAny(null, Lifecycle.Event.ON_RESUME) - model.onAny(null, Lifecycle.Event.ON_PAUSE) - model.onAny(null, Lifecycle.Event.ON_STOP) - model.onAny(null, Lifecycle.Event.ON_DESTROY) - model.onAny(null, Lifecycle.Event.ON_ANY) + model.onStateChanged(Lifecycle.Event.ON_START) + model.onStateChanged(Lifecycle.Event.ON_RESUME) + model.onStateChanged(Lifecycle.Event.ON_PAUSE) + model.onStateChanged(Lifecycle.Event.ON_STOP) + model.onStateChanged(Lifecycle.Event.ON_DESTROY) + model.onStateChanged(Lifecycle.Event.ON_ANY) Mockito.verifyNoMoreInteractions(observerMock) Mockito.verifyNoMoreInteractions(actionObserverMock) @@ -69,18 +65,19 @@ class PokemonDetailsViewModelTest { } @Test - fun testSetup_LaunchError() = runBlockingTest { + fun testSetup_LaunchError() = runBlocking { val error = RuntimeException("TestError") Mockito.`when`(repo.getPokemonById(0)).thenThrow(error) - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer + Mockito.mock(Observer::class.java) as Observer> model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_CREATE) + model.onStateChanged(Lifecycle.Event.ON_CREATE) model.setPokemonId(0) Thread.sleep(2000) @@ -93,20 +90,21 @@ class PokemonDetailsViewModelTest { } @Test - fun testSetup_LaunchOk() = runBlockingTest { + fun testSetup_LaunchOk() = runBlocking { val pokemon = Mocks.getPokemon() Mockito.`when`(repo.getPokemonById(0)).thenReturn(pokemon) - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer + Mockito.mock(Observer::class.java) as Observer> model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_CREATE) + model.onStateChanged(Lifecycle.Event.ON_CREATE) model.setPokemonId(0) Thread.sleep(2000) diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt index 293387a..67a3b66 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenEffectTest.kt @@ -1,7 +1,7 @@ package com.mdgd.pokemon.ui.pokemons import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -22,29 +22,33 @@ class PokemonsScreenEffectTest { } @Test - fun test_ErrorState() = runBlockingTest { + fun test_ErrorState() = runBlocking { val error = Throwable("TestError") val state = PokemonsScreenEffect.Error(error) state.visit(view) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).showError(error) state.visit(view) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) verifyNoMoreInteractions() } @Test - fun test_ShowDetailsState() = runBlockingTest { + fun test_ShowDetailsState() = runBlocking { val pokemonId = 1L val state = PokemonsScreenEffect.ShowDetails(pokemonId) state.visit(view) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).proceedToNextScreen(pokemonId) state.visit(view) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) verifyNoMoreInteractions() } diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenStateTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenStateTest.kt index 02c5c9c..78efeb0 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenStateTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsScreenStateTest.kt @@ -4,7 +4,7 @@ import com.mdgd.pokemon.models.filters.FilterData import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState import com.nhaarman.mockitokotlin2.argumentCaptor -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test @@ -26,7 +26,7 @@ class PokemonsScreenStateTest { } @Test - fun test_LoadingState() = runBlockingTest { + fun test_LoadingState() = runBlocking { val list = ArrayList() val filters = ArrayList() val prevState = PokemonsScreenState.SetData(list, filters) @@ -36,28 +36,26 @@ class PokemonsScreenStateTest { state.visit(view) - Mockito.verify(view, Mockito.times(1)).setProgressVisibility(isProgressVisible) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(true) Mockito.verify(view, Mockito.times(1)).setItems(list) verifyNoMoreInteractions() } @Test - fun test_SetDataState() = runBlockingTest { + fun test_SetDataState() = runBlocking { val list = ArrayList() val filters = ArrayList() val state = PokemonsScreenState.SetData(list, filters) - state.visit(view) - - Mockito.verify(view, Mockito.times(1)).hideProgress() + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).setItems(list) verifyNoMoreInteractions() } @Test - fun test_AddDataState() = runBlockingTest { + fun test_AddDataState() = runBlocking { val list = ArrayList() val filters = ArrayList() val prevState = PokemonsScreenState.SetData(list, filters) @@ -72,7 +70,7 @@ class PokemonsScreenStateTest { state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).setItems(listCaptor.capture()) val capturedList = listCaptor.firstValue Assert.assertEquals(1, capturedList.size) @@ -82,7 +80,7 @@ class PokemonsScreenStateTest { } @Test - fun test_UpdateDataState() = runBlockingTest { + fun test_UpdateDataState() = runBlocking { val list = ArrayList() val filters = ArrayList() val prevState = PokemonsScreenState.SetData(list, filters) @@ -97,9 +95,8 @@ class PokemonsScreenStateTest { state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).setItems(listCaptor.capture()) - Mockito.verify(view, Mockito.times(1)).scrollToStart() val capturedList = listCaptor.firstValue Assert.assertEquals(1, capturedList.size) Assert.assertEquals(newList[0], capturedList[0]) @@ -108,27 +105,28 @@ class PokemonsScreenStateTest { } @Test - fun test_ChangeFilterState() = runBlockingTest { + fun test_ChangeFilterState() = runBlocking { val list = ArrayList() val availableFilters = listOf(FilterData.FILTER_ATTACK, FilterData.FILTER_SPEED) val prevState = PokemonsScreenState.SetData(list, availableFilters) - val filter = FilterData.FILTER_ATTACK + val filter = listOf(FilterData.FILTER_ATTACK) val activeStateCaptor = argumentCaptor() val filterTypeCaptor = argumentCaptor() - val state = PokemonsScreenState.ChangeFilterState(filter) + var state: PokemonsScreenState = PokemonsScreenState.ChangeFilterState(filter) - state.merge(prevState) + state = state.merge(prevState) state.visit(view) Mockito.verify(view, Mockito.times(1)).setItems(list) - Mockito.verify(view, Mockito.times(1)).hideProgress() - Mockito.verify(view, Mockito.times(2)).updateFilterButtons(activeStateCaptor.capture(), filterTypeCaptor.capture()) + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) + Mockito.verify(view, Mockito.times(2)) + .updateFilterButtons(activeStateCaptor.capture(), filterTypeCaptor.capture()) for (i in 0..1) { Assert.assertEquals(availableFilters[i], filterTypeCaptor.allValues[i]) - Assert.assertEquals(availableFilters[i] == filter, activeStateCaptor.allValues[i]) + Assert.assertEquals(availableFilters[i] == filter[0], activeStateCaptor.allValues[i]) } verifyNoMoreInteractions() diff --git a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt index d5082aa..bd96c83 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModelTest.kt @@ -3,6 +3,8 @@ package com.mdgd.pokemon.ui.pokemons import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer +import com.mdgd.mvi.states.ScreenState +import com.mdgd.pokemon.MainDispatcherRule import com.mdgd.pokemon.Mocks import com.mdgd.pokemon.TestSuit import com.mdgd.pokemon.models.filters.FilterData @@ -15,22 +17,22 @@ import com.mdgd.pokemon.models.util.DispatchersHolder import com.mdgd.pokemon.models_impl.filters.StatsFiltersFactory import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState -import com.nhaarman.mockitokotlin2.firstValue +import com.nhaarman.mockitokotlin2.argumentCaptor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain import org.junit.* import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor import org.mockito.Mockito @RunWith(JUnit4::class) class PokemonsViewModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() @get:Rule val rule = InstantTaskExecutorRule() + private val PAGE_SIZE = 5 private lateinit var model: PokemonsViewModel private lateinit var repo: PokemonsRepo @@ -39,7 +41,6 @@ class PokemonsViewModelTest { @Before fun setup() { - Dispatchers.setMain(Dispatchers.Unconfined) dispatchers = Mockito.mock(DispatchersHolder::class.java) Mockito.`when`(dispatchers.getIO()).thenReturn(Dispatchers.Unconfined) Mockito.`when`(dispatchers.getMain()).thenReturn(Dispatchers.Unconfined) @@ -49,11 +50,6 @@ class PokemonsViewModelTest { model = PokemonsViewModel(repo, filtersFactory, dispatchers) } - @After - fun tearDown() { - Dispatchers.resetMain() - } - private fun verifyNoMoreInteractions() { Mockito.verifyNoMoreInteractions(repo) Mockito.verifyNoMoreInteractions(filtersFactory) @@ -61,20 +57,21 @@ class PokemonsViewModelTest { @Test fun testSetup_NotingHappened() = runBlocking { - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer + Mockito.mock(Observer::class.java) as Observer> model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_START) - model.onAny(null, Lifecycle.Event.ON_RESUME) - model.onAny(null, Lifecycle.Event.ON_PAUSE) - model.onAny(null, Lifecycle.Event.ON_STOP) - model.onAny(null, Lifecycle.Event.ON_DESTROY) - model.onAny(null, Lifecycle.Event.ON_ANY) + model.onStateChanged(Lifecycle.Event.ON_START) + model.onStateChanged(Lifecycle.Event.ON_RESUME) + model.onStateChanged(Lifecycle.Event.ON_PAUSE) + model.onStateChanged(Lifecycle.Event.ON_STOP) + model.onStateChanged(Lifecycle.Event.ON_DESTROY) + model.onStateChanged(Lifecycle.Event.ON_ANY) Mockito.verifyNoMoreInteractions(observerMock) @@ -91,35 +88,35 @@ class PokemonsViewModelTest { Mockito.`when`(repo.getPokemons()).thenReturn(ArrayList()) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) - val observerMock = Mockito.mock(Observer::class.java) as Observer - val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) + val observerMock = + Mockito.mock(Observer::class.java) as Observer> + val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.loadPage(0) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.reload() Thread.sleep(TestSuit.DELAY) Mockito.verify(observerMock, Mockito.times(2)).onChanged(stateCaptor.capture()) - Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) + Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) + var loadingCounter = 0 var updatesCounter = 0 var errorCounter = 0 + var scrollCounter = 0 for (state in stateCaptor.allValues) { - when (state) { - is PokemonsScreenState.Loading -> { - loadingCounter += 1 - } - is PokemonsScreenState.UpdateData -> { - updatesCounter += 1 - Assert.assertTrue(state.getItems().isEmpty()) - } + if (state.isProgressVisible) { + loadingCounter += 1 + } else if (state.activeFilters.isEmpty()) { + Assert.assertTrue(state.list.isEmpty()) + updatesCounter += 1 } } for (action in actionCaptor.allValues) { @@ -128,12 +125,16 @@ class PokemonsViewModelTest { errorCounter += 1 Assert.assertEquals(error.message, action.error?.message) } + + is PokemonsScreenEffect.ScrollToStart -> scrollCounter += 1 + else -> {} } } Assert.assertEquals(1, loadingCounter) Assert.assertEquals(1, updatesCounter) Assert.assertEquals(1, errorCounter) + Assert.assertEquals(1, scrollCounter) Mockito.verify(repo, Mockito.times(1)).getPage(0) Mockito.verify(repo, Mockito.times(1)).getPokemons() @@ -153,12 +154,13 @@ class PokemonsViewModelTest { Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) @@ -178,13 +180,14 @@ class PokemonsViewModelTest { @Test fun testSetup_Ok() = runBlocking { - val observerMock = Mockito.mock(Observer::class.java) as Observer - val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) + val observerMock = + Mockito.mock(Observer::class.java) as Observer> + val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) @@ -195,28 +198,27 @@ class PokemonsViewModelTest { } - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.loadPage(0) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.reload() Thread.sleep(TestSuit.DELAY) Mockito.verify(observerMock, Mockito.times(3)).onChanged(stateCaptor.capture()) + Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) + Assert.assertTrue(actionCaptor.firstValue is PokemonsScreenEffect.ScrollToStart) + var loadingCounter = 0 var setsCounter = 0 var updatesCounter = 0 for (state in stateCaptor.allValues) { - when (state) { - is PokemonsScreenState.Loading -> { - loadingCounter += 1 - } - is PokemonsScreenState.SetData -> { - setsCounter += 1 - Assert.assertEquals(pokemons, state.getItems()) - } - is PokemonsScreenState.UpdateData -> { - Assert.assertEquals(pokemons, state.getItems()) - updatesCounter += 1 - } + if (state.isProgressVisible) { + loadingCounter += 1 + } else if (setsCounter == 1 && state.list.isNotEmpty() && state.activeFilters.isEmpty()) { + Assert.assertEquals(pokemons, state.list) + updatesCounter += 1 + } else if (state.list.isNotEmpty() && state.activeFilters.isEmpty()) { + setsCounter += 1 + Assert.assertEquals(pokemons, state.list) } } Assert.assertEquals(1, loadingCounter) @@ -249,13 +251,14 @@ class PokemonsViewModelTest { @Test fun test_NextPageOk() = runBlocking { - val observerMock = Mockito.mock(Observer::class.java) as Observer - val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) + val observerMock = + Mockito.mock(Observer::class.java) as Observer> + val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) @@ -274,37 +277,34 @@ class PokemonsViewModelTest { } - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.loadPage(0) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.reload() Thread.sleep(TestSuit.DELAY) - model.loadPage(1) + model.onScroll(19, 24) Thread.sleep(TestSuit.DELAY) Mockito.verify(observerMock, Mockito.times(5)).onChanged(stateCaptor.capture()) + Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) + Assert.assertTrue(actionCaptor.firstValue is PokemonsScreenEffect.ScrollToStart) var loadingCounter = 0 var setsCounter = 0 var addingsCounter = 0 var updatesCounter = 0 for (state in stateCaptor.allValues) { - when (state) { - is PokemonsScreenState.Loading -> { - loadingCounter += 1 - } - is PokemonsScreenState.SetData -> { - setsCounter += 1 - Assert.assertEquals(page1, state.getItems()) - } - is PokemonsScreenState.AddData -> { - addingsCounter += 1 - Assert.assertEquals(list, state.getItems()) - } - is PokemonsScreenState.UpdateData -> { - updatesCounter += 1 - Assert.assertEquals(page1, state.getItems()) - } + if (state.isProgressVisible) { + loadingCounter += 1 + } else if (setsCounter != 0 && state.list.isNotEmpty() && state.list == list && state.activeFilters.isEmpty()) { + Assert.assertEquals(list, state.list) + addingsCounter += 1 + } else if (setsCounter != 0 && state.list.isNotEmpty() && state.activeFilters.isEmpty()) { + Assert.assertEquals(page1, state.list) + updatesCounter += 1 + } else if (state.list.isNotEmpty() && state.activeFilters.isEmpty()) { + setsCounter += 1 + Assert.assertEquals(page1, state.list) } } Assert.assertEquals(2, loadingCounter) @@ -330,13 +330,14 @@ class PokemonsViewModelTest { Mockito.`when`(filtersFactory.getFilters()).thenReturn(StatsFiltersFactory().getFilters()) val testFilter = FilterData.FILTER_ATTACK - val observerMock = Mockito.mock(Observer::class.java) as Observer - val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) + val observerMock = + Mockito.mock(Observer::class.java) as Observer> + val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) @@ -346,8 +347,8 @@ class PokemonsViewModelTest { } - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.loadPage(0) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.reload() Thread.sleep(TestSuit.DELAY) model.sort(testFilter) @@ -355,57 +356,58 @@ class PokemonsViewModelTest { Mockito.verify(observerMock, Mockito.times(5)).onChanged(stateCaptor.capture()) + Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) + for (effect in actionCaptor.allValues) { + Assert.assertTrue(effect is PokemonsScreenEffect.ScrollToStart) + } var loadingCounter = 0 var setsCounter = 0 var filtersCounter = 0 var updatesCounter = 0 for (state in stateCaptor.allValues) { - when (state) { - is PokemonsScreenState.SetData -> { - Assert.assertEquals(page1, state.getItems()) - setsCounter += 1 - } - is PokemonsScreenState.UpdateData -> { - if (updatesCounter == 0) { - Assert.assertEquals(page1, state.getItems()) - } else { - val items = state.getItems() - Assert.assertEquals(PAGE_SIZE, items.size) - Assert.assertNotEquals(page1, items) - for ((idx, item) in items.withIndex()) { - if (idx == 0) { - continue - } - var prevStat: Stat? = null - for (ps in items[idx - 1].stats) { - if (testFilter == ps.stat?.name) { - prevStat = ps - break - } + if (state.isProgressVisible) { + loadingCounter += 1 + } else if (filtersCounter == 0 && state.activeFilters.isNotEmpty()) { + filtersCounter += 1 + val items = state.list + Assert.assertEquals(PAGE_SIZE, items.size) + Assert.assertTrue(state.activeFilters.contains(testFilter)) + } else if (setsCounter != 0 && state.list.isNotEmpty() + && ((updatesCounter == 0 && state.activeFilters.isEmpty()) || state.activeFilters.isNotEmpty()) + ) { + updatesCounter += 1 + if (updatesCounter == 1) { + Assert.assertEquals(page1, state.list) + } else { + val items = state.list + Assert.assertEquals(PAGE_SIZE, items.size) + Assert.assertNotEquals(page1, items) + for ((idx, item) in items.withIndex()) { + if (idx == 0) { + continue + } + var prevStat: Stat? = null + for (ps in items[idx - 1].stats) { + if (testFilter == ps.stat?.name) { + prevStat = ps + break } + } - var stat: Stat? = null - for (ps in item.stats) { - if (testFilter == ps.stat?.name) { - stat = ps - break - } + var stat: Stat? = null + for (ps in item.stats) { + if (testFilter == ps.stat?.name) { + stat = ps + break } - Assert.assertTrue(prevStat!!.baseStat!! >= stat!!.baseStat!!) } + Assert.assertTrue(prevStat!!.baseStat!! >= stat!!.baseStat!!) } - updatesCounter += 1 - } - is PokemonsScreenState.ChangeFilterState -> { - filtersCounter += 1 - val items = state.getItems() - Assert.assertEquals(PAGE_SIZE, items.size) - Assert.assertTrue(state.getActiveFilters().contains(testFilter)) - } - is PokemonsScreenState.Loading -> { - loadingCounter += 1 } + } else if (state.list.isNotEmpty() && state.activeFilters.isEmpty()) { + setsCounter += 1 + Assert.assertEquals(page1, state.list) } } Assert.assertEquals(1, loadingCounter) @@ -431,13 +433,14 @@ class PokemonsViewModelTest { Mockito.`when`(filtersFactory.getFilters()).thenReturn(StatsFiltersFactory().getFilters()) val testFilter = FilterData.FILTER_ATTACK - val observerMock = Mockito.mock(Observer::class.java) as Observer - val stateCaptor = ArgumentCaptor.forClass(PokemonsScreenState::class.java) + val observerMock = + Mockito.mock(Observer::class.java) as Observer> + val stateCaptor = argumentCaptor() model.getStateObservable().observeForever(observerMock) val actionObserverMock = - Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(PokemonsScreenEffect::class.java) + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) @@ -447,8 +450,8 @@ class PokemonsViewModelTest { } - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.loadPage(0) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.reload() Thread.sleep(TestSuit.DELAY) model.sort(testFilter) @@ -459,67 +462,68 @@ class PokemonsViewModelTest { Mockito.verify(observerMock, Mockito.times(7)).onChanged(stateCaptor.capture()) + Mockito.verify(actionObserverMock, Mockito.times(3)).onChanged(actionCaptor.capture()) + for (effect in actionCaptor.allValues) { + Assert.assertTrue(effect is PokemonsScreenEffect.ScrollToStart) + } var loadingCounter = 0 var setsCounter = 0 var filtersCounter = 0 var updatesCounter = 0 - for (state in stateCaptor.allValues) { - when (state) { - is PokemonsScreenState.Loading -> { - loadingCounter += 1 - } - is PokemonsScreenState.SetData -> { - setsCounter += 1 - Assert.assertEquals(page1, state.getItems()) + for (idx in stateCaptor.allValues.indices) { + val state = stateCaptor.allValues[idx] + if (state.isProgressVisible) { + loadingCounter += 1 + } else if (idx == 3 || idx == 5) { + filtersCounter += 1 + val items = state.list + Assert.assertEquals(PAGE_SIZE, items.size) + if (filtersCounter == 1) { + Assert.assertTrue(state.activeFilters.contains(testFilter)) + } else { + Assert.assertFalse(state.activeFilters.contains(testFilter)) } - is PokemonsScreenState.ChangeFilterState -> { - val items = state.getItems() + } else if (idx == 2 || idx == 4 || idx == 6) { + updatesCounter += 1 + if (updatesCounter == 1) { + Assert.assertEquals(page1, state.list) + } else if (updatesCounter == 2) { + val items = state.list Assert.assertEquals(PAGE_SIZE, items.size) - if (filtersCounter == 0) { - Assert.assertTrue(state.getActiveFilters().contains(testFilter)) - } else { - Assert.assertFalse(state.getActiveFilters().contains(testFilter)) - } - filtersCounter += 1 - } - is PokemonsScreenState.UpdateData -> { - if (updatesCounter == 0) { - Assert.assertEquals(page1, state.getItems()) - } else if (updatesCounter == 1) { - val items = state.getItems() - Assert.assertNotEquals(page1, items) - Assert.assertEquals(PAGE_SIZE, items.size) - for ((idx, item) in items.withIndex()) { - if (idx == 0) { - continue - } - var prevStat: Stat? = null - for (ps in items[idx - 1].stats) { - if (FilterData.FILTER_ATTACK == ps.stat?.name) { - prevStat = ps - break - } + Assert.assertNotEquals(page1, items) + for ((idx, item) in items.withIndex()) { + if (idx == 0) { + continue + } + var prevStat: Stat? = null + for (ps in items[idx - 1].stats) { + if (testFilter == ps.stat?.name) { + prevStat = ps + break } + } - var stat: Stat? = null - for (ps in item.stats) { - if (FilterData.FILTER_ATTACK == ps.stat?.name) { - stat = ps - break - } + var stat: Stat? = null + for (ps in item.stats) { + if (testFilter == ps.stat?.name) { + stat = ps + break } - Assert.assertTrue(prevStat!!.baseStat!! >= stat!!.baseStat!!) } - } else if (updatesCounter == 2) { - val items = state.getItems() - Assert.assertEquals(page1, items) - Assert.assertEquals(PAGE_SIZE, items.size) + Assert.assertTrue(prevStat!!.baseStat!! >= stat!!.baseStat!!) } - updatesCounter += 1 + } else if (updatesCounter == 3) { + val items = state.list + Assert.assertEquals(page1, items) + Assert.assertEquals(PAGE_SIZE, items.size) } + } else if (idx == 1) { + setsCounter += 1 + Assert.assertEquals(page1, state.list) } } + Assert.assertEquals(1, loadingCounter) Assert.assertEquals(1, setsCounter) Assert.assertEquals(2, filtersCounter) diff --git a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt index 0d981be..119d70f 100644 --- a/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt +++ b/app/src/test/java/com/mdgd/pokemon/ui/splash/SplashViewModelTest.kt @@ -3,59 +3,59 @@ package com.mdgd.pokemon.ui.splash import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer +import com.mdgd.mvi.states.ScreenState +import com.mdgd.pokemon.MainDispatcherRule import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result import com.mdgd.pokemon.ui.splash.state.SplashScreenEffect -import com.mdgd.pokemon.ui.splash.state.SplashScreenState -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.test.setMain +import com.nhaarman.mockitokotlin2.argumentCaptor +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.* import org.junit.* import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor import org.mockito.Mockito @RunWith(JUnit4::class) class SplashViewModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @get:Rule val rule = InstantTaskExecutorRule() + private lateinit var model: SplashViewModel private lateinit var cache: Cache @Before fun setup() { - Dispatchers.setMain(Dispatchers.Unconfined) cache = Mockito.mock(Cache::class.java) model = SplashViewModel(cache) } - @After - fun tearDown() { - Dispatchers.resetMain() - } - private fun verifyNoMoreInteractions() { Mockito.verifyNoMoreInteractions(cache) } @Test - fun testSetup_NotingHappened() = runBlockingTest { - val stateObserverMock = Mockito.mock(Observer::class.java) as Observer + fun testSetup_NotingHappened() = runBlocking { + val stateObserverMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(stateObserverMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_CREATE) - model.onAny(null, Lifecycle.Event.ON_RESUME) - model.onAny(null, Lifecycle.Event.ON_PAUSE) - model.onAny(null, Lifecycle.Event.ON_STOP) - model.onAny(null, Lifecycle.Event.ON_DESTROY) - model.onAny(null, Lifecycle.Event.ON_ANY) + model.onStateChanged(Lifecycle.Event.ON_CREATE) + model.onStateChanged(Lifecycle.Event.ON_RESUME) + model.onStateChanged(Lifecycle.Event.ON_PAUSE) + model.onStateChanged(Lifecycle.Event.ON_STOP) + model.onStateChanged(Lifecycle.Event.ON_DESTROY) + model.onStateChanged(Lifecycle.Event.ON_ANY) Mockito.verifyNoMoreInteractions(stateObserverMock) Mockito.verifyNoMoreInteractions(actionObserverMock) @@ -65,30 +65,33 @@ class SplashViewModelTest { } @Test - fun testSetup_LaunchError() = runBlockingTest { + fun testSetup_LaunchError() = runBlocking { val error = Throwable("TestError") - val progressChanel = Channel>(Channel.Factory.CONFLATED) + val progressChanel = MutableSharedFlow>( + extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) Mockito.`when`(cache.getProgressChanel()).thenReturn(progressChanel) - val stateObserverMock = Mockito.mock(Observer::class.java) as Observer + val stateObserverMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(stateObserverMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_START) - + model.onStateChanged(Lifecycle.Event.ON_START) Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) + Assert.assertTrue(actionCaptor.firstValue is SplashScreenEffect.LaunchWorker) - progressChanel.send(Result(error)) + progressChanel.tryEmit(Result(error)) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) - val errorState = actionCaptor.value + val errorState = actionCaptor.thirdValue Assert.assertTrue(errorState is SplashScreenEffect.ShowError) Assert.assertEquals((errorState as SplashScreenEffect.ShowError).e, error) @@ -100,30 +103,30 @@ class SplashViewModelTest { } @Test - fun testSetup_LaunchCrash() = runBlockingTest { + fun testSetup_LaunchCrash() = runBlocking { val error = RuntimeException("TestError") Mockito.`when`(cache.getProgressChanel()).thenThrow(error) - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_START) - + model.onStateChanged(Lifecycle.Event.ON_START) - Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) - val errorState = actionCaptor.value - Assert.assertTrue(errorState is SplashScreenEffect.ShowError) - Assert.assertEquals((errorState as SplashScreenEffect.ShowError).e, error) + val errorEffect = actionCaptor.allValues + Assert.assertTrue(errorEffect[0] is SplashScreenEffect.ShowError) + Assert.assertEquals((errorEffect[0] as SplashScreenEffect.ShowError).e, error) + Assert.assertTrue(errorEffect[1] is SplashScreenEffect.LaunchWorker) Mockito.verifyNoInteractions(observerMock) Mockito.verifyNoMoreInteractions(actionObserverMock) @@ -133,29 +136,33 @@ class SplashViewModelTest { } @Test - fun testSetup_LaunchOk() = runBlockingTest { - val progressChanel = Channel>(Channel.Factory.CONFLATED) + fun testSetup_LaunchOk() = runBlocking { + val progressChanel = MutableSharedFlow>( + extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) Mockito.`when`(cache.getProgressChanel()).thenReturn(progressChanel) - val observerMock = Mockito.mock(Observer::class.java) as Observer + val observerMock = + Mockito.mock(Observer::class.java) as Observer> model.getStateObservable().observeForever(observerMock) - val actionObserverMock = Mockito.mock(Observer::class.java) as Observer - val actionCaptor = ArgumentCaptor.forClass(SplashScreenEffect::class.java) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() model.getEffectObservable().observeForever(actionObserverMock) - model.onAny(null, Lifecycle.Event.ON_START) + model.onStateChanged(Lifecycle.Event.ON_START) Mockito.verify(actionObserverMock, Mockito.times(1)).onChanged(actionCaptor.capture()) - Assert.assertTrue(actionCaptor.value is SplashScreenEffect.LaunchWorker) + Assert.assertTrue(actionCaptor.firstValue is SplashScreenEffect.LaunchWorker) - progressChanel.send(Result(90L)) + progressChanel.tryEmit(Result(90L)) Thread.sleep(SplashContract.SPLASH_DELAY * 2) Mockito.verify(cache, Mockito.times(1)).getProgressChanel() Mockito.verify(actionObserverMock, Mockito.times(2)).onChanged(actionCaptor.capture()) - val errorState = actionCaptor.value + val errorState = actionCaptor.thirdValue Assert.assertTrue(errorState is SplashScreenEffect.NextScreen) diff --git a/models_impl/src/test/java/com/mdgd/pokemon/models_impl/TestSuit.kt b/models_impl/src/test/java/com/mdgd/pokemon/models_impl/TestSuit.kt new file mode 100644 index 0000000..ed6bd37 --- /dev/null +++ b/models_impl/src/test/java/com/mdgd/pokemon/models_impl/TestSuit.kt @@ -0,0 +1,11 @@ +package com.mdgd.pokemon.models_impl + +import com.mdgd.pokemon.models_impl.repo.PokemonsRepositoryTest +import org.junit.runner.RunWith +import org.junit.runners.Suite + +@RunWith(Suite::class) +@Suite.SuiteClasses( + PokemonsRepositoryTest::class +) +class TestSuit From 3e13b0876b76ddddaa2d8ad4042e74359d2404ab Mon Sep 17 00:00:00 2001 From: DanDdl Date: Sat, 22 Apr 2023 14:17:06 +0300 Subject: [PATCH 44/47] # duplication removed --- app/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fee9f17..d155c06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,10 +21,6 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - buildFeatures { compose true } From ac675da338051cf8b6d1236ebbff5bcde47ed568 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Tue, 25 Apr 2023 15:04:39 +0300 Subject: [PATCH 45/47] # migration --- app/build.gradle | 2 +- .../com/mdgd/pokemon/ui/error/ErrorScreen.kt | 17 +++++++++++----- .../ui/pokemon/PokemonDetailsFragment.kt | 18 +++++++++++------ .../pokemon/ui/pokemons/PokemonsFragment.kt | 20 ++++++++++++------- .../mdgd/pokemon/ui/splash/SplashFragment.kt | 17 ++++++++++------ build.gradle | 2 +- 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d155c06..59cb9d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { implementation "androidx.compose.material:material:$composeVersion" implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" implementation "androidx.compose.ui:ui-tooling:$composeVersion" - implementation "com.google.android.material:compose-theme-adapter:$composeVersionTheme" + implementation "com.google.accompanist:accompanist-themeadapter-material:$composeVersionThemeMaterial" implementation "androidx.appcompat:appcompat:$compat" diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt index 24c17af..0247b99 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt @@ -3,13 +3,14 @@ package com.mdgd.pokemon.ui.error import android.content.res.Configuration import androidx.compose.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.google.android.material.composethemeadapter.MdcTheme @Composable fun ErrorScreen(params: MutableState) { @@ -48,8 +49,11 @@ fun ErrorScreen(params: MutableState) { ) @Composable fun ErrorPreviewThemeLight() { - MdcTheme { - ErrorScreen(mutableStateOf(DefaultErrorParams())) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams()) + } + ErrorScreen(state) } } @@ -60,7 +64,10 @@ fun ErrorPreviewThemeLight() { ) @Composable fun ErrorPreviewThemeDark() { - MdcTheme { - ErrorScreen(mutableStateOf(DefaultErrorParams(true))) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(true)) + } + ErrorScreen(state) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index 5d067b8..5f58757 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar @@ -44,7 +45,6 @@ import androidx.compose.ui.unit.sp import androidx.fragment.app.viewModels import coil.compose.rememberImagePainter import coil.request.ImageRequest -import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.R import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema @@ -109,7 +109,7 @@ fun PokemonScreen( model: PokemonDetailsContract.ViewModel? ) { val errorDialogTrigger = remember { screenState as MutableState } - MdcTheme { + MaterialTheme { Scaffold( topBar = { TopAppBar( @@ -303,8 +303,11 @@ fun PokemonPreviewThemeLight() { speed.stat?.name = "speed" speed.baseStat = 100502 pokemon.stats.add(speed) - MdcTheme { - PokemonScreen(mutableStateOf(PokemonUiState(properties = listOf())), null) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonUiState(properties = listOf())) + } + PokemonScreen(state, null) } } @@ -330,7 +333,10 @@ fun PokemonPreviewThemeDark() { properties.add(TextPropertyData("bro, king", 1)) properties.add(TitlePropertyData(R.string.pokemon_detail_game_indicies)) properties.add(TextPropertyData("some indicies here", 1)) - MdcTheme { - PokemonScreen(mutableStateOf(PokemonUiState(properties = properties)), null) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonUiState(properties = properties)) + } + PokemonScreen(state, null) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index e582003..188330d 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -24,6 +24,7 @@ import androidx.compose.material.Card import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar @@ -51,7 +52,6 @@ import coil.compose.rememberImagePainter import coil.request.ImageRequest import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.R import com.mdgd.pokemon.models.filters.FilterData @@ -133,7 +133,7 @@ fun PokemonsScreen(screenState: MutableState, model: PokemonsCo val errorDialogTrigger = remember { screenState as MutableState } val scope = rememberCoroutineScope() val scrollState = rememberLazyListState() - MdcTheme { + MaterialTheme { Scaffold( topBar = { TopAppBar( @@ -391,7 +391,7 @@ fun PokemonItemPreviewThemeDark() { speed.baseStat = 100502 pokemon.stats.add(speed) - MdcTheme { + MaterialTheme { PokemonItem(pokemon, null) } } @@ -426,8 +426,11 @@ fun PokemonsPreviewThemeLight() { speed.baseStat = 100502 pokemon.stats.add(speed) - MdcTheme { - PokemonsScreen(mutableStateOf(PokemonsUiState(pokemons = listOf(pokemon))), null) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonsUiState(pokemons = listOf(pokemon))) + } + PokemonsScreen(state, null) } } @@ -438,7 +441,10 @@ fun PokemonsPreviewThemeLight() { ) @Composable fun PokemonsPreviewThemeDark() { - MdcTheme { - PokemonsScreen(mutableStateOf(PokemonsUiState()), null) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonsUiState()) + } + PokemonsScreen(state, null) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt index acbfb0a..8dd5730 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashFragment.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.fragment.app.viewModels import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.google.android.material.composethemeadapter.MdcTheme import com.mdgd.mvi.fragments.HostedFragment import com.mdgd.pokemon.R import com.mdgd.pokemon.bg.UploadWorker @@ -81,7 +80,7 @@ class SplashFragment : @Composable fun SplashScreen(errorParams: MutableState) { val errorDialogTrigger = remember { errorParams } - MdcTheme { + MaterialTheme { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -115,8 +114,11 @@ fun SplashScreen(errorParams: MutableState) { ) @Composable fun SplashPreviewThemeLight() { - MdcTheme { - SplashScreen(mutableStateOf(DefaultErrorParams(false))) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(false)) + } + SplashScreen(state) } } @@ -127,7 +129,10 @@ fun SplashPreviewThemeLight() { ) @Composable fun SplashPreviewThemeDark() { - MdcTheme { - SplashScreen(mutableStateOf(DefaultErrorParams(true))) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(true)) + } + SplashScreen(state) } } diff --git a/build.gradle b/build.gradle index b287640..59e1744 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ buildscript { ext.composeVersion = "1.4.2" // update kotlinCompilerExtensionVersion when composeVersion updated - ext.composeVersionTheme = "1.2.1" // update kotlinCompilerExtensionVersion when composeVersionTheme updated + ext.composeVersionThemeMaterial = "0.30.1" ext.hilt = "2.42" ext.hilt_jetpack = "1.0.0" From 2f3ffdc4f3511df5a0b05cb3562312da5589155e Mon Sep 17 00:00:00 2001 From: DanDdl Date: Mon, 11 Sep 2023 18:15:45 +0300 Subject: [PATCH 46/47] # review improvement --- .../ui/pokemon/PokemonDetailsFragment.kt | 4 ++-- .../ui/pokemon/PokemonDetailsViewModel.kt | 2 +- .../pokemon/ui/pokemons/PokemonsFragment.kt | 2 +- .../pokemon/ui/pokemons/PokemonsViewModel.kt | 3 ++- .../mdgd/pokemon/ui/splash/SplashViewModel.kt | 3 ++- mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt | 8 ++++++-- .../com/mdgd/mvi/fragments/HostedFragment.kt | 18 ++++++++---------- 7 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt index 5f58757..4e16ba2 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsFragment.kt @@ -75,7 +75,7 @@ class PokemonDetailsFragment : HostedFragment< override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { - model?.setPokemonId(PokemonDetailsFragmentArgs.fromBundle(requireArguments()).pokemonId) + viewModel?.setPokemonId(PokemonDetailsFragmentArgs.fromBundle(requireArguments()).pokemonId) } } @@ -89,7 +89,7 @@ class PokemonDetailsFragment : HostedFragment< ): View { val view = ComposeView(requireContext()) view.setContent { - PokemonScreen(screenState, model) + PokemonScreen(screenState, viewModel) } return view } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt index 5e459a0..a6e9bb5 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonDetailsViewModel.kt @@ -31,7 +31,7 @@ import javax.inject.Inject @HiltViewModel class PokemonDetailsViewModel @Inject constructor( private val repo: PokemonsRepo -) : MviViewModel(), +) : MviViewModel(), PokemonDetailsContract.ViewModel { private val pokemonIdFlow = MutableStateFlow(-1L) diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt index 188330d..cdafd67 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsFragment.kt @@ -83,7 +83,7 @@ class PokemonsFragment : HostedFragment< ): View { val view = ComposeView(requireContext()) view.setContent { - PokemonsScreen(screenState, model) + PokemonsScreen(screenState, viewModel) } return view } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt index b52022f..f077493 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsViewModel.kt @@ -26,7 +26,8 @@ class PokemonsViewModel @Inject constructor( private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder -) : MviViewModel(), PokemonsContract.ViewModel { +) : MviViewModel(), + PokemonsContract.ViewModel { private var firstVisibleIndex: Int = 0 private val exceptionHandler = CoroutineExceptionHandler { _, e -> diff --git a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt index be1cfd4..3a03015 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/splash/SplashViewModel.kt @@ -19,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( private val cache: Cache -) : MviViewModel(), SplashContract.ViewModel { +) : MviViewModel(), + SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> setEffect(SplashScreenEffect.ShowError(e)) diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 6634392..4abd80e 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mdgd.mvi.fragments.FragmentContract +import com.mdgd.mvi.states.AbstractEffect import com.mdgd.mvi.states.AbstractState import com.mdgd.mvi.states.ScreenState -abstract class MviViewModel> : ViewModel(), +abstract class MviViewModel, EFFECT : AbstractEffect> : + ViewModel(), FragmentContract.ViewModel { private val stateHolder = MutableLiveData>() private val effectHolder = MutableLiveData>() @@ -23,7 +25,9 @@ abstract class MviViewModel> : ViewModel(), protected fun getState() = stateHolder.value as STATE? - protected fun setEffect(action: ScreenState) { + protected fun getEffect() = stateHolder.value as EFFECT? + + protected fun setEffect(action: EFFECT) { effectHolder.value = action } diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index e9c0f16..a0b19dd 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -17,7 +17,7 @@ abstract class HostedFragment< : NavHostFragment(), FragmentContract.View, Observer>, LifecycleEventObserver { - protected var model: VIEW_MODEL? = null + protected var viewModel: VIEW_MODEL? = null private set protected var fragmentHost: HOST? = null @@ -32,8 +32,7 @@ abstract class HostedFragment< val hostClassName = ((javaClass.genericSuperclass as ParameterizedType) .actualTypeArguments[1] as Class<*>).canonicalName throw RuntimeException( - "Activity must implement $hostClassName to attach ${this.javaClass.simpleName}", - e + "Activity must implement $hostClassName to attach ${this.javaClass.simpleName}", e ) } } @@ -48,20 +47,19 @@ abstract class HostedFragment< super.onCreate(savedInstanceState) setModel(createModel()) lifecycle.addObserver(this) - model?.getStateObservable()?.observe(this, this) - model?.getEffectObservable()?.observe(this) { it.visit(this@HostedFragment as VIEW) } + viewModel?.let { + it.getStateObservable().observe(this, this) + it.getEffectObservable().observe(this, this) + } } protected abstract fun createModel(): VIEW_MODEL override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - model?.onStateChanged(event) + viewModel?.onStateChanged(event) if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) - // order matters - model?.getEffectObservable()?.removeObservers(this) - model?.getStateObservable()?.removeObservers(this) } } @@ -70,6 +68,6 @@ abstract class HostedFragment< } protected fun setModel(model: VIEW_MODEL) { - this.model = model + this.viewModel = model } } From dbff4f1ee705a6e21f0647364d27a8a0cc162586 Mon Sep 17 00:00:00 2001 From: DanDdl Date: Tue, 12 Sep 2023 20:33:06 +0300 Subject: [PATCH 47/47] # dsl migration --- app/build.gradle | 109 ------------------ app/build.gradle.kts | 98 ++++++++++++++++ app/proguard-rules.pro | 2 +- build.gradle | 69 ----------- build.gradle.kts | 54 +++++++++ models/build.gradle | 51 -------- models/build.gradle.kts | 54 +++++++++ models/proguard-rules.pro | 2 +- models_impl/build.gradle | 78 ------------- models_impl/build.gradle.kts | 72 ++++++++++++ models_impl/proguard-rules.pro | 2 +- mvi/build.gradle | 45 -------- mvi/build.gradle.kts | 51 ++++++++ mvi/proguard-rules.pro | 2 +- .../main/java/com/mdgd/mvi/MviViewModel.kt | 7 +- .../com/mdgd/mvi/fragments/HostedFragment.kt | 4 +- settings.gradle | 5 - settings.gradle.kts | 21 ++++ 18 files changed, 362 insertions(+), 364 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 models/build.gradle create mode 100644 models/build.gradle.kts delete mode 100644 models_impl/build.gradle create mode 100644 models_impl/build.gradle.kts delete mode 100644 mvi/build.gradle create mode 100644 mvi/build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 59cb9d0..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,109 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'kotlin-kapt' - id 'androidx.navigation.safeargs.kotlin' - id 'dagger.hilt.android.plugin' -} - -android { - compileSdkVersion project.compile - buildToolsVersion project.tools - - defaultConfig { - applicationId "com.mdgd.pokemon" - minSdkVersion project.min - targetSdkVersion project.target - versionCode 1 - versionName "1.0" - multiDexEnabled true - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.4.6" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - namespace 'com.mdgd.pokemon' -} - -dependencies { - implementation project(':mvi') - implementation project(':models') - implementation project(':models_impl') - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "androidx.multidex:multidex:2.0.1" - - implementation "androidx.compose.runtime:runtime:$composeVersion" - implementation "androidx.compose.ui:ui:$composeVersion" - implementation "androidx.compose.foundation:foundation:$composeVersion" - implementation "androidx.compose.foundation:foundation-layout:$composeVersion" - implementation "androidx.compose.material:material:$composeVersion" - implementation "androidx.compose.runtime:runtime-livedata:$composeVersion" - implementation "androidx.compose.ui:ui-tooling:$composeVersion" - implementation "com.google.accompanist:accompanist-themeadapter-material:$composeVersionThemeMaterial" - - implementation "androidx.appcompat:appcompat:$compat" - - // swipe-refresh - implementation "com.google.accompanist:accompanist-swiperefresh:0.19.0" - - // navigation - implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" - implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - - // image loading - implementation 'io.coil-kt:coil-compose:1.3.0' - - // json - implementation "com.google.code.gson:gson:$gson" - - // hilt - implementation "com.google.dagger:hilt-android:2.45" - kapt("com.google.dagger:hilt-android-compiler:2.45") - - implementation "androidx.hilt:hilt-navigation-fragment:$hilt_jetpack" - kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") - implementation 'androidx.hilt:hilt-work:1.0.0' - - // lifecycle - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ktx" - - implementation "androidx.work:work-runtime-ktx:$work_version" - - testImplementation "junit:junit:$junit" - testImplementation "org.mockito:mockito-core:$mockito_core" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin" - testImplementation "android.arch.core:core-testing:$testing_core" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$testing_coroutine" - testImplementation "com.google.code.gson:gson:$gson" - - androidTestImplementation "androidx.test.ext:junit:$junit_android" - androidTestImplementation "androidx.test.espresso:espresso-core:$espresso" - androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.3.0" - - implementation "androidx.core:core-ktx:$ktx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" -} - -kapt { - correctErrorTypes true -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3512d2b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + id("com.android.application") + kotlin("android") + id("com.google.devtools.ksp") + id("androidx.navigation.safeargs.kotlin") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "com.mdgd.pokemon" + compileSdk = rootProject.extra["compile"] as Int? + + defaultConfig { + applicationId = "com.mdgd.pokemon" + minSdk = rootProject.extra["min"] as Int? + + versionCode = 1 + versionName = "1.0" + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.6" + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(project(":mvi")) + implementation(project(":models")) + implementation(project(":models_impl")) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + + // swipe-refresh + implementation("com.google.accompanist:accompanist-swiperefresh:0.19.0") + + // navigation + implementation("androidx.navigation:navigation-fragment-ktx:${rootProject.extra["nav_version"]}") + implementation("androidx.navigation:navigation-ui-ktx:${rootProject.extra["nav_version"]}") + + // image loading + implementation("io.coil-kt:coil-compose:1.3.0") + + // json + implementation("com.google.code.gson:gson:${rootProject.extra["gson"]}") + + // hilt + implementation("com.google.dagger:hilt-android:${rootProject.extra["hilt"]}") + ksp("com.google.dagger:hilt-android-compiler:${rootProject.extra["hilt"]}") + + implementation("androidx.hilt:hilt-navigation-fragment:${rootProject.extra["hilt_jetpack"]}") + ksp("androidx.hilt:hilt-compiler:${rootProject.extra["hilt_jetpack"]}") + implementation("androidx.hilt:hilt-work:1.0.0") + + // lifecycle + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${rootProject.extra["lifecycle_ktx"]}") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:${rootProject.extra["lifecycle_ktx"]}") + + implementation("androidx.work:work-runtime-ktx:${rootProject.extra["work_version"]}") + + testImplementation("junit:junit:${rootProject.extra["ver_junit"]}") + testImplementation("org.mockito:mockito-core:${rootProject.extra["mockito_core"]}") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${rootProject.extra["mockito_kotlin"]}") + testImplementation("android.arch.core:core-testing:${rootProject.extra["testing_core"]}") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${rootProject.extra["testing_coroutine"]}") + testImplementation("com.google.code.gson:gson:${rootProject.extra["gson"]}") + + androidTestImplementation("androidx.test.ext:junit:${rootProject.extra["junit_android"]}") + androidTestImplementation("androidx.test.espresso:espresso-core:${rootProject.extra["espresso"]}") + androidTestImplementation("com.android.support.test.espresso:espresso-contrib:3.3.0") + + implementation("androidx.core:core-ktx:${rootProject.extra["ktx"]}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${rootProject.extra["coroutines"]}") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..2f9dc5a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 59e1744..0000000 --- a/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.kotlin_version = '1.8.20' // update according to composeVersion - ext.compat = "1.6.1" - ext.nav_version = "2.5.3" - ext.work_version = "2.8.1" - - ext.ktx = "1.10.0" - ext.coroutines = "1.6.4" - ext.lifecycle_ktx = "2.6.1" - - ext.room = "2.5.1" - ext.room_compiler = "2.5.1" - - ext.gson = "2.9.0" - ext.retrofit = "2.9.0" - ext.retrofit_gson = "2.9.0" - ext.okhttp_log = "4.10.0" - ext.okhttp = "4.10.0" - - ext.composeVersion = "1.4.2" - // update kotlinCompilerExtensionVersion when composeVersion updated - // update kotlinCompilerExtensionVersion when composeVersionTheme updated - ext.composeVersionThemeMaterial = "0.30.1" - - ext.hilt = "2.42" - ext.hilt_jetpack = "1.0.0" - - ext.junit = "4.13.2" - ext.junit_android = "1.1.5" - ext.mockito_core = "5.2.0" - ext.mockito_kotlin = "2.2.0" - ext.testing_core = "1.1.1" - ext.testing_coroutine = "1.6.0" - ext.espresso = "3.5.1" - - project.ext { - min = 21 - target = 33 - compile = 33 - tools = "30.0.3" - } - - repositories { - mavenCentral() - google() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" - classpath "com.google.dagger:hilt-android-gradle-plugin:2.45" - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - mavenCentral() - google() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d2e7e48 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,54 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + extra.apply { + set("kotlin_version", "1.8.20") // update according to composeVersion + set("lifecycle_ktx", "2.6.1") + set("nav_version", "2.7.2") + set("work_version", "2.8.1") + set("room", "2.5.2") + set("room_compiler", "2.5.2") + set("gson", "2.9.0") + set("retrofit", "2.9.0") + set("retrofit_gson", "2.9.0") + set("okhttp_log", "4.10.0") + set("okhttp", "4.10.0") + set("ver_junit", "4.13.2") + set("junit_android", "1.1.5") + set("espresso", "3.5.1") + + set("hilt", "2.46.1") + set("hilt_jetpack", "1.0.0") + set("testing_core", "1.1.1") + set("testing_coroutine", "1.6.0") + + // update kotlinCompilerExtensionVersion when composeVersion updated + // update kotlinCompilerExtensionVersion when composeVersionTheme updated + set("composeVersion", "1.4.2") + set("composeVersionThemeMaterial", "0.30.1") + + set("ktx", "1.12.0") + set("coroutines", "1.7.3") + set("mockito_core", "5.3.1") + set("mockito_kotlin", "2.2.0") + set("testing", "1.1.1") + } + + project.extra.apply { + set("min", 20) + set("target", 34) + set("compile", 34) + } +} + +plugins { + id("com.android.application") version "8.1.1" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("androidx.navigation.safeargs") version "2.7.2" apply false + id("com.android.library") version "8.1.1" apply false + id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false + id("com.google.dagger.hilt.android") version "2.46.1" apply false +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} diff --git a/models/build.gradle b/models/build.gradle deleted file mode 100644 index a3a4034..0000000 --- a/models/build.gradle +++ /dev/null @@ -1,51 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion project.compile - buildToolsVersion project.tools - - defaultConfig { - minSdkVersion project.min - targetSdkVersion project.target - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - namespace 'com.mdgd.pokemon.models' -} - -dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "androidx.appcompat:appcompat:$compat" - - // room - implementation "androidx.room:room-runtime:$room" - annotationProcessor "androidx.room:room-compiler:$room_compiler" - - implementation "com.google.code.gson:gson:$gson" - - implementation "androidx.core:core-ktx:$ktx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" -} - -repositories { - mavenCentral() -} diff --git a/models/build.gradle.kts b/models/build.gradle.kts new file mode 100644 index 0000000..854f920 --- /dev/null +++ b/models/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("com.android.library") + kotlin("android") + id("com.google.devtools.ksp") +} + +android { + namespace = "com.mdgd.pokemon.models" + compileSdk = rootProject.extra["compile"] as Int? + + defaultConfig { + minSdk = rootProject.extra["min"] as Int? + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + + // room + implementation("androidx.room:room-runtime:${rootProject.extra["room"]}") + ksp("androidx.room:room-compiler:${rootProject.extra["room_compiler"]}") + + implementation("com.google.code.gson:gson:${rootProject.extra["gson"]}") + + implementation("androidx.core:core-ktx:${rootProject.extra["ktx"]}") + + // coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${rootProject.extra["coroutines"]}") +} diff --git a/models/proguard-rules.pro b/models/proguard-rules.pro index f1b4245..2f9dc5a 100644 --- a/models/proguard-rules.pro +++ b/models/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/models_impl/build.gradle b/models_impl/build.gradle deleted file mode 100644 index e55b3e0..0000000 --- a/models_impl/build.gradle +++ /dev/null @@ -1,78 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'kotlin-kapt' - id 'dagger.hilt.android.plugin' -} - -android { - compileSdkVersion project.compile - buildToolsVersion project.tools - - defaultConfig { - minSdkVersion project.min - targetSdkVersion project.target - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - namespace 'com.mdgd.pokemon.models_impl' -} - -dependencies { - implementation project(':models') - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "androidx.appcompat:appcompat:$compat" - - // room - implementation "androidx.room:room-runtime:$room" - kapt "androidx.room:room-compiler:$room_compiler" - - // retrofit - implementation "com.squareup.retrofit2:retrofit:$retrofit" - implementation "com.squareup.retrofit2:converter-gson:$retrofit_gson" - - // okHttp - implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_log" - implementation "com.squareup.okhttp3:okhttp:$okhttp" - - implementation "androidx.core:core-ktx:$ktx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - - // hilt - implementation "com.google.dagger:hilt-android:2.45" - kapt("com.google.dagger:hilt-android-compiler:2.45") - kapt("androidx.hilt:hilt-compiler:$hilt_jetpack") - - testImplementation "junit:junit:$junit" - testImplementation "org.mockito:mockito-core:$mockito_core" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin" - testImplementation "android.arch.core:core-testing:$testing_core" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$testing_coroutine" - testImplementation "com.google.code.gson:gson:$gson" -} - -repositories { - mavenCentral() -} - -kapt{ - correctErrorTypes true -} diff --git a/models_impl/build.gradle.kts b/models_impl/build.gradle.kts new file mode 100644 index 0000000..cdbc0a6 --- /dev/null +++ b/models_impl/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + id("com.android.library") + kotlin("android") + id("com.google.devtools.ksp") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "com.mdgd.pokemon.models_impl" + compileSdk = rootProject.extra["compile"] as Int? + + defaultConfig { + minSdk = rootProject.extra["min"] as Int? + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(project(":models")) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + + // room + implementation("androidx.room:room-runtime:${rootProject.extra["room"]}") + ksp("androidx.room:room-compiler:${rootProject.extra["room_compiler"]}") + + // retrofit + implementation("com.squareup.retrofit2:retrofit:${rootProject.extra["retrofit"]}") + implementation("com.squareup.retrofit2:converter-gson:${rootProject.extra["retrofit_gson"]}") + + // okHttp + implementation("com.squareup.okhttp3:logging-interceptor:${rootProject.extra["okhttp_log"]}") + implementation("com.squareup.okhttp3:okhttp:${rootProject.extra["okhttp"]}") + + implementation("androidx.core:core-ktx:${rootProject.extra["ktx"]}") + + // coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${rootProject.extra["coroutines"]}") + + implementation("com.google.dagger:hilt-android:${rootProject.extra["hilt"]}") + ksp("com.google.dagger:hilt-android-compiler:${rootProject.extra["hilt"]}") + + testImplementation("junit:junit:${rootProject.extra["ver_junit"]}") + testImplementation("org.mockito:mockito-core:${rootProject.extra["mockito_core"]}") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${rootProject.extra["mockito_kotlin"]}") + testImplementation("android.arch.core:core-testing:${rootProject.extra["testing"]}") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${rootProject.extra["coroutines"]}") + testImplementation("com.google.code.gson:gson:${rootProject.extra["gson"]}") +} diff --git a/models_impl/proguard-rules.pro b/models_impl/proguard-rules.pro index f1b4245..2f9dc5a 100644 --- a/models_impl/proguard-rules.pro +++ b/models_impl/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/mvi/build.gradle b/mvi/build.gradle deleted file mode 100644 index 8738b84..0000000 --- a/mvi/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion project.compile - buildToolsVersion project.tools - - defaultConfig { - minSdkVersion project.min - targetSdkVersion project.target - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - namespace 'com.mdgd.mvi' -} - -dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "androidx.appcompat:appcompat:$compat" - - // lifecycle - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx" - - // navigation - implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" - implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - - implementation "androidx.core:core-ktx:$ktx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/mvi/build.gradle.kts b/mvi/build.gradle.kts new file mode 100644 index 0000000..338bc27 --- /dev/null +++ b/mvi/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + namespace = "com.mdgd.mvi" + compileSdk = rootProject.extra["compile"] as Int? + + defaultConfig { + minSdk = rootProject.extra["min"] as Int? + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + + // lifecycle + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${rootProject.extra["lifecycle_ktx"]}") + + // navigation + implementation("androidx.navigation:navigation-fragment-ktx:${rootProject.extra["nav_version"]}") + implementation("androidx.navigation:navigation-ui-ktx:${rootProject.extra["nav_version"]}") + + implementation("androidx.core:core-ktx:${rootProject.extra["ktx"]}") +} diff --git a/mvi/proguard-rules.pro b/mvi/proguard-rules.pro index 6670b3d..3e9b0c8 100644 --- a/mvi/proguard-rules.pro +++ b/mvi/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 4abd80e..0f98d11 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -10,8 +10,8 @@ import com.mdgd.mvi.states.AbstractState import com.mdgd.mvi.states.ScreenState abstract class MviViewModel, EFFECT : AbstractEffect> : - ViewModel(), - FragmentContract.ViewModel { + ViewModel(), FragmentContract.ViewModel { + private val stateHolder = MutableLiveData>() private val effectHolder = MutableLiveData>() @@ -19,12 +19,15 @@ abstract class MviViewModel, EFFECT : Abstrac override fun getEffectObservable() = effectHolder + @Suppress("UNCHECKED_CAST") protected fun setState(state: STATE) { stateHolder.value = stateHolder.value?.let { state.merge(it as STATE) } ?: state } + @Suppress("UNCHECKED_CAST") protected fun getState() = stateHolder.value as STATE? + @Suppress("UNCHECKED_CAST") protected fun getEffect() = stateHolder.value as EFFECT? protected fun setEffect(action: EFFECT) { diff --git a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt index a0b19dd..9d8e3cd 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -23,6 +23,7 @@ abstract class HostedFragment< protected var fragmentHost: HOST? = null private set + @Suppress("UNCHECKED_CAST") override fun onAttach(context: Context) { super.onAttach(context) // keep the call back @@ -30,7 +31,7 @@ abstract class HostedFragment< context as HOST } catch (e: Throwable) { val hostClassName = ((javaClass.genericSuperclass as ParameterizedType) - .actualTypeArguments[1] as Class<*>).canonicalName + .actualTypeArguments[1] as Class<*>).canonicalName throw RuntimeException( "Activity must implement $hostClassName to attach ${this.javaClass.simpleName}", e ) @@ -63,6 +64,7 @@ abstract class HostedFragment< } } + @Suppress("UNCHECKED_CAST") override fun onChanged(value: ScreenState) { value.visit(this@HostedFragment as VIEW) } diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 33cd65b..0000000 --- a/settings.gradle +++ /dev/null @@ -1,5 +0,0 @@ -include ':models_impl' -include ':mvi' -include ':models' -include ':app' -rootProject.name = "Pokemons" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8a7090e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,21 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +include(":models_impl") +include(":mvi") +include(":models") +include(":adapter") +include(":app") +rootProject.name = "Pokemons"