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 c938e59..0000000 Binary files a/Android Developer-Practical Task-Updated-June-2020-pdf.pdf and /dev/null differ diff --git a/README.md b/README.md index ee54a92..8f8efa1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # Pokemons Exam task VMedia + + +TODO: animate screen changes diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 16aeb75..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,80 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'kotlin-kapt' - id 'androidx.navigation.safeargs.kotlin' -} - -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" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -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.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' - - // navigation - implementation "androidx.navigation:navigation-fragment:$nav_version" - implementation "androidx.navigation:navigation-ui:$nav_version" - - // images - implementation 'com.squareup.picasso:picasso:2.71828' - - // json - implementation "com.google.code.gson:gson:$gson" - - // 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: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 "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" - - implementation "androidx.core:core-ktx:$ktx" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" -} 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/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 6e21701..91303f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,20 +1,22 @@ + xmlns:tools="http://schemas.android.com/tools"> + android:theme="@style/Theme.Pokemons"> - + @@ -22,6 +24,11 @@ + + 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/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/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/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 6c54699..faea836 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/MainActivity.kt @@ -6,12 +6,13 @@ import androidx.navigation.NavOptions import androidx.navigation.Navigation import com.mdgd.mvi.HostActivity import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.error.ErrorFragment.Companion.newInstance 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 @@ -27,23 +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()) - } - - override fun showError(error: Throwable?) { - error?.printStackTrace() - if (supportFragmentManager.findFragmentByTag("error") == null) { - val errorFragment = newInstance(R.string.dialog_error_title, R.string.dialog_error_message) - errorFragment.setError(error) - errorFragment.show(supportFragmentManager, "error") - } + ?: -1).toBundle()) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/adapter/ClickData.kt b/app/src/main/java/com/mdgd/pokemon/ui/adapter/ClickData.kt deleted file mode 100644 index 468aae3..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/adapter/ClickData.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mdgd.pokemon.ui.adapter - -sealed class ClickEvent { - - class EmptyData() : ClickEvent() - - class ClickData(val data: T) : ClickEvent() -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerAdapter.kt b/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerAdapter.kt deleted file mode 100644 index 8804e16..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.mdgd.pokemon.ui.adapter - -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -abstract class RecyclerAdapter : RecyclerView.Adapter>() { - protected val items: MutableList = ArrayList() - protected val clicksSubject = MutableStateFlow>(ClickEvent.EmptyData()) - - fun getItemClickFlow(): Flow> { - return clicksSubject - } - - override fun getItemViewType(position: Int): Int { - if (items.isEmpty()) { - return EMPTY_VIEW - } - return super.getItemViewType(position) - } - - override fun getItemCount(): Int { - return if (items.isEmpty()) 1 else items.size - } - - override fun onBindViewHolder(holder: RecyclerVH, position: Int) { - if (items.isNotEmpty()) { - holder.bindItem(items[position], position) - } - } - - override fun onViewAttachedToWindow(holder: RecyclerVH) { - super.onViewAttachedToWindow(holder) - holder.setupSubscriptions() - } - - override fun onViewDetachedFromWindow(holder: RecyclerVH) { - holder.clearSubscriptions() - super.onViewDetachedFromWindow(holder) - } - - fun setItems(items: List) { - if (this.items.isEmpty()) { - this.items.addAll(items) - notifyDataSetChanged() - } else { - val oldList: List = ArrayList(this.items) - this.items.clear() - this.items.addAll(items) - dispatchChanges(oldList, items) - } - } - - protected fun dispatchChanges(oldList: List, items: List) { - DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return items.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldList[oldItemPosition] == items[newItemPosition] - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldList[oldItemPosition] == items[newItemPosition] - } - }).dispatchUpdatesTo(this) - } - - - companion object { - const val EMPTY_VIEW = 1 - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerVH.kt b/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerVH.kt deleted file mode 100644 index fb69630..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/adapter/RecyclerVH.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mdgd.pokemon.ui.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -open class RecyclerVH(itemView: View) : RecyclerView.ViewHolder(itemView) { - - open fun setupSubscriptions() { - } - - open fun clearSubscriptions() { - } - - open fun bindItem(item: T, position: Int) { - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/DefaultErrorParams.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/DefaultErrorParams.kt new file mode 100644 index 0000000..549bc71 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/DefaultErrorParams.kt @@ -0,0 +1,10 @@ +package com.mdgd.pokemon.ui.error + +data class DefaultErrorParams( + override val isVisible: Boolean = false, + override val title: String = "", + override val message: String = "" +) : ErrorParams { + + override fun hide() = copy(isVisible = false) +} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorContract.kt deleted file mode 100644 index 193f1cd..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorContract.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mdgd.pokemon.ui.error - -import com.mdgd.mvi.fragments.FragmentContract -import com.mdgd.pokemon.ui.error.state.ErrorFragmentAction -import com.mdgd.pokemon.ui.error.state.ErrorFragmentState - -class ErrorContract { - interface ViewModel : FragmentContract.ViewModel - interface View : FragmentContract.View - interface Host : FragmentContract.Host -} 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 deleted file mode 100644 index c0804aa..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorFragment.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.mdgd.pokemon.ui.error - -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import androidx.appcompat.app.AlertDialog -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.error.state.ErrorFragmentAction -import com.mdgd.pokemon.ui.error.state.ErrorFragmentState - -class ErrorFragment : MessageDialog< - ErrorContract.View, - ErrorFragmentState, - ErrorFragmentAction, - ErrorContract.ViewModel, - ErrorContract.Host>(), - ErrorContract.View, DialogInterface.OnClickListener { - private var error: Throwable? = null - - override fun createModel(): ErrorContract.ViewModel? { - return null - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(requireActivity()) - val args = arguments - // todo add trace printing, add retry - if (args != null) { - val type = args.getInt(KEY_TYPE) - if (TYPE_INT == type) { - val titleResId = args.getInt(KEY_TITLE, R.string.empty) - if (titleResId != 0) { - builder.setTitle(titleResId) - } - val messageResId = args.getInt(KEY_MSG, R.string.empty) - if (messageResId != 0) { - val message = getString(messageResId) + if (error == null) "" else " " + error!!.message - builder.setMessage(message) - } - } else if (TYPE_STR == type) { - builder.setTitle(args.getString(KEY_TITLE_STR, "")) - val message = args.getString(KEY_MSG_STR, "") + if (error == null) "" else " " + error!!.message - builder.setMessage(message) - } - } - builder.setPositiveButton(android.R.string.ok, this) - return builder.create() - } - - override fun onClick(dialog: DialogInterface, which: Int) { - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - dismissAllowingStateLoss() - } - } - } - - 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/ErrorParams.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorParams.kt new file mode 100644 index 0000000..e4e4b61 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorParams.kt @@ -0,0 +1,9 @@ +package com.mdgd.pokemon.ui.error + +interface ErrorParams { + val title: String + val message: String + val isVisible: Boolean + + fun hide(): ErrorParams +} 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..0247b99 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/error/ErrorScreen.kt @@ -0,0 +1,73 @@ +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 + +@Composable +fun ErrorScreen(params: MutableState) { + if (params.value.isVisible) { + AlertDialog( + title = { + Text(text = params.value.title) + }, + text = { + Text(text = params.value.message) + }, + confirmButton = { + Button( + onClick = { + params.value = params.value.hide() + }, + ) { + 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. + params.value = params.value.hide() + } + ) + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + name = "Light Mode" +) +@Composable +fun ErrorPreviewThemeLight() { + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams()) + } + ErrorScreen(state) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun ErrorPreviewThemeDark() { + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(true)) + } + ErrorScreen(state) + } +} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/MessageDialog.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/MessageDialog.kt deleted file mode 100644 index 50e9b1e..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/MessageDialog.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mdgd.pokemon.ui.error - -import com.mdgd.mvi.fragments.FragmentContract -import com.mdgd.mvi.fragments.HostedDialogFragment -import com.mdgd.mvi.states.AbstractAction -import com.mdgd.mvi.states.ScreenState - -abstract class MessageDialog< - VIEW : FragmentContract.View, - STATE : ScreenState, - ACTION : AbstractAction, - VIEW_MODEL : FragmentContract.ViewModel, - HOST : FragmentContract.Host> - : HostedDialogFragment() { - - companion object { - const val KEY_TITLE = "key_title" - const val KEY_MSG = "key_msg" - const val KEY_TITLE_STR = "key_title_str" - const val KEY_MSG_STR = "key_msg_str" - const val KEY_TYPE = "key_type" - const val TYPE_INT = 1 - const val TYPE_STR = 2 - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentAction.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentAction.kt deleted file mode 100644 index 2ba0a18..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentAction.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mdgd.pokemon.ui.error.state - -import com.mdgd.mvi.states.AbstractAction -import com.mdgd.pokemon.ui.error.ErrorContract - -sealed class ErrorFragmentAction : AbstractAction() { - override fun handle(screen: ErrorContract.View) {} -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentState.kt b/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentState.kt deleted file mode 100644 index 7f79c0a..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/error/state/ErrorFragmentState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mdgd.pokemon.ui.error.state - -import com.mdgd.mvi.states.ScreenState -import com.mdgd.pokemon.ui.error.ErrorContract - -sealed class ErrorFragmentState : ScreenState { - override fun visit(screen: ErrorContract.View) {} - - override fun merge(prevState: ErrorFragmentState) {} -} 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 7c94e41..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 @@ -1,18 +1,20 @@ package com.mdgd.pokemon.ui.pokemon import com.mdgd.mvi.fragments.FragmentContract -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState +import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty class PokemonDetailsContract { - interface ViewModel : FragmentContract.ViewModel { + interface ViewModel : FragmentContract.ViewModel { fun setPokemonId(pokemonId: Long) + fun onBackPressed() } interface View : FragmentContract.View { fun setItems(items: List) + fun goBack() } - interface Host : FragmentContract.Host + interface Host : FragmentContract.Host { + 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 41ea658..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 @@ -1,51 +1,342 @@ package com.mdgd.pokemon.ui.pokemon +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.foundation.Image +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.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 +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +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.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 +import androidx.compose.ui.text.font.FontFamily +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.compose.ui.unit.sp +import androidx.fragment.app.viewModels +import coil.compose.rememberImagePainter +import coil.request.ImageRequest import com.mdgd.mvi.fragments.HostedFragment -import com.mdgd.pokemon.PokemonsApp.Companion.instance import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.pokemon.adapter.PokemonPropertiesAdapter -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenState +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.repo.schemas.Stat_ +import com.mdgd.pokemon.ui.error.ErrorParams +import com.mdgd.pokemon.ui.error.ErrorScreen +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, - PokemonDetailsScreenAction, PokemonDetailsContract.ViewModel, PokemonDetailsContract.Host>(), PokemonDetailsContract.View { - private val adapter = PokemonPropertiesAdapter() + + private val screenState = mutableStateOf(PokemonUiState()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (arguments != null) { - model?.setPokemonId(PokemonDetailsFragmentArgs.fromBundle(requireArguments()).pokemonId) + arguments?.let { + viewModel?.setPokemonId(PokemonDetailsFragmentArgs.fromBundle(requireArguments()).pokemonId) } } 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(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_pokemon_properties, container, false) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val view = ComposeView(requireContext()) + view.setContent { + PokemonScreen(screenState, viewModel) + } + return view } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val recycler: RecyclerView = view.findViewById(R.id.pokemon_details_recycler) - recycler.layoutManager = LinearLayoutManager(activity) - recycler.adapter = adapter + override fun setItems(items: List) { + screenState.value = screenState.value.copy(properties = items) } - override fun setItems(items: List) { - adapter.setItems(items) + override fun goBack() { + fragmentHost?.onBackPressed() + } +} + +@Composable +fun PokemonScreen( + screenState: MutableState, + model: PokemonDetailsContract.ViewModel? +) { + val errorDialogTrigger = remember { screenState as MutableState } + MaterialTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.app_name)) }, + navigationIcon = { + IconButton(onClick = { model?.onBackPressed() }) { + Icon( + Icons.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.button_back) + ) + } + } + ) + } + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + if (screenState.value.properties.isNullOrEmpty()) { + items(items = listOf(System.currentTimeMillis()), key = { it }) { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + textAlign = TextAlign.Center, + text = stringResource(id = R.string.no_pokemons), + ) + } + } + } else { + items(items = screenState.value.properties) { PokemonDetailItem(it) } + } + } + ErrorScreen(errorDialogTrigger) + } + } + } +} + +@Composable +fun PokemonDetailItem(property: PokemonProperty) { + when (property.type) { + PokemonProperty.PROPERTY_IMAGE -> { + val p = property as ImageProperty + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + contentDescription = stringResource(id = R.string.fragment_pokemon_picture), + contentScale = ContentScale.Inside, + modifier = Modifier.size(200.dp), + painter = rememberImagePainter( + data = p.imageUrl, + builder = { + ImageRequest.Builder(LocalContext.current) + .placeholder(R.drawable.ic_pokemon) + .error(R.drawable.ic_pokemon) + .build() + } + )) + } + } + PokemonProperty.PROPERTY_LABEL -> { + val startPadding = dimensionResource(id = R.dimen.pokemon_details_nesting_level_padding) + val p = property as LabelProperty + + val title = if (p.titleResId == 0) { + p.titleStr + } else { + stringResource(id = p.titleResId) + } + Row(modifier = Modifier.fillMaxWidth()) { + Text( + text = title, + fontSize = 16.sp, + style = TextStyle(fontWeight = FontWeight.Bold), + modifier = Modifier + .weight(1F) + .padding( + PaddingValues( + startPadding * (1 + p.nestingLevel), + 0.dp, 0.dp, 0.dp + ) + ) + ) + Text( + text = p.text, + fontSize = 16.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .weight(1F) + .padding(5.dp) + ) + } + } + PokemonProperty.PROPERTY_TEXT -> { + val startPadding = dimensionResource(id = R.dimen.pokemon_details_nesting_level_padding) + val p = property as TextProperty + Text( + text = p.text, + modifier = Modifier + .padding(PaddingValues(startPadding * (2 + p.nestingLevel), 5.dp, 5.dp, 5.dp)) + .fillMaxWidth() + ) + } + PokemonProperty.PROPERTY_TITLE -> { + val startPadding = dimensionResource(id = R.dimen.pokemon_details_nesting_level_padding) + val p = property as TitleProperty + + val params = if (p.nestingLevel == 0) { + Pair( + TextStyle( + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Default + ), + Modifier + .padding(0.dp) + .fillMaxWidth() + ) + } else { + Pair( + TextStyle( + textAlign = TextAlign.Start, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily.Serif + ), + Modifier + .padding( + PaddingValues( + startPadding * (2 + p.nestingLevel), + 5.dp, 5.dp, 5.dp + ) + ) + .fillMaxWidth() + ) + } + Text( + text = if (p.titleResId == 0) { + "" + } else { + stringResource(id = p.titleResId) + }, + fontSize = 18.sp, + style = params.first, + modifier = params.second + ) + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + name = "Pokemon Light Mode" +) +@Composable +fun PokemonPreviewThemeLight() { + val pokemon = PokemonFullDataSchema() + pokemon.pokemonSchema = PokemonSchema() + pokemon.pokemonSchema?.name = "SlowPock" + pokemon.stats = mutableListOf() + + val attack = Stat() + attack.stat = Stat_() + attack.stat?.name = "attack" + attack.baseStat = 100500 + pokemon.stats.add(attack) + + val defence = Stat() + defence.stat = Stat_() + defence.stat?.name = "defense" + defence.baseStat = 100501 + pokemon.stats.add(defence) + + val speed = Stat() + speed.stat = Stat_() + speed.stat?.name = "speed" + speed.baseStat = 100502 + pokemon.stats.add(speed) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonUiState(properties = listOf())) + } + PokemonScreen(state, null) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Pokemon Dark Mode" +) +@Composable +fun PokemonPreviewThemeDark() { + val properties: MutableList = ArrayList() + properties.add(ImagePropertyData("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/154.png")) + properties.add(LabelPropertyData(R.string.pokemon_detail_name, "SlowPock")) + properties.add(LabelPropertyData(R.string.pokemon_detail_height, "100")) + properties.add(LabelPropertyData(R.string.pokemon_detail_weight, "90")) + properties.add(TitlePropertyData(R.string.pokemon_detail_stats)) + properties.add(LabelPropertyData("Speed", "50", 1)) + properties.add(TitlePropertyData(R.string.pokemon_detail_abilities)) + properties.add(TextPropertyData("wololo, wololo", 1)) + properties.add(TitlePropertyData(R.string.pokemon_detail_forms)) + properties.add(TextPropertyData("wololo, wololo", 1)) + properties.add(TitlePropertyData(R.string.pokemon_detail_types)) + properties.add(TextPropertyData("bro, king", 1)) + properties.add(TitlePropertyData(R.string.pokemon_detail_game_indicies)) + properties.add(TextPropertyData("some indicies here", 1)) + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonUiState(properties = properties)) + } + PokemonScreen(state, null) } } 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 effc623..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 @@ -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 @@ -11,38 +10,47 @@ 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.infra.* -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +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 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 kotlin.collections.ArrayList +import java.util.LinkedList +import javax.inject.Inject -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 override fun setPokemonId(pokemonId: Long) { - viewModelScope.launch { - pokemonIdFlow.emit(pokemonId) - } + 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 { id: Long -> repo.getPokemonById(id) } - .map { details: PokemonFullDataSchema? -> if (details == null) LinkedList() else mapToListPokemon(details) } - .flowOn(Dispatchers.IO) - .collect { setState(PokemonDetailsScreenState.SetData(it)) } + .filter { it != -1L } + .map { repo.getPokemonById(it) } + .map { it?.let { mapToListPokemon(it) } ?: LinkedList() } + .flowOn(Dispatchers.IO) + .collect { setState(PokemonDetailsScreenState.SetData(it)) } } } } @@ -101,6 +109,10 @@ class PokemonDetailsViewModel(private val repo: PokemonsRepo) return properties } + override fun onBackPressed() { + setEffect(PokemonDetailsScreenEffect.EffectBack()) + } + override fun onCleared() { super.onCleared() pokemonLoadingJob = 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/pokemon/PokemonUiState.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonUiState.kt new file mode 100644 index 0000000..edac5c0 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/PokemonUiState.kt @@ -0,0 +1,15 @@ +package com.mdgd.pokemon.ui.pokemon + +import com.mdgd.pokemon.ui.error.ErrorParams +import com.mdgd.pokemon.ui.pokemon.dto.PokemonProperty + +data class PokemonUiState( + val properties: List = listOf(), + + override val title: String = "", + override val message: String = "", + override val isVisible: Boolean = false +) : ErrorParams { + + override fun hide() = copy(isVisible = false) +} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/PokemonPropertiesAdapter.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/PokemonPropertiesAdapter.kt deleted file mode 100644 index 91ee132..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/PokemonPropertiesAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.adapter.RecyclerAdapter -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.adapter.holders.* -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty -import java.util.* - -class PokemonPropertiesAdapter : RecyclerAdapter() { - private val resolver: MutableMap - - init { - resolver = HashMap() - resolver[PokemonProperty.PROPERTY_IMAGE] = object : ViewHolderFactory { - override fun create(parent: ViewGroup?): RecyclerVH { - return PokemonImageViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_pokemon_image, parent, false)) - } - } - resolver[PokemonProperty.PROPERTY_LABEL] = object : ViewHolderFactory { - override fun create(parent: ViewGroup?): RecyclerVH { - return PokemonLabelViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_pokemon_label, parent, false)) - } - } - resolver[PokemonProperty.PROPERTY_TITLE] = object : ViewHolderFactory { - override fun create(parent: ViewGroup?): RecyclerVH { - return PokemonTitleViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_pokemon_title, parent, false)) - } - } - resolver[PokemonProperty.PROPERTY_TEXT] = object : ViewHolderFactory { - override fun create(parent: ViewGroup?): RecyclerVH { - return PokemonTextViewHolder(LayoutInflater.from(parent?.context).inflate(R.layout.item_pokemon_label_image, parent, false)) - } - } - } - - override fun getItemViewType(position: Int): Int { - return items[position].type - } - - @Suppress("UNCHECKED_CAST") - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerVH { - if (viewType == EMPTY_VIEW) { - return EmptyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_empty, parent, false)) - } - return resolver[viewType]!!.create(parent) as RecyclerVH - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/ViewHolderFactory.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/ViewHolderFactory.kt deleted file mode 100644 index 03be239..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/ViewHolderFactory.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter - -import android.view.ViewGroup -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty - -interface ViewHolderFactory { - fun create(parent: ViewGroup?): RecyclerVH -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/EmptyViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/EmptyViewHolder.kt deleted file mode 100644 index ad78d7e..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/EmptyViewHolder.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter.holders - -import android.view.View -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty - -class EmptyViewHolder(itemView: View) : RecyclerVH(itemView) \ No newline at end of file diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonImageViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonImageViewHolder.kt deleted file mode 100644 index 4130862..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonImageViewHolder.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter.holders - -import android.text.TextUtils -import android.view.View -import android.widget.ImageView -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.ImageProperty -import com.squareup.picasso.Picasso - -class PokemonImageViewHolder(view: View) : RecyclerVH(view) { - private val image: ImageView = view.findViewById(R.id.pokemon_details_image) - - override fun bindItem(item: ImageProperty, position: Int) { - if (!TextUtils.isEmpty(item.imageUrl)) { - Picasso.get().load(item.imageUrl).into(image) - } - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonLabelViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonLabelViewHolder.kt deleted file mode 100644 index 01e62de..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonLabelViewHolder.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter.holders - -import android.view.View -import android.widget.TextView -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.LabelProperty - -class PokemonLabelViewHolder(view: View) : RecyclerVH(view) { - private val label: TextView = view.findViewById(R.id.pokemon_details_label_text) - private val value: TextView = view.findViewById(R.id.pokemon_details_label_value) - - override fun bindItem(item: LabelProperty, position: Int) { - label.setPaddingRelative( - label.resources.getDimensionPixelSize(R.dimen.pokemon_details_nesting_level_padding) * (1 + item.nestingLevel), - 0, 0, 0) - - if (item.titleResId == 0) { - label.text = item.titleStr - } else { - label.setText(item.titleResId) - } - value.text = item.text - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTextViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTextViewHolder.kt deleted file mode 100644 index 18be12e..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTextViewHolder.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter.holders - -import android.view.View -import android.widget.TextView -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.TextProperty - -class PokemonTextViewHolder(view: View) : RecyclerVH(view) { - private val label: TextView = view.findViewById(R.id.pokemon_details_text) - - override fun bindItem(item: TextProperty, position: Int) { - label.text = item.text - label.setPaddingRelative( - label.resources.getDimensionPixelSize(R.dimen.pokemon_details_nesting_level_padding) * (2 + item.nestingLevel), - label.paddingTop, label.paddingEnd, label.paddingBottom) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTitleViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTitleViewHolder.kt deleted file mode 100644 index 4cee724..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/adapter/holders/PokemonTitleViewHolder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.adapter.holders - -import android.graphics.Typeface -import android.view.Gravity -import android.view.View -import android.widget.TextView -import com.mdgd.pokemon.R -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.mdgd.pokemon.ui.pokemon.infra.TitleProperty - -class PokemonTitleViewHolder(view: View) : RecyclerVH(view) { - private val title: TextView = view.findViewById(R.id.pokemon_property_title) - - override fun bindItem(item: TitleProperty, position: Int) { - if (item.titleResId != 0) { - title.setText(item.titleResId) - } - if (item.nestingLevel == 0) { - title.typeface = Typeface.DEFAULT_BOLD - title.gravity = Gravity.CENTER - title.setPaddingRelative(0, 0, 0, 0) - } else { - title.typeface = Typeface.SERIF - title.gravity = Gravity.NO_GRAVITY - title.setPaddingRelative( - title.resources.getDimensionPixelSize(R.dimen.pokemon_details_nesting_level_padding) * (2 + item.nestingLevel), - 0, 0, 0) - } - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImageProperty.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImageProperty.kt similarity index 63% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImageProperty.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImageProperty.kt index 32e661d..d1234d3 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImageProperty.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImageProperty.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto interface ImageProperty : PokemonProperty { val imageUrl: String diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImagePropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt similarity index 50% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImagePropertyData.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt index 73aa3d0..9c0008d 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/ImagePropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/ImagePropertyData.kt @@ -1,6 +1,6 @@ -package com.mdgd.pokemon.ui.pokemon.infra +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 } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelProperty.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelProperty.kt similarity index 68% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelProperty.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelProperty.kt index 9a05949..6e64e09 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelProperty.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelProperty.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto interface LabelProperty : TitleProperty { val text: String diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelPropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt similarity index 81% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelPropertyData.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt index 66e0b26..75296ea 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/LabelPropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/LabelPropertyData.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto class LabelPropertyData : TitlePropertyData, LabelProperty { override val text: String @@ -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/infra/PokemonProperty.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/PokemonProperty.kt similarity index 86% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/PokemonProperty.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/PokemonProperty.kt index 7a3442d..0e0eb8a 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/PokemonProperty.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/PokemonProperty.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto interface PokemonProperty { val type: Int diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextProperty.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextProperty.kt similarity index 61% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextProperty.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextProperty.kt index cd0c289..7a18248 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextProperty.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextProperty.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto interface TextProperty : PokemonProperty { val text: String diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextPropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt similarity index 56% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextPropertyData.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt index f5c8c2a..19c6b54 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TextPropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TextPropertyData.kt @@ -1,6 +1,6 @@ -package com.mdgd.pokemon.ui.pokemon.infra +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/infra/TitleProperty.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitleProperty.kt similarity index 62% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TitleProperty.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitleProperty.kt index 49bb7b8..2e51cde 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TitleProperty.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitleProperty.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto interface TitleProperty : PokemonProperty { val titleResId: Int diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TitlePropertyData.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitlePropertyData.kt similarity index 83% rename from app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TitlePropertyData.kt rename to app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitlePropertyData.kt index cde3183..8beb262 100644 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/infra/TitlePropertyData.kt +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/dto/TitlePropertyData.kt @@ -1,4 +1,4 @@ -package com.mdgd.pokemon.ui.pokemon.infra +package com.mdgd.pokemon.ui.pokemon.dto open class TitlePropertyData @JvmOverloads constructor(override val titleResId: Int, override val nestingLevel: Int = 0) : TitleProperty { override val type: Int 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 deleted file mode 100644 index 3d9aefb..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenAction.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mdgd.pokemon.ui.pokemon.state - -import com.mdgd.mvi.states.AbstractAction -import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract - -sealed class PokemonDetailsScreenAction : AbstractAction() diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt new file mode 100644 index 0000000..65300b4 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemon/state/PokemonDetailsScreenEffect.kt @@ -0,0 +1,13 @@ +package com.mdgd.pokemon.ui.pokemon.state + +import com.mdgd.mvi.states.AbstractEffect +import com.mdgd.pokemon.ui.pokemon.PokemonDetailsContract + +sealed class PokemonDetailsScreenEffect : AbstractEffect() { + + class EffectBack : PokemonDetailsScreenEffect() { + override fun handle(screen: PokemonDetailsContract.View) { + screen.goBack() + } + } +} 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 812a57a..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.infra.PokemonProperty +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/PokemonsContract.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsContract.kt index eb2e3e3..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,20 +2,18 @@ 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.PokemonsScreenState class PokemonsContract { - interface ViewModel : FragmentContract.ViewModel { + interface ViewModel : FragmentContract.ViewModel { fun reload() - fun loadPage(page: Int) fun sort(filter: String) fun onItemClicked(pokemon: PokemonFullDataSchema) + fun onScroll(firstVisibleIndex: Int, lastVisibleIndex: Int) + fun firstVisible(): Int } interface View : FragmentContract.View { - fun showProgress() - fun hideProgress() + fun setProgressVisibility(isProgressVisible: Boolean) fun setItems(list: List) fun showError(error: Throwable?) fun proceedToNextScreen(pokemonId: Long?) @@ -25,6 +23,5 @@ class PokemonsContract { interface Host : FragmentContract.Host { fun proceedToPokemonScreen(pokemonId: Long?) - fun showError(error: Throwable?) } } 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 01a9a6b..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 @@ -1,167 +1,450 @@ package com.mdgd.pokemon.ui.pokemons +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.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 +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 +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +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.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.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 -import com.mdgd.pokemon.ui.adapter.ClickEvent -import com.mdgd.pokemon.ui.pokemons.adapter.PokemonsAdapter -import com.mdgd.pokemon.ui.pokemons.infra.EndlessScrollListener -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenState -import kotlinx.coroutines.flow.collect +import com.mdgd.pokemon.models.repo.dao.schemas.PokemonSchema +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 dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +@AndroidEntryPoint class PokemonsFragment : HostedFragment< PokemonsContract.View, - PokemonsScreenState, - PokemonsScreenAction, PokemonsContract.ViewModel, PokemonsContract.Host>(), - PokemonsContract.View, View.OnClickListener, SwipeRefreshLayout.OnRefreshListener { + PokemonsContract.View { - private val adapter = PokemonsAdapter(lifecycleScope) - private var refreshSwipe: SwipeRefreshLayout? = null + private val screenState = mutableStateOf(PokemonsUiState(isLoading = true)) - // maybe paging library? - private val scrollListener: EndlessScrollListener = object : EndlessScrollListener() { - override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { - model!!.loadPage(page) - } + override fun createModel(): PokemonsContract.ViewModel { + val model: PokemonsViewModel by viewModels() + return model } - private var recyclerView: RecyclerView? = null - private var filterAttack: ImageButton? = null - private var filterDefence: ImageButton? = null - private var filterSpeed: ImageButton? = null - private var refresh: View? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycleScope.launch { - adapter.getItemClickFlow().collect { - if (it is ClickEvent.ClickData) { - model!!.onItemClicked(it.data) - } - } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val view = ComposeView(requireContext()) + view.setContent { + PokemonsScreen(screenState, viewModel) } + return view } - override fun createModel(): PokemonsContract.ViewModel { - return ViewModelProvider(this, PokemonsViewModelFactory(PokemonsApp.instance?.appComponent!!)).get(PokemonsViewModel::class.java) + override fun proceedToNextScreen(pokemonId: Long?) { + fragmentHost?.proceedToPokemonScreen(pokemonId) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_pokemons, container, false) + override fun updateFilterButtons(activateFilter: Boolean, filter: String) { + val value = when (filter) { + FilterData.FILTER_ATTACK -> screenState.value.copy(isAttackActive = activateFilter) + FilterData.FILTER_DEFENCE -> screenState.value.copy(isDefenceActive = activateFilter) + FilterData.FILTER_SPEED -> screenState.value.copy(isSpeedActive = activateFilter) + else -> return + } + screenState.value = value } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - recyclerView = view.findViewById(R.id.pokemons_recycler) - recyclerView?.layoutManager = LinearLayoutManager(activity) - recyclerView?.addOnScrollListener(scrollListener) - recyclerView?.adapter = adapter - - refresh = view.findViewById(R.id.pokemons_refresh) - refreshSwipe = view.findViewById(R.id.pokemons_swipe_refresh) - filterAttack = view.findViewById(R.id.pokemons_filter_attack) - filterDefence = view.findViewById(R.id.pokemons_filter_defence) - filterSpeed = view.findViewById(R.id.pokemons_filter_movement) - - refresh?.setOnClickListener(this) - refreshSwipe?.setOnRefreshListener(this) - filterAttack?.setOnClickListener(this) - filterDefence?.setOnClickListener(this) - filterSpeed?.setOnClickListener(this) + override fun setProgressVisibility(isProgressVisible: Boolean) { + screenState.value = screenState.value.copy(isLoading = isProgressVisible) } - override fun proceedToNextScreen(pokemonId: Long?) { - if (hasHost()) { - fragmentHost!!.proceedToPokemonScreen(pokemonId) - } + override fun setItems(list: List) { + screenState.value = screenState.value.copy( + isLoading = false, pokemons = list, isVisible = false + ) } - override fun updateFilterButtons(activateFilter: Boolean, filter: String) { - val view = when (filter) { - FilterData.FILTER_ATTACK -> { - filterAttack - } - FilterData.FILTER_DEFENCE -> { - filterDefence - } - FilterData.FILTER_SPEED -> { - filterSpeed - } - else -> null - } + override fun scrollToStart() { + screenState.value = screenState.value.copy() + } - if (activateFilter) { - view?.setColorFilter(ContextCompat.getColor(requireContext(), R.color.filter_active)) - } else { - view?.setColorFilter(ContextCompat.getColor(requireContext(), R.color.filter_inactive)) - } + override fun showError(error: Throwable?) { + screenState.value = screenState.value.copy( + isLoading = false, isVisible = true, title = getString(R.string.dialog_error_title), + message = error?.let { + getString(R.string.dialog_error_message) + " " + error.message + } ?: kotlin.run { + getString(R.string.dialog_error_message) + }) } +} - override fun onClick(view: View) { - if (view === refresh) { - if (!refreshSwipe!!.isRefreshing) { - refreshSwipe!!.isRefreshing = true - } - } else { - when { - filterAttack === view -> { - model!!.sort(FilterData.FILTER_ATTACK) - } - filterDefence === view -> { - model!!.sort(FilterData.FILTER_DEFENCE) + +@Composable +fun PokemonsScreen(screenState: MutableState, model: PokemonsContract.ViewModel?) { + val errorDialogTrigger = remember { screenState as MutableState } + val scope = rememberCoroutineScope() + val scrollState = rememberLazyListState() + MaterialTheme { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.app_name)) }, + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { model?.reload() }, + modifier = Modifier.padding(0.dp, 50.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.screen_pokemons_refresh) + ) } - filterSpeed === view -> { - model!!.sort(FilterData.FILTER_SPEED) + } + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + ) { + SwipeRefresh( + modifier = Modifier + .fillMaxHeight(0.94F) + .fillMaxWidth(), + state = rememberSwipeRefreshState(screenState.value.isLoading), + onRefresh = { model?.reload() }, + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + state = scrollState, + ) { + if (model?.firstVisible() == 0 && scrollState.firstVisibleItemIndex != 0) { + scope.launch { + scrollState.scrollToItem(0) + } + } + if (scrollState.isScrollInProgress) { + model?.onScroll( + scrollState.firstVisibleItemIndex, + scrollState.layoutInfo.visibleItemsInfo.last().index + ) + } + + if (screenState.value.pokemons.isNullOrEmpty()) { + items(items = listOf(System.currentTimeMillis()), key = { it }) { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + textAlign = TextAlign.Center, + text = stringResource(id = R.string.no_pokemons), + ) + } + } + } else { + items( + items = screenState.value.pokemons, + key = { it.pokemonSchema?.id ?: 0L } + ) { PokemonItem(it, model) } + } + } } + BottomBar(screenState, model) + ErrorScreen(errorDialogTrigger) } } } +} - override fun onRefresh() { - scrollListener.resetState() - model!!.reload() +@Composable +fun PokemonItem(item: PokemonFullDataSchema, model: PokemonsContract.ViewModel?) { + var attackVal = "--" + var defenceVal = "--" + var speedVal = "--" + for (s in item.stats) { + s.stat?.name?.let { + when (it) { + FilterData.FILTER_DEFENCE -> defenceVal = s.baseStat.toString() + FilterData.FILTER_ATTACK -> attackVal = s.baseStat.toString() + FilterData.FILTER_SPEED -> speedVal = s.baseStat.toString() + } + } } - - override fun showProgress() { - if (refreshSwipe != null && !refreshSwipe!!.isRefreshing) { - refreshSwipe!!.isRefreshing = true + defenceVal = LocalContext.current.getString(R.string.item_pokemon_defence, defenceVal) + attackVal = LocalContext.current.getString(R.string.item_pokemon_attack, attackVal) + speedVal = LocalContext.current.getString(R.string.item_pokemon_speed, speedVal) + Card( + shape = RoundedCornerShape(3.dp), + elevation = 5.dp, + modifier = Modifier + .clickable { model?.onItemClicked(item) } + .background(color = Color.Cyan) + .fillMaxWidth() + .wrapContentHeight(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Image( + painter = item.pokemonSchema?.sprites?.other?.officialArtwork?.frontDefault?.let { + rememberImagePainter( + data = it, + builder = { + ImageRequest.Builder(LocalContext.current) + .placeholder(R.drawable.ic_pokemon) + .error(R.drawable.ic_pokemon) + .build() + }) + } ?: kotlin.run { + painterResource(R.drawable.ic_pokemon) + }, + contentDescription = stringResource(id = R.string.screen_pokemons_icon), + modifier = Modifier + .fillMaxWidth(0.3F) + .aspectRatio(1F), + contentScale = ContentScale.Inside + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + style = TextStyle(fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), + modifier = Modifier + .padding(5.dp) + .fillMaxHeight(0.4F), + text = item.pokemonSchema?.name ?: "", + ) + Row(modifier = Modifier.fillMaxWidth()) { + Text( + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier.weight(1F), + text = attackVal, + maxLines = 2 + ) + Text( + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier.weight(1F), + text = defenceVal, + maxLines = 2 + ) + Text( + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier.weight(1F), + text = speedVal, + maxLines = 2 + ) + } + } } } +} - override fun hideProgress() { - if (refreshSwipe != null && refreshSwipe!!.isRefreshing) { - refreshSwipe!!.isRefreshing = false +@Composable +fun BottomBar(screenState: MutableState, model: PokemonsContract.ViewModel?) { + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { model?.sort(FilterData.FILTER_ATTACK) }, + modifier = Modifier.weight(1F), + ) { + Icon( + painter = painterResource(R.drawable.ic_attack), + contentDescription = stringResource(R.string.screen_filter_attack), + tint = colorResource( + id = if (screenState.value.isAttackActive) { + R.color.filter_active + } else { + R.color.filter_inactive + } + ) + ) + } + IconButton( + onClick = { model?.sort(FilterData.FILTER_DEFENCE) }, + modifier = Modifier.weight(1F), + ) { + Icon( + painter = painterResource(R.drawable.ic_defense), + contentDescription = stringResource(R.string.screen_filter_defence), + modifier = Modifier.weight(1F), + tint = colorResource( + id = if (screenState.value.isDefenceActive) { + R.color.filter_active + } else { + R.color.filter_inactive + } + ), + ) + } + IconButton( + onClick = { model?.sort(FilterData.FILTER_SPEED) }, + modifier = Modifier.weight(1F), + ) { + Icon( + painter = painterResource(R.drawable.ic_speed), + contentDescription = stringResource(R.string.screen_filter_speed), + modifier = Modifier.weight(1F), + tint = colorResource( + id = if (screenState.value.isSpeedActive) { + R.color.filter_active + } else { + R.color.filter_inactive + } + ), + ) } } +} - override fun setItems(list: List) { - adapter.setItems(list) + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Pokemons Item Dark Mode" +) +@Composable +fun PokemonItemPreviewThemeDark() { + val pokemon = PokemonFullDataSchema() + pokemon.pokemonSchema = PokemonSchema() + pokemon.pokemonSchema?.name = "SlowPock" + pokemon.stats = mutableListOf() + + val attack = Stat() + attack.stat = Stat_() + attack.stat?.name = "attack" + attack.baseStat = 100500 + pokemon.stats.add(attack) + + val defence = Stat() + defence.stat = Stat_() + defence.stat?.name = "defense" + defence.baseStat = 100501 + pokemon.stats.add(defence) + + val speed = Stat() + speed.stat = Stat_() + speed.stat?.name = "speed" + speed.baseStat = 100502 + pokemon.stats.add(speed) + + MaterialTheme { + PokemonItem(pokemon, null) } +} - override fun scrollToStart() { - recyclerView!!.scrollToPosition(0) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + name = "Pokemons Light Mode" +) +@Composable +fun PokemonsPreviewThemeLight() { + val pokemon = PokemonFullDataSchema() + pokemon.pokemonSchema = PokemonSchema() + pokemon.pokemonSchema?.name = "SlowPock" + pokemon.stats = mutableListOf() + + val attack = Stat() + attack.stat = Stat_() + attack.stat?.name = "attack" + attack.baseStat = 100500 + pokemon.stats.add(attack) + + val defence = Stat() + defence.stat = Stat_() + defence.stat?.name = "defense" + defence.baseStat = 100501 + pokemon.stats.add(defence) + + val speed = Stat() + speed.stat = Stat_() + speed.stat?.name = "speed" + speed.baseStat = 100502 + pokemon.stats.add(speed) + + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonsUiState(pokemons = listOf(pokemon))) + } + PokemonsScreen(state, null) } +} - override fun showError(error: Throwable?) { - if (hasHost()) { - fragmentHost!!.showError(error) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Pokemons Dark Mode" +) +@Composable +fun PokemonsPreviewThemeDark() { + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(PokemonsUiState()) } + PokemonsScreen(state, null) } } diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsUiState.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsUiState.kt new file mode 100644 index 0000000..d142ef0 --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/PokemonsUiState.kt @@ -0,0 +1,21 @@ +package com.mdgd.pokemon.ui.pokemons + +import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema +import com.mdgd.pokemon.ui.error.ErrorParams + +data class PokemonsUiState( + val isLoading: Boolean = false, + + val isAttackActive: Boolean = false, + val isDefenceActive: Boolean = false, + val isSpeedActive: Boolean = false, + + val pokemons: List = listOf(), + + override val isVisible: Boolean = false, + override val title: String = "", + override val message: String = "" +) : ErrorParams { + + override fun hide() = copy(isVisible = false) +} \ No newline at end of file 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 3728f1a..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 @@ -1,66 +1,88 @@ 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.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.ui.pokemons.state.PokemonsScreenAction +import com.mdgd.pokemon.models.util.DispatchersHolder +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 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 java.util.* +import javax.inject.Inject -class PokemonsViewModel(private val repo: PokemonsRepo, private val filtersFactory: StatsFilter, private val dispatchers: DispatchersHolder) - : MviViewModel(), PokemonsContract.ViewModel { +@HiltViewModel +class PokemonsViewModel @Inject constructor( + private val repo: PokemonsRepo, + private val filtersFactory: StatsFilter, + private val dispatchers: DispatchersHolder +) : MviViewModel(), + PokemonsContract.ViewModel { + private var firstVisibleIndex: Int = 0 + private val exceptionHandler = CoroutineExceptionHandler { _, e -> + setEffect(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 { + launch = viewModelScope.launch(exceptionHandler) { pageFlow - .onEach { setState(PokemonsScreenState.Loading()) } - .flowOn(dispatchers.getMain()) - .map { page -> Pair(page, repo.getPage(page)) } - .flowOn(dispatchers.getIO()) - .catch { e: Throwable -> setAction(PokemonsScreenAction.Error(e)) } - .collect { pagePair: Pair> -> - if (pagePair.first == 0) { - setState(PokemonsScreenState.SetData(pagePair.second, filtersFactory.getAvailableFilters())) - } else { - setState(PokemonsScreenState.AddData(pagePair.second)) - } + .onEach { setState(PokemonsScreenState.Loading()) } + .flowOn(dispatchers.getMain()) + .map { page -> Pair(page, repo.getPage(page)) } + .flowOn(dispatchers.getIO()) + .catch { e: Throwable -> setEffect(PokemonsScreenEffect.Error(e)) } + .collect { pagePair: Pair> -> + if (pagePair.first == 0) { + setState( + PokemonsScreenState.SetData( + pagePair.second, filtersFactory.getAvailableFilters() + ) + ) + } else { + setState(PokemonsScreenState.AddData(pagePair.second)) } + } } - viewModelScope.launch { + viewModelScope.launch(exceptionHandler) { filterFlow - .map { sort(it, repo.getPokemons()) } - .collect { sortedList -> - setState(PokemonsScreenState.UpdateData(sortedList)) - } + .map { sort(it, repo.getPokemons()) } + .collect { sortedList -> + firstVisibleIndex = 0 + setState(PokemonsScreenState.UpdateData(sortedList)) + setEffect(PokemonsScreenEffect.ScrollToStart()) + } } } } - private fun sort(filters: FilterData, pokemons: List): List { - // potentially, we can create a custom list of filters in separate model. In UI we can show them in recyclerView + private fun sort( + filters: FilterData, pokemons: List + ): List { + val list = ArrayList(pokemons) if (!filters.isEmpty) { val comparators = filtersFactory.getFilters() - Collections.sort(pokemons) { pokemon1: PokemonFullDataSchema?, pokemon2: PokemonFullDataSchema? -> + 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 } @@ -68,32 +90,38 @@ class PokemonsViewModel(private val repo: PokemonsRepo, private val filtersFacto compare } } - return pokemons + return list } override fun reload() { - viewModelScope.launch { - pageFlow.emit(0) - } - } - - override fun loadPage(page: Int) { - viewModelScope.launch { - pageFlow.emit(page) - } + pageFlow.tryEmit(0) } override fun sort(filter: String) { - setState(PokemonsScreenState.ChangeFilterState(filter)) - viewModelScope.launch { - filterFlow.emit(FilterData(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(filters)) } override fun onItemClicked(pokemon: PokemonFullDataSchema) { - setAction(PokemonsScreenAction.ShowDetails(pokemon.pokemonSchema?.id)) + setEffect(PokemonsScreenEffect.ShowDetails(pokemon.pokemonSchema?.id)) + } + + override fun onScroll(firstVisibleIndex: Int, lastVisibleIndex: Int) { + this.firstVisibleIndex = firstVisibleIndex + val page = pageFlow.value + 1 + if (lastVisibleIndex >= page * PokemonsRepo.PAGE_SIZE - 8) { + pageFlow.tryEmit(page) + } } + override fun firstVisible() = firstVisibleIndex + public override fun onCleared() { launch = null super.onCleared() 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/pokemons/adapter/EmptyViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/EmptyViewHolder.kt deleted file mode 100644 index 18eae11..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/EmptyViewHolder.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons.adapter - -import android.view.View -import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import com.mdgd.pokemon.ui.adapter.RecyclerVH - -class EmptyViewHolder(itemView: View) : RecyclerVH(itemView) \ No newline at end of file diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonViewHolder.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonViewHolder.kt deleted file mode 100644 index f638b79..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonViewHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons.adapter - -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.lifecycle.LifecycleCoroutineScope -import com.mdgd.pokemon.R -import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import com.mdgd.pokemon.ui.adapter.ClickEvent -import com.mdgd.pokemon.ui.adapter.RecyclerVH -import com.squareup.picasso.Picasso -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - - -class PokemonViewHolder(itemView: View, private val clicksSubject: MutableStateFlow>, - private val lifecycleScope: LifecycleCoroutineScope) - : RecyclerVH(itemView), View.OnClickListener { - - private val image: ImageView = itemView.findViewById(R.id.item_pokemon_image) - private val name: TextView = itemView.findViewById(R.id.item_pokemon_name) - private val attack: TextView = itemView.findViewById(R.id.item_pokemon_attack) - private val defence: TextView = itemView.findViewById(R.id.item_pokemon_defence) - private val speed: TextView = itemView.findViewById(R.id.item_pokemon_speed) - private var item: PokemonFullDataSchema? = null - - override fun bindItem(item: PokemonFullDataSchema, position: Int) { - this.item = item - val url = item.pokemonSchema!!.sprites!!.other!!.officialArtwork!!.frontDefault - Picasso.get().load(url).into(image) - name.text = item.pokemonSchema!!.name - val resources = itemView.context.resources - var attackVal = "--" - for (s in item.stats) { - if ("attack" == s.stat!!.name) { - attackVal = s.baseStat.toString() - } - } - attack.text = resources.getString(R.string.item_pokemon_attack, attackVal) - var defenceVal = "--" - for (s in item.stats) { - if ("defense" == s.stat!!.name) { - defenceVal = s.baseStat.toString() - } - } - defence.text = resources.getString(R.string.item_pokemon_defence, defenceVal) - var speedVal = "--" - for (s in item.stats) { - if ("speed" == s.stat!!.name) { - speedVal = s.baseStat.toString() - } - } - speed.text = resources.getString(R.string.item_pokemon_speed, speedVal) - } - - override fun onClick(view: View) { - if (item != null) { - lifecycleScope.launch { - clicksSubject.emit(ClickEvent.ClickData(item!!)) - } - } - } - - override fun setupSubscriptions() { - itemView.setOnClickListener(this) - } - - override fun clearSubscriptions() { - itemView.setOnClickListener(null) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonsAdapter.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonsAdapter.kt deleted file mode 100644 index edf5fe3..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/adapter/PokemonsAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.lifecycle.LifecycleCoroutineScope -import com.mdgd.pokemon.R -import com.mdgd.pokemon.models.repo.dao.schemas.PokemonFullDataSchema -import com.mdgd.pokemon.ui.adapter.RecyclerAdapter -import com.mdgd.pokemon.ui.adapter.RecyclerVH - -class PokemonsAdapter(private val lifecycleScope: LifecycleCoroutineScope) : RecyclerAdapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerVH { - if (viewType == EMPTY_VIEW) { - return EmptyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_empty, parent, false)) - } - return PokemonViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_pokemon, parent, false), clicksSubject, lifecycleScope) - } -} diff --git a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/infra/EndlessScrollListener.kt b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/infra/EndlessScrollListener.kt deleted file mode 100644 index 0c106ac..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/infra/EndlessScrollListener.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons.infra - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -abstract class EndlessScrollListener : RecyclerView.OnScrollListener { - // The minimum amount of items to have below your current scroll position - // before loading more. - private val visibleThreshold = 5 - - // The current offset index of data you have loaded - private var currentPage = 0 - - // The total number of items in the dataset after the last load - private var previousTotalItemCount = 0 - - // True if we are still waiting for the last set of data to load. - private var loading = true - - // Sets the starting page index - private val startingPageIndex = 0 - private var reverseDirection = false - - internal constructor() {} - constructor(reverseDirection: Boolean) { - this.reverseDirection = reverseDirection - } - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - - //only trigger action if scrolling down - if (if (!reverseDirection) dy > 0 else dy < 0) { - val lm = recyclerView.layoutManager - val totalItemCount = lm!!.itemCount - val lastVisibleItemPosition = (lm as LinearLayoutManager?)!!.findLastVisibleItemPosition() - - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - currentPage = startingPageIndex - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loading = true - } - } - - // If it’s still loading, we check to see if the dataset count has - // changed, if so we conclude it has finished loading and update the current page - // number and total item count. - if (loading && totalItemCount > previousTotalItemCount) { - loading = false - previousTotalItemCount = totalItemCount - } - - // If it isn’t currently loading, we check to see if we have breached - // the visibleThreshold and need to reload more data. - // If we do need to reload some more data, we execute onLoadMore to fetch the data. - // threshold should reflect how many total columns there are too - if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) { - currentPage++ - onLoadMore(currentPage, totalItemCount, recyclerView) - loading = true - } - } - } - - // Call this method whenever performing new searches - fun resetState() { - currentPage = startingPageIndex - previousTotalItemCount = 0 - loading = true - } - - // Defines the process for actually loading more data based on page - abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) -} 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/PokemonsScreenAction.kt deleted file mode 100644 index d842543..0000000 --- a/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenAction.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.mdgd.pokemon.ui.pokemons.state - -import com.mdgd.mvi.states.AbstractAction -import com.mdgd.pokemon.ui.pokemons.PokemonsContract - -sealed class PokemonsScreenAction() : AbstractAction() { - - class Error(val error: Throwable?) : PokemonsScreenAction() { - - override fun handle(screen: PokemonsContract.View) { - screen.hideProgress() - screen.showError(error) - } - } - - class ShowDetails(val id: Long?) : PokemonsScreenAction() { - - override fun handle(screen: PokemonsContract.View) { - screen.hideProgress() - screen.proceedToNextScreen(id) - } - } -} 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 new file mode 100644 index 0000000..ed2865e --- /dev/null +++ b/app/src/main/java/com/mdgd/pokemon/ui/pokemons/state/PokemonsScreenEffect.kt @@ -0,0 +1,30 @@ +package com.mdgd.pokemon.ui.pokemons.state + +import com.mdgd.mvi.states.AbstractEffect +import com.mdgd.pokemon.ui.pokemons.PokemonsContract + +sealed class PokemonsScreenEffect : AbstractEffect() { + + class Error(val error: Throwable?) : PokemonsScreenEffect() { + + override fun handle(screen: PokemonsContract.View) { + screen.setProgressVisibility(false) + screen.showError(error) + } + } + + class ShowDetails(val id: Long?) : PokemonsScreenEffect() { + + override fun handle(screen: PokemonsContract.View) { + screen.setProgressVisibility(false) + 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 5a1505d..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 @@ -1,31 +1,18 @@ 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 { - return ArrayList(list) - } - - @JvmName("getActiveFilters1") - fun getActiveFilters(): List { - return ArrayList(activeFilters) - } +open class PokemonsScreenState( + val isProgressVisible: Boolean = false, + val list: List = listOf(), + protected val availableFilters: List = listOf(), + val activeFilters: List = listOf() +) : AbstractState() { override fun visit(screen: PokemonsContract.View) { - if (isProgressVisible) { - screen.showProgress() - } else { - screen.hideProgress() - } + screen.setProgressVisibility(isProgressVisible) screen.setItems(list) for (filter in availableFilters) { screen.updateFilterButtons(activeFilters.contains(filter), filter) @@ -33,58 +20,60 @@ sealed class PokemonsScreenState( } - class Loading() : PokemonsScreenState(true) { + // 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(private val filter: String) : PokemonsScreenState() { + class ChangeFilterState(filters: List) : PokemonsScreenState(activeFilters = filters) { - override fun merge(prevState: PokemonsScreenState) { - list.addAll(prevState.list) - availableFilters.addAll(prevState.availableFilters) - activeFilters.addAll(prevState.activeFilters) - - if (activeFilters.contains(filter)) { - activeFilters.remove(filter) - } else { - activeFilters.add(filter) - } + override fun merge(prevState: PokemonsScreenState): PokemonsScreenState { + return PokemonsScreenState( + isProgressVisible, prevState.list, prevState.availableFilters, activeFilters + ) } } + + // EOF: PARTIAL STATES } 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 e3cd178..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.SplashScreenAction -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() @@ -20,6 +18,5 @@ class SplashContract { interface Host : FragmentContract.Host { fun proceedToPokemonsScreen() - fun showError(error: Throwable?) } } 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..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 @@ -1,46 +1,138 @@ 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.lifecycle.ViewModelProvider +import androidx.compose.foundation.Image +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.fragment.app.viewModels 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 import com.mdgd.pokemon.bg.UploadWorker -import com.mdgd.pokemon.ui.splash.state.SplashScreenAction -import com.mdgd.pokemon.ui.splash.state.SplashScreenState +import com.mdgd.pokemon.ui.error.DefaultErrorParams +import com.mdgd.pokemon.ui.error.ErrorParams +import com.mdgd.pokemon.ui.error.ErrorScreen +import dagger.hilt.android.AndroidEntryPoint -class SplashFragment : HostedFragment(), SplashContract.View { +@AndroidEntryPoint +class SplashFragment : + HostedFragment(), + SplashContract.View { + + 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(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_splash, container, false) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val view = ComposeView(requireContext()) + view.setContent { + SplashScreen(errorDialogTrigger) + } + return view } override fun proceedToNextScreen() { - if (hasHost()) { - fragmentHost!!.proceedToPokemonsScreen() - } + fragmentHost?.proceedToPokemonsScreen() } override fun launchWorker() { - if (hasHost()) { - val uploadWorkRequest: WorkRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java).build() - WorkManager.getInstance(requireContext()).enqueue(uploadWorkRequest) + context?.let { + WorkManager.getInstance(it) + .enqueue(OneTimeWorkRequest.Builder(UploadWorker::class.java).build()) } } override fun showError(error: Throwable?) { - if (hasHost()) { - fragmentHost!!.showError(error) + errorDialogTrigger.value = DefaultErrorParams( + true, getString(R.string.dialog_error_title), + error?.let { + getString(R.string.dialog_error_message) + " " + error.message + } ?: kotlin.run { + getString(R.string.dialog_error_message) + }) + } +} + +@Composable +fun SplashScreen(errorParams: MutableState) { + val errorDialogTrigger = remember { errorParams } + MaterialTheme { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + // .scrollable( + // enabled = true, orientation = Orientation.Vertical, + // state = ScrollableState { 0F }) + ) { + 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.weight(1F) + ) + ErrorScreen(errorDialogTrigger) + } + } +} + + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, + name = "Splash Light Mode" +) +@Composable +fun SplashPreviewThemeLight() { + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(false)) + } + SplashScreen(state) + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Splash Dark Mode" +) +@Composable +fun SplashPreviewThemeDark() { + MaterialTheme { + val state: MutableState = remember { + mutableStateOf(DefaultErrorParams(true)) } + SplashScreen(state) } } 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..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 @@ -1,38 +1,52 @@ 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.ui.splash.state.SplashScreenAction +import com.mdgd.pokemon.models.infra.Result +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 import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import javax.inject.Inject -class SplashViewModel(private val cache: Cache) : MviViewModel(), SplashContract.ViewModel { +@HiltViewModel +class SplashViewModel @Inject constructor( + private val cache: Cache +) : MviViewModel(), + SplashContract.ViewModel { private val exceptionHandler = CoroutineExceptionHandler { _, e -> - setAction(SplashScreenAction.ShowError(e)) + setEffect(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) { - delay(SplashContract.SPLASH_DELAY) - val value = cache.getProgressChanel().receive() - if (value.isError()) { - setAction(SplashScreenAction.ShowError(value.getError())) - } else if (value.getValue() != 0L) { - setAction(SplashScreenAction.NextScreen) + flow { + delay(SplashContract.SPLASH_DELAY) + emit(System.currentTimeMillis()) + }.combine(cache.getProgressChanel()) { _: Long, result: Result -> + result + // Result(Throwable("Dummy")) + }.collect { + if (it.isError()) { + setEffect(SplashScreenEffect.ShowError(it.getError())) + } else if (it.getValue() != 0L) { + setEffect(SplashScreenEffect.NextScreen) + } } } - setAction(SplashScreenAction.LaunchWorker) + setEffect(SplashScreenEffect.LaunchWorker) } } 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/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 c761213..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,26 +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/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/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 7706ab9..2b068d1 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -27,4 +27,4 @@ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" android:strokeWidth="1" android:strokeColor="#00000000" /> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_defense.png b/app/src/main/res/drawable/ic_defense.png deleted file mode 100644 index 60272f9..0000000 Binary files a/app/src/main/res/drawable/ic_defense.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_defense.xml b/app/src/main/res/drawable/ic_defense.xml new file mode 100644 index 0000000..a98c703 --- /dev/null +++ b/app/src/main/res/drawable/ic_defense.xml @@ -0,0 +1,12 @@ + + + + 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_pokemon_properties.xml b/app/src/main/res/layout/fragment_pokemon_properties.xml deleted file mode 100644 index 81965ad..0000000 --- a/app/src/main/res/layout/fragment_pokemon_properties.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/app/src/main/res/layout/fragment_pokemons.xml b/app/src/main/res/layout/fragment_pokemons.xml deleted file mode 100644 index c330e78..0000000 --- a/app/src/main/res/layout/fragment_pokemons.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml deleted file mode 100644 index 27ccd79..0000000 --- a/app/src/main/res/layout/fragment_splash.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_empty.xml b/app/src/main/res/layout/item_empty.xml deleted file mode 100644 index 8436b9c..0000000 --- a/app/src/main/res/layout/item_empty.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/item_pokemon.xml b/app/src/main/res/layout/item_pokemon.xml deleted file mode 100644 index 28dbf3b..0000000 --- a/app/src/main/res/layout/item_pokemon.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_pokemon_image.xml b/app/src/main/res/layout/item_pokemon_image.xml deleted file mode 100644 index 5a48b56..0000000 --- a/app/src/main/res/layout/item_pokemon_image.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/item_pokemon_label.xml b/app/src/main/res/layout/item_pokemon_label.xml deleted file mode 100644 index bad2162..0000000 --- a/app/src/main/res/layout/item_pokemon_label.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_pokemon_label_image.xml b/app/src/main/res/layout/item_pokemon_label_image.xml deleted file mode 100644 index a0db2fd..0000000 --- a/app/src/main/res/layout/item_pokemon_label_image.xml +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/app/src/main/res/layout/item_pokemon_title.xml b/app/src/main/res/layout/item_pokemon_title.xml deleted file mode 100644 index daa2f43..0000000 --- a/app/src/main/res/layout/item_pokemon_title.xml +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6b78462..eca70cf 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6b78462..eca70cf 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a571e60..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 61da551..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index c41dd28..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index db5080a..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6dba46d..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index da31a87..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 15ac681..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b216f2d..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index f25a419..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e96783c..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/navigation_graph.xml b/app/src/main/res/navigation/navigation_graph.xml index 32688dd..df29971 100644 --- a/app/src/main/res/navigation/navigation_graph.xml +++ b/app/src/main/res/navigation/navigation_graph.xml @@ -1,15 +1,13 @@ + android:label="SplashFragment"> + android:label="PokemonsFragment"> @@ -30,8 +27,7 @@ + android:label="PokemonDetailsFragment"> - diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..1f85999 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + 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/strings.xml b/app/src/main/res/values/strings.xml index 0f0ab85..016aab8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,12 @@ Some error happened Details: Oooops!\nThere is no pokemons for some reason + splash logo + Filter by defence + Filter by attack + Filter by speed + Refresh pokemons list + Pokemon\'s icon + Pokemon\'s image + Back button diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index 391ec9a..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 @@ + + + + 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/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/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 c3eebee..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 @@ -1,9 +1,9 @@ package com.mdgd.pokemon.ui.pokemon -import com.mdgd.pokemon.ui.pokemon.infra.PokemonProperty +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 2953d94..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,20 +3,18 @@ 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 -import com.mdgd.pokemon.ui.pokemon.infra.ImagePropertyData -import com.mdgd.pokemon.ui.pokemon.infra.LabelPropertyData -import com.mdgd.pokemon.ui.pokemon.infra.TextPropertyData -import com.mdgd.pokemon.ui.pokemon.infra.TitlePropertyData -import com.mdgd.pokemon.ui.pokemon.state.PokemonDetailsScreenAction +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.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,54 +32,52 @@ 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 - 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) - 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) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @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 - 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.onStateChanged(Lifecycle.Event.ON_CREATE) model.setPokemonId(0) Thread.sleep(2000) @@ -87,23 +86,25 @@ class PokemonDetailsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @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 - 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.onStateChanged(Lifecycle.Event.ON_CREATE) model.setPokemonId(0) Thread.sleep(2000) @@ -201,6 +202,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 57% 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..67a3b66 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,7 +1,7 @@ package com.mdgd.pokemon.ui.pokemons -import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenAction -import kotlinx.coroutines.test.runBlockingTest +import com.mdgd.pokemon.ui.pokemons.state.PokemonsScreenEffect +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -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 @@ -22,33 +22,33 @@ class PokemonsScreenActionTest { } @Test - fun test_ErrorState() = runBlockingTest { + fun test_ErrorState() = runBlocking { 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)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).showError(error) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() + Mockito.verify(view, Mockito.times(1)).setProgressVisibility(false) verifyNoMoreInteractions() } @Test - fun test_ShowDetailsState() = runBlockingTest { + fun test_ShowDetailsState() = runBlocking { 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)).setProgressVisibility(false) Mockito.verify(view, Mockito.times(1)).proceedToNextScreen(pokemonId) state.visit(view) - Mockito.verify(view, Mockito.times(1)).hideProgress() + 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 710fad6..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)).showProgress() + 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 529d4d0..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,7 +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.util.DispatchersHolder +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 @@ -12,25 +13,26 @@ 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.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,26 +57,28 @@ 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 - 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) - 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) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -90,48 +88,53 @@ 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + 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) { when (action) { - is PokemonsScreenAction.Error -> { + is PokemonsScreenEffect.Error -> { 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() @@ -139,7 +142,7 @@ class PokemonsViewModelTest { Mockito.verifyNoMoreInteractions(actionObserverMock) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @Test @@ -151,12 +154,14 @@ 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() + model.getEffectObservable().observeForever(actionObserverMock) model.onItemClicked(pokemon) @@ -164,24 +169,26 @@ 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 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() + model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) val pokemons = getPage(0) @@ -191,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) @@ -226,7 +232,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 { @@ -245,13 +251,15 @@ 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() + model.getEffectObservable().observeForever(actionObserverMock) Mockito.`when`(filtersFactory.getAvailableFilters()).thenReturn(listOf()) @@ -269,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) @@ -315,22 +320,25 @@ 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 - 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() + model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) Mockito.`when`(repo.getPage(0)).then { @@ -339,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) @@ -348,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) @@ -414,22 +423,25 @@ 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 - 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(PokemonsScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + val actionObserverMock = + Mockito.mock(Observer::class.java) as Observer> + val actionCaptor = argumentCaptor() + model.getEffectObservable().observeForever(actionObserverMock) val page1 = getPage(0) Mockito.`when`(repo.getPage(0)).then { @@ -438,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) @@ -450,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) @@ -525,6 +538,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..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,166 +3,173 @@ 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.SplashScreenAction -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.mdgd.pokemon.ui.splash.state.SplashScreenEffect +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 - 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) - 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) verifyNoMoreInteractions() model.getStateObservable().removeObserver(stateObserverMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @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(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) - + 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 SplashScreenAction.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 - Assert.assertTrue(errorState is SplashScreenAction.ShowError) - Assert.assertEquals((errorState as SplashScreenAction.ShowError).e, error) + val errorState = actionCaptor.thirdValue + 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 - 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(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + 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 SplashScreenAction.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) + 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) verifyNoMoreInteractions() model.getStateObservable().removeObserver(observerMock) - model.getActionObservable().removeObserver(actionObserverMock) + model.getEffectObservable().removeObserver(actionObserverMock) } @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(SplashScreenAction::class.java) - model.getActionObservable().observeForever(actionObserverMock) + 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 SplashScreenAction.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 - Assert.assertTrue(errorState is SplashScreenAction.NextScreen) + val errorState = actionCaptor.thirdValue + 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 deleted file mode 100644 index 3e08e48..0000000 --- a/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.kotlin_version = '1.5.21' - ext.lifecycle_ktx = "2.3.1" - ext.nav_version = "2.3.5" - ext.work_version = "2.6.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.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" - - project.ext { - min = 20 - target = 30 - compile = 30 - tools = "30.0.2" - } - - repositories { - mavenCentral() - google() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" - - // 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/gradle.properties b/gradle.properties index 2f26404..3ed8aa7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,13 @@ 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 +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 2be7f07..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.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/models/build.gradle b/models/build.gradle deleted file mode 100644 index 000ec1c..0000000 --- a/models/build.gradle +++ /dev/null @@ -1,47 +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_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -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" - - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" -} - -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/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/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/models/src/main/java/com/mdgd/pokemon/models/cache/Cache.kt b/models/src/main/java/com/mdgd/pokemon/models/cache/Cache.kt index b54390f..1d004c4 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/cache/Cache.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/cache/Cache.kt @@ -1,9 +1,9 @@ package com.mdgd.pokemon.models.cache import com.mdgd.pokemon.models.infra.Result -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow interface Cache { - suspend fun putLoadingProgress(value: Result) - fun getProgressChanel(): Channel> + fun putLoadingProgress(value: Result) + fun getProgressChanel(): Flow> } diff --git a/models/src/main/java/com/mdgd/pokemon/models/filters/FilterData.kt b/models/src/main/java/com/mdgd/pokemon/models/filters/FilterData.kt index 7761313..da27495 100644 --- a/models/src/main/java/com/mdgd/pokemon/models/filters/FilterData.kt +++ b/models/src/main/java/com/mdgd/pokemon/models/filters/FilterData.kt @@ -18,7 +18,7 @@ class FilterData { companion object { const val FILTER_ATTACK = "attack" - const val FILTER_DEFENCE = "defence" + const val FILTER_DEFENCE = "defense" const val FILTER_SPEED = "speed" } } 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 47e24a7..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 @@ -15,19 +15,10 @@ class Result { this.error = error } - fun isError(): Boolean { - return error != null - } + fun isError() = error != null + fun hasValue() = error == null - fun getValue(): T { - return value!! - } + fun getValue() = value!! - fun getError(): Throwable { - return error!! - } - - fun hasValue(): Boolean { - return error == null - } + fun getError() = error!! } 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/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/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/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 deleted file mode 100644 index 0ba316e..0000000 --- a/models_impl/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'kotlin-kapt' -} -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_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -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" - - // coroutins - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutins" - - 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 "com.google.code.gson:gson:$gson" -} - -repositories { - mavenCentral() -} 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/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/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/cache/CacheImpl.kt b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/cache/CacheImpl.kt index cdb0b95..ad375a9 100644 --- a/models_impl/src/main/java/com/mdgd/pokemon/models_impl/cache/CacheImpl.kt +++ b/models_impl/src/main/java/com/mdgd/pokemon/models_impl/cache/CacheImpl.kt @@ -2,16 +2,20 @@ package com.mdgd.pokemon.models_impl.cache import com.mdgd.pokemon.models.cache.Cache import com.mdgd.pokemon.models.infra.Result -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow class CacheImpl : Cache { - private val progressChanel = Channel>(Channel.Factory.CONFLATED) + private val progressChanel = MutableSharedFlow>( + extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) - override suspend fun putLoadingProgress(value: Result) { - progressChanel.send(value) + override fun putLoadingProgress(value: Result) { + progressChanel.tryEmit(value) } - override fun getProgressChanel(): Channel> { + override fun getProgressChanel(): Flow> { return progressChanel } } 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 cd8b034..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 @@ -6,13 +6,17 @@ 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 -class PokemonsRepository(private val dao: PokemonsDao, private val network: Network, private val cache: PokemonsCache) : PokemonsRepo { +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 { @@ -37,16 +41,12 @@ class PokemonsRepository(private val dao: PokemonsDao, private val network: Netw val count = dao.getCount() val pokemonsCount = network.getPokemonsCount() return when (count) { - pokemonsCount -> { - count - } - else -> { - loadPokemonsInner(pokemonsCount, initialAmount) - } + pokemonsCount -> count + else -> loadPokemonsInner(pokemonsCount, initialAmount) } } - private suspend fun loadPokemonsInner(pokemonsCount: Long, offset: Long): Long { // = withContext(Dispatchers.IO) + private suspend fun loadPokemonsInner(pokemonsCount: Long, offset: Long): Long { val page = network.loadPokemons(pokemonsCount, offset) dao.save(page) return page.size.toLong() 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 7e51158..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,11 +4,18 @@ 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]) +@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 82a44ec..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,36 +5,30 @@ 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 java.util.* class PokemonsDaoImpl(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) + pokemonsRoomDao?.save(list) } 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 == 0 -> ArrayList(0) + (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() - } + 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 b72be92..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,17 +20,18 @@ 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/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 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) diff --git a/mvi/build.gradle b/mvi/build.gradle deleted file mode 100644 index b34e4d0..0000000 --- a/mvi/build.gradle +++ /dev/null @@ -1,37 +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' - } - } -} - -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:$nav_version" - implementation "androidx.navigation:navigation-ui:$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 8207eb6..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 @@ -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/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 @@ - + diff --git a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt index 2c8ca18..0f98d11 100644 --- a/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt +++ b/mvi/src/main/java/com/mdgd/mvi/MviViewModel.kt @@ -1,37 +1,40 @@ package com.mdgd.mvi -import androidx.lifecycle.* +import androidx.annotation.CallSuper +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, A> : ViewModel(), FragmentContract.ViewModel { - private val stateHolder = MutableLiveData() // TODO: use StateFlow: val uiState: StateFlow = _uiState ? - private val actionHolder = MutableLiveData() +abstract class MviViewModel, EFFECT : AbstractEffect> : + ViewModel(), FragmentContract.ViewModel { - override fun getStateObservable(): MutableLiveData { - return stateHolder - } + private val stateHolder = MutableLiveData>() + private val effectHolder = MutableLiveData>() - protected fun setState(state: S) { - if (stateHolder.value != null) { - state.merge(stateHolder.value as S) - } - stateHolder.value = state - } + override fun getStateObservable() = stateHolder - protected fun getState(): S? { - return stateHolder.value - } + override fun getEffectObservable() = effectHolder - override fun getActionObservable(): MutableLiveData { - return actionHolder + @Suppress("UNCHECKED_CAST") + protected fun setState(state: STATE) { + stateHolder.value = stateHolder.value?.let { state.merge(it as STATE) } ?: state } - protected fun setAction(action: A) { - actionHolder.value = action + @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) { + effectHolder.value = action } - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - protected open fun onAny(owner: LifecycleOwner?, event: Lifecycle.Event) { + @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 1479b42..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,12 +1,15 @@ package com.mdgd.mvi.fragments +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.LiveData +import com.mdgd.mvi.states.ScreenState class FragmentContract { - interface ViewModel : LifecycleObserver { - fun getStateObservable(): MutableLiveData - fun getActionObservable(): MutableLiveData + interface ViewModel : LifecycleObserver { + 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 961f60e..0000000 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedDialogFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.mdgd.mvi.fragments - -import android.content.Context -import android.os.Bundle -import androidx.appcompat.app.AppCompatDialogFragment -import androidx.lifecycle.Observer -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 { - - 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()) - if (model != null) { - lifecycle.addObserver(model!!) - model!!.getStateObservable().observe(this, this) - model!!.getActionObservable().observe(this, { action -> - action.visit(this as VIEW) - }) - } - } - - override fun onChanged(state: STATE) { - 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 - } -} 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 cd00c60..9d8e3cd 100644 --- a/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt +++ b/mvi/src/main/java/com/mdgd/mvi/fragments/HostedFragment.kt @@ -2,26 +2,28 @@ package com.mdgd.mvi.fragments import android.content.Context import android.os.Bundle +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 import java.lang.reflect.ParameterizedType abstract class HostedFragment< VIEW : FragmentContract.View, - STATE : ScreenState, - ACTION : ScreenAction, - VIEW_MODEL : FragmentContract.ViewModel, + VIEW_MODEL : FragmentContract.ViewModel, HOST : FragmentContract.Host> - : NavHostFragment(), FragmentContract.View, Observer { + : NavHostFragment(), FragmentContract.View, Observer>, + LifecycleEventObserver { - protected var model: VIEW_MODEL? = null + protected var viewModel: VIEW_MODEL? = null private set protected var fragmentHost: HOST? = null private set + @Suppress("UNCHECKED_CAST") override fun onAttach(context: Context) { super.onAttach(context) // keep the call back @@ -29,9 +31,10 @@ abstract class HostedFragment< 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 " + this.javaClass.simpleName, e) + .actualTypeArguments[1] as Class<*>).canonicalName + throw RuntimeException( + "Activity must implement $hostClassName to attach ${this.javaClass.simpleName}", e + ) } } @@ -41,40 +44,32 @@ abstract class HostedFragment< fragmentHost = null } - protected fun hasHost(): Boolean { - return 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) + viewModel?.let { + it.getStateObservable().observe(this, this) + it.getEffectObservable().observe(this, this) } } 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!!) + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + viewModel?.onStateChanged(event) + + if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { + lifecycle.removeObserver(this) } - super.onDestroy() } - override fun onChanged(screenState: STATE) { - screenState.visit(this as VIEW) + @Suppress("UNCHECKED_CAST") + override fun onChanged(value: ScreenState) { + value.visit(this@HostedFragment as VIEW) } protected fun setModel(model: VIEW_MODEL) { - this.model = model + this.viewModel = model } } 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 57% 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..2174ebc 100644 --- a/mvi/src/main/java/com/mdgd/mvi/states/AbstractAction.kt +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractEffect.kt @@ -1,14 +1,16 @@ package com.mdgd.mvi.states -abstract class AbstractAction : ScreenAction { +abstract class AbstractEffect : ScreenState { var isHandled = false override fun visit(screen: T) { if (!isHandled) { - handle(screen); + handle(screen) isHandled = true } } - abstract fun handle(screen: T); -} \ No newline at end of file + open fun handle(screen: T) { + + } +} 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..f650597 --- /dev/null +++ b/mvi/src/main/java/com/mdgd/mvi/states/AbstractState.kt @@ -0,0 +1,10 @@ +package com.mdgd.mvi.states + +abstract class AbstractState : ScreenState { + + override fun visit(screen: V) { + + } + + open fun merge(prevState: S): S = this as S +} 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) } 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..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) +interface ScreenState { + fun visit(screen: V) } 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 - } -} 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"