├── app ├── .gitignore ├── proguard │ ├── .gitignore │ ├── leak-canary.pro │ ├── proguard-google-analytics.pro │ ├── proguard-testfairy.pro │ ├── proguard-facebook.pro │ ├── proguard-square-okhttp.pro │ ├── proguard-square-retrofit.pro │ ├── proguard-hilt.pro │ ├── proguard-support-v7-appcompat.pro │ ├── okhttp3.pro │ ├── proguard-google-play-services.pro │ ├── retrofit2.pro │ ├── kotlin.pro │ └── proguard-project.pro ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── layout │ │ │ │ ├── fragment_container.xml │ │ │ │ ├── activity_splash.xml │ │ │ │ ├── fragment_search.xml │ │ │ │ ├── layout_progress_dialog.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_home.xml │ │ │ │ ├── item_pokemon.xml │ │ │ │ └── dialog_custom.xml │ │ │ ├── drawable │ │ │ │ ├── bg_bottom_sheet.xml │ │ │ │ ├── scrollbar.xml │ │ │ │ ├── ic_arrow.xml │ │ │ │ ├── ic_favorite.xml │ │ │ │ ├── ic_baseline_search_24.xml │ │ │ │ ├── ic_home.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── anim │ │ │ │ ├── slide_out_left.xml │ │ │ │ ├── slide_in_right.xml │ │ │ │ ├── slide_out_right.xml │ │ │ │ ├── slide_in_left.xml │ │ │ │ ├── fade_in.xml │ │ │ │ └── fade_out.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ └── bottom_navigation.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ └── dimens.xml │ │ │ └── raw │ │ │ │ └── material_wave_loading.json │ │ ├── java │ │ │ └── com │ │ │ │ └── thuanpx │ │ │ │ └── view_mvvm_architecture │ │ │ │ ├── data │ │ │ │ ├── local │ │ │ │ │ ├── db │ │ │ │ │ │ └── AppDatabase.kt │ │ │ │ │ └── datastore │ │ │ │ │ │ └── PreferenceDataStore.kt │ │ │ │ ├── remote │ │ │ │ │ ├── datasource │ │ │ │ │ │ └── PokemonDataSource.kt │ │ │ │ │ └── api │ │ │ │ │ │ ├── ApiService.kt │ │ │ │ │ │ └── middleware │ │ │ │ │ │ └── DefaultInterceptor.kt │ │ │ │ └── repository │ │ │ │ │ └── AppRepository.kt │ │ │ │ ├── base │ │ │ │ ├── network │ │ │ │ │ ├── NetworkError.kt │ │ │ │ │ ├── ErrorJson.kt │ │ │ │ │ ├── RetrofitException.kt │ │ │ │ │ └── AppErrors.kt │ │ │ │ ├── viewmodel │ │ │ │ │ ├── EmptyViewModel.kt │ │ │ │ │ └── BaseViewModel.kt │ │ │ │ ├── recyclerview │ │ │ │ │ ├── BasePagingDataAdapter.kt │ │ │ │ │ ├── BaseListAdapter.kt │ │ │ │ │ ├── EndlessRecyclerOnScrollListener.kt │ │ │ │ │ └── BaseRecyclerViewAdapter.kt │ │ │ │ ├── BaseDataSource.kt │ │ │ │ ├── fragment │ │ │ │ │ ├── BaseFragment.kt │ │ │ │ │ ├── BaseDialogFragment.kt │ │ │ │ │ └── BaseBottomSheetFragment.kt │ │ │ │ └── BaseActivity.kt │ │ │ │ ├── utils │ │ │ │ ├── extension │ │ │ │ │ ├── boolean │ │ │ │ │ │ └── BooleanExt.kt │ │ │ │ │ ├── collection │ │ │ │ │ │ └── ListExt.kt │ │ │ │ │ ├── AnimationType.kt │ │ │ │ │ ├── flow │ │ │ │ │ │ └── FlowExt.kt │ │ │ │ │ ├── glide │ │ │ │ │ │ └── GlideExt.kt │ │ │ │ │ ├── gson │ │ │ │ │ │ └── GsonExt.kt │ │ │ │ │ ├── number │ │ │ │ │ │ └── NumberExt.kt │ │ │ │ │ ├── moshi │ │ │ │ │ │ └── MoshiUtils.kt │ │ │ │ │ ├── view │ │ │ │ │ │ └── ViewExt.kt │ │ │ │ │ ├── recyclerView │ │ │ │ │ │ └── RecyclerViewExt.kt │ │ │ │ │ ├── string │ │ │ │ │ │ └── StringExt.kt │ │ │ │ │ ├── context │ │ │ │ │ │ ├── TimeAgoExt.kt │ │ │ │ │ │ ├── ActivityExt.kt │ │ │ │ │ │ └── FragmentExt.kt │ │ │ │ │ ├── widget │ │ │ │ │ │ └── DialogBuilder.kt │ │ │ │ │ └── date │ │ │ │ │ │ └── DateExt.kt │ │ │ │ ├── AppGlideModule.kt │ │ │ │ ├── KeyboardDetector.kt │ │ │ │ └── DataResult.kt │ │ │ │ ├── di │ │ │ │ ├── AppDispatchers.kt │ │ │ │ ├── DispatchersModule.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ ├── DataStoreModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ │ ├── feature │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── MainViewPagerAdapter.kt │ │ │ │ ├── home │ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ │ ├── HomeAdapter.kt │ │ │ │ │ └── HomeFragment.kt │ │ │ │ ├── splash │ │ │ │ │ └── SplashActivity.kt │ │ │ │ ├── search │ │ │ │ │ ├── SearchFragment.kt │ │ │ │ │ └── SearchViewModel.kt │ │ │ │ └── MainActivity.kt │ │ │ │ ├── model │ │ │ │ ├── response │ │ │ │ │ └── BaseResponse.kt │ │ │ │ ├── entity │ │ │ │ │ ├── PokemonInfo.kt │ │ │ │ │ └── Pokemon.kt │ │ │ │ └── exception │ │ │ │ │ └── ApiException.kt │ │ │ │ ├── app │ │ │ │ ├── Constant.kt │ │ │ │ └── App.kt │ │ │ │ └── widget │ │ │ │ ├── ProgressDialog.kt │ │ │ │ └── KeyboardUtils.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── thuanpx │ │ │ └── view_mvvm_architecture │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── thuanpx │ │ └── view_mvvm_architecture │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── images └── summary.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | -------------------------------------------------------------------------------- /images/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/images/summary.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/proguard/leak-canary.pro: -------------------------------------------------------------------------------- 1 | # LeakCanary 2 | -keep class org.eclipse.mat.** { *; } 3 | -keep class com.squareup.leakcanary.** { *; } -------------------------------------------------------------------------------- /app/proguard/proguard-google-analytics.pro: -------------------------------------------------------------------------------- 1 | ## Google Analytics 3.0 specific rules ## 2 | 3 | -keep class com.google.analytics.** { *; } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThuanPx/MVVM-Architecture/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/proguard/proguard-testfairy.pro: -------------------------------------------------------------------------------- 1 | #TestFairy 2 | -keep class com.testfairy.** { *; } 3 | -dontwarn com.testfairy.** 4 | -keepattributes Exceptions, Signature, LineNumberTable -------------------------------------------------------------------------------- /app/proguard/proguard-facebook.pro: -------------------------------------------------------------------------------- 1 | # Facebook 3.2 2 | 3 | -keep class com.sromku.simple.fb.entities.** { *; } 4 | -dontwarn com.facebook.** 5 | -keep class com.facebook.** { *; } 6 | -keepattributes Signature -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/local/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.local.db 2 | 3 | /** 4 | * https://developer.android.com/training/data-storage/room 5 | */ 6 | class AppDatabase 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/network/NetworkError.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.network 2 | 3 | data class NetworkError( 4 | val errorCode: String?, 5 | val message: String?, 6 | val status: String? 7 | ) -------------------------------------------------------------------------------- /app/proguard/proguard-square-okhttp.pro: -------------------------------------------------------------------------------- 1 | # OkHttp 2 | -keepattributes Signature 3 | -keepattributes *Annotation* 4 | -keep class com.squareup.okhttp.** { *; } 5 | -keep interface com.squareup.okhttp.** { *; } 6 | -dontwarn com.squareup.okhttp.** 7 | -dontwarn com.squareup.picasso.** -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_container.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jan 22 10:27:04 ICT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/boolean/BooleanExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.boolean 2 | 3 | /** 4 | * Created by ThuanPx on 3/15/20. 5 | */ 6 | 7 | fun Boolean?.isTrue() = this == true 8 | 9 | fun Boolean?.isNotTrue() = !this.isTrue() 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrollbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/proguard/proguard-square-retrofit.pro: -------------------------------------------------------------------------------- 1 | -keep class com.squareup.okhttp.** { *; } 2 | -keep interface com.squareup.okhttp.** { *; } 3 | -dontwarn com.squareup.okhttp.** 4 | 5 | -dontwarn rx.** 6 | -dontwarn retrofit.** 7 | -dontwarn okio.** 8 | -keep class retrofit.** { *; } 9 | -keepclasseswithmembers class * { 10 | @retrofit.http.* ; 11 | } -------------------------------------------------------------------------------- /app/proguard/proguard-hilt.pro: -------------------------------------------------------------------------------- 1 | # Hilt ProGuard rules. 2 | # https://github.com/google/dagger/blob/master/java/dagger/hilt/android/lifecycle/proguard-rules.pro 3 | 4 | # Keep class names of Hilt injected ViewModels since their name are used as a multibinding map key. 5 | -keepnames @dagger.hilt.android.lifecycle.HiltViewModel class * extends androidx.lifecycle.ViewModel -------------------------------------------------------------------------------- /app/proguard/proguard-support-v7-appcompat.pro: -------------------------------------------------------------------------------- 1 | -keep public class android.support.v7.widget.** { *; } 2 | -keep public class android.support.v7.internal.widget.** { *; } 3 | -keep public class android.support.v7.internal.view.menu.** { *; } 4 | 5 | -keep public class * extends android.support.v4.view.ActionProvider { 6 | public (android.content.Context); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/AppDispatchers.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import javax.inject.Qualifier 4 | import kotlin.annotation.AnnotationRetention.RUNTIME 5 | 6 | @Qualifier 7 | @Retention(RUNTIME) 8 | annotation class Dispatcher(val dispatcher: AppDispatchers) 9 | 10 | enum class AppDispatchers { 11 | IO 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/collection/ListExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.collection 2 | 3 | /** 4 | * Created by ThuanPx on 3/12/20. 5 | */ 6 | 7 | inline fun List?.equalsExt(listCompare: MutableList?) = this?.size == listCompare?.size && 8 | this?.containsAll(listCompare ?: mutableListOf()) == true 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "MVVM-Architecture-Views" 17 | include(":app") 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature 2 | 3 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Created by ThuanPx on 8/8/20. 9 | */ 10 | @HiltViewModel 11 | class MainViewModel @Inject constructor() : BaseViewModel() 12 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/viewmodel/EmptyViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.viewmodel 2 | 3 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Created by ThuanPx on 3/24/21. 9 | */ 10 | @HiltViewModel 11 | class EmptyViewModel @Inject constructor() : BaseViewModel() 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/model/response/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.model.response 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | /** 7 | * Created by ThuanPx on 8/8/20. 8 | */ 9 | 10 | @JsonClass(generateAdapter = true) 11 | class BaseResponse( 12 | @Json(name = "count") val count: Int? = null, 13 | @Json(name = "results") val data: T? 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/network/ErrorJson.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.network 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ErrorJson( 8 | @Json(name = "message") val message: String? = null, 9 | val error: String? = null, 10 | @Json(name = "status") val status: String? = null, 11 | val error_code: String? = null, 12 | ) -------------------------------------------------------------------------------- /app/src/test/java/com/thuanpx/view_mvvm_architecture/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/model/entity/PokemonInfo.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.model.entity 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | /** 7 | * Created by ThuanPx on 8/13/20. 8 | */ 9 | @JsonClass(generateAdapter = true) 10 | data class PokemonInfo( 11 | @Json(name = "id") val id: Int? = null, 12 | @Json(name = "name") val name: String? = null 13 | ) { 14 | fun getImageUrl(): String { 15 | return "https://pokeres.bastionbot.org/images/pokemon/$id.png" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/AnimationType.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.mvvm_architecture.utils.extension 2 | 3 | import androidx.annotation.IntDef 4 | 5 | /** 6 | * Created by ThuanPx on 5/21/20. 7 | */ 8 | 9 | @IntDef(NONE, FADE, SLIDE_LEFT, SLIDE_RIGHT, SLIDE_DOWN, SLIDE_UP) 10 | @Retention(AnnotationRetention.SOURCE) 11 | annotation class AnimationType 12 | 13 | const val NONE = 0x00 14 | const val FADE = 0x01 15 | const val SLIDE_RIGHT = 0x02 16 | const val SLIDE_LEFT = 0x03 17 | const val SLIDE_DOWN = 0x04 18 | const val SLIDE_UP = 0x05 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/flow/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.flow 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.onCompletion 6 | import kotlinx.coroutines.flow.onStart 7 | 8 | /** 9 | * Created by ThuanPx on 4/2/22. 10 | */ 11 | 12 | fun Flow.loading(isLoading: MutableLiveData): Flow { 13 | return onStart { isLoading.postValue(true) } 14 | .onCompletion { isLoading.postValue(false) } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/app/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.app 2 | 3 | /** 4 | * Created by ThuanPx on 8/5/20. 5 | */ 6 | object Constant { 7 | /** 8 | * Date 9 | */ 10 | const val KTEXT_TIME_ZONE_UTC = "UTC" 11 | const val KTEXT_DATE_TIME_FORMAT_UTC = "yyyy-MM-dd'T'HH:mm:ssX" 12 | const val KTEXT_TIME_FORMAT_HH_MM = "HH:mm" 13 | const val KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD_EN = "yyyy/MM/dd" 14 | const val KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD = "yyyy-MM-dd" 15 | const val KTEXT_DAY_OF_WEEK = "E" 16 | } 17 | -------------------------------------------------------------------------------- /app/proguard/okhttp3.pro: -------------------------------------------------------------------------------- 1 | # JSR 305 annotations are for embedding nullability information. 2 | -dontwarn javax.annotation.** 3 | 4 | # A resource is loaded with a relative path so the package of this class must be preserved. 5 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 6 | 7 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 8 | -dontwarn org.codehaus.mojo.animal_sniffer.* 9 | 10 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 11 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/model/exception/ApiException.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.model.exception 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class ApiException(val code: Int, val error: ApiError) : RuntimeException() { 6 | 7 | override val message: String? by lazy { 8 | return@lazy ("$code: ${error.message}") 9 | } 10 | 11 | /** 12 | * Sample error with single property is message 13 | */ 14 | data class ApiError( 15 | @SerializedName("message") 16 | val message: String 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/glide/GlideExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.glide 2 | 3 | import android.widget.ImageView 4 | import com.bumptech.glide.Glide 5 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 6 | import com.thuanpx.view_mvvm_architecture.utils.GlideApp 7 | 8 | /** 9 | * Created by ThuanPx on 3/23/22. 10 | */ 11 | 12 | fun ImageView.loadImageUrl(url: String?) { 13 | GlideApp.with(context).load(url) 14 | .transition(DrawableTransitionOptions.withCrossFade()) 15 | .into(this) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/gson/GsonExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.gson 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | 6 | /** 7 | * Created by ThuanPx on 24/12/2021. 8 | */ 9 | 10 | inline fun Gson.fromJsonType(json: String): T = fromJson(json, object : TypeToken() {}.type) 11 | 12 | inline fun Gson.toJsonType(obj: T): String = toJson(obj) 13 | 14 | inline fun T.clone(gson: Gson = Gson()): T { 15 | return gson.fromJsonType(json = gson.toJsonType(this)) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_search_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/app/App.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.app 2 | 3 | import android.app.Application 4 | import com.thuanpx.view_mvvm_architecture.BuildConfig 5 | import dagger.hilt.android.HiltAndroidApp 6 | import timber.log.Timber 7 | 8 | /** 9 | * Created by ThuanPx on 8/7/20. 10 | */ 11 | 12 | @HiltAndroidApp 13 | class App : Application() { 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | configTimber() 18 | } 19 | 20 | private fun configTimber() { 21 | if (BuildConfig.DEBUG) { 22 | Timber.plant(Timber.DebugTree()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/DispatchersModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import com.thuanpx.view_mvvm_architecture.di.AppDispatchers 4 | import com.thuanpx.view_mvvm_architecture.di.Dispatcher 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import kotlinx.coroutines.CoroutineDispatcher 10 | import kotlinx.coroutines.Dispatchers 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DispatchersModule { 15 | @Provides 16 | @Dispatcher(AppDispatchers.IO) 17 | fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * Created by ThuanPx on 8/7/20. 13 | */ 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object AppModule { 18 | 19 | @Singleton 20 | @Provides 21 | fun provideMoshi(): Moshi { 22 | return Moshi.Builder() 23 | .addLast(KotlinJsonAdapterFactory()) 24 | .build() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/remote/datasource/PokemonDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.remote.datasource 2 | 3 | import com.thuanpx.view_mvvm_architecture.base.BaseDataSource 4 | import com.thuanpx.view_mvvm_architecture.data.remote.api.ApiService 5 | import com.thuanpx.view_mvvm_architecture.model.entity.Pokemon 6 | import com.thuanpx.view_mvvm_architecture.model.response.BaseResponse 7 | 8 | /** 9 | * Created by ThuanPx on 4/3/22. 10 | */ 11 | class PokemonDataSource( 12 | private val apiService: ApiService, 13 | ) : BaseDataSource() { 14 | 15 | override suspend fun requestMore(nextPage: Int): BaseResponse> { 16 | return apiService.fetchPokemons(page = nextPage * 20) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_progress_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/number/NumberExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.number 2 | 3 | import java.text.DecimalFormat 4 | 5 | /** 6 | * Created by ThuanPx on 3/15/20. 7 | */ 8 | 9 | fun Int?.nullToDefault() = this ?: -1 10 | 11 | fun Int?.nullToZero() = this ?: 0 12 | 13 | fun Int?.isTrue() = this == 1 14 | 15 | fun Double?.isNullOrZero() = this == 0.0 || this == null 16 | 17 | fun Double?.nullToDefault() = this ?: -1.0 18 | 19 | fun Double?.nullToZero() = this ?: 0.0 20 | 21 | fun Double?.convertToPrice(currency: String) = String.format("%s%s", this.nullToZero().formatMoney(), currency) 22 | 23 | fun Double.formatMoney(): String { 24 | val formatter = DecimalFormat("###,###,###") 25 | return formatter.format(this) 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard/proguard-google-play-services.pro: -------------------------------------------------------------------------------- 1 | ## Google Play Services 4.3.23 specific rules ## 2 | ## https://developer.android.com/google/play-services/setup.html#Proguard ## 3 | 4 | -keep class * extends java.util.ListResourceBundle { 5 | protected Object[][] getContents(); 6 | } 7 | 8 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 9 | public static final *** NULL; 10 | } 11 | 12 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 13 | -keepclassmembernames class * { 14 | @com.google.android.gms.common.annotation.KeepName *; 15 | } 16 | 17 | -keepnames class * implements android.os.Parcelable { 18 | public static final ** CREATOR; 19 | } 20 | 21 | -keep public class com.google.android.gms.* { public *; } 22 | -dontwarn com.google.android.gms.** -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/remote/api/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.remote.api 2 | 3 | import com.thuanpx.view_mvvm_architecture.model.entity.Pokemon 4 | import com.thuanpx.view_mvvm_architecture.model.entity.PokemonInfo 5 | import com.thuanpx.view_mvvm_architecture.model.response.BaseResponse 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | import retrofit2.http.Query 9 | 10 | /** 11 | * Created by ThuanPx on 8/5/20. 12 | */ 13 | interface ApiService { 14 | 15 | @GET("pokemon") 16 | suspend fun fetchPokemons( 17 | @Query("limit") limit: Int = 20, 18 | @Query("offset") page: Int = 0 19 | ): BaseResponse> 20 | 21 | @GET("pokemon/{name}") 22 | suspend fun fetchPokemon(@Path("name") name: String): PokemonInfo 23 | } 24 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thuanpx/view_mvvm_architecture/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.thuanpx.view_mvvm_architecture", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/widget/ProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.widget 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.view.Window 6 | import com.thuanpx.view_mvvm_architecture.R 7 | 8 | @Suppress("DEPRECATION") 9 | class ProgressDialog(context: Context) : Dialog(context) { 10 | 11 | init { 12 | initView() 13 | } 14 | 15 | private fun initView() { 16 | requestWindowFeature(Window.FEATURE_NO_TITLE) 17 | setContentView(R.layout.layout_progress_dialog) 18 | window?.setBackgroundDrawableResource(android.R.color.transparent) 19 | setCancelable(false) 20 | setCanceledOnTouchOutside(false) 21 | } 22 | 23 | override fun onBackPressed() { 24 | super.onBackPressed() 25 | dismiss() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/recyclerview/BasePagingDataAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.recyclerview 2 | 3 | import androidx.paging.PagingDataAdapter 4 | import androidx.recyclerview.widget.DiffUtil 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | /** 8 | * Created by ThuanPx on 04/02/2023. 9 | */ 10 | abstract class BasePagingDataAdapter( 11 | diffUtil: DiffUtil.ItemCallback 12 | ) : PagingDataAdapter(diffUtil) { 13 | 14 | fun getItems() = snapshot().toMutableList() 15 | 16 | fun updateItem(index: Int, item: T, onUpdateSuccess: (() -> Unit)? = null) { 17 | val items = snapshot().toMutableList() 18 | items[index] = item 19 | notifyItemChanged(index) { 20 | onUpdateSuccess?.invoke() 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/MainViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.viewpager2.adapter.FragmentStateAdapter 6 | import com.thuanpx.view_mvvm_architecture.feature.home.HomeFragment 7 | import com.thuanpx.view_mvvm_architecture.feature.search.SearchFragment 8 | 9 | /** 10 | * Created by ThuanPx on 10/02/2023. 11 | */ 12 | class MainViewPagerAdapter(fragmentActivity: FragmentActivity) : 13 | FragmentStateAdapter(fragmentActivity) { 14 | 15 | override fun getItemCount(): Int { 16 | return 2 17 | } 18 | 19 | override fun createFragment(position: Int): Fragment { 20 | return when (position) { 21 | 0 -> HomeFragment() 22 | 1 -> SearchFragment() 23 | else -> Fragment() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MVVM-Architecture 3 | OK 4 | No 5 | Connection timeout! 6 | Not connect internet! 7 | Parse data error! 8 | Just now 9 | A minute ago 10 | minutes ago 11 | An hour ago 12 | hours ago 13 | A day ago 14 | days ago 15 | A week ago 16 | weeks ago 17 | More than a month ago 18 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/AppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils 2 | 3 | import android.content.Context 4 | import com.bumptech.glide.GlideBuilder 5 | import com.bumptech.glide.annotation.GlideModule 6 | import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory 7 | import com.bumptech.glide.load.engine.cache.LruResourceCache 8 | import com.bumptech.glide.module.AppGlideModule 9 | 10 | /** 11 | * Created by ThuanPx on 3/14/22. 12 | */ 13 | 14 | @GlideModule 15 | class AppGlideModule : AppGlideModule() { 16 | 17 | override fun applyOptions(context: Context, builder: GlideBuilder) { 18 | val memoryCacheSizeBytes = 1024 * 1024 * 150 // 150mb 19 | builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong())) 20 | val diskCacheSizeBytes = 1024 * 1024 * 250 // 250 MB 21 | builder.setDiskCache(InternalCacheDiskCacheFactory(context, diskCacheSizeBytes.toLong())) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.home 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import androidx.paging.cachedIn 5 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 6 | import com.thuanpx.view_mvvm_architecture.data.repository.AppRepository 7 | import com.thuanpx.view_mvvm_architecture.di.AppDispatchers 8 | import com.thuanpx.view_mvvm_architecture.di.Dispatcher 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Created by ThuanPx on 8/8/20. 15 | */ 16 | @HiltViewModel 17 | class HomeViewModel @Inject constructor( 18 | private val appRepository: AppRepository, 19 | @Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, 20 | ) : BaseViewModel() { 21 | 22 | val pagingPokemonFlow = appRepository.fetchPokemon() 23 | .cachedIn(viewModelScope) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import com.thuanpx.view_mvvm_architecture.data.remote.api.ApiService 4 | import com.thuanpx.view_mvvm_architecture.data.repository.AppRepository 5 | import com.thuanpx.view_mvvm_architecture.data.repository.DefaultAppRepository 6 | import com.thuanpx.view_mvvm_architecture.di.AppDispatchers 7 | import com.thuanpx.view_mvvm_architecture.di.Dispatcher 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import kotlinx.coroutines.CoroutineDispatcher 13 | import javax.inject.Singleton 14 | 15 | /** 16 | * Created by ThuanPx on 8/7/20. 17 | */ 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object RepositoryModule { 22 | 23 | @Singleton 24 | @Provides 25 | fun provideAppRepository(apiService: ApiService, @Dispatcher(AppDispatchers.IO) ioDispatcher: CoroutineDispatcher): AppRepository { 26 | return DefaultAppRepository(apiService, ioDispatcher) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/model/entity/Pokemon.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.model.entity 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | /** 8 | * Created by ThuanPx on 8/13/20. 9 | */ 10 | @JsonClass(generateAdapter = true) 11 | data class Pokemon( 12 | @Json(name = "name") val name: String? = null, 13 | @Json(name = "url") val url: String? = null 14 | ) { 15 | fun getImageUrl(): String { 16 | val index = url?.split("/".toRegex())?.dropLast(1)?.last() 17 | return "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$index.png" 18 | } 19 | } 20 | 21 | object PokemonDiffCallback : DiffUtil.ItemCallback() { 22 | override fun areItemsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean { 23 | return oldItem == newItem 24 | } 25 | 26 | override fun areContentsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean { 27 | return oldItem.name == newItem.name 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard/retrofit2.pro: -------------------------------------------------------------------------------- 1 | # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and 2 | # EnclosingMethod is required to use InnerClasses. 3 | -keepattributes Signature, InnerClasses, EnclosingMethod 4 | 5 | # Retrofit does reflection on method and parameter annotations. 6 | -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations 7 | 8 | # Retain service method parameters when optimizing. 9 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 10 | @retrofit2.http.* ; 11 | } 12 | 13 | # Ignore annotation used for build tooling. 14 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 15 | 16 | # Ignore JSR 305 annotations for embedding nullability information. 17 | -dontwarn javax.annotation.** 18 | 19 | # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. 20 | -dontwarn kotlin.Unit 21 | 22 | # Top-level functions that can only be used by Kotlin. 23 | -dontwarn retrofit2.KotlinExtensions 24 | 25 | # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy 26 | # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. 27 | -if interface * { @retrofit2.http.* ; } 28 | -keep,allowobfuscation interface <1> -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.splash 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.view.LayoutInflater 6 | import androidx.lifecycle.lifecycleScope 7 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 8 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.EmptyViewModel 9 | import com.thuanpx.view_mvvm_architecture.databinding.ActivitySplashBinding 10 | import com.thuanpx.view_mvvm_architecture.feature.MainActivity 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | 15 | /** 16 | * Created by ThuanPx on 16/09/2021. 17 | */ 18 | @SuppressLint("CustomSplashScreen") 19 | @AndroidEntryPoint 20 | class SplashActivity : BaseActivity(EmptyViewModel::class) { 21 | 22 | override fun inflateViewBinding(inflater: LayoutInflater): ActivitySplashBinding { 23 | return ActivitySplashBinding.inflate(inflater) 24 | } 25 | 26 | override fun initialize() { 27 | lifecycleScope.launch { 28 | delay(1_000) 29 | startActivity(Intent(this@SplashActivity, MainActivity::class.java)) 30 | finish() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/recyclerview/BaseListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.recyclerview 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import androidx.recyclerview.widget.ListAdapter 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | /** 8 | * Created by ThuanPx on 10/08/2021. 9 | */ 10 | abstract class BaseListAdapter( 11 | diffUtil: DiffUtil.ItemCallback 12 | ) : ListAdapter(diffUtil) { 13 | 14 | internal var itemClickListener: ((T, Int) -> Unit)? = null 15 | 16 | fun unRegisterItemClickListener() { 17 | itemClickListener = null 18 | } 19 | 20 | fun addItem(index: Int, item: T) { 21 | val currentList = currentList.toMutableList() 22 | currentList.add(index, item) 23 | submitList(currentList) 24 | } 25 | 26 | fun updateItems(items: List?, listener: (() -> Unit)? = null) { 27 | if (items.isNullOrEmpty()) return 28 | val currentList = currentList.toMutableList() 29 | currentList.addAll(items) 30 | submitList(currentList) { 31 | listener?.invoke() 32 | } 33 | } 34 | 35 | fun removeItem(index: Int) { 36 | val currentList = currentList.toMutableList() 37 | currentList.removeAt(index) 38 | submitList(currentList) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/proguard/kotlin.pro: -------------------------------------------------------------------------------- 1 | -keep class kotlin.** { *; } 2 | -keep class kotlin.Metadata { *; } 3 | -dontwarn kotlin.** 4 | -keepclassmembers class **$WhenMappings { 5 | ; 6 | } 7 | -keepclassmembers class kotlin.Metadata { 8 | public ; 9 | } 10 | #get rid of null checks at runtime you may use the following rule 11 | #add more rules for remove utility null checks 12 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 13 | public static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); 14 | public static void checkFieldIsNotNull(java.lang.Object, java.lang.String); 15 | public static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String); 16 | public static void checkNotNull(java.lang.Object); 17 | public static void checkNotNull(java.lang.Object, java.lang.String); 18 | public static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); 19 | public static void checkNotNullParameter(java.lang.Object, java.lang.String); 20 | public static void checkParameterIsNotNull(java.lang.Object, java.lang.String); 21 | public static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); 22 | public static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); 23 | public static void throwUninitializedPropertyAccessException(java.lang.String); 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/ 39 | 40 | # Keystore files 41 | # Uncomment the following lines if you do not want to check your keystore files in. 42 | #*.jks 43 | #*.keystore 44 | 45 | # External native build folder generated in Android Studio 2.2 and later 46 | .externalNativeBuild 47 | .cxx/ 48 | 49 | # Google Services (e.g. APIs or Firebase) 50 | # google-services.json 51 | 52 | # Freeline 53 | freeline.py 54 | freeline/ 55 | freeline_project_description.json 56 | 57 | # fastlane 58 | fastlane/report.xml 59 | fastlane/Preview.html 60 | fastlane/screenshots 61 | fastlane/test_output 62 | fastlane/readme.md 63 | 64 | # Version control 65 | vcs.xml 66 | 67 | # lint 68 | lint/intermediates/ 69 | lint/generated/ 70 | lint/outputs/ 71 | lint/tmp/ 72 | # lint/reports/ -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/local/datastore/PreferenceDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.local.datastore 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import androidx.datastore.preferences.core.stringPreferencesKey 7 | import com.thuanpx.view_mvvm_architecture.utils.extension.string.nullToEmpty 8 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStoreDefault.PreferencesKeys.PREF_TOKEN 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | /** 15 | * Created by ThuanPx on 4/1/22. 16 | */ 17 | 18 | interface PreferenceDataStore { 19 | suspend fun token(token: String) 20 | val token: Flow 21 | } 22 | 23 | @Singleton 24 | class PreferenceDataStoreDefault @Inject constructor( 25 | private val dataStore: DataStore 26 | ) : PreferenceDataStore { 27 | 28 | object PreferencesKeys { 29 | val PREF_TOKEN = stringPreferencesKey("pref_token") 30 | } 31 | 32 | override suspend fun token(token: String) { 33 | dataStore.edit { it[PREF_TOKEN] = token } 34 | } 35 | 36 | override val token: Flow = dataStore.data.map { it[PREF_TOKEN].nullToEmpty() } 37 | } 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/search/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.search 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.core.widget.doBeforeTextChanged 6 | import com.thuanpx.view_mvvm_architecture.base.fragment.BaseFragment 7 | import com.thuanpx.view_mvvm_architecture.databinding.FragmentSearchBinding 8 | import com.thuanpx.view_mvvm_architecture.utils.extension.context.launchAndRepeatWithViewLifecycle 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import kotlinx.coroutines.launch 11 | import timber.log.Timber 12 | 13 | /** 14 | * Created by ThuanPx on 8/8/20. 15 | */ 16 | 17 | @AndroidEntryPoint 18 | class SearchFragment : BaseFragment(SearchViewModel::class) { 19 | 20 | override fun inflateViewBinding( 21 | inflater: LayoutInflater, 22 | container: ViewGroup? 23 | ): FragmentSearchBinding { 24 | return FragmentSearchBinding.inflate(inflater, container, false) 25 | } 26 | 27 | override fun initialize() { 28 | viewBinding.etSearch.doBeforeTextChanged { text, start, count, after -> 29 | viewModel.query.value = text.toString() 30 | } 31 | } 32 | 33 | override fun onSubscribeObserver() { 34 | super.onSubscribeObserver() 35 | launchAndRepeatWithViewLifecycle { 36 | launch { 37 | viewModel.pokemonInfo.collect { 38 | Timber.i("$it") 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #D53A47 5 | #D53A47 6 | #D53A47 7 | #B6B6B6 8 | #007AFF 9 | #F8F8F8 10 | #707070 11 | #FAD831 12 | #2E2809 13 | #F44200 14 | #F2D130 15 | #F7F7F2 16 | #D6D6D2 17 | #FCFCD5 18 | #0D000000 19 | #A1A09A 20 | #F0F0F0 21 | #F4EDED 22 | #F49D00 23 | #787257 24 | #BF000000 25 | #434343 26 | #8E8E93 27 | #F1F1F2 28 | #EDECE4 29 | #CBCBC7 30 | #EFEFEF 31 | #E6FFFFFF 32 | #ffffff 33 | #2B292B 34 | #424242 35 | #1976d2 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/home/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.PagingDataAdapter 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.thuanpx.view_mvvm_architecture.databinding.ItemPokemonBinding 8 | import com.thuanpx.view_mvvm_architecture.feature.home.HomeAdapter.ItemViewHolder 9 | import com.thuanpx.view_mvvm_architecture.model.entity.Pokemon 10 | import com.thuanpx.view_mvvm_architecture.model.entity.PokemonDiffCallback 11 | import com.thuanpx.view_mvvm_architecture.utils.GlideApp 12 | 13 | /** 14 | * Created by ThuanPx on 4/3/22. 15 | */ 16 | class HomeAdapter : PagingDataAdapter(PokemonDiffCallback) { 17 | 18 | class ItemViewHolder( 19 | private val viewBinding: ItemPokemonBinding 20 | ) : RecyclerView.ViewHolder(viewBinding.root) { 21 | fun onBindData(item: Pokemon?) { 22 | with(viewBinding) { 23 | tvName.text = item?.name 24 | GlideApp.with(tvImage) 25 | .load(item?.getImageUrl()) 26 | .into(tvImage) 27 | } 28 | } 29 | } 30 | 31 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { 32 | return ItemViewHolder( 33 | ItemPokemonBinding.inflate(LayoutInflater.from(parent.context), parent, false) 34 | ) 35 | } 36 | 37 | override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { 38 | holder.onBindData(getItem(position)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/moshi/MoshiUtils.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.moshi 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.Types 6 | 7 | inline fun T.toJson( 8 | moshi: Moshi 9 | ): String = moshi 10 | .adapter(T::class.java).toJson(this) 11 | 12 | 13 | inline fun String.fromJson( 14 | moshi: Moshi 15 | ): T? = moshi 16 | .adapter(T::class.java).fromJson(this) 17 | 18 | 19 | inline fun String.toJsonObjectList( 20 | factory: JsonAdapter.Factory, 21 | customBuilder: Moshi.Builder = Moshi.Builder() 22 | ): List? { 23 | return Types.newParameterizedType(List::class.java, T::class.java).let { type -> 24 | customBuilder 25 | .add(factory).build() 26 | .adapter>(type).fromJson(this) 27 | } 28 | } 29 | 30 | 31 | inline fun String.toJsonObjectArrayList( 32 | factory: JsonAdapter.Factory, 33 | customBuilder: Moshi.Builder = Moshi.Builder() 34 | ): ArrayList? { 35 | return Types.newParameterizedType(ArrayList::class.java, T::class.java).let { type -> 36 | customBuilder 37 | .add(factory).build() 38 | .adapter>(type).fromJson(this) 39 | } 40 | } 41 | 42 | 43 | inline fun String.toJsonObjectMutableList( 44 | moshi: Moshi 45 | ): MutableList? { 46 | return Types.newParameterizedType(MutableList::class.java, T::class.java).let { type -> 47 | moshi.adapter>(type).fromJson(this) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/remote/api/middleware/DefaultInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.remote.api.middleware 2 | 3 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStore 4 | import kotlinx.coroutines.DelicateCoroutinesApi 5 | import kotlinx.coroutines.flow.first 6 | import kotlinx.coroutines.runBlocking 7 | import okhttp3.Interceptor 8 | import okhttp3.Request 9 | import okhttp3.Response 10 | import java.io.IOException 11 | import javax.inject.Inject 12 | 13 | @DelicateCoroutinesApi 14 | class DefaultInterceptor @Inject constructor( 15 | private val preferenceDataStore: PreferenceDataStore 16 | ) : Interceptor { 17 | 18 | @Throws(IOException::class) 19 | override fun intercept(chain: Interceptor.Chain): Response { 20 | 21 | val builder = initializeHeader(chain) 22 | val request = builder.build() 23 | 24 | return chain.proceed(request) 25 | } 26 | 27 | private fun initializeHeader(chain: Interceptor.Chain): Request.Builder { 28 | val originRequest = chain.request() 29 | val token = runBlocking { preferenceDataStore.token.first() } 30 | 31 | return originRequest.newBuilder() 32 | .header("Accept", "application/json") 33 | .addHeader("Cache-Control", "no-cache") 34 | .addHeader("Cache-Control", "no-store") 35 | .addHeader(KEY_TOKEN, TOKEN_TYPE + token) 36 | .method(originRequest.method, originRequest.body) 37 | } 38 | 39 | companion object { 40 | private const val TOKEN_TYPE = "Bearer " 41 | private const val KEY_TOKEN = "Authorization" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.search 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 5 | import com.thuanpx.view_mvvm_architecture.data.repository.AppRepository 6 | import com.thuanpx.view_mvvm_architecture.di.AppDispatchers 7 | import com.thuanpx.view_mvvm_architecture.di.Dispatcher 8 | import com.thuanpx.view_mvvm_architecture.model.entity.PokemonInfo 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | import kotlinx.coroutines.flow.* 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | /** 16 | * Created by ThuanPx on 4/3/22. 17 | */ 18 | @HiltViewModel 19 | class SearchViewModel @Inject constructor( 20 | private val appRepository: AppRepository, 21 | @Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, 22 | ) : BaseViewModel() { 23 | 24 | val query = MutableStateFlow("") 25 | val pokemonInfo = MutableStateFlow(PokemonInfo()) 26 | 27 | init { 28 | viewModelScope.launch { 29 | query 30 | .debounce(2000) 31 | .distinctUntilChanged() 32 | .filter { it.isNotBlank() } 33 | .flatMapLatest { query -> 34 | appRepository.fetchPokemonInfo(query) 35 | .catch { _error.emit(it) } 36 | } 37 | .flowOn(ioDispatcher) 38 | .collect { 39 | pokemonInfo.emit(it) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/data/repository/AppRepository.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.data.repository 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import com.thuanpx.view_mvvm_architecture.data.remote.api.ApiService 7 | import com.thuanpx.view_mvvm_architecture.data.remote.datasource.PokemonDataSource 8 | import com.thuanpx.view_mvvm_architecture.di.AppDispatchers 9 | import com.thuanpx.view_mvvm_architecture.di.Dispatcher 10 | import com.thuanpx.view_mvvm_architecture.model.entity.Pokemon 11 | import com.thuanpx.view_mvvm_architecture.model.entity.PokemonInfo 12 | import kotlinx.coroutines.CoroutineDispatcher 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.flow 15 | import kotlinx.coroutines.flow.flowOn 16 | import javax.inject.Inject 17 | 18 | /** 19 | * Created by ThuanPx on 17/09/2021. 20 | */ 21 | 22 | interface AppRepository { 23 | fun fetchPokemon(): Flow> 24 | fun fetchPokemonInfo(query: String): Flow 25 | } 26 | 27 | class DefaultAppRepository @Inject constructor( 28 | private val apiService: ApiService, 29 | @Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, 30 | ) : AppRepository { 31 | 32 | override fun fetchPokemon(): Flow> { 33 | return Pager(config = PagingConfig(20, enablePlaceholders = false), 34 | pagingSourceFactory = { PokemonDataSource(apiService) }).flow 35 | } 36 | 37 | override fun fetchPokemonInfo(query: String): Flow { 38 | return flow { emit(apiService.fetchPokemon(query)) } 39 | .flowOn(ioDispatcher) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/DataStoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 6 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 7 | import androidx.datastore.preferences.core.Preferences 8 | import androidx.datastore.preferences.core.emptyPreferences 9 | import androidx.datastore.preferences.preferencesDataStoreFile 10 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStore 11 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStoreDefault 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | /** 20 | * Created by ThuanPx on 4/1/22. 21 | */ 22 | 23 | @InstallIn(SingletonComponent::class) 24 | @Module 25 | object DataStoreModule { 26 | 27 | private const val DATA_STORE_FILE_NAME = "app_prefs.pb" 28 | 29 | @Singleton 30 | @Provides 31 | fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore { 32 | return PreferenceDataStoreFactory.create( 33 | corruptionHandler = ReplaceFileCorruptionHandler( 34 | produceNewData = { emptyPreferences() } 35 | ), 36 | produceFile = { appContext.preferencesDataStoreFile(DATA_STORE_FILE_NAME) } 37 | ) 38 | } 39 | 40 | @Singleton 41 | @Provides 42 | fun providePreferenceDataStore(dataStore: DataStore): PreferenceDataStore { 43 | return PreferenceDataStoreDefault(dataStore) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/network/RetrofitException.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.network 2 | 3 | import retrofit2.HttpException 4 | import retrofit2.Response 5 | import java.io.IOException 6 | 7 | 8 | class RetrofitException( 9 | /** The request URL which produced the error. */ 10 | val url: String?, 11 | /** Response object containing status code, headers, body, etc. */ 12 | val response: Response?, 13 | /** The event kind which triggered this error. */ 14 | val kind: Kind, 15 | val exception: Throwable? 16 | ) : RuntimeException(exception) { 17 | 18 | /** Identifies the event kind which triggered a [RetrofitException]. */ 19 | enum class Kind { 20 | /** An [IOException] occurred while communicating to the server. */ 21 | NETWORK, 22 | /** User login requirement **/ 23 | NO_AUTHENTICATOR, 24 | /** A non-200 HTTP status code was received from the server. */ 25 | HTTP, 26 | /** 27 | * An internal error occurred while attempting to execute a request. It is best practice to 28 | * re-throw this exception so your application crashes. 29 | */ 30 | UNEXPECTED 31 | } 32 | 33 | companion object { 34 | 35 | fun authenticatorError(): RetrofitException { 36 | return RetrofitException(null, null, Kind.NO_AUTHENTICATOR, null) 37 | } 38 | 39 | fun httpError(url: String, response: Response, throwable: HttpException): RetrofitException { 40 | return RetrofitException(url, response, Kind.HTTP, throwable) 41 | } 42 | 43 | fun networkError(exception: IOException): RetrofitException { 44 | return RetrofitException(null, null, Kind.NETWORK, exception) 45 | } 46 | 47 | fun unexpectedError(exception: Throwable): RetrofitException { 48 | return RetrofitException(null, null, Kind.UNEXPECTED, exception) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/KeyboardDetector.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils 2 | 3 | import android.view.View 4 | import android.view.ViewTreeObserver.OnGlobalLayoutListener 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleObserver 8 | import androidx.lifecycle.OnLifecycleEvent 9 | 10 | class KeyboardDetector( 11 | activity: AppCompatActivity, 12 | private val rootLayout: View, 13 | private val onChangedListener: (show: Boolean) -> Unit 14 | ) { 15 | private var keyboardListenersAttached = false 16 | private var originalHeight = 0 17 | private var isOpen = false 18 | 19 | private val keyboardLayoutListener = OnGlobalLayoutListener { 20 | val heightDiff = originalHeight - rootLayout.height 21 | val isOpen = heightDiff > 0 22 | if (this.isOpen == isOpen) return@OnGlobalLayoutListener 23 | this.isOpen = isOpen 24 | onChangedListener(isOpen) 25 | } 26 | 27 | init { 28 | activity.lifecycle.addObserver(ActivityObserver()) 29 | } 30 | 31 | private inner class ActivityObserver : LifecycleObserver { 32 | 33 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) 34 | fun connectListener() { 35 | rootLayout.post { 36 | if (keyboardListenersAttached) { 37 | return@post 38 | } 39 | originalHeight = rootLayout.height 40 | rootLayout.viewTreeObserver.addOnGlobalLayoutListener(keyboardLayoutListener) 41 | keyboardListenersAttached = true 42 | } 43 | } 44 | 45 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) 46 | fun disconnectListener() { 47 | if (keyboardListenersAttached) 48 | rootLayout.viewTreeObserver.removeOnGlobalLayoutListener(keyboardLayoutListener) 49 | keyboardListenersAttached = false 50 | } 51 | } 52 | 53 | fun isOpen() = isOpen 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/viewmodel/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.flow.* 5 | 6 | /** 7 | * Created by ThuanPx on 8/5/20. 8 | */ 9 | abstract class BaseViewModel : ViewModel() { 10 | 11 | private var loadingCount: Int = 0 12 | private val _isLoading = MutableStateFlow(false) 13 | val isLoading: StateFlow 14 | get() = _isLoading 15 | 16 | protected val _error = MutableSharedFlow() 17 | val error: SharedFlow 18 | get() = _error 19 | 20 | /** 21 | * To show loading manually, should call `hideLoading` after 22 | */ 23 | protected fun showLoading() { 24 | if (loadingCount == 0) { 25 | _isLoading.value = true 26 | } 27 | loadingCount++ 28 | } 29 | 30 | /** 31 | * To hide loading manually, should be called after `showLoading` 32 | */ 33 | protected fun hideLoading() { 34 | loadingCount-- 35 | if (loadingCount == 0) { 36 | _isLoading.value = false 37 | } 38 | } 39 | 40 | protected suspend inline fun Flow.async( 41 | isRefresh: MutableStateFlow, 42 | crossinline action: suspend (T) -> Unit 43 | ) { 44 | this.onStart { isRefresh.emit(true) } 45 | .onCompletion { isRefresh.emit(false) } 46 | .catch { _error.emit(it) } 47 | .collect { 48 | action.invoke(it) 49 | } 50 | } 51 | 52 | protected suspend inline infix fun Flow.async( 53 | crossinline action: suspend (T) -> Unit 54 | ) { 55 | this.onStart { showLoading() } 56 | .onCompletion { hideLoading() } 57 | .catch { _error.emit(it) } 58 | .collect { 59 | action.invoke(it) 60 | } 61 | } 62 | 63 | protected suspend inline fun Flow.async() { 64 | this.onStart { showLoading() } 65 | .onCompletion { hideLoading() } 66 | .catch { _error.emit(it) } 67 | .collect {} 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/widget/KeyboardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.mvvm_architecture.widget 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.inputmethod.InputMethodManager 10 | import android.widget.EditText 11 | 12 | @SuppressLint("ClickableViewAccessibility") 13 | object KeyboardUtils { 14 | fun setupKeyBoard(view: View) { 15 | if (view !is EditText || !view.isFocusable()) { 16 | view.setOnTouchListener { arg0: View?, _: MotionEvent? -> 17 | hideSoftKeyboard(arg0) 18 | false 19 | } 20 | } 21 | if (view is ViewGroup) { 22 | for (i in 0 until view.childCount) { 23 | val innerView = view.getChildAt(i) 24 | setupKeyBoard(innerView) 25 | } 26 | } 27 | } 28 | 29 | fun isKeyBoard(isShow: Boolean): Boolean { 30 | return isShow 31 | } 32 | 33 | fun setupKeyBoard(activity: Activity) { 34 | setupKeyBoard(activity.window.decorView.rootView) 35 | } 36 | 37 | fun showSoftKeyboard(activity: Activity) { 38 | showSoftKeyboard(activity.window.decorView.rootView) 39 | } 40 | 41 | fun hideSoftKeyboard(activity: Activity) { 42 | hideSoftKeyboard(activity.window.decorView.rootView) 43 | } 44 | 45 | fun showSoftKeyboard(view: View?) { 46 | if (view != null) { 47 | val inputManager = view.context 48 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 49 | inputManager.toggleSoftInput( 50 | InputMethodManager.HIDE_IMPLICIT_ONLY, 51 | 0 52 | ) 53 | } 54 | } 55 | 56 | fun hideSoftKeyboard(view: View?) { 57 | if (view != null) { 58 | val inputManager = view.context 59 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 60 | inputManager.hideSoftInputFromWindow(view.windowToken, 0) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/view/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.view 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | import android.widget.TextView 7 | import androidx.annotation.FontRes 8 | import androidx.core.content.res.ResourcesCompat 9 | 10 | /** 11 | * Created by ThuanPx 12 | */ 13 | 14 | fun View.show(isShow: Boolean = true) { 15 | visibility = if (isShow) View.VISIBLE else View.INVISIBLE 16 | } 17 | 18 | fun View.show() { 19 | visibility = View.VISIBLE 20 | } 21 | 22 | fun View.hide() { 23 | visibility = View.INVISIBLE 24 | } 25 | 26 | fun View.gone(isGone: Boolean = true) { 27 | visibility = if (isGone) View.GONE else View.VISIBLE 28 | } 29 | 30 | fun View.gone() { 31 | visibility = View.GONE 32 | } 33 | 34 | inline fun View.clicks(coolDown: Long = 1000L, crossinline action: (view: View) -> Unit) { 35 | setOnClickListener(object : View.OnClickListener { 36 | var lastTime = 0L 37 | override fun onClick(v: View) { 38 | val now = System.currentTimeMillis() 39 | if (now - lastTime > coolDown) { 40 | action(v) 41 | lastTime = now 42 | } 43 | } 44 | }) 45 | } 46 | 47 | fun View.showKeyboard() { 48 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 49 | this.requestFocus() 50 | imm.showSoftInput(this, 0) 51 | } 52 | 53 | /** 54 | * Try to hide the keyboard and returns whether it worked 55 | * https://stackoverflow.com/questions/1109022/close-hide-the-android-soft-keyboard 56 | */ 57 | fun View.hideKeyboard(): Boolean { 58 | try { 59 | val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 60 | return inputMethodManager.hideSoftInputFromWindow(windowToken, 0) 61 | } catch (ignored: RuntimeException) { } 62 | return false 63 | } 64 | 65 | /** 66 | * Extension method to set font for TextView. 67 | */ 68 | fun TextView.setFont(@FontRes font: Int) { 69 | typeface = ResourcesCompat.getFont(this.context, font) 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/network/AppErrors.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.network 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import okhttp3.ResponseBody 6 | import retrofit2.HttpException 7 | import timber.log.Timber 8 | 9 | object AppErrors { 10 | 11 | private val adapter = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build().adapter( 12 | ErrorJson::class.java) 13 | 14 | @JvmStatic 15 | fun from(throwable: Throwable?): NetworkError? { 16 | if (throwable is HttpException) { 17 | val errorBody = throwable.response()?.errorBody() ?: return null 18 | try { 19 | val error = adapter.fromJson(errorBody.string()) 20 | if (error != null) { 21 | return NetworkError(error.error_code, error.message, error.status) 22 | } 23 | } catch (e: Exception) { 24 | Timber.e( e.message ?: "") 25 | } 26 | } 27 | return null 28 | } 29 | 30 | @JvmStatic 31 | fun fromThrowable(throwable: Throwable?): NetworkError? { 32 | var errorBody: ResponseBody? = null 33 | var httpCode = String() 34 | 35 | when (throwable) { 36 | is HttpException -> { 37 | errorBody = throwable.response()?.errorBody() ?: return null 38 | httpCode = throwable.code().toString() 39 | } 40 | is RetrofitException -> { 41 | errorBody = throwable.response?.errorBody() ?: return null 42 | httpCode = throwable.response.code().toString() 43 | } 44 | } 45 | try { 46 | errorBody?.let { body -> 47 | val error = adapter.fromJson(body.string()) 48 | if (error != null) { 49 | val code: String = error.error_code ?: httpCode 50 | val message = error.message ?: error.error 51 | return NetworkError(code, message, error.status) 52 | } 53 | } 54 | } catch (e: Exception) { 55 | Timber.e( e.message ?: "") 56 | } 57 | 58 | return null 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_pokemon.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 21 | 22 | 31 | 32 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/recyclerView/RecyclerViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.recyclerView 2 | 3 | import android.view.MotionEvent 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | /** 8 | * Created by ThuanPx on 8/15/20. 9 | */ 10 | 11 | val RecyclerView.Adapter.isEmpty: Boolean where VH : RecyclerView.ViewHolder 12 | get() = itemCount == 0 13 | 14 | val RecyclerView.Adapter.isNotEmpty: Boolean where VH : RecyclerView.ViewHolder 15 | get() = !isEmpty 16 | 17 | /** 18 | * Disable all user input to a recyclerview, passing touch events out 19 | */ 20 | fun RecyclerView.disableTouch() { 21 | this.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() { 22 | override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { 23 | return true 24 | } 25 | }) 26 | } 27 | 28 | /**Set adapter of recyclerView 29 | * @param yourAdapter your adapter(must extend RecyclerView.Adapter) 30 | * @param layoutOrientation LinearLayoutManager orientation of adapter, default is RecyclerView.VERTICAL 31 | * @param fixedSize isFixed size of recyclerView, default is true*/ 32 | fun > RecyclerView.initRecyclerViewAdapter( 33 | yourAdapter: T?, 34 | layoutOrientation: Int = RecyclerView.VERTICAL, 35 | fixedSize: Boolean = false, 36 | reverseLayout: Boolean = false 37 | ) { 38 | apply { 39 | layoutManager = LinearLayoutManager(context, layoutOrientation, reverseLayout) 40 | adapter = yourAdapter 41 | setHasFixedSize(fixedSize) 42 | } 43 | } 44 | 45 | /**Set adapter of recyclerView 46 | * @param yourAdapter your adapter(must extend RecyclerView.Adapter) 47 | * @param yourLayoutManager Pass your own layout manager 48 | * @param fixedSize isFixed size of recyclerView, default is true*/ 49 | fun > RecyclerView.initRecyclerViewAdapter( 50 | yourAdapter: T?, 51 | yourLayoutManager: RecyclerView.LayoutManager, 52 | fixedSize: Boolean = false 53 | ) { 54 | apply { 55 | layoutManager = yourLayoutManager 56 | adapter = yourAdapter 57 | setHasFixedSize(fixedSize) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.LoadState 6 | import androidx.recyclerview.widget.GridLayoutManager 7 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 8 | import com.thuanpx.view_mvvm_architecture.base.fragment.BaseFragment 9 | import com.thuanpx.view_mvvm_architecture.databinding.FragmentHomeBinding 10 | import com.thuanpx.view_mvvm_architecture.utils.extension.context.launchAndRepeatWithViewLifecycle 11 | import com.thuanpx.view_mvvm_architecture.utils.extension.recyclerView.initRecyclerViewAdapter 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.launch 15 | 16 | /** 17 | * Created by ThuanPx on 8/8/20. 18 | */ 19 | 20 | @AndroidEntryPoint 21 | class HomeFragment : BaseFragment(HomeViewModel::class) { 22 | 23 | private var homeAdapter: HomeAdapter? = null 24 | 25 | override fun inflateViewBinding( 26 | inflater: LayoutInflater, 27 | container: ViewGroup? 28 | ): FragmentHomeBinding { 29 | return FragmentHomeBinding.inflate(inflater, container, false) 30 | } 31 | 32 | override fun initialize() { 33 | initRecyclerView() 34 | } 35 | 36 | override fun onSubscribeObserver() { 37 | super.onSubscribeObserver() 38 | with(viewModel) { 39 | launchAndRepeatWithViewLifecycle { 40 | launch { 41 | pagingPokemonFlow.collectLatest { 42 | homeAdapter?.submitData(it) 43 | } 44 | } 45 | launch { 46 | homeAdapter?.loadStateFlow?.collectLatest { loadStates -> 47 | if (loadStates.refresh is LoadState.Error) { 48 | (activity as? BaseActivity<*, *>)?.handleApiError( 49 | (loadStates.refresh as? LoadState.Error)?.error ?: return@collectLatest 50 | ) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | private fun initRecyclerView() { 59 | homeAdapter = HomeAdapter() 60 | viewBinding.rvHome.initRecyclerViewAdapter( 61 | homeAdapter, 62 | GridLayoutManager(requireContext(), 2), 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/string/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.string 2 | 3 | import android.util.Patterns 4 | import com.thuanpx.view_mvvm_architecture.app.Constant 5 | import java.text.ParseException 6 | import java.text.SimpleDateFormat 7 | import java.util.Date 8 | import java.util.Locale 9 | import java.util.TimeZone 10 | import java.util.regex.Pattern 11 | 12 | @Throws(ParseException::class) 13 | fun String.toDate(format: String): Date { 14 | val parser = SimpleDateFormat(format, Locale.getDefault()) 15 | return parser.parse(this) 16 | } 17 | 18 | fun String?.isBlank(): Boolean { 19 | return this == null || isEmpty() 20 | } 21 | 22 | @Throws(ParseException::class) 23 | fun String.toDateWithFormat(inputFormat: String, outputFormat: String): String { 24 | val gmtTimeZone = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC) 25 | val inputDateTimeFormat = SimpleDateFormat(inputFormat, Locale.getDefault()) 26 | inputDateTimeFormat.timeZone = gmtTimeZone 27 | 28 | val outputDateTimeFormat = SimpleDateFormat(outputFormat, Locale.getDefault()) 29 | outputDateTimeFormat.timeZone = gmtTimeZone 30 | return outputDateTimeFormat.format(inputDateTimeFormat.parse(this)) 31 | } 32 | 33 | @Throws(ParseException::class) 34 | fun String.toDateWithFormat( 35 | inputFormat: String, 36 | outputFormat: String, 37 | outputTimeZone: TimeZone = TimeZone.getDefault() 38 | ): String { 39 | val gmtTimeZone = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC) 40 | val inputDateTimeFormat = SimpleDateFormat(inputFormat, Locale.getDefault()) 41 | inputDateTimeFormat.timeZone = gmtTimeZone 42 | 43 | val outputDateTimeFormat = SimpleDateFormat(outputFormat, Locale.getDefault()) 44 | outputDateTimeFormat.timeZone = outputTimeZone 45 | return outputDateTimeFormat.format(inputDateTimeFormat.parse(this)) 46 | } 47 | 48 | fun String.validWithPattern(pattern: Pattern): Boolean { 49 | return pattern.matcher(toLowerCase()).find() 50 | } 51 | 52 | fun String.validWithPattern(regex: String): Boolean { 53 | return Pattern.compile(regex).matcher(this).find() 54 | } 55 | 56 | fun String.removeWhitespaces(): String { 57 | return this.replace("[\\s-]*".toRegex(), "") 58 | } 59 | 60 | fun String.toIntOrZero() = if (this.toIntOrNull() == null) 0 else this.toInt() 61 | 62 | fun String.isNumeric(): Boolean = this matches "-?\\d+(\\.\\d+)?".toRegex() 63 | 64 | fun String.containsWebUrl() = Patterns.WEB_URL.matcher(this).find() 65 | 66 | fun String?.nullToEmpty(): String = this ?: "" 67 | 68 | fun String?.isNullOrZero() = this == "0" || this == null 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/recyclerview/EndlessRecyclerOnScrollListener.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.recyclerview 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.RecyclerView 6 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 7 | 8 | abstract class EndlessRecyclerOnScrollListener : RecyclerView.OnScrollListener() { 9 | 10 | open fun onEndlessAtTop() {} 11 | open fun onEndlessAtBottom(){} 12 | 13 | private var lastVisibleItemPosition = -1 14 | private var firstVisibleItemPosition = -1 15 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 16 | super.onScrolled(recyclerView, dx, dy) 17 | when (val layoutManager = recyclerView.layoutManager) { 18 | is StaggeredGridLayoutManager -> { 19 | val lastVisibleItemPositions = layoutManager.findLastVisibleItemPositions(null) 20 | // get maximum element within the list 21 | lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions) 22 | } 23 | is GridLayoutManager -> { 24 | lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() 25 | } 26 | is LinearLayoutManager -> { 27 | lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() 28 | firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() 29 | } 30 | } 31 | } 32 | 33 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 34 | super.onScrollStateChanged(recyclerView, newState) 35 | val layoutManager = recyclerView.layoutManager 36 | val totalItemCount = layoutManager!!.itemCount 37 | val visibleItemCount = layoutManager.childCount 38 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 39 | if (visibleItemCount > 0 && lastVisibleItemPosition >= totalItemCount - 1) { 40 | onEndlessAtBottom() 41 | } 42 | if (firstVisibleItemPosition == 0) { 43 | onEndlessAtTop() 44 | } 45 | } 46 | } 47 | 48 | 49 | private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int { 50 | var maxSize = 0 51 | for (i in lastVisibleItemPositions.indices) { 52 | if (i == 0) { 53 | maxSize = lastVisibleItemPositions[i] 54 | } else if (lastVisibleItemPositions[i] > maxSize) { 55 | maxSize = lastVisibleItemPositions[i] 56 | } 57 | } 58 | return maxSize 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/recyclerview/BaseRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.recyclerview 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | /** 9 | * Created by ThuanPx on 8/5/20. 10 | */ 11 | 12 | abstract class BaseRecyclerViewAdapter( 13 | private var dataList: MutableList = mutableListOf() 14 | ) : RecyclerView.Adapter() { 15 | 16 | protected var itemClickListener: ((T) -> Unit)? = null 17 | 18 | private var handler = Handler(Looper.getMainLooper()) 19 | 20 | override fun getItemCount(): Int { 21 | return dataList.size 22 | } 23 | 24 | open fun getItem(position: Int): T? { 25 | return if (position < 0 || position >= dataList.size) { 26 | null 27 | } else dataList[position] 28 | } 29 | 30 | fun getData(): MutableList { 31 | return dataList 32 | } 33 | 34 | fun updateData(newData: MutableList?, diffUtilCallback: DiffUtil.Callback) { 35 | handler.post { 36 | newData?.let { 37 | val diffResult = DiffUtil.calculateDiff(diffUtilCallback) 38 | dataList = it 39 | diffResult.dispatchUpdatesTo(this) 40 | } 41 | } 42 | } 43 | 44 | fun addData(newData: MutableList?) { 45 | handler.post { 46 | newData?.let { 47 | dataList.addAll(it) 48 | notifyDataSetChanged() 49 | } 50 | } 51 | } 52 | 53 | fun replaceData(newData: MutableList?) { 54 | handler.post { 55 | newData?.let { 56 | dataList = it 57 | notifyDataSetChanged() 58 | } 59 | } 60 | } 61 | 62 | fun clearData(isNotify: Boolean = true) { 63 | dataList.clear() 64 | if (isNotify) notifyDataSetChanged() 65 | } 66 | 67 | fun addItem(data: T, position: Int) { 68 | dataList.add(position, data) 69 | notifyItemInserted(position) 70 | } 71 | 72 | fun removeItem(position: Int, isNotifyAll: Boolean = false) { 73 | if (position < 0 || position >= dataList.size) { 74 | return 75 | } 76 | dataList.removeAt(position) 77 | if (isNotifyAll) notifyDataSetChanged() else notifyItemChanged(position) 78 | } 79 | 80 | fun replaceItem(item: T, position: Int, isNotifyAll: Boolean = false) { 81 | if (position < 0 || position >= dataList.size) { 82 | return 83 | } 84 | dataList[position] = item 85 | if (isNotifyAll) notifyDataSetChanged() else notifyItemChanged(position) 86 | } 87 | 88 | fun registerItemClickListener(onItemClickListener: (T) -> Unit) { 89 | itemClickListener = onItemClickListener 90 | } 91 | 92 | fun unRegisterItemClickListener() { 93 | itemClickListener = null 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.di 2 | 3 | import android.app.Application 4 | import com.squareup.moshi.Moshi 5 | import com.thuanpx.view_mvvm_architecture.BuildConfig 6 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStore 7 | import com.thuanpx.view_mvvm_architecture.data.remote.api.ApiService 8 | import com.thuanpx.view_mvvm_architecture.data.remote.api.middleware.DefaultInterceptor 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import okhttp3.Cache 14 | import okhttp3.Interceptor 15 | import okhttp3.OkHttpClient 16 | import okhttp3.logging.HttpLoggingInterceptor 17 | import retrofit2.Retrofit 18 | import retrofit2.converter.moshi.MoshiConverterFactory 19 | import java.util.concurrent.TimeUnit 20 | import javax.inject.Singleton 21 | 22 | /** 23 | * Created by ThuanPx on 8/7/20. 24 | */ 25 | 26 | @Module 27 | @InstallIn(SingletonComponent::class) 28 | object NetworkModule { 29 | 30 | @Singleton 31 | @Provides 32 | fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit { 33 | return Retrofit.Builder() 34 | .baseUrl(BuildConfig.END_POINT) 35 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 36 | .client(okHttpClient) 37 | .build() 38 | } 39 | 40 | @Singleton 41 | @Provides 42 | fun provideOkHttpCache(app: Application): Cache { 43 | val cacheSize: Long = 10 * 1024 * 1024 // 10 MiB 44 | return Cache(app.cacheDir, cacheSize) 45 | } 46 | 47 | @Singleton 48 | @Provides 49 | fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient { 50 | val httpClientBuilder = OkHttpClient.Builder() 51 | httpClientBuilder.cache(cache) 52 | httpClientBuilder.addInterceptor(interceptor) 53 | 54 | httpClientBuilder.readTimeout( 55 | READ_TIMEOUT, TimeUnit.SECONDS 56 | ) 57 | httpClientBuilder.writeTimeout( 58 | WRITE_TIMEOUT, TimeUnit.SECONDS 59 | ) 60 | httpClientBuilder.connectTimeout( 61 | CONNECTION_TIMEOUT, TimeUnit.SECONDS 62 | ) 63 | 64 | if (BuildConfig.DEBUG) { 65 | val logging = HttpLoggingInterceptor() 66 | httpClientBuilder.addInterceptor(logging) 67 | logging.level = HttpLoggingInterceptor.Level.BODY 68 | } 69 | 70 | return httpClientBuilder.build() 71 | } 72 | 73 | @Singleton 74 | @Provides 75 | fun provideInterceptor(preferenceDataStore: PreferenceDataStore): Interceptor { 76 | return DefaultInterceptor(preferenceDataStore) 77 | } 78 | 79 | @Singleton 80 | @Provides 81 | fun provideApi(retrofit: Retrofit): ApiService { 82 | return retrofit.create(ApiService::class.java) 83 | } 84 | 85 | private const val READ_TIMEOUT: Long = 30 86 | private const val WRITE_TIMEOUT: Long = 30 87 | private const val CONNECTION_TIMEOUT: Long = 30 88 | } 89 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | License 3 | API 4 |

5 | 6 |

7 | Base project Android with Hilt, Coroutines, Flow, Jetpack (Room, ViewModel), and Material Design based on MVVM architecture. 8 |

9 |

10 | 11 |

12 |
13 | 14 | # Tech stack & Open-source libraries 15 | - Minimum SDK level 21 16 | - [Kotlin](https://kotlinlang.org/) based, [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous. 17 | - [Hilt](https://dagger.dev/hilt/) for dependency injection. 18 | - Jetpack 19 | - Lifecycle - Observe Android lifecycles and handle UI states upon the lifecycle changes. 20 | - ViewModel - Manages UI-related data holder and lifecycle aware. Allows data to survive configuration changes such as screen rotations. 21 | - ViewBinding - Is a feature that allows you to more easily write code that interacts with views. Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views that have an ID in the corresponding layout. 22 | - Room Persistence - Constructs Database by providing an abstraction layer over SQLite to allow fluent database access. 23 | - Architecture 24 | - MVVM Architecture (Model - View - ViewModel) 25 | - [Repository Pattern](https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern#0) 26 | - [Retrofit2 & OkHttp3](https://github.com/square/retrofit) - Construct the REST APIs. 27 | - [Sandwich](https://github.com/skydoves/Sandwich) - Construct a lightweight and modern response interface to handle network payload for Android. 28 | - [Gson](https://github.com/google/gson) - A modern JSON library for Kotlin and Java. 29 | - [Glide](https://github.com/bumptech/glide) - Loading images from network. 30 | - [Timber](https://github.com/JakeWharton/timber) - A logger with a small, extensible API. 31 | - [Material-Components](https://github.com/material-components/material-components-android) - Material design components for building ripple animation, and CardView. 32 | 33 | # License 34 | ```xml 35 | Designed and developed by 2022 ThuanPx 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | ``` -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/DataResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.thuanpx.view_mvvm_architecture.utils 18 | 19 | import androidx.lifecycle.LiveData 20 | 21 | /** 22 | * A generic class that holds a value with its loading status. 23 | * @param 24 | */ 25 | sealed class DataResult { 26 | 27 | data class Success(val data: T) : DataResult() 28 | data class Error(val exception: Exception) : DataResult() 29 | 30 | inline fun executeIfSucceed(block: (data: R) -> Unit): DataResult { 31 | if (this is Success) block(this.data) 32 | return this 33 | } 34 | 35 | inline fun executeIfFailed(block: (ex: Exception) -> Unit): DataResult { 36 | if (this is Error) block(this.exception) 37 | return this 38 | } 39 | 40 | inline fun map(block: (R) -> M): DataResult { 41 | return when (this) { 42 | is Success -> Success(block(data)) 43 | is Error -> Error(exception) 44 | } 45 | } 46 | 47 | inline fun mapWithoutResult(success: (R) -> M): M? { 48 | return when (this) { 49 | is Success -> success(data) 50 | else -> null // Ignore loading and failed for synchronize code 51 | } 52 | } 53 | 54 | /** 55 | * Get data of result by status 56 | * If succeeded return the data 57 | * else return null 58 | */ 59 | fun getResultData(): R? = when (this) { 60 | is Success -> data 61 | else -> null // Ignore loading and failed for synchronize code 62 | } 63 | 64 | fun isCompleted() = this is Success || this is Error 65 | 66 | override fun toString(): String { 67 | return when (this) { 68 | is Success<*> -> "Success[data=$data]" 69 | is Error -> "Error[exception=$exception]" 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * `true` if [DataResult] is of type [Success] & holds non-null [Success.data]. 76 | */ 77 | val DataResult<*>.succeeded get() = this is DataResult.Success && data != null 78 | 79 | val LiveData>.dataOfResult: T? get() = if (value is DataResult.Success) (value as DataResult.Success).data else null 80 | 81 | /** 82 | * A observable list items include success case and error case 83 | */ 84 | typealias LiveResultItems = LiveData>> 85 | 86 | /** 87 | * A observable a item include success case and error case 88 | */ 89 | typealias LiveResult = LiveData> 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/BaseDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.thuanpx.view_mvvm_architecture.model.response.BaseResponse 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.flow.collect 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.flow.flowOn 11 | import retrofit2.HttpException 12 | import java.io.IOException 13 | 14 | /** 15 | * Created by ThuanPx on 4/3/22. 16 | */ 17 | 18 | abstract class BaseDataSource : PagingSource() { 19 | 20 | companion object { 21 | private const val STARTING_PAGE_INDEX = 0 22 | private const val LOAD_DELAY_MILLIS = 2_000L 23 | } 24 | 25 | abstract suspend fun requestMore(nextPage: Int): BaseResponse> 26 | 27 | override suspend fun load(params: LoadParams): LoadResult { 28 | 29 | val pageNumber = params.key ?: STARTING_PAGE_INDEX 30 | if (pageNumber != STARTING_PAGE_INDEX) delay(LOAD_DELAY_MILLIS) 31 | 32 | return try { 33 | var prevKey: Int? = null 34 | var nextKey: Int? = null 35 | var items = listOf() 36 | flow>> { 37 | items = requestMore(nextPage = pageNumber).data ?: emptyList() 38 | // Since 0 is the lowest page number, return null to signify no more pages should 39 | prevKey = if (pageNumber == STARTING_PAGE_INDEX) null else pageNumber.minus(1) 40 | // data, we return `null` to signify no more pages should be loaded 41 | nextKey = if (items.isNotEmpty()) pageNumber + 1 else null 42 | } 43 | .flowOn(Dispatchers.IO) 44 | .collect() 45 | 46 | LoadResult.Page( 47 | data = items, 48 | prevKey = prevKey, 49 | nextKey = nextKey 50 | ) 51 | } catch (e: IOException) { 52 | // IOException for network failures. 53 | return LoadResult.Error(e) 54 | } catch (e: HttpException) { 55 | // HttpException for any non-2xx HTTP status codes. 56 | return LoadResult.Error(e) 57 | } catch (e: Throwable) { 58 | return LoadResult.Error(e) 59 | } 60 | } 61 | 62 | override fun getRefreshKey(state: PagingState): Int? { 63 | // Try to find the page key of the closest page to anchorPosition, from 64 | // either the prevKey or the nextKey, but you need to handle nullability 65 | // here: 66 | // * prevKey == null -> anchorPage is the first page. 67 | // * nextKey == null -> anchorPage is the last page. 68 | // * both prevKey and nextKey null -> anchorPage is the initial page, so 69 | // just return null. 70 | return state.anchorPosition?.let { anchorPosition -> 71 | val anchorPage = state.closestPageToPosition(anchorPosition) 72 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 22 | 23 | 29 | 30 | 37 | 38 | 58 | 59 | 60 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.createViewModelLazy 9 | import androidx.lifecycle.Lifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.lifecycle.repeatOnLifecycle 12 | import androidx.viewbinding.ViewBinding 13 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 14 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 15 | import com.thuanpx.view_mvvm_architecture.feature.MainActivity 16 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.launch 19 | import kotlin.reflect.KClass 20 | 21 | /** 22 | * Created by ThuanPx on 8/5/20. 23 | * 24 | * @viewModel -> view model 25 | * @viewModelClass -> class view model 26 | * @viewBinding -> class binding 27 | * @initialize -> init UI, adapter, listener... 28 | * @onSubscribeObserver -> subscribe observer live data 29 | * 30 | */ 31 | 32 | abstract class BaseFragment(viewModelClass: KClass) : 33 | Fragment() { 34 | 35 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore }) 36 | private var _viewBinding: viewBinding? = null 37 | protected val viewBinding get() = _viewBinding!! // ktlint-disable 38 | 39 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding 40 | 41 | protected var progressDialog: ProgressDialog? = null 42 | 43 | fun getMainActivity(): MainActivity? = activity as? MainActivity 44 | 45 | protected abstract fun initialize() 46 | 47 | override fun onCreateView( 48 | inflater: LayoutInflater, 49 | container: ViewGroup?, 50 | savedInstanceState: Bundle? 51 | ): View? { 52 | _viewBinding = inflateViewBinding(inflater, container) 53 | return viewBinding.root 54 | } 55 | 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 | super.onViewCreated(view, savedInstanceState) 58 | progressDialog = ProgressDialog(requireContext()) 59 | initialize() 60 | onSubscribeObserver() 61 | } 62 | 63 | /** 64 | * Fragments outlive their views. Make sure you clean up any references to 65 | * the binding class instance in the fragment's onDestroyView() method. 66 | */ 67 | override fun onDestroyView() { 68 | super.onDestroyView() 69 | _viewBinding = null 70 | } 71 | 72 | fun showLoading(isShow: Boolean) { 73 | (activity as? BaseActivity<*, *>)?.showLoading(isShow) 74 | } 75 | 76 | open fun onSubscribeObserver() { 77 | viewModel.run { 78 | isLoading.launchAndCollect { 79 | showLoading(it) 80 | } 81 | error.launchAndCollect { 82 | (activity as? BaseActivity<*, *>)?.handleApiError(it) 83 | } 84 | } 85 | } 86 | 87 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) { 88 | viewLifecycleOwner.lifecycleScope.launch { 89 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 90 | collect { action(it) } 91 | } 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.feature 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.activity.OnBackPressedCallback 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.viewpager2.widget.ViewPager2 8 | import com.thuanpx.mvvm_architecture.widget.KeyboardUtils.hideSoftKeyboard 9 | import com.thuanpx.view_mvvm_architecture.R 10 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 11 | import com.thuanpx.view_mvvm_architecture.databinding.ActivityMainBinding 12 | import dagger.hilt.android.AndroidEntryPoint 13 | 14 | @AndroidEntryPoint 15 | class MainActivity : BaseActivity(MainViewModel::class) { 16 | 17 | companion object { 18 | const val TAB1 = 0 19 | const val TAB2 = 1 20 | } 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | } 25 | 26 | override fun inflateViewBinding(inflater: LayoutInflater): ActivityMainBinding { 27 | return ActivityMainBinding.inflate(inflater) 28 | } 29 | 30 | override fun initialize() { 31 | onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) { 32 | override fun handleOnBackPressed() { 33 | if (viewBinding.viewPager.currentItem != 0) { 34 | viewBinding.viewPager.setCurrentItem(0, false) 35 | } else { 36 | finish() 37 | } 38 | } 39 | 40 | }) 41 | initBottomNav() 42 | initViewPager() 43 | } 44 | 45 | private fun initViewPager() { 46 | viewBinding.run { 47 | viewPager.apply { 48 | isUserInputEnabled = false 49 | adapter = MainViewPagerAdapter(this@MainActivity) 50 | offscreenPageLimit = 3 51 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { 52 | override fun onPageSelected(position: Int) { 53 | super.onPageSelected(position) 54 | selectedBottomNav(position) 55 | } 56 | }) 57 | } 58 | } 59 | } 60 | 61 | private fun selectedBottomNav(position: Int) { 62 | when (position) { 63 | 0 -> { 64 | viewBinding.bottomNav.post { 65 | viewBinding.bottomNav.menu.findItem(R.id.tab1).isChecked = true 66 | } 67 | } 68 | 1 -> { 69 | viewBinding.bottomNav.post { 70 | viewBinding.bottomNav.menu.findItem(R.id.tab2).isChecked = true 71 | } 72 | } 73 | } 74 | viewBinding.viewPager.setCurrentItem(position, false) 75 | } 76 | 77 | private fun initBottomNav() { 78 | viewBinding.run { 79 | 80 | bottomNav.setOnItemSelectedListener { item -> 81 | hideSoftKeyboard(bottomNav) 82 | when (item.itemId) { 83 | R.id.tab1 -> { 84 | viewBinding.viewPager.setCurrentItem(0, false) 85 | } 86 | 87 | R.id.tab2 -> { 88 | viewBinding.viewPager.setCurrentItem(1, false) 89 | } 90 | 91 | } 92 | return@setOnItemSelectedListener true 93 | } 94 | } 95 | } 96 | 97 | override fun onSubscribeObserver() { 98 | super.onSubscribeObserver() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.ViewModelLazy 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.lifecycle.repeatOnLifecycle 10 | import androidx.viewbinding.ViewBinding 11 | import com.thuanpx.view_mvvm_architecture.base.network.AppErrors 12 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 13 | import com.thuanpx.view_mvvm_architecture.utils.extension.boolean.isNotTrue 14 | import com.thuanpx.view_mvvm_architecture.utils.extension.boolean.isTrue 15 | import com.thuanpx.view_mvvm_architecture.utils.extension.string.toIntOrZero 16 | import com.thuanpx.view_mvvm_architecture.utils.extension.widget.dialog 17 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog 18 | import kotlinx.coroutines.flow.Flow 19 | import kotlinx.coroutines.launch 20 | import java.net.HttpURLConnection 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * Created by ThuanPx on 8/5/20. 25 | * 26 | * @viewModel -> view model 27 | * @viewModelClass -> class view model 28 | * @viewBinding -> class binding 29 | * @initialize -> init UI, adapter, listener... 30 | * @onSubscribeObserver -> subscribe observer 31 | * 32 | */ 33 | 34 | abstract class BaseActivity(viewModelClass: KClass) : 35 | AppCompatActivity() { 36 | 37 | protected val viewModel by ViewModelLazy( 38 | viewModelClass, 39 | { viewModelStore }, 40 | { defaultViewModelProviderFactory }, 41 | { this.defaultViewModelCreationExtras }) 42 | protected lateinit var viewBinding: viewBinding 43 | abstract fun inflateViewBinding(inflater: LayoutInflater): viewBinding 44 | 45 | protected var progressDialog: ProgressDialog? = null 46 | 47 | protected abstract fun initialize() 48 | 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | viewBinding = inflateViewBinding(layoutInflater) 52 | progressDialog = ProgressDialog(this) 53 | setContentView(viewBinding.root) 54 | initialize() 55 | onSubscribeObserver() 56 | } 57 | 58 | fun showLoading(isShow: Boolean) { 59 | if (isShow && progressDialog?.isShowing.isNotTrue()) { 60 | progressDialog?.show() 61 | } else if (progressDialog?.isShowing.isTrue()) { 62 | progressDialog?.dismiss() 63 | } 64 | } 65 | 66 | open fun onSubscribeObserver() { 67 | viewModel.run { 68 | isLoading.launchAndCollect { 69 | showLoading(it) 70 | } 71 | error.launchAndCollect { 72 | handleApiError(it) 73 | } 74 | } 75 | } 76 | 77 | fun handleApiError(throwable: Throwable) { 78 | val networkError = AppErrors.fromThrowable(throwable) 79 | if (networkError?.errorCode?.toIntOrZero() == HttpURLConnection.HTTP_UNAUTHORIZED) { 80 | dialog { 81 | message = networkError.message ?: throwable.message ?: "Unknown" 82 | } 83 | return 84 | } 85 | dialog { 86 | message = networkError?.message ?: throwable.message ?: "Unknown" 87 | } 88 | } 89 | 90 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) { 91 | with(this) { 92 | lifecycleScope.launch { 93 | repeatOnLifecycle(Lifecycle.State.STARTED) { 94 | collect { action(it) } 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/TimeAgoExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.text.format.DateUtils 6 | import com.thuanpx.view_mvvm_architecture.R 7 | import java.text.ParseException 8 | import java.text.SimpleDateFormat 9 | import java.time.Instant 10 | import java.time.LocalDateTime 11 | import java.time.ZoneId 12 | import java.time.format.DateTimeFormatter 13 | import java.util.Date 14 | import java.util.TimeZone 15 | import kotlin.math.abs 16 | 17 | private const val SECOND_MILLIS = 1000 18 | private const val MINUTE_MILLIS = 60 * SECOND_MILLIS 19 | private const val HOUR_MILLIS = 60 * MINUTE_MILLIS 20 | private const val DAY_MILLIS = 24 * HOUR_MILLIS 21 | 22 | @Throws(ParseException::class) 23 | fun getLocalTime(timestamp: String?, simpleDateFormat: String?): Date? { 24 | val formatter = SimpleDateFormat(simpleDateFormat).apply { 25 | timeZone = TimeZone.getTimeZone("UTC") 26 | } 27 | return timestamp?.let { formatter.parse(it) } 28 | } 29 | 30 | @Throws(ParseException::class) 31 | fun timestampToMilli(timestamp: String?, simpleDateFormat: String?): Long { 32 | val dateUtc = getLocalTime(timestamp, simpleDateFormat) 33 | val dateFormatter = 34 | SimpleDateFormat(simpleDateFormat).apply { timeZone = TimeZone.getDefault() } 35 | val localTimeString = dateUtc?.let { dateFormatter.format(it) } 36 | 37 | val date = localTimeString?.let { SimpleDateFormat(simpleDateFormat).parse(it) } 38 | return date!!.time 39 | } 40 | 41 | @SuppressLint("NewApi") 42 | fun milliToStringTime(milli: Long): String { 43 | val instant = Instant.ofEpochMilli(milli) 44 | val date = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) 45 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") 46 | return formatter.format(date) 47 | } 48 | 49 | fun Context.getTimeAgo( 50 | timeString: String? = "", 51 | pattern: String = "yyyy-MM-dd'T'HH:mm:ss" 52 | ): String { 53 | if (timeString.isNullOrEmpty() ) { 54 | return resources.getString(R.string.just_now) 55 | } 56 | try { 57 | var time = timestampToMilli(timeString, pattern) 58 | if (time < 1000000000000L) { 59 | time *= 1000 60 | } 61 | val diff: Long = abs(System.currentTimeMillis() - time) 62 | return if (diff < MINUTE_MILLIS) { 63 | resources.getString(R.string.just_now) 64 | } else if (diff < 2 * MINUTE_MILLIS) { 65 | resources.getString(R.string.min_ago) 66 | } else if (diff < 50 * MINUTE_MILLIS) { 67 | val minutes = diff / MINUTE_MILLIS 68 | "$minutes " + resources.getString(R.string.mins_ago) 69 | } else if (diff < 90 * MINUTE_MILLIS) { 70 | resources.getString(R.string.hour_ago) 71 | } else if (diff < 24 * HOUR_MILLIS) { 72 | val hours = (diff / HOUR_MILLIS).toString() 73 | "$hours " + resources.getString(R.string.hours_ago) 74 | } else if (diff < 7 * DAY_MILLIS) { 75 | if ((diff / DAY_MILLIS) == 1L) { 76 | resources.getString(R.string.day_ago) 77 | } else { 78 | val day = diff / DAY_MILLIS 79 | "$day " + resources.getString(R.string.days_ago) 80 | } 81 | } else if (diff < 4 * DateUtils.WEEK_IN_MILLIS) { 82 | if (diff / DateUtils.WEEK_IN_MILLIS == 1L) { 83 | resources.getString(R.string.week_ago) 84 | } else { 85 | val week = diff / DateUtils.WEEK_IN_MILLIS 86 | "$week " + resources.getString(R.string.weeks_ago) 87 | } 88 | } else { 89 | resources.getString(R.string.more_than_months_ago) 90 | } 91 | } catch (e: Exception) { 92 | return resources.getString(R.string.just_now) 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.fragment 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.ColorDrawable 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.DialogFragment 10 | import androidx.fragment.app.createViewModelLazy 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.lifecycleScope 13 | import androidx.lifecycle.repeatOnLifecycle 14 | import androidx.viewbinding.ViewBinding 15 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 16 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 17 | import com.thuanpx.view_mvvm_architecture.R 18 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog 19 | import kotlinx.coroutines.flow.Flow 20 | import kotlinx.coroutines.launch 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * Created by ThuanPx on 8/5/20. 25 | * 26 | * @viewModel -> name view model 27 | * @classViewModel -> class view model 28 | * @viewBinding -> class binding 29 | * @initialize -> init UI, adapter, listener... 30 | * @onSubscribeObserver -> subscribe observer 31 | * 32 | */ 33 | 34 | abstract class BaseDialogFragment(viewModelClass: KClass) : 35 | DialogFragment() { 36 | 37 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore }) 38 | private var _viewBinding: viewBinding? = null 39 | protected val viewBinding get() = _viewBinding!! // ktlint-disable 40 | protected var progressDialog: ProgressDialog? = null 41 | 42 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding 43 | 44 | protected abstract fun initialize() 45 | 46 | override fun onCreateView( 47 | inflater: LayoutInflater, 48 | container: ViewGroup?, 49 | savedInstanceState: Bundle? 50 | ): View? { 51 | _viewBinding = inflateViewBinding(inflater, container) 52 | return viewBinding.root 53 | } 54 | 55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 56 | super.onViewCreated(view, savedInstanceState) 57 | progressDialog = ProgressDialog(requireContext()) 58 | initialize() 59 | onSubscribeObserver() 60 | } 61 | 62 | override fun onCreate(savedInstanceState: Bundle?) { 63 | super.onCreate(savedInstanceState) 64 | setStyle(STYLE_NORMAL, R.style.AppTheme_Dialog) 65 | } 66 | 67 | override fun onStart() { 68 | super.onStart() 69 | dialog?.let { 70 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 71 | val width = ViewGroup.LayoutParams.MATCH_PARENT 72 | val height = ViewGroup.LayoutParams.MATCH_PARENT 73 | it.window?.setLayout(width, height) 74 | } 75 | } 76 | 77 | /** 78 | * Fragments outlive their views. Make sure you clean up any references to 79 | * the binding class instance in the fragment's onDestroyView() method. 80 | */ 81 | override fun onDestroyView() { 82 | super.onDestroyView() 83 | _viewBinding = null 84 | } 85 | 86 | private fun showLoading(isShow: Boolean) { 87 | (activity as? BaseActivity<*, *>)?.showLoading(isShow) 88 | } 89 | 90 | open fun onSubscribeObserver() { 91 | viewModel.run { 92 | isLoading.launchAndCollect { 93 | showLoading(it) 94 | } 95 | error.launchAndCollect { 96 | (activity as? BaseActivity<*, *>)?.handleApiError(it) 97 | } 98 | } 99 | } 100 | 101 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) { 102 | with(viewLifecycleOwner) { 103 | lifecycleScope.launch { 104 | repeatOnLifecycle(Lifecycle.State.STARTED) { 105 | collect { action(it) } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/ActivityExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context 2 | 3 | import android.content.Intent 4 | import android.graphics.Color 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.view.View 8 | import androidx.annotation.ColorRes 9 | import androidx.annotation.IdRes 10 | import androidx.core.content.ContextCompat 11 | import androidx.fragment.app.Fragment 12 | import androidx.fragment.app.FragmentActivity 13 | import androidx.lifecycle.Lifecycle 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.lifecycle.repeatOnLifecycle 16 | import com.thuanpx.mvvm_architecture.utils.extension.AnimationType 17 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_LEFT 18 | import kotlinx.coroutines.CoroutineScope 19 | import kotlinx.coroutines.launch 20 | import kotlin.reflect.KClass 21 | 22 | /** 23 | * Created by ThuanPx on 3/15/20. 24 | */ 25 | 26 | /** 27 | * Launches a new coroutine and repeats `block` every time the Activity's viewLifecycleOwner 28 | * is in and out of `minActiveState` lifecycle state. 29 | * Source: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333 30 | */ 31 | inline fun FragmentActivity.launchAndRepeatWithViewLifecycle( 32 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 33 | crossinline block: suspend CoroutineScope.() -> Unit 34 | ) { 35 | lifecycleScope.launch { 36 | repeatOnLifecycle(minActiveState) { 37 | block() 38 | } 39 | } 40 | } 41 | 42 | fun FragmentActivity.replaceFragment( 43 | @IdRes containerId: Int, 44 | fragment: Fragment, 45 | addToBackStack: Boolean = true, 46 | tag: String = fragment::class.java.simpleName, 47 | @AnimationType animateType: Int = SLIDE_LEFT 48 | ) { 49 | supportFragmentManager.transact(animateType) { 50 | if (addToBackStack) { 51 | addToBackStack(tag) 52 | } 53 | replace(containerId, fragment, tag) 54 | } 55 | } 56 | 57 | fun FragmentActivity.addFragment( 58 | @IdRes containerId: Int, 59 | fragment: Fragment, 60 | addToBackStack: Boolean = true, 61 | tag: String = fragment::class.java.simpleName, 62 | @AnimationType animateType: Int = SLIDE_LEFT 63 | ) { 64 | supportFragmentManager.transact(animateType) { 65 | if (addToBackStack) { 66 | addToBackStack(tag) 67 | } 68 | add(containerId, fragment, tag) 69 | } 70 | } 71 | 72 | fun FragmentActivity.isVisibleFragment(tag: String): Boolean { 73 | val fragment = supportFragmentManager.findFragmentByTag(tag) 74 | return fragment?.isAdded ?: false && fragment?.isVisible ?: false 75 | } 76 | 77 | inline fun FragmentActivity.getFragment(clazz: KClass): T? { 78 | val tag = clazz.java.simpleName 79 | return supportFragmentManager.findFragmentByTag(tag) as? T? 80 | } 81 | 82 | /** 83 | * val test = extra("test") 84 | * */ 85 | inline fun FragmentActivity.extra(key: String, default: T? = null) = lazy { 86 | val value = intent?.extras?.get(key) 87 | if (value is T) value else default 88 | } 89 | 90 | fun FragmentActivity.getCurrentFragment(@IdRes containerId: Int): Fragment? { 91 | return supportFragmentManager.findFragmentById(containerId) 92 | } 93 | 94 | fun FragmentActivity.setTransparentStatusBar(isDarkBackground: Boolean = false) { 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 96 | window.statusBarColor = Color.TRANSPARENT 97 | window.decorView.systemUiVisibility = if (isDarkBackground) 98 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 99 | else 100 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 101 | } 102 | } 103 | 104 | fun FragmentActivity.setStatusBarColor(@ColorRes color: Int, isDarkColor: Boolean = false) { 105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 106 | window?.apply { 107 | decorView.systemUiVisibility = if (isDarkColor) 0 else View.SYSTEM_UI_FLAG_VISIBLE 108 | statusBarColor = ContextCompat.getColor(context, color) 109 | } 110 | } 111 | } 112 | 113 | fun FragmentActivity.openWithUrl(url: String) { 114 | val defaultBrowser = 115 | Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER) 116 | defaultBrowser.data = Uri.parse(url) 117 | this.startActivity(defaultBrowser) 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseBottomSheetFragment.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.base.fragment 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.FrameLayout 11 | import androidx.core.content.ContextCompat 12 | import androidx.fragment.app.createViewModelLazy 13 | import androidx.lifecycle.Lifecycle 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.lifecycle.repeatOnLifecycle 16 | import androidx.viewbinding.ViewBinding 17 | import com.google.android.material.bottomsheet.BottomSheetBehavior 18 | import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED 19 | import com.google.android.material.bottomsheet.BottomSheetDialog 20 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 21 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity 22 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel 23 | import com.thuanpx.view_mvvm_architecture.R 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.coroutines.launch 26 | import kotlin.reflect.KClass 27 | 28 | 29 | /** 30 | * Copyright © 2020 Neolab VN. 31 | * Created by ThuanPx on 8/5/20. 32 | * 33 | * @viewModel -> view model 34 | * @viewModelClass -> class view model 35 | * @viewBinding -> class binding 36 | * @initialize -> init UI, adapter, listener... 37 | * @onSubscribeObserver -> subscribe observer 38 | * 39 | */ 40 | 41 | abstract class BaseBottomSheetFragment( 42 | viewModelClass: KClass 43 | ) : BottomSheetDialogFragment() { 44 | 45 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore }) 46 | private var _viewBinding: viewBinding? = null 47 | protected val viewBinding get() = _viewBinding!! // ktlint-disable 48 | 49 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding 50 | 51 | protected abstract fun initialize() 52 | 53 | override fun onCreateView( 54 | inflater: LayoutInflater, 55 | container: ViewGroup?, 56 | savedInstanceState: Bundle? 57 | ): View? { 58 | _viewBinding = inflateViewBinding(inflater, container) 59 | return viewBinding.root 60 | } 61 | 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 63 | super.onViewCreated(view, savedInstanceState) 64 | initialize() 65 | onSubscribeObserver() 66 | } 67 | 68 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 69 | val dialog = super.onCreateDialog(savedInstanceState) 70 | dialog.setOnShowListener { 71 | Handler(Looper.getMainLooper()).post { 72 | val bottomSheet = (dialog as? BottomSheetDialog)?.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout 73 | bottomSheet?.let { 74 | BottomSheetBehavior.from(it).state = STATE_EXPANDED 75 | bottomSheet.background = ContextCompat.getDrawable(requireContext(), 76 | R.drawable.bg_bottom_sheet) 77 | } 78 | } 79 | 80 | } 81 | return dialog 82 | } 83 | 84 | /** 85 | * Fragments outlive their views. Make sure you clean up any references to 86 | * the binding class instance in the fragment's onDestroyView() method. 87 | */ 88 | override fun onDestroyView() { 89 | super.onDestroyView() 90 | _viewBinding = null 91 | } 92 | 93 | private fun showLoading(isShow: Boolean) { 94 | (activity as? BaseActivity<*, *>)?.showLoading(isShow) 95 | } 96 | 97 | open fun onSubscribeObserver() { 98 | viewModel.run { 99 | isLoading.launchAndCollect { 100 | showLoading(it) 101 | } 102 | error.launchAndCollect { 103 | (activity as? BaseActivity<*, *>)?.handleApiError(it) 104 | } 105 | } 106 | } 107 | 108 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) { 109 | with(viewLifecycleOwner) { 110 | lifecycleScope.launch { 111 | repeatOnLifecycle(Lifecycle.State.STARTED) { 112 | collect { action(it) } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/FragmentExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.IdRes 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.FragmentManager 7 | import androidx.fragment.app.FragmentTransaction 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.lifecycle.repeatOnLifecycle 11 | import com.thuanpx.mvvm_architecture.utils.extension.AnimationType 12 | import com.thuanpx.mvvm_architecture.utils.extension.FADE 13 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_DOWN 14 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_LEFT 15 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_RIGHT 16 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_UP 17 | import com.thuanpx.view_mvvm_architecture.R 18 | import kotlinx.coroutines.CoroutineScope 19 | import kotlinx.coroutines.launch 20 | 21 | /** 22 | * Created by ThuanPx on 3/15/20. 23 | */ 24 | 25 | /** 26 | * Launches a new coroutine and repeats `block` every time the Fragment's viewLifecycleOwner 27 | * is in and out of `minActiveState` lifecycle state.* 28 | * Source: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333 29 | */ 30 | inline fun Fragment.launchAndRepeatWithViewLifecycle( 31 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 32 | crossinline block: suspend CoroutineScope.() -> Unit 33 | ) { 34 | viewLifecycleOwner.lifecycleScope.launch { 35 | viewLifecycleOwner.repeatOnLifecycle(minActiveState) { 36 | block() 37 | } 38 | } 39 | } 40 | 41 | fun Fragment.addOrReplaceFragment( 42 | @IdRes containerId: Int, 43 | fragmentManager: FragmentManager? = parentFragmentManager, 44 | fragment: Fragment, 45 | isAddFrag: Boolean, 46 | addToBackStack: Boolean = true, 47 | @AnimationType animateType: Int, 48 | tag: String = fragment::class.java.simpleName 49 | ) { 50 | fragmentManager?.transact(animateType) { 51 | if (addToBackStack) { 52 | addToBackStack(tag) 53 | } 54 | 55 | if (isAddFrag) { 56 | add(containerId, fragment, tag) 57 | } else { 58 | replace(containerId, fragment, tag) 59 | } 60 | } 61 | } 62 | 63 | fun Fragment.replaceFragment( 64 | @IdRes containerId: Int, 65 | fragment: Fragment, 66 | addToBackStack: Boolean = true, 67 | tag: String = fragment::class.java.simpleName, 68 | @AnimationType animateType: Int = SLIDE_LEFT 69 | ) { 70 | childFragmentManager.transact(animateType) { 71 | if (addToBackStack) { 72 | addToBackStack(tag) 73 | } 74 | replace(containerId, fragment, tag) 75 | } 76 | } 77 | 78 | fun Fragment.addFragment( 79 | @IdRes containerId: Int, 80 | fragment: Fragment, 81 | addToBackStack: Boolean = true, 82 | tag: String = fragment::class.java.simpleName, 83 | @AnimationType animateType: Int = SLIDE_LEFT 84 | ) { 85 | childFragmentManager.transact(animateType) { 86 | if (addToBackStack) { 87 | addToBackStack(tag) 88 | } 89 | add(containerId, fragment, tag) 90 | } 91 | } 92 | 93 | fun Fragment.generateTag(): String { 94 | return this::class.java.simpleName 95 | } 96 | 97 | fun Fragment.popBackFragment(): Boolean { 98 | with(parentFragmentManager) { 99 | val isShowPreviousPage = this.backStackEntryCount > 0 100 | if (isShowPreviousPage) { 101 | this.popBackStackImmediate() 102 | } 103 | return isShowPreviousPage 104 | } 105 | } 106 | 107 | fun FragmentManager.isExitFragment(tag: String): Boolean { 108 | return this.findFragmentByTag(tag) != null 109 | } 110 | 111 | fun T.withArgs(argsBuilder: Bundle.() -> Unit): T = 112 | this.apply { arguments = Bundle().apply(argsBuilder) } 113 | 114 | /** 115 | * Runs a FragmentTransaction, then calls commitAllowingStateLoss(). 116 | */ 117 | inline fun FragmentManager.transact( 118 | @AnimationType animateType: Int = SLIDE_LEFT, 119 | action: FragmentTransaction.() -> Unit, 120 | ) { 121 | beginTransaction().apply { 122 | setAnimations(animateType) 123 | action() 124 | }.commitAllowingStateLoss() 125 | } 126 | 127 | fun FragmentTransaction.setAnimations(@AnimationType animateType: Int) { 128 | when (animateType) { 129 | FADE -> { 130 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) 131 | } 132 | SLIDE_DOWN -> { 133 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) 134 | } 135 | SLIDE_UP -> { 136 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) 137 | } 138 | SLIDE_LEFT -> { 139 | setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right, 0, 0) 140 | } 141 | SLIDE_RIGHT -> { 142 | setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) 143 | } 144 | else -> { 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/widget/DialogBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.widget 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AlertDialog 8 | import androidx.core.content.ContextCompat 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.FragmentActivity 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 12 | import com.thuanpx.view_mvvm_architecture.R 13 | import com.thuanpx.view_mvvm_architecture.utils.extension.view.clicks 14 | import com.thuanpx.view_mvvm_architecture.utils.extension.view.gone 15 | 16 | /** 17 | * Created by ThuanPx on 15/09/2021. 18 | */ 19 | 20 | @DslDialog 21 | fun Fragment.dialog(setup: DialogBuilder.() -> Unit) { 22 | val builder = DialogBuilder(requireContext(), setup = setup) 23 | builder.build().show() 24 | } 25 | 26 | @DslDialog 27 | fun FragmentActivity.dialog(setup: DialogBuilder.() -> Unit) { 28 | val builder = DialogBuilder(this, setup = setup) 29 | builder.build().show() 30 | } 31 | 32 | data class DialogOptions( 33 | val title: String, 34 | val message: String, 35 | val positiveText: String, 36 | val negativeText: String, 37 | val positiveListener: (() -> Unit)? = null, 38 | val negativeListener: (() -> Unit)? = null, 39 | var positiveColor: Int, 40 | var negativeColor: Int, 41 | var messageColor: Int, 42 | var titleColor: Int, 43 | val cancelable: Boolean, 44 | val isShowNegative: Boolean 45 | ) 46 | 47 | @DslMarker 48 | annotation class DslDialog 49 | 50 | @DslDialog 51 | class DialogBuilder( 52 | private val context: Context, 53 | val setup: DialogBuilder.() -> Unit = {} 54 | ) { 55 | 56 | var title: String = "" 57 | var message: String = "" 58 | var titleColor: Int = android.R.color.black 59 | var messageColor: Int = android.R.color.black 60 | var positiveText: String = "OK" 61 | var negativeText: String = "No" 62 | var positiveListener: (() -> Unit)? = null 63 | var negativeListener: (() -> Unit)? = null 64 | var positiveColor: Int = R.color.blue_700 65 | var negativeColor: Int = R.color.blue_700 66 | var cancelable: Boolean = false 67 | var isShowNegative = false 68 | private lateinit var dialog: AlertDialog 69 | 70 | fun build(): AlertDialog { 71 | setup() 72 | if (message.isEmpty()) { 73 | throw IllegalArgumentException("You should fill all mandatory fields in the options") 74 | } 75 | val options = DialogOptions( 76 | title = title, 77 | message = message, 78 | positiveText = positiveText, 79 | negativeText = negativeText, 80 | positiveListener = positiveListener, 81 | negativeListener = negativeListener, 82 | cancelable = cancelable, 83 | isShowNegative = isShowNegative, 84 | titleColor = titleColor, 85 | messageColor = messageColor, 86 | negativeColor = negativeColor, 87 | positiveColor = positiveColor 88 | ) 89 | 90 | dialog = setupCustomAlertDialog(options) 91 | 92 | return dialog 93 | } 94 | 95 | private fun setupCustomAlertDialog(options: DialogOptions): AlertDialog { 96 | val view = LayoutInflater.from(context).inflate(R.layout.dialog_custom, null) 97 | 98 | val alertDialog = 99 | MaterialAlertDialogBuilder(context, R.style.DialogCustomTheme) 100 | .setView(view) 101 | .setCancelable(options.cancelable) 102 | .create() 103 | 104 | alertDialog.window?.setBackgroundDrawableResource(android.R.color.transparent) 105 | 106 | val tvTitle = view.findViewById(R.id.tvTitle) 107 | tvTitle.text = options.title 108 | tvTitle.setTextColor(ContextCompat.getColor(context, options.titleColor)) 109 | tvTitle.gone(isGone = options.title.isEmpty()) 110 | 111 | val tvMessage = view.findViewById(R.id.tvMessage) 112 | tvMessage.text = options.message 113 | tvMessage.setTextColor(ContextCompat.getColor(context, options.messageColor)) 114 | 115 | val buttonNegative = view.findViewById(R.id.btNegative) 116 | buttonNegative.setTextColor(ContextCompat.getColor(context, options.negativeColor)) 117 | buttonNegative.visibility = if (isShowNegative) View.VISIBLE else View.GONE 118 | buttonNegative.text = options.negativeText 119 | buttonNegative.clicks { 120 | options.negativeListener?.invoke() 121 | if (alertDialog.isShowing) { 122 | alertDialog.dismiss() 123 | } 124 | } 125 | 126 | val buttonPositive = view.findViewById(R.id.btPositive) 127 | buttonPositive.setTextColor(ContextCompat.getColor(context, options.positiveColor)) 128 | buttonPositive.text = options.positiveText 129 | buttonPositive.clicks { 130 | options.positiveListener?.invoke() 131 | if (alertDialog.isShowing) { 132 | alertDialog.dismiss() 133 | } 134 | } 135 | 136 | return alertDialog 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_custom.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 24 | 25 | 40 | 41 | 56 | 57 | 65 | 66 | 80 | 81 | 89 | 90 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.text.SimpleDateFormat 2 | import java.util.Calendar 3 | 4 | plugins { 5 | id("com.android.application") 6 | id("org.jetbrains.kotlin.android") 7 | kotlin("kapt") 8 | id("com.google.dagger.hilt.android") 9 | } 10 | 11 | android { 12 | namespace = "com.thuanpx.view_mvvm_architecture" 13 | compileSdk = 34 14 | flavorDimensions += "default" 15 | 16 | defaultConfig { 17 | applicationId = "com.thuanpx.view_mvvm_architecture" 18 | minSdk = 25 19 | targetSdk = 34 20 | versionCode = 1 21 | versionName = "1.0" 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | } 25 | 26 | productFlavors { 27 | create("dev") { 28 | applicationIdSuffix = ".dev" 29 | versionCode = 1 30 | versionName = "1.0.0" 31 | 32 | buildConfigField("String", "END_POINT", "\"https://pokeapi.co/api/v2/\"") 33 | } 34 | 35 | create("prod") { 36 | versionCode = 1 37 | versionName = "1.0.0" 38 | 39 | buildConfigField("String", "END_POINT", "\"https://pokeapi.co/api/v2/\"") 40 | } 41 | } 42 | 43 | buildTypes { 44 | release { 45 | isMinifyEnabled = true 46 | isShrinkResources = false 47 | proguardFiles( 48 | getDefaultProguardFile("proguard-android-optimize.txt"), 49 | file("proguard-rules.pro") 50 | ) 51 | proguardFile("proguard/proguard-google-play-services.pro") 52 | proguardFile("proguard/proguard-square-okhttp.pro") 53 | proguardFile("proguard/proguard-square-retrofit.pro") 54 | proguardFile("proguard/proguard-google-analytics.pro") 55 | proguardFile("proguard/proguard-facebook.pro") 56 | proguardFile("proguard/proguard-project.pro") 57 | proguardFile("proguard/proguard-hilt.pro") 58 | proguardFile("proguard/proguard-support-v7-appcompat.pro") 59 | proguardFile("proguard/okhttp3.pro") 60 | proguardFile("proguard/kotlin.pro") 61 | proguardFile("proguard/retrofit2.pro") 62 | proguardFile("proguard/proguard-testfairy.pro") 63 | } 64 | } 65 | 66 | compileOptions { 67 | sourceCompatibility = JavaVersion.VERSION_1_8 68 | targetCompatibility = JavaVersion.VERSION_1_8 69 | } 70 | 71 | kotlinOptions { 72 | jvmTarget = "1.8" 73 | } 74 | 75 | buildFeatures { 76 | viewBinding = true 77 | buildConfig = true 78 | } 79 | 80 | applicationVariants.all { 81 | val outputFileName = name + 82 | "_versionName_$versionName" + 83 | "_versionCode_$versionCode" + 84 | "_time_${SimpleDateFormat("HH_mm_dd_MM_yyyy").format(Calendar.getInstance().time)}.apk" 85 | outputs.all { 86 | val output = this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl 87 | output?.outputFileName = outputFileName 88 | } 89 | } 90 | 91 | } 92 | 93 | kapt { 94 | useBuildCache = true 95 | correctErrorTypes = true 96 | } 97 | 98 | dependencies { 99 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 100 | // App compat & design 101 | implementation("androidx.appcompat:appcompat:1.6.1") 102 | implementation("com.google.android.material:material:1.11.0") 103 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 104 | // Coroutines 105 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") 106 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 107 | // Retrofit 108 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 109 | implementation("com.squareup.retrofit2:converter-moshi:2.9.0") 110 | // Okhttp 111 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3") 112 | implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") 113 | // Glide 114 | implementation("com.github.bumptech.glide:glide:4.16.0") 115 | annotationProcessor("com.github.bumptech.glide:compiler:4.15.1") 116 | kapt("com.github.bumptech.glide:compiler:4.15.1") 117 | // Gson 118 | implementation("com.google.code.gson:gson:2.10.1") 119 | // Moshi 120 | implementation("com.squareup.moshi:moshi-kotlin:1.15.0") 121 | // Leak canary 122 | // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.0") 123 | // Timber 124 | implementation("com.jakewharton.timber:timber:5.0.1") 125 | // KTX 126 | implementation("androidx.core:core-ktx:1.12.0") 127 | implementation("androidx.fragment:fragment-ktx:1.6.2") 128 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") 129 | implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") 130 | implementation("androidx.activity:activity-ktx:1.8.2") 131 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") 132 | // Hilt 133 | implementation("com.google.dagger:hilt-android:2.50") 134 | kapt("com.google.dagger:hilt-android-compiler:2.50") 135 | // Lottie 136 | implementation("com.airbnb.android:lottie:6.1.0") 137 | // DataStore 138 | implementation("androidx.datastore:datastore-preferences:1.0.0") 139 | // Paging 140 | implementation("androidx.paging:paging-runtime-ktx:3.2.1") 141 | } -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1dp 4 | 2dp 5 | 3dp 6 | 4dp 7 | 5dp 8 | 6dp 9 | 7dp 10 | 8dp 11 | 9dp 12 | 10dp 13 | 11dp 14 | 12dp 15 | 13dp 16 | 14dp 17 | 15dp 18 | 16dp 19 | 17dp 20 | 18dp 21 | 19dp 22 | 20dp 23 | 21dp 24 | 22dp 25 | 23dp 26 | 24dp 27 | 25dp 28 | 26dp 29 | 27dp 30 | 28dp 31 | 29dp 32 | 30dp 33 | 31dp 34 | 32dp 35 | 33dp 36 | 34dp 37 | 35dp 38 | 36dp 39 | 37dp 40 | 38dp 41 | 39dp 42 | 40dp 43 | 42dp 44 | 44dp 45 | 45dp 46 | 47dp 47 | 48dp 48 | 50dp 49 | 51dp 50 | 53dp 51 | 55dp 52 | 56dp 53 | 58dp 54 | 60dp 55 | 61dp 56 | 62dp 57 | 68dp 58 | 72dp 59 | 74dp 60 | 76dp 61 | 77dp 62 | 80dp 63 | 84dp 64 | 85dp 65 | 86dp 66 | 89dp 67 | 90dp 68 | 92dp 69 | 96dp 70 | 98dp 71 | 99dp 72 | 100dp 73 | 104dp 74 | 110dp 75 | 114dp 76 | 120dp 77 | 125dp 78 | 140dp 79 | 150dp 80 | 154dp 81 | 158dp 82 | 180dp 83 | 187dp 84 | 196dp 85 | 204dp 86 | 254dp 87 | 270dp 88 | 290dp 89 | 300dp 90 | 320dp 91 | 358dp 92 | 360dp 93 | 400dp 94 | 480dp 95 | 488dp 96 | 8sp 97 | 9sp 98 | 10sp 99 | 11sp 100 | 12sp 101 | 13sp 102 | 14sp 103 | 15sp 104 | 16sp 105 | 17sp 106 | 18sp 107 | 20sp 108 | 22sp 109 | 23sp 110 | 24sp 111 | 28sp 112 | 32sp 113 | 53sp 114 | 228dp 115 | 264dp 116 | 357dp 117 | 63dp 118 | 46dp 119 | 34sp 120 | 250dp 121 | 25sp 122 | 192dp 123 | 66dp 124 | 200dp 125 | 43dp 126 | 510dp 127 | 78dp 128 | 82dp 129 | 160dp 130 | 169dp 131 | 41dp 132 | 167dp 133 | 175dp 134 | -------------------------------------------------------------------------------- /app/src/main/res/raw/material_wave_loading.json: -------------------------------------------------------------------------------- 1 | {"v":"4.6.8","fr":29.9700012207031,"ip":0,"op":40.0000016292334,"w":256,"h":256,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[208.6,127.969,0],"e":[208.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":30,"s":[208.6,88,0],"e":[208.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":40.0000016292334}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9843137,0.5490196,0,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[168.6,128,0],"e":[168.6,88,0],"to":[0,-6.66666650772095,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":25,"s":[168.6,88,0],"e":[168.6,128,0],"to":[0,0,0],"ti":[0,-6.66666650772095,0]},{"t":35.0000014255792}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9921569,0.8470588,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[128.594,127.969,0],"e":[128.594,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[128.594,88,0],"e":[128.594,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":30.0000012219251}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2627451,0.627451,0.2784314,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":5,"s":[88.6,127.969,0],"e":[88.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[88.6,88,0],"e":[88.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":25.0000010182709}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.1176471,0.5333334,0.8980392,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 5","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[48.6,127.969,0],"e":[48.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[48.6,88,0],"e":[48.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":20.0000008146167}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.8980392,0.2235294,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1}]} -------------------------------------------------------------------------------- /app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/date/DateExt.kt: -------------------------------------------------------------------------------- 1 | package com.thuanpx.view_mvvm_architecture.utils.extension.date 2 | 3 | import android.text.TextUtils 4 | import com.thuanpx.view_mvvm_architecture.app.Constant 5 | import java.text.ParseException 6 | import java.text.SimpleDateFormat 7 | import java.util.Calendar 8 | import java.util.Date 9 | import java.util.Locale 10 | import java.util.TimeZone 11 | 12 | fun String.convertUiFormatToDataFormat( 13 | inputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 14 | outputFormat: String, 15 | locale: Locale = Locale.JAPAN 16 | ): String? { 17 | if (this.isEmpty()) { 18 | return "" 19 | } 20 | val gmtTime = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC) 21 | val sdf = SimpleDateFormat(inputFormat, locale) 22 | sdf.timeZone = gmtTime 23 | val newSdf = SimpleDateFormat(outputFormat, locale) 24 | newSdf.timeZone = gmtTime 25 | return try { 26 | newSdf.format(sdf.parse(this)) 27 | } catch (e: ParseException) { 28 | null 29 | } 30 | } 31 | 32 | fun String.convertToUTC( 33 | inputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 34 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 35 | locale: Locale = Locale.JAPAN 36 | ): String? { 37 | if (TextUtils.isEmpty(this)) { 38 | return "" 39 | } 40 | val gmtTime = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC) 41 | val sdf = SimpleDateFormat(inputFormat, locale) 42 | val newSdf = SimpleDateFormat(outputFormat, locale) 43 | newSdf.timeZone = gmtTime 44 | return try { 45 | newSdf.format(sdf.parse(this)) 46 | } catch (e: ParseException) { 47 | null 48 | } 49 | } 50 | 51 | fun Date.convertUiFormatToDataFormat( 52 | outputFormat: String, 53 | locale: Locale = Locale.JAPAN 54 | ): String? { 55 | val sdf = SimpleDateFormat(outputFormat, locale) 56 | return try { 57 | sdf.format(this.time) 58 | } catch (e: ParseException) { 59 | null 60 | } 61 | } 62 | 63 | fun Calendar.getDateTime( 64 | outputFormat: String = Constant.KTEXT_TIME_FORMAT_HH_MM, 65 | locale: Locale = Locale.JAPAN 66 | ): String? { 67 | return this.time.convertDateToString(outputFormat, locale) 68 | } 69 | 70 | fun Calendar.getCurrentDate( 71 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD_EN, 72 | locale: Locale = Locale.JAPAN 73 | ): String? { 74 | val calendar = Calendar.getInstance(locale) 75 | return calendar.time.convertDateToString(outputFormat) 76 | } 77 | 78 | fun Date.getCurrentDate( 79 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD, 80 | locale: Locale = Locale.JAPAN 81 | ): String? { 82 | return convertDateToString(outputFormat, locale) 83 | } 84 | 85 | fun Date.convertDateToDate( 86 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 87 | locale: Locale = Locale.JAPAN 88 | ): Date? { 89 | val df = SimpleDateFormat(outputFormat, locale) 90 | return df.format(this).convertStringToDate(outputFormat, locale) 91 | } 92 | 93 | fun Date.convertDateToString( 94 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 95 | locale: Locale = Locale.JAPAN 96 | ): String? { 97 | val df = SimpleDateFormat(outputFormat, locale) 98 | return df.format(this) 99 | } 100 | 101 | fun String.convertStringToDate( 102 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 103 | locale: Locale = Locale.JAPAN 104 | ): Date { 105 | val parser = SimpleDateFormat(outputFormat, locale) 106 | return try { 107 | parser.parse(this) 108 | } catch (e: ParseException) { 109 | Date() 110 | } 111 | } 112 | 113 | fun Date.getDayOfWeek(locale: Locale = Locale.JAPAN): String { 114 | return this.convertDateToString(Constant.KTEXT_DAY_OF_WEEK, locale).toString() 115 | } 116 | 117 | fun Date.getDayOfMonth(locale: Locale = Locale.JAPAN): String { 118 | val calendar = Calendar.getInstance(locale) 119 | calendar.time = this 120 | return calendar.get(Calendar.DAY_OF_MONTH).toString() 121 | } 122 | 123 | fun Date.getMonthOfYear(locale: Locale = Locale.JAPAN): String { 124 | val calendar = Calendar.getInstance(locale) 125 | calendar.time = this 126 | return (calendar.get(Calendar.MONTH) + 1).toString() 127 | } 128 | 129 | fun String.getFirstDayOfWeek(locale: Locale = Locale.JAPAN): Date { 130 | val calendar = Calendar.getInstance(locale) 131 | calendar.time = this.convertStringToDate() 132 | while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) { 133 | calendar.add(Calendar.DATE, -1) 134 | } 135 | return calendar.time 136 | } 137 | 138 | fun String.isValidDateFormat(format: String, locale: Locale = Locale.JAPAN): Boolean { 139 | val formatter = SimpleDateFormat(format, locale) 140 | formatter.isLenient = false 141 | return try { 142 | formatter.parse(this) 143 | true 144 | } catch (e: ParseException) { 145 | false 146 | } 147 | } 148 | 149 | fun Date.isSameDay(expectedDay: Int, locale: Locale = Locale.JAPAN): Boolean { 150 | val calendar = Calendar.getInstance(locale) 151 | calendar.time = this 152 | return calendar.get(Calendar.DAY_OF_WEEK) == expectedDay 153 | } 154 | 155 | fun String.convertDateStringWithPlusTime( 156 | plusTime: Long, 157 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC, 158 | locale: Locale = Locale.JAPAN 159 | ): String { 160 | val date = this.convertStringToDate(outputFormat) 161 | val calendar = Calendar.getInstance(locale) 162 | calendar.time = date 163 | return Date(calendar.timeInMillis + plusTime).convertDateToString().toString() 164 | } 165 | 166 | fun Date.convertDateWithPlusTime(plusTime: Long, locale: Locale = Locale.JAPAN): Date { 167 | val calendar = Calendar.getInstance(locale) 168 | calendar.time = this 169 | return Date(calendar.timeInMillis + plusTime) 170 | } 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/proguard/proguard-project.pro: -------------------------------------------------------------------------------- 1 | # AndroidAnnotations 2 | -dontwarn org.androidannotations.api.rest.* 3 | 4 | 5 | -optimizationpasses 5 6 | -dontusemixedcaseclassnames 7 | -dontskipnonpubliclibraryclasses 8 | -dontskipnonpubliclibraryclassmembers 9 | -dontpreverify 10 | -verbose 11 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 12 | 13 | -allowaccessmodification 14 | -keepattributes *Annotation* 15 | -renamesourcefileattribute SourceFile 16 | -keepattributes SourceFile,LineNumberTable 17 | -repackageclasses '' 18 | 19 | -dontwarn android.support.** 20 | -dontwarn com.atinternet.** 21 | -dontwarn org.apache.** 22 | -dontwarn javax.annotation.** 23 | -dontwarn com.google.protobuf.** 24 | 25 | -keepattributes InnerClasses 26 | -keepattributes *Annotation* 27 | -keepattributes Signature 28 | -keepattributes EnclosingMethod 29 | 30 | -dontwarn com.sothree.** 31 | -keep class com.sothree.** 32 | -keep interface com.sothree.** 33 | 34 | -dontwarn org.xmlpull.v1.** 35 | 36 | -keepclasseswithmembernames class * { 37 | native ; 38 | } 39 | 40 | -keepclasseswithmembers class * { 41 | public (android.content.Context, android.util.AttributeSet); 42 | } 43 | 44 | -keepclasseswithmembers class * { 45 | public (android.content.Context, android.util.AttributeSet, int); 46 | } 47 | 48 | # keep setters in Views so that animations can still work. 49 | # see http://proguard.sourceforge.net/manual/examples.html#beans 50 | -keepclassmembers public class * extends android.view.View { 51 | void set*(***); 52 | *** get*(); 53 | } 54 | 55 | -keepclassmembers class * extends android.app.Activity { 56 | public void *(android.view.View); 57 | } 58 | 59 | -keepclassmembers enum * { 60 | public static **[] values(); 61 | public static ** valueOf(java.lang.String); 62 | } 63 | 64 | -keep class * implements android.os.Parcelable { 65 | public static final android.os.Parcelable$Creator *; 66 | } 67 | 68 | -dontwarn java.awt.** 69 | -dontwarn **CompatHoneycomb 70 | -keep class android.support.v4.** { *; } 71 | 72 | -dontwarn uk.co.senab.photoview.** 73 | -keep class uk.co.senab.photoview.** { *; } 74 | 75 | -keep class com.crashlytics.** { *; } 76 | -keep class com.crashlytics.android.** 77 | -keepattributes SourceFile,LineNumberTable 78 | -dontwarn com.crashlytics.** 79 | -keep public class * extends java.lang.Exception 80 | 81 | -keep class android.support.v4.view.ViewPager 82 | -keepclassmembers class android.support.v4.view.ViewPager$LayoutParams { *; } 83 | -keep class android.support.v4.app.Fragment { *; } 84 | 85 | -keep class com.mixpanel.android.mpmetrics.MixpanelAPI { *;} 86 | -keep class com.google.android.gms.analytics.Tracker { *; } 87 | -keep class com.google.analytics.tracking.android.Tracker { *; } 88 | -keep class com.flurry.android.FlurryAgent { *; } 89 | -keep class com.omniture.AppMeasurementBase { *;} 90 | -keep class com.adobe.adms.measurement.ADMS_Measurement { *;} 91 | 92 | ##---------------Begin: proguard configuration common for all Android apps ---------- 93 | 94 | # Explicitly preserve all serialization members. The Serializable interface 95 | # is only a marker interface, so it wouldn't save them. 96 | -keepclassmembers class * implements java.io.Serializable { 97 | static final long serialVersionUID; 98 | private static final java.io.ObjectStreamField[] serialPersistentFields; 99 | private void writeObject(java.io.ObjectOutputStream); 100 | private void readObject(java.io.ObjectInputStream); 101 | java.lang.Object writeReplace(); 102 | java.lang.Object readResolve(); 103 | } 104 | 105 | # Preserve all native method names and the names of their classes. 106 | -keepclasseswithmembernames class * { 107 | native ; 108 | } 109 | 110 | -keepclasseswithmembernames class * { 111 | public (android.content.Context, android.util.AttributeSet); 112 | } 113 | 114 | -keepclasseswithmembernames class * { 115 | public (android.content.Context, android.util.AttributeSet, int); 116 | } 117 | 118 | # Preserve static fields of inner classes of R classes that might be accessed 119 | # through introspection. 120 | -keepclassmembers class **.R$* { 121 | public static ; 122 | } 123 | 124 | # Preserve the special static methods that are required in all enumeration classes. 125 | -keepclassmembers enum * { 126 | public static **[] values(); 127 | public static ** valueOf(java.lang.String); 128 | } 129 | 130 | -keep class * implements android.os.Parcelable { 131 | public static final android.os.Parcelable$Creator *; 132 | } 133 | ##---------------End: proguard configuration common for all Android apps ---------- 134 | 135 | ##---------------Begin: proguard configuration for Gson ---------- 136 | # Gson uses generic type information stored in a class file when working with fields. Proguard 137 | # removes such information by default, so configure it to keep all of it. 138 | -keepattributes Signature 139 | 140 | # For using GSON @Expose annotation 141 | -keepattributes *Annotation* 142 | 143 | # Gson specific classes 144 | -keep class sun.misc.Unsafe { *; } 145 | #-keep class com.google.gson.stream.** { *; } 146 | 147 | # Application classes that will be serialized/deserialized over Gson 148 | -keep class net.itify.cookpad.model.** { *; } 149 | -keep class net.itify.cookpad.base.network.** { *; } 150 | 151 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, 152 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 153 | -keep class * extends com.google.gson.TypeAdapter 154 | -keep class * implements com.google.gson.TypeAdapterFactory 155 | -keep class * implements com.google.gson.JsonSerializer 156 | -keep class * implements com.google.gson.JsonDeserializer 157 | 158 | # Prevent R8 from leaving Data object members always null 159 | -keepclassmembers,allowobfuscation class * { 160 | @com.google.gson.annotations.SerializedName ; 161 | } 162 | 163 | # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. 164 | -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken 165 | -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken 166 | 167 | # https://github.com/google/gson/issues/2069 168 | -keep class com.google.gson.reflect.TypeToken 169 | -keep class * extends com.google.gson.reflect.TypeToken 170 | -keep public class * implements java.lang.reflect.Type 171 | 172 | ##---------------End: proguard configuration for Gson ---------- 173 | 174 | 175 | ##---------------Begin: proguard configuration for appboy ---------- 176 | -dontwarn com.amazon.device.messaging.** 177 | -dontwarn bo.app.** 178 | -dontwarn com.google.android.gms.** 179 | -dontwarn com.appboy.ui.** 180 | -keep class bo.app.** { *; } 181 | -keep class com.appboy.** { *; } 182 | ##---------------End: proguard configuration for appboy ---------- 183 | 184 | ##---------------Begin: proguard configuration for Tealium ---------- 185 | -keepclassmembers class fqcn.of.javascript.interface.for.webview { 186 | public *; 187 | } 188 | 189 | # Allow obfuscation of android.support.v7.internal.view.menu.** 190 | # to avoid problem on Samsung 4.2.2 devices with appcompat v21 191 | # see https://code.google.com/p/android/issues/detail?id=78377 192 | -keep class !android.support.v7.internal.view.menu.**,android.support.** {*;} 193 | -keep interface !android.support.v7.internal.view.menu.**,android.support.** {*;} 194 | 195 | # Config for Google Play Services: http://developer.android.com/google/play-services/setup.html#Setup 196 | -keep class * extends java.util.ListResourceBundle { 197 | protected Object[][] getContents(); 198 | } 199 | 200 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 201 | public static final *** NULL; 202 | } 203 | 204 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 205 | -keepclassmembernames class * { 206 | @ccom.google.android.gms.common.annotation.KeepName *; 207 | } 208 | 209 | -keepnames class * implements android.os.Parcelable { 210 | public static final ** CREATOR; 211 | } 212 | 213 | -dontwarn com.google.android.gms.** 214 | 215 | -keep class com.tealium.library.* { 216 | public (...); 217 | ; 218 | } 219 | 220 | -dontwarn com.tealium.** 221 | ##---------------End: proguard configuration for Tealium ---------- 222 | 223 | ##---------------Begin: proguard configuration for Newrelic ---------- 224 | 225 | -keepattributes Exceptions, Signature, InnerClasses, LineNumberTable 226 | 227 | -keep class com.google.firebase.** { *; } 228 | #-dontwarn com.google.j2objc.annotations.** 229 | 230 | # moshi 231 | -keep class com.squareup.moshi.** { *; } 232 | -keep interface com.squareup.moshi.** { *; } 233 | -dontwarn com.squareup.moshi.** 234 | -dontwarn okio.** 235 | 236 | # ThreeTen-Backport 237 | -keep class org.threeten.bp.zone.* 238 | -dontwarn org.threeten.bp.chrono.JapaneseEra 239 | 240 | ###### Fix exception: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' 241 | -dontwarn kotlinx.atomicfu.AtomicBoolean 242 | # ServiceLoader support 243 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} 244 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} 245 | -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} 246 | -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} 247 | 248 | # Most of volatile fields are updated with AFU and should not be mangled 249 | -keepclassmembernames class kotlinx.** { 250 | volatile ; 251 | } 252 | 253 | # Amplitude 254 | -keep class com.google.android.gms.ads.** { *; } 255 | -dontwarn okio.** 256 | 257 | ####### 258 | 259 | # Glide 260 | -keep public class * implements com.bumptech.glide.module.GlideModule 261 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 262 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 263 | **[] $VALUES; 264 | public *; 265 | } 266 | 267 | # for DexGuard only 268 | # -keepresourcexmlelements manifest/application/meta-data@value=GlideModule 269 | -dontwarn com.chotot.vn.dashboard.fragments.** 270 | 271 | -dontwarn org.json.JSONStringer 272 | 273 | #Jsoup 274 | -keep public class org.jsoup.** { 275 | public *; 276 | } 277 | -keeppackagenames org.jsoup.nodes 278 | #zalo sign-in 279 | -keep class com.zing.zalo.**{ *; } 280 | -keep enum com.zing.zalo.**{ *; } 281 | -keep interface com.zing.zalo.**{ *; } 282 | #google sign-in 283 | -keep class com.google.googlesignin.** { *; } 284 | -keepnames class com.google.googlesignin.* { *; } 285 | 286 | -keep class com.google.android.gms.auth.** { *; } 287 | 288 | # Protobuf 289 | -keep public class * extends com.google.protobuf.GeneratedMessageLite { *; } 290 | -keep class CxSvcProto.** { *; } 291 | --------------------------------------------------------------------------------