├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mohsen │ │ └── soltanian │ │ └── cleanarchitecture │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mohsen │ │ │ └── soltanian │ │ │ └── cleanarchitecture │ │ │ ├── app │ │ │ ├── AppConfig.kt │ │ │ └── Application.kt │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── mvi │ │ │ │ ├── BaseMviActivity.kt │ │ │ │ └── BaseMviFragment.kt │ │ │ └── mvvm │ │ │ │ ├── BaseMvvmActivity.kt │ │ │ │ └── BaseMvvmFragment.kt │ │ │ ├── di │ │ │ └── Module.kt │ │ │ ├── ui │ │ │ ├── activities │ │ │ │ └── main │ │ │ │ │ └── MainActivity.kt │ │ │ └── fragments │ │ │ │ ├── details │ │ │ │ ├── MovieDetailsFragment.kt │ │ │ │ ├── MovieDetailsViewModel.kt │ │ │ │ ├── adapter │ │ │ │ │ └── CastsAdapter.kt │ │ │ │ └── contract │ │ │ │ │ └── MovieDetailsContract.kt │ │ │ │ └── main │ │ │ │ ├── MainFragment.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── adapter │ │ │ │ ├── MoviesAdapter.kt │ │ │ │ └── SearchMoviesAdapter.kt │ │ │ │ └── contract │ │ │ │ └── MainPageContract.kt │ │ │ └── utils │ │ │ └── Genres.kt │ └── res │ │ ├── anim │ │ ├── h_fragment_enter.xml │ │ ├── h_fragment_exit.xml │ │ ├── h_fragment_pop_enter.xml │ │ └── h_fragment_pop_exit.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_search_box_radius.xml │ │ ├── ic_baseline_close_24.xml │ │ ├── ic_baseline_play_arrow_24.xml │ │ ├── ic_baseline_star_24.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_star_yellow.xml │ │ └── ripple_effect.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── cast_item.xml │ │ ├── fragment_main.xml │ │ ├── fragment_movie_details.xml │ │ ├── row_movie.xml │ │ └── row_search_movie.xml │ │ ├── menu │ │ └── sort_type.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── 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 │ │ ├── navigation │ │ └── main_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ └── java │ └── mohsen │ └── soltanian │ └── cleanarchitecture │ ├── core │ ├── AppUnitTest.kt │ └── TestCoroutineRule.kt │ └── ui │ └── fragments │ ├── details │ └── MovieDetailsViewModelTest.kt │ └── main │ └── MainViewModelTest.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── copyright.kt ├── detekt.yml └── src │ └── main │ └── java │ ├── Configs.kt │ ├── Deps.kt │ ├── GradlePlugins.kt │ ├── ProductFlavors.kt │ ├── Versions.kt │ ├── extensions │ └── DependencyHandlerExtensions.kt │ └── plugins │ ├── AndroidLibraryPlugin.kt │ ├── AppPlugin.kt │ └── KotlinLibraryPlugin.kt ├── core ├── data │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── mohsen │ │ │ │ └── soltanian │ │ │ │ └── cleanarchitecture │ │ │ │ └── core │ │ │ │ └── data │ │ │ │ ├── config │ │ │ │ └── RemoteConfig.kt │ │ │ │ ├── di │ │ │ │ └── Module.kt │ │ │ │ ├── enviroment │ │ │ │ └── CoreEnvironment.kt │ │ │ │ ├── extention │ │ │ │ ├── MoshiExtension.kt │ │ │ │ └── Value.kt │ │ │ │ ├── helper │ │ │ │ ├── NullToEmptyListAdapter.kt │ │ │ │ └── NullToEmptyStringAdapter.kt │ │ │ │ ├── implClasses │ │ │ │ └── ServiceImp.kt │ │ │ │ ├── models │ │ │ │ └── response │ │ │ │ │ ├── Cast.kt │ │ │ │ │ ├── CastsResponse.kt │ │ │ │ │ ├── Movie.kt │ │ │ │ │ └── MoviesResponse.kt │ │ │ │ ├── network │ │ │ │ ├── HandleError.kt │ │ │ │ ├── HttpStatusCode.kt │ │ │ │ ├── interceptor │ │ │ │ │ └── HttpRequestInterceptor.kt │ │ │ │ └── moshi │ │ │ │ │ └── EnumValueJsonAdapter.kt │ │ │ │ ├── scopes │ │ │ │ └── ServerService.kt │ │ │ │ └── services │ │ │ │ └── RemoteApi.kt │ │ └── resources │ │ │ └── mock │ │ │ └── get-casts.json │ │ └── test │ │ └── java │ │ └── mohsen │ │ └── soltanian │ │ └── cleanarchitecture │ │ └── core │ │ └── data │ │ ├── core │ │ ├── DataUnitTest.kt │ │ ├── ModelTesting.kt │ │ ├── ModelUnitTest.kt │ │ ├── ServiceUnitTesting.kt │ │ └── ServicesUnitTest.kt │ │ ├── di │ │ └── ModuleTest.kt │ │ ├── helper │ │ ├── NullToEmptyListAdapterTest.kt │ │ └── NullToEmptyStringAdapterTest.kt │ │ ├── implClasses │ │ └── ServiceImpTest.kt │ │ ├── models │ │ └── response │ │ │ ├── CastModelTest.kt │ │ │ ├── CastsResponseModelTest.kt │ │ │ ├── MovieModelTest.kt │ │ │ └── MoviesResponseModelTest.kt │ │ └── services │ │ └── RemoteApiTest.kt └── domain │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── mohsen │ │ └── soltanian │ │ └── cleanarchitecture │ │ └── core │ │ └── domain │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── mohsen │ │ └── soltanian │ │ └── cleanarchitecture │ │ └── core │ │ └── domain │ │ ├── base │ │ ├── BasePagingUseCase.kt │ │ └── BaseUseCase.kt │ │ ├── pagingSource │ │ └── MoviePagingSource.kt │ │ └── usecase │ │ ├── MovieCastUseCase.kt │ │ ├── MoviesUseCase.kt │ │ └── SearchMovieUseCase.kt │ └── test │ └── java │ └── mohsen │ └── soltanian │ └── cleanarchitecture │ └── core │ └── domain │ ├── base │ └── BaseUseCaseTest.kt │ ├── core │ └── DomainUnitTest.kt │ └── usecase │ ├── MovieCastUseCaseTest.kt │ ├── MovieUseCaseTest.kt │ └── SearchMovieUseCaseTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore.properties ├── libraries └── framework │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── mohsen │ │ └── soltanian │ │ └── cleanarchitecture │ │ └── libraries │ │ └── framework │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mohsen │ │ │ └── soltanian │ │ │ └── cleanarchitecture │ │ │ └── libraries │ │ │ └── framework │ │ │ ├── binding │ │ │ └── CommonBindingAdapter.kt │ │ │ ├── component │ │ │ └── ProgressDialog.kt │ │ │ ├── core │ │ │ ├── adapter │ │ │ │ ├── BasicPagingRecyclerAdapter.kt │ │ │ │ ├── BasicRecyclerAdapter.kt │ │ │ │ ├── BasicViewHolder.kt │ │ │ │ └── paging │ │ │ │ │ └── PagingLoadStateAdapter.kt │ │ │ ├── base │ │ │ │ ├── annotation │ │ │ │ │ ├── ActivityAttribute.kt │ │ │ │ │ ├── FragmentAttribute.kt │ │ │ │ │ └── Layout.kt │ │ │ │ ├── application │ │ │ │ │ ├── AppInitializer.kt │ │ │ │ │ ├── AppInitializerImpl.kt │ │ │ │ │ ├── CoreApplication.kt │ │ │ │ │ ├── CoreConfig.kt │ │ │ │ │ ├── CoreConfigProvider.kt │ │ │ │ │ ├── CoreEnvironment.kt │ │ │ │ │ └── TimberInitializer.kt │ │ │ │ ├── binding │ │ │ │ │ ├── BindingViewHolder.kt │ │ │ │ │ └── ViewBindingExtension.kt │ │ │ │ ├── core │ │ │ │ │ ├── CoreActivity.kt │ │ │ │ │ └── CoreFragment.kt │ │ │ │ ├── mvi │ │ │ │ │ ├── MviActivity.kt │ │ │ │ │ ├── MviFragment.kt │ │ │ │ │ └── MviViewModel.kt │ │ │ │ └── mvvm │ │ │ │ │ ├── MvvmActivity.kt │ │ │ │ │ ├── MvvmFragment.kt │ │ │ │ │ └── MvvmViewModel.kt │ │ │ └── flow │ │ │ │ └── EventFlow.kt │ │ │ ├── extensions │ │ │ ├── ActivityExtension.kt │ │ │ ├── ContextExtension.kt │ │ │ ├── DeviceExtension.kt │ │ │ ├── FragmentExtension.kt │ │ │ ├── IntentExtension.kt │ │ │ ├── KeyboardExtension.kt │ │ │ ├── LifecycleOwnerExtension.kt │ │ │ ├── SnackBarExtension.kt │ │ │ ├── ToastExtension.kt │ │ │ ├── ViewExtension.kt │ │ │ └── helper │ │ │ │ └── DividerItemDecorator.kt │ │ │ ├── receivers │ │ │ └── ConnectionCheck.kt │ │ │ └── utils │ │ │ └── NetworkHandler.kt │ └── res │ │ ├── anim │ │ ├── anim_fall_down.xml │ │ ├── anim_fragment_in.xml │ │ ├── anim_fragment_in_from_pop.xml │ │ ├── anim_fragment_out.xml │ │ ├── anim_fragment_out_from_pop.xml │ │ ├── anim_scale_fragment_in.xml │ │ ├── anim_scale_fragment_in_from_pop.xml │ │ ├── anim_scale_fragment_out.xml │ │ ├── anim_scale_fragment_out_from_pop.xml │ │ ├── anim_vertical_fragment_in_from_pop_long.xml │ │ ├── anim_vertical_fragment_in_long.xml │ │ ├── anim_vertical_fragment_out_from_pop_long.xml │ │ ├── anim_vertical_fragment_out_long.xml │ │ ├── layout_animation_fall_down.xml │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── drawable │ │ ├── bg_dialog_radius.xml │ │ ├── ic_baseline_wifi_off_24.xml │ │ └── pb_progress.xml │ │ ├── layout │ │ ├── dialog_progress.xml │ │ └── row_paging_load_state.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ └── strings.xml │ └── test │ └── java │ └── mohsen │ └── soltanian │ └── cleanarchitecture │ └── libraries │ └── framework │ └── ExampleUnitTest.kt ├── settings.gradle └── signing └── appName-release.jks /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows thumbnail db 2 | Thumbs.db 3 | 4 | # OSX files 5 | .DS_Store 6 | 7 | # built application files 8 | *.apk 9 | *.ap_ 10 | 11 | # files for the dex VM 12 | *.dex 13 | 14 | # Java class files 15 | *.class 16 | 17 | # generated files 18 | bin/ 19 | gen/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Eclipse project files 26 | .classpath 27 | .project 28 | 29 | # Android Studio 30 | .idea 31 | .gradle 32 | /*/local.properties 33 | /*/out 34 | /*/*/build 35 | /*/*/*/build 36 | build 37 | /*/*/production 38 | /*/*/*/production 39 | *.iml 40 | *.iws 41 | *.ipr 42 | *~ 43 | *.swp 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ANDROID CLEAN ARCHITECTURE 2 | 3 | Android Movie App for displaying all popular movies, searching any movie, find information about it using [TMDb](https://www.themoviedb.org/) (The Movie Database) API 4 | 5 | Note: Use your API key as mine is hidden. After you get it, put it in [RemoteApi Class](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/services/RemoteApi.kt) 6 | 7 | `API_KEY="XXXXXXXXXXXXXXXXXXXXXXXX"` 8 | 9 | ## v1.1 - 2022-07-26 10 | - use the annotation [@ActivityAttribute(R.layout.xxx, ...)](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/ActivityAttribute.kt) on the class of your Activity. 11 | - use the annotation [@FragmentAttribute(R.layout.xxx, ...)](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/FragmentAttribute.kt) on the class of your Fragment.[Example](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/MainFragment.kt) 12 | - use then annotation [@Layout(R.layout.xxx)](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/Layout.kt) on the class of your RecyclerAdapter.[Example](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/adapter/MoviesAdapter.kt) 13 | - use the annotation [@ModelTesting(...)](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ModelTesting.kt) on the class of your Model test class.[Example](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/CastModelTest.kt) 14 | - use the annotation [@ServiceUnitTesting(...)](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ServiceUnitTesting.kt) on the class of your Service test class.[Example](https://github.com/mohsenSoltanian/AndroidCleanArchitecture/blob/master/core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/services/RemoteApiTest.kt) 15 | 16 | ## Features 17 | 18 | - Display all upcoming movie trailers. 19 | - Get a list of popular movies of the current time. 20 | - Search any movie & get its information: Title, Rating, Release Date. 21 | 22 | ## Libraries included in this project: 23 | - [mockk for testing ](https://mockk.io/) 24 | - [mockWebserver](https://github.com/square/okhttp/tree/master/mockwebserver) 25 | - Paging 26 | - Glide 27 | - Retrofit 28 | - Data Binding (Android Architecture Components) 29 | - Live Data (Android Architecture Components) 30 | - ViewModel (Android Architecture Components) 31 | - RecyclerView 32 | - CardView 33 | - DI with Hilt 34 | - coroutine flow 35 | - Moshi 36 | - Timber 37 | - Chucker 38 | 39 | ## Screenshots 40 | | | | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extensions.* 2 | import plugins.AppPlugin 3 | 4 | apply() 5 | apply(plugin = GradlePlugins.Navigation) 6 | 7 | dependencies { 8 | implementation(project(path = ":libraries:framework")) 9 | 10 | implementation(Deps.Hilt.Android) 11 | kapt(Deps.Hilt.AndroidCompiler) 12 | 13 | 14 | 15 | testImplementation(Deps.Retrofit.Base) 16 | testImplementation(Deps.Retrofit.Moshi) 17 | testImplementation(Deps.Okhttp.Base) 18 | testImplementation(Deps.Okhttp.LoggingInterceptor) 19 | testImplementation(Deps.Test.LiveData) 20 | testImplementation(Deps.Test.Junit_Ext) 21 | testImplementation(Deps.Test.TestCoreExt) 22 | testImplementation(Deps.Test.Robolectric) 23 | testImplementation(Deps.Test.kluent) 24 | testImplementation(Deps.Test.Mockk) 25 | testImplementation(Deps.Test.Junit) 26 | androidTestImplementation(Deps.Test.JunitExt) 27 | androidTestImplementation(Deps.Test.EspressoCore) 28 | } -------------------------------------------------------------------------------- /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/androidTest/java/mohsen/soltanian/cleanarchitecture/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture 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("ir.stsepehr.hamrahcard", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/app/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.app 2 | 3 | import mohsen.soltanian.cleanarchitecture.BuildConfig 4 | import mohsen.soltanian.cleanarchitecture.ui.activities.main.MainActivity 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.CoreConfig 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.CoreEnvironment 7 | 8 | 9 | class AppConfig : CoreConfig() { 10 | override fun appName(): String { 11 | return "Application-Name" 12 | } 13 | 14 | override fun environment(): CoreEnvironment { 15 | return if (isDev()) { 16 | CoreEnvironment.DEV 17 | } else { 18 | CoreEnvironment.PROD 19 | } 20 | } 21 | 22 | override fun isDev(): Boolean { 23 | return BuildConfig.DEBUG 24 | } 25 | 26 | override fun uncaughtExceptionPage(): Class<*> { 27 | return MainActivity::class.java 28 | } 29 | 30 | override fun uncaughtExceptionMessage(): String { 31 | return "Unknown Error" 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/app/Application.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.app 2 | 3 | import dagger.hilt.android.HiltAndroidApp 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.AppInitializer 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.CoreApplication 6 | import javax.inject.Inject 7 | 8 | 9 | @HiltAndroidApp 10 | class Application: CoreApplication() { 11 | 12 | @Inject 13 | lateinit var initializer: AppInitializer 14 | 15 | override fun appConfig(): AppConfig = AppConfig() 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | initializer.init(this@Application) 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base 2 | 3 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.core.CoreActivity 4 | 5 | abstract class BaseActivity: CoreActivity() -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base 2 | 3 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.core.CoreFragment 4 | 5 | abstract class BaseFragment : CoreFragment() -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/mvi/BaseMviActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base.mvi 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.component.ProgressDialog 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviActivity 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviViewModel 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.showSnackBar 8 | import timber.log.Timber 9 | 10 | abstract class BaseMviActivity> : 11 | MviActivity() { 12 | 13 | private var progressDialog: ProgressDialog? = null 14 | 15 | override fun showProgress() { 16 | if (progressDialog == null) { 17 | progressDialog = ProgressDialog(this) 18 | } 19 | progressDialog?.show() 20 | } 21 | 22 | override fun hideProgress() { 23 | progressDialog?.dismiss() 24 | } 25 | 26 | override fun networkAvailable() { 27 | super.networkAvailable() 28 | Timber.tag("ConnectionState").e("connection state is: true") 29 | 30 | } 31 | 32 | override fun networkNotAvailable() { 33 | super.networkNotAvailable() 34 | Timber.tag("ConnectionState").e("connection state is: false") 35 | 36 | } 37 | 38 | override fun showError(throwable: Throwable) { 39 | handleErrorMessage(throwable.message.toString()) 40 | } 41 | 42 | protected open fun handleErrorMessage(message: String?) { 43 | if (message.isNullOrBlank()) return 44 | hideProgress() 45 | Timber.e(message) 46 | showSnackBar(binding.root, message) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/mvi/BaseMviFragment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base.mvi 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.component.ProgressDialog 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviFragment 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviViewModel 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.showSnackBar 8 | import timber.log.Timber 9 | 10 | abstract class BaseMviFragment> : 11 | MviFragment() { 12 | 13 | private var progressDialog: ProgressDialog? = null 14 | 15 | 16 | override fun showProgress() { 17 | if (progressDialog == null) { 18 | progressDialog = ProgressDialog(requireContext()) 19 | } 20 | progressDialog?.show() 21 | } 22 | 23 | override fun hideProgress() { 24 | progressDialog?.dismiss() 25 | } 26 | 27 | override fun networkAvailable() { 28 | super.networkAvailable() 29 | Timber.tag("ConnectionState").e("connection state is: Network available") 30 | 31 | } 32 | 33 | override fun networkNotAvailable() { 34 | super.networkNotAvailable() 35 | Timber.tag("ConnectionState").e("connection state is: Network unavailable") 36 | 37 | } 38 | 39 | override fun showError(throwable: Throwable) { 40 | handleErrorMessage(throwable.message.toString()) 41 | } 42 | 43 | protected open fun handleErrorMessage(message: String?) { 44 | if (message.isNullOrBlank()) return 45 | hideProgress() 46 | Timber.e(message) 47 | binding?.let { showSnackBar(it.root, message) } 48 | Result 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/mvvm/BaseMvvmActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base.mvvm 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.component.ProgressDialog 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmActivity 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmViewModel 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.showSnackBar 8 | import timber.log.Timber 9 | 10 | abstract class BaseMvvmActivity : 11 | MvvmActivity() { 12 | 13 | private var progressDialog: ProgressDialog? = null 14 | 15 | override fun showProgress() { 16 | if (progressDialog == null) { 17 | progressDialog = ProgressDialog(this) 18 | } 19 | progressDialog?.show() 20 | } 21 | 22 | override fun hideProgress() { 23 | progressDialog?.dismiss() 24 | } 25 | 26 | override fun showError(throwable: Throwable) { 27 | handleErrorMessage(throwable.message.toString()) 28 | } 29 | 30 | protected open fun handleErrorMessage(message: String?) { 31 | if (message.isNullOrBlank()) return 32 | hideProgress() 33 | Timber.e(message) 34 | showSnackBar(binding.root, message) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/base/mvvm/BaseMvvmFragment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.base.mvvm 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.component.ProgressDialog 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmFragment 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmViewModel 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.showSnackBar 8 | import timber.log.Timber 9 | 10 | abstract class BaseMvvmFragment : 11 | MvvmFragment() { 12 | 13 | private var progressDialog: ProgressDialog? = null 14 | 15 | override fun showProgress() { 16 | if (progressDialog == null) { 17 | progressDialog = ProgressDialog(requireContext()) 18 | } 19 | progressDialog?.show() 20 | } 21 | 22 | override fun hideProgress() { 23 | progressDialog?.dismiss() 24 | } 25 | 26 | override fun showError(throwable: Throwable) { 27 | handleErrorMessage(throwable.message.toString()) 28 | } 29 | 30 | protected open fun handleErrorMessage(message: String?) { 31 | if (message.isNullOrBlank()) return 32 | hideProgress() 33 | Timber.e(message) 34 | binding?.let { showSnackBar(it.root, message) } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/di/Module.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import mohsen.soltanian.cleanarchitecture.app.Application 8 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.AppInitializer 9 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.AppInitializerImpl 10 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.CoreConfig 11 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application.TimberInitializer 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class Module { 17 | 18 | @Provides 19 | @Singleton 20 | fun provideApplication(): Application = Application() 21 | 22 | 23 | @Provides 24 | @Singleton 25 | fun provideAppConfig(app: Application): CoreConfig = app.appConfig() 26 | 27 | 28 | @Provides 29 | @Singleton 30 | fun provideTimberInitializer() = TimberInitializer() 31 | 32 | @Provides 33 | @Singleton 34 | fun provideAppInitializer(timberManager: TimberInitializer): AppInitializer { 35 | return AppInitializerImpl(timberManager) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/activities/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.activities.main 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import dagger.hilt.android.AndroidEntryPoint 6 | import mohsen.soltanian.cleanarchitecture.R 7 | 8 | @AndroidEntryPoint 9 | class MainActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_main) 14 | supportActionBar?.title = "" 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/details/MovieDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.details 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.fragment.app.viewModels 6 | import androidx.navigation.fragment.navArgs 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import dagger.hilt.android.AndroidEntryPoint 9 | import mohsen.soltanian.cleanarchitecture.BR 10 | import mohsen.soltanian.cleanarchitecture.R 11 | import mohsen.soltanian.cleanarchitecture.base.mvi.BaseMviFragment 12 | import mohsen.soltanian.cleanarchitecture.databinding.FragmentMovieDetailsBinding 13 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation.FragmentAttribute 14 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.gone 15 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.toast 16 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.visible 17 | import mohsen.soltanian.cleanarchitecture.ui.fragments.details.adapter.CastsAdapter 18 | import mohsen.soltanian.cleanarchitecture.ui.fragments.details.contract.MovieDetailsContract 19 | import mohsen.soltanian.cleanarchitecture.utils.Genres 20 | 21 | @SuppressLint("NonConstantResourceId") 22 | @FragmentAttribute(layoutId = R.layout.fragment_movie_details) 23 | @AndroidEntryPoint 24 | class MovieDetailsFragment : BaseMviFragment() { 26 | 27 | private val movieDetailsViewModel: MovieDetailsViewModel by viewModels() 28 | private val navArgs: MovieDetailsFragmentArgs by navArgs() 29 | private val castsAdapter = CastsAdapter().apply { 30 | clickListener = { cast -> 31 | toast( 32 | "name is: ${cast.name}\n" + 33 | "character name is: ${cast.character}" 34 | ) 35 | } 36 | } 37 | 38 | override fun fragmentStart() { 39 | super.fragmentStart() 40 | me?.supportActionBar?.title = "Movie Details" 41 | } 42 | 43 | override val viewModel: MovieDetailsViewModel 44 | get() = movieDetailsViewModel 45 | 46 | override fun bindingVariables(): HashMap { 47 | val hashMap: HashMap = hashMapOf() 48 | hashMap[BR.click] = ClickProxy() 49 | hashMap[BR.detailsModel] = navArgs.model 50 | hashMap[BR.viewModel] = viewModel 51 | hashMap[BR.castAdapter] = castsAdapter 52 | hashMap[BR.rvLayoutManager] = LinearLayoutManager( 53 | requireCompatActivity().applicationContext, 54 | LinearLayoutManager.HORIZONTAL, 55 | false 56 | ) 57 | return hashMap 58 | } 59 | 60 | override fun onViewReady(bundle: Bundle?) { 61 | viewModel.onTriggerEvent(MovieDetailsContract.Event.fetchMovieCast(movieId = navArgs.model.movieId.toString())) 62 | getBinging()?.counterFab?.count = navArgs.model.movieVote!!?.toInt() 63 | 64 | var genresStr = "" 65 | navArgs.model.genreIds?.forEach { item -> 66 | genresStr += " ${Genres.realGenres[item]}" 67 | } 68 | if (genresStr.isEmpty()) { 69 | getBinging()?.lGenres?.gone() 70 | } else { 71 | getBinging()?.lGenres?.visible() 72 | getBinging()?.genres?.apply { 73 | text = genresStr 74 | } 75 | } 76 | } 77 | 78 | inner class ClickProxy { 79 | fun fabClicked() { 80 | toast("movie vote is: ${navArgs.model.movieVote}") 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/details/MovieDetailsViewModel.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.details 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import androidx.lifecycle.MutableLiveData 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Cast 7 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.MovieCastUseCase 8 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviViewModel 9 | import mohsen.soltanian.cleanarchitecture.ui.fragments.details.contract.MovieDetailsContract 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class MovieDetailsViewModel @Inject constructor( 14 | private val useCase: MovieCastUseCase 15 | ) : MviViewModel() { 16 | 17 | val list: MutableLiveData> = MutableLiveData>(emptyList()) 18 | 19 | private var movieId: String = "" 20 | 21 | override fun onTriggerEvent(eventType: MovieDetailsContract.Event) { 22 | when (eventType) { 23 | is MovieDetailsContract.Event.fetchMovieCast -> { 24 | movieId = eventType.movieId 25 | fetchMovieCast() 26 | } 27 | } 28 | } 29 | 30 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 31 | private fun fetchMovieCast() = safeLaunch { 32 | callWithProgress(useCase(params = MovieCastUseCase.Params(movieId = movieId))) { data -> 33 | list.value = data.cast 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/details/adapter/CastsAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.details.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import mohsen.soltanian.cleanarchitecture.BR 8 | import mohsen.soltanian.cleanarchitecture.R 9 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Cast 10 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 11 | import mohsen.soltanian.cleanarchitecture.databinding.CastItemBinding 12 | import mohsen.soltanian.cleanarchitecture.databinding.RowMovieBinding 13 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicPagingRecyclerAdapter 14 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicRecyclerAdapter 15 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation.Layout 16 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding.getDataBinding 17 | 18 | @SuppressLint("NonConstantResourceId") 19 | @Layout(value = R.layout.cast_item) 20 | class CastsAdapter : BasicRecyclerAdapter() { 21 | 22 | 23 | internal var clickListener: (Cast) -> Unit = { _ -> } 24 | 25 | override fun createBinding(inflater: LayoutInflater, parent: ViewGroup): CastItemBinding? { 26 | return getDataBinding(layoutInflater = inflater, container = parent) 27 | } 28 | 29 | override fun bindView(binding: CastItemBinding?, position: Int, item: Cast) { 30 | binding?.setVariable(BR.model, item) 31 | binding?.setVariable(BR.click, ClickProxy()) 32 | } 33 | 34 | inner class ClickProxy { 35 | fun itemSelection(model: Cast) { 36 | clickListener(model) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/details/contract/MovieDetailsContract.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.details.contract 2 | 3 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 4 | 5 | class MovieDetailsContract { 6 | 7 | sealed class Event { 8 | data class fetchMovieCast(val movieId: String = "") : MovieDetailsContract.Event() 9 | } 10 | 11 | sealed class State { 12 | data class MovieDetailData(val data: CastsResponse) : State() 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.main 2 | 3 | import androidx.databinding.ObservableBoolean 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.paging.PagingConfig 7 | import androidx.paging.cachedIn 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.catch 10 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 11 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.MoviesUseCase 12 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.SearchMovieUseCase 13 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi.MviViewModel 14 | import mohsen.soltanian.cleanarchitecture.ui.fragments.main.contract.MainPageContract 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class MainViewModel @Inject constructor( 19 | private val useCase: MoviesUseCase, private val searchMovieUseCase: SearchMovieUseCase 20 | ) : MviViewModel() { 21 | 22 | val searchList: MutableLiveData?> = MutableLiveData?>(emptyList()) 23 | var searchQuery = "" 24 | private val config = PagingConfig(pageSize = 20) 25 | 26 | val progressBar: ObservableBoolean = ObservableBoolean(true) 27 | val showSearchBox: ObservableBoolean = ObservableBoolean(false) 28 | val showRv: ObservableBoolean = ObservableBoolean(true) 29 | 30 | init { 31 | onTriggerEvent(MainPageContract.Event.fetchMovies) 32 | } 33 | override fun onTriggerEvent(eventType: MainPageContract.Event) { 34 | when(eventType){ 35 | is MainPageContract.Event.fetchMovies -> { 36 | fetchMovies() 37 | } 38 | is MainPageContract.Event.fetchSearchMovies -> { 39 | fetchSearchMovies() 40 | } 41 | } 42 | } 43 | 44 | private fun fetchMovies() = safeLaunch { 45 | val params = MoviesUseCase.Params(pagingConfig = config, 46 | sortBy = "popular") 47 | useCase(params = params) 48 | .cachedIn(scope = viewModelScope) 49 | .catch { 50 | progressBar.set(false) 51 | passError(it) } 52 | .collect { 53 | progressBar.set(false) 54 | showSearchBox.set(true) 55 | showRv.set(true) 56 | setState(MainPageContract.State.MoviesData(it)) 57 | } 58 | } 59 | 60 | private fun fetchSearchMovies() = safeLaunch { 61 | callWithProgress(callFlow = searchMovieUseCase(params = SearchMovieUseCase.Params(query = searchQuery)), 62 | completionHandler = { data -> 63 | showRv.set(false) 64 | searchList.value = data.results 65 | }) 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/adapter/MoviesAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.main.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import mohsen.soltanian.cleanarchitecture.BR 8 | import mohsen.soltanian.cleanarchitecture.R 9 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 10 | import mohsen.soltanian.cleanarchitecture.databinding.RowMovieBinding 11 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicPagingRecyclerAdapter 12 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation.Layout 13 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding.getDataBinding 14 | 15 | @SuppressLint("NonConstantResourceId") 16 | @Layout(value = R.layout.row_movie) 17 | class MoviesAdapter : BasicPagingRecyclerAdapter(diffCallback = Comparator) { 18 | 19 | companion object Comparator : DiffUtil.ItemCallback() { 20 | override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = 21 | oldItem.movieId == newItem.movieId 22 | 23 | @SuppressLint("DiffUtilEquals") 24 | override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = 25 | oldItem == newItem 26 | } 27 | internal var clickListener: (Movie) -> Unit = { _ -> } 28 | 29 | override fun createBinding(inflater: LayoutInflater, parent: ViewGroup): RowMovieBinding? { 30 | return getDataBinding(layoutInflater = inflater, container = parent) 31 | } 32 | 33 | override fun bindView(binding: RowMovieBinding?, position: Int, item: Movie) { 34 | binding?.setVariable(BR.model, item) 35 | binding?.setVariable(BR.click, ClickProxy()) 36 | } 37 | 38 | inner class ClickProxy { 39 | fun itemSelection(movie: Movie) { 40 | clickListener(movie) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/adapter/SearchMoviesAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.main.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import mohsen.soltanian.cleanarchitecture.BR 7 | import mohsen.soltanian.cleanarchitecture.R 8 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 9 | import mohsen.soltanian.cleanarchitecture.databinding.RowSearchMovieBinding 10 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicRecyclerAdapter 11 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation.Layout 12 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding.getDataBinding 13 | 14 | @SuppressLint("NonConstantResourceId") 15 | @Layout(value = R.layout.row_search_movie) 16 | class SearchMoviesAdapter : BasicRecyclerAdapter() { 17 | 18 | internal var clickListener: (Movie) -> Unit = { _ -> } 19 | 20 | override fun createBinding(inflater: LayoutInflater, parent: ViewGroup): RowSearchMovieBinding? { 21 | return getDataBinding(layoutInflater = inflater, container = parent) 22 | } 23 | 24 | override fun bindView(binding: RowSearchMovieBinding?, position: Int, item: Movie) { 25 | binding?.setVariable(BR.model, item) 26 | binding?.setVariable(BR.click, ClickProxy()) 27 | } 28 | 29 | inner class ClickProxy { 30 | fun itemSelection(movie: Movie) { 31 | clickListener(movie) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/contract/MainPageContract.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.main.contract 2 | 3 | import androidx.paging.PagingData 4 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 5 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 6 | 7 | class MainPageContract { 8 | sealed class Event { 9 | object fetchMovies : Event() 10 | object fetchSearchMovies : Event() 11 | } 12 | 13 | sealed class State { 14 | data class MoviesData( 15 | val pagedData: PagingData = 16 | PagingData.empty() 17 | ) : State() 18 | 19 | object SearchMoviesData : State() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/mohsen/soltanian/cleanarchitecture/utils/Genres.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.utils 2 | 3 | import java.util.HashMap 4 | 5 | object Genres { 6 | val realGenres: HashMap = object : HashMap() { 7 | init { 8 | put(28, "Action") 9 | put(12, "Adventure") 10 | put(16, "Animation") 11 | put(35, "Comedy") 12 | put(80, "Crime") 13 | put(99, "Documentary") 14 | put(18, "Drama") 15 | put(10751, "Family") 16 | put(14, "Fantasy") 17 | put(36, "History") 18 | put(27, "Horror") 19 | put(10402, "Music") 20 | put(9648, "Mystery") 21 | put(10749, "Romance") 22 | put(878, "Science Fiction") 23 | put(10770, "TV Movie") 24 | put(53, "Thriller") 25 | put(10752, "War") 26 | put(37, "Western") 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_pop_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/h_fragment_pop_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_search_box_radius.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_star_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_yellow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_effect.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cast_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 25 | 26 | 32 | 33 | 34 | 38 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 71 | 72 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_movie.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 27 | 28 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/row_search_movie.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 27 | 28 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/menu/sort_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/navigation/main_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/top_rated_label 5 | @string/now_playing_label 6 | @string/upcoming_label 7 | 8 | 9 | 10 | @string/popular_value 11 | @string/top_rated_value 12 | @string/now_playing_value 13 | @string/upcoming_value 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #282828 5 | #FFFFFF 6 | #282828 7 | #ED000000 8 | 9 | #1B1B1B 10 | #FF018786 11 | #fff 12 | #000 13 | 14 | #282828 15 | #ccc 16 | 17 | 18 | #FFBB86FC 19 | #FF6200EE 20 | #FF3700B3 21 | #FF03DAC5 22 | #FF018786 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CleanArchitecture 4 | 5 | Movies 6 | Movie Details 7 | Summary 8 | Genres 9 | Name 10 | Cast 11 | Release Date 12 | Movie Vote 13 | 14 | 15 | popular 16 | top_rated 17 | now_playing 18 | upcoming 19 | 20 | 21 | Popular 22 | Top Rated 23 | Now Playing 24 | Upcoming 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 18 | 19 | 24 | 25 | 29 | 30 | 36 | 37 | 42 | 43 | 52 | 53 | 59 | 60 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/mohsen/soltanian/cleanarchitecture/core/AppUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import io.mockk.MockKAnnotations 5 | import org.junit.After 6 | import org.junit.Before 7 | import org.junit.Rule 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.BlockJUnit4ClassRunner 10 | 11 | @RunWith(BlockJUnit4ClassRunner::class) 12 | open class AppUnitTest { 13 | 14 | @get:Rule 15 | var instantExecutorRule = InstantTaskExecutorRule() 16 | 17 | // @get:Rule 18 | // var testCoroutineRule = TestCoroutineRule() 19 | 20 | open fun onSetUpTest() {} 21 | 22 | open fun onStopTest() {} 23 | 24 | @Before 25 | fun onSetup() { 26 | MockKAnnotations.init(this) 27 | onSetUpTest() 28 | } 29 | 30 | @After 31 | fun onTearDown() { 32 | onStopTest() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/test/java/mohsen/soltanian/cleanarchitecture/core/TestCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022, developersancho 3 | * All rights reserved. 4 | */ 5 | package mohsen.soltanian.cleanarchitecture.core 6 | 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.test.* 9 | import org.junit.rules.TestRule 10 | import org.junit.runner.Description 11 | import org.junit.runners.model.Statement 12 | 13 | class TestCoroutineRule : TestRule { 14 | 15 | private val testCoroutineDispatcher = UnconfinedTestDispatcher() 16 | 17 | val testCoroutineScope = TestScope(testCoroutineDispatcher) 18 | 19 | override fun apply(base: Statement, description: Description?) = object : Statement() { 20 | @Throws(Throwable::class) 21 | override fun evaluate() { 22 | Dispatchers.setMain(testCoroutineDispatcher) 23 | 24 | base.evaluate() 25 | 26 | Dispatchers.resetMain() 27 | } 28 | } 29 | 30 | fun runTest(block: suspend TestScope.() -> Unit) = 31 | testCoroutineScope.runTest { block() } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/test/java/mohsen/soltanian/cleanarchitecture/ui/fragments/details/MovieDetailsViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.details 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.impl.annotations.InjectMockKs 6 | import io.mockk.impl.annotations.MockK 7 | import io.mockk.mockk 8 | import io.mockk.verify 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.test.runTest 11 | import mohsen.soltanian.cleanarchitecture.core.AppUnitTest 12 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 13 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.MovieCastUseCase 14 | import mohsen.soltanian.cleanarchitecture.ui.fragments.details.contract.MovieDetailsContract 15 | import org.amshove.kluent.`should be instance of` 16 | import org.amshove.kluent.`should equal` 17 | import org.junit.Test 18 | 19 | class MovieDetailsViewModelTest: AppUnitTest() { 20 | 21 | @MockK lateinit var movieCastUseCase: MovieCastUseCase 22 | 23 | @MockK 24 | @InjectMockKs 25 | lateinit var viewModel: MovieDetailsViewModel 26 | 27 | @Test 28 | fun `verify onTriggerEvent method`() { 29 | val eventType = mockk(relaxed = true) 30 | viewModel.onTriggerEvent(eventType = eventType ) 31 | 32 | verify(exactly = 1) { viewModel.onTriggerEvent(eventType= any()) } 33 | } 34 | 35 | @Test 36 | fun `fetch Movie Cast`() = runTest { 37 | val params = mockk(relaxed = true) 38 | val castsResponse = mockk(relaxed = true) 39 | coEvery { movieCastUseCase.invoke(params= any()) } returns flow { 40 | emit(value = castsResponse) 41 | } 42 | 43 | viewModel.onTriggerEvent(eventType = MovieDetailsContract.Event.fetchMovieCast(movieId = "-1")) 44 | movieCastUseCase.invoke(params = params).collect { 45 | it `should be instance of` CastsResponse::class.java 46 | it.id `should equal` 0L 47 | it.cast `should equal` listOf() 48 | } 49 | 50 | coVerify { 51 | viewModel.onTriggerEvent(eventType = any()) 52 | movieCastUseCase.invoke(params = any()) 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /app/src/test/java/mohsen/soltanian/cleanarchitecture/ui/fragments/main/MainViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.ui.fragments.main 2 | 3 | import androidx.paging.PagingData 4 | import androidx.paging.map 5 | import io.mockk.* 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.impl.annotations.SpyK 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.test.runTest 11 | import mohsen.soltanian.cleanarchitecture.core.AppUnitTest 12 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 13 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 14 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.MoviesUseCase 15 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.SearchMovieUseCase 16 | import mohsen.soltanian.cleanarchitecture.ui.fragments.main.contract.MainPageContract 17 | import org.amshove.kluent.`should be instance of` 18 | import org.amshove.kluent.`should equal` 19 | import org.junit.Test 20 | 21 | class MainViewModelTest: AppUnitTest() { 22 | 23 | @MockK lateinit var moviesUseCase: MoviesUseCase 24 | @MockK lateinit var searchMovieUseCase: SearchMovieUseCase 25 | 26 | @SpyK 27 | @InjectMockKs 28 | lateinit var viewModel: MainViewModel 29 | 30 | @Test 31 | fun `verify onTriggerEvent method`() { 32 | viewModel.onTriggerEvent(eventType = MainPageContract.Event.fetchMovies) 33 | verify(exactly = 1) { viewModel.onTriggerEvent(any())} 34 | } 35 | 36 | @Test 37 | fun `fetch movies`() = runTest { 38 | val params = mockk(relaxed = true) 39 | every { moviesUseCase.invoke(params = any()) } returns flow { 40 | emit(value = PagingData.empty()) 41 | } 42 | 43 | viewModel.onTriggerEvent(eventType = MainPageContract.Event.fetchMovies) 44 | moviesUseCase.invoke(params = params).collect { 45 | it `should be instance of` PagingData::class.java 46 | it.map { model -> 47 | model `should be instance of` Movie::class.java 48 | } 49 | } 50 | 51 | verify { 52 | viewModel.onTriggerEvent(eventType = any()) 53 | moviesUseCase.invoke(params = any()) 54 | } 55 | } 56 | 57 | @Test 58 | fun `fetch search movie`() = runTest { 59 | val moviesResponse = mockk(relaxed = true) 60 | val params = mockk(relaxed = true) 61 | coEvery { searchMovieUseCase.invoke(params = any()) } returns flow { 62 | emit(value = moviesResponse) 63 | } 64 | 65 | viewModel.onTriggerEvent(eventType = MainPageContract.Event.fetchSearchMovies) 66 | searchMovieUseCase.invoke(params= params).collect { 67 | it `should be instance of` MoviesResponse::class.java 68 | it.page `should equal` 0 69 | it.results `should equal` listOf() 70 | } 71 | 72 | coVerify { 73 | viewModel.onTriggerEvent(eventType = any()) 74 | searchMovieUseCase.invoke(params = any()) 75 | } 76 | } 77 | 78 | 79 | 80 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | tasks.named("wrapper") { 3 | distributionType = Wrapper.DistributionType.BIN 4 | gradleVersion = "7.3.3" 5 | } -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | `java-gradle-plugin` 4 | } 5 | 6 | repositories { 7 | google() 8 | jcenter() 9 | mavenCentral() 10 | gradlePluginPortal() 11 | } 12 | 13 | java { 14 | toolchain.languageVersion.set(JavaLanguageVersion.of(11)) 15 | } 16 | 17 | tasks.withType { 18 | kotlinOptions { 19 | jvmTarget = "11" 20 | } 21 | } 22 | 23 | gradlePlugin { 24 | plugins { 25 | register("AppPlugin") { 26 | id = "AppPlugin" 27 | implementationClass = "plugins.AppPlugin" 28 | } 29 | register("AndroidLibraryPlugin") { 30 | id = "AndroidLibraryPlugin" 31 | implementationClass = "plugins.AndroidLibraryPlugin" 32 | } 33 | register("KotlinLibraryPlugin") { 34 | id = "KotlinLibraryPlugin" 35 | implementationClass = "plugins.KotlinLibraryPlugin" 36 | } 37 | } 38 | } 39 | 40 | object Versions { 41 | const val GRADLE = "7.2.0" 42 | const val KOTLIN = "1.6.10" 43 | const val HILT = "2.40.5" 44 | const val NAVIGATION = "2.4.2" 45 | } 46 | 47 | object Deps { 48 | const val ANDROID_GRADLE = "com.android.tools.build:gradle:${Versions.GRADLE}" 49 | const val KOTLIN_GRADLE = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}" 50 | const val HILT_GRADLE = "com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT}" 51 | const val Navigation_GRADLE = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.NAVIGATION}" 52 | } 53 | 54 | dependencies { 55 | implementation(gradleApi()) 56 | implementation(Deps.ANDROID_GRADLE) 57 | implementation(Deps.KOTLIN_GRADLE) 58 | implementation(Deps.HILT_GRADLE) 59 | implementation(Deps.Navigation_GRADLE) 60 | } -------------------------------------------------------------------------------- /buildSrc/copyright.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) $YEAR, developersancho 3 | * All rights reserved. 4 | */ 5 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/Configs.kt: -------------------------------------------------------------------------------- 1 | object Configs { 2 | 3 | const val Id = "mohsen.soltanian.cleanarchitecture" 4 | const val VersionCode = 1 5 | const val VersionName = "1.01" 6 | const val MinSdk = 26 7 | const val TargetSdk = 32 8 | const val CompileSdk = 32 9 | const val AndroidJunitRunner = "androidx.test.runner.AndroidJUnitRunner" 10 | 11 | object Release { 12 | const val BaseUrl = "https://api.themoviedb.org/3/" 13 | } 14 | 15 | object Debug { 16 | const val BaseUrl = "https://api.themoviedb.org/3/" 17 | } 18 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/GradlePlugins.kt: -------------------------------------------------------------------------------- 1 | object GradlePlugins { 2 | const val AndroidApplication = "com.android.application" 3 | const val AndroidLibrary = "com.android.library" 4 | const val KotlinLibrary = "org.jetbrains.kotlin.jvm" 5 | const val KotlinAndroid = "org.jetbrains.kotlin.android" 6 | 7 | const val KotlinKapt = "kotlin-kapt" 8 | const val KotlinParcelize = "kotlin-parcelize" 9 | 10 | const val Hilt = "dagger.hilt.android.plugin" 11 | const val Navigation = "androidx.navigation.safeargs" 12 | 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/ProductFlavors.kt: -------------------------------------------------------------------------------- 1 | object FlavorDimensions { 2 | const val DEFAULT = "default" 3 | } 4 | object ProductFlavors { 5 | const val DEV = "dev" 6 | const val INTERNAL = "internal" 7 | const val PUBLIC = "public" 8 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Versions.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | 3 | const val CoreKtx = "1.7.0" 4 | const val AppCompat = "1.4.0" 5 | const val Navigation = "2.4.2" 6 | const val LegacySupport = "1.0.0" 7 | const val Material = "1.4.0" 8 | const val ConstraintLayout = "2.1.2" 9 | const val RecyclerView = "1.2.1" 10 | const val ActivityKtx = "1.4.0" 11 | const val FragmentKtx = "1.4.0" 12 | const val Paging = "3.1.0" 13 | const val Lifecycle = "2.4.0" 14 | const val Arch = "2.1.0" 15 | const val Moshi = "1.12.0" 16 | const val MoshiLazy = "2.2" 17 | const val Retrofit = "2.9.0" 18 | const val Chucker = "3.5.2" 19 | const val Okhttp = "4.9.3" 20 | const val glide = "4.13.0" 21 | const val fabCounter = "1.2.2" 22 | // hilt 23 | const val Hilt = "2.40.5" 24 | 25 | // Coroutines dependencies 26 | const val Coroutine = "1.6.0" 27 | const val Timber = "5.0.1" 28 | 29 | const val Junit = "4.13.2" 30 | const val JunitExt = "1.1.3" 31 | const val TestCoreExt = "1.4.0" 32 | const val EspressoCore = "3.4.0" 33 | const val Robolectric = "4.8.1" 34 | const val Mockk = "1.12.1" 35 | const val Assertj = "3.21.0" 36 | const val Test = "1.4.0" 37 | const val Hamcrest = "2.2" 38 | const val Json = "20210307" 39 | const val Turbine = "0.7.0" 40 | const val Truth = "1.1.3" 41 | const val Jupiter = "5.3.1" 42 | const val kluent = "1.14" 43 | const val robolectric = "4.4" 44 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/extensions/DependencyHandlerExtensions.kt: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import org.gradle.api.artifacts.Dependency 4 | import org.gradle.api.artifacts.dsl.DependencyHandler 5 | 6 | /** 7 | * Adds a dependency to the `releaseImplementation` configuration. 8 | * 9 | * @param dependencyNotation name of dependency to add at specific configuration 10 | * 11 | * @return the dependency 12 | */ 13 | fun DependencyHandler.releaseImplementation(dependencyNotation: Any): Dependency? = 14 | add("releaseImplementation", dependencyNotation) 15 | 16 | /** 17 | * Adds a dependency to the `debugImplementation` configuration. 18 | * 19 | * @param dependencyNotation name of dependency to add at specific configuration 20 | * 21 | * @return the dependency 22 | */ 23 | fun DependencyHandler.debugImplementation(dependencyNotation: Any): Dependency? = 24 | add("debugImplementation", dependencyNotation) 25 | 26 | /** 27 | * Adds a dependency to the `implementation` configuration. 28 | * 29 | * @param dependencyNotation name of dependency to add at specific configuration 30 | * 31 | * @return the dependency 32 | */ 33 | fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? = 34 | add("implementation", dependencyNotation) 35 | 36 | /** 37 | * Adds a dependency to the `api` configuration. 38 | * 39 | * @param dependencyNotation name of dependency to add at specific configuration 40 | * 41 | * @return the dependency 42 | */ 43 | fun DependencyHandler.api(dependencyNotation: Any): Dependency? = 44 | add("api", dependencyNotation) 45 | 46 | /** 47 | * Adds a dependency to the `kapt` configuration. 48 | * 49 | * @param dependencyNotation name of dependency to add at specific configuration 50 | * 51 | * @return the dependency 52 | */ 53 | fun DependencyHandler.kapt(dependencyNotation: Any): Dependency? = 54 | add("kapt", dependencyNotation) 55 | 56 | /** 57 | * Adds a dependency to the `testImplementation` configuration. 58 | * 59 | * @param dependencyNotation name of dependency to add at specific configuration 60 | * 61 | * @return the dependency 62 | */ 63 | fun DependencyHandler.testImplementation(dependencyNotation: Any): Dependency? = 64 | add("testImplementation", dependencyNotation) 65 | 66 | 67 | /** 68 | * Adds a dependency to the `androidTestImplementation` configuration. 69 | * 70 | * @param dependencyNotation name of dependency to add at specific configuration 71 | * 72 | * @return the dependency 73 | */ 74 | fun DependencyHandler.androidTestImplementation(dependencyNotation: Any): Dependency? = 75 | add("androidTestImplementation", dependencyNotation) 76 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugins/AndroidLibraryPlugin.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import Configs 4 | import GradlePlugins 5 | import com.android.build.api.dsl.BuildType 6 | import com.android.build.gradle.LibraryExtension 7 | import org.gradle.api.JavaVersion 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.gradle.kotlin.dsl.getByType 11 | import org.gradle.kotlin.dsl.withType 12 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 13 | 14 | class AndroidLibraryPlugin: Plugin { 15 | override fun apply(target: Project) { 16 | target.applyPlugins() 17 | target.configureAndroid() 18 | } 19 | 20 | private fun Project.applyPlugins() { 21 | plugins.apply(GradlePlugins.AndroidLibrary) 22 | plugins.apply(GradlePlugins.KotlinAndroid) 23 | plugins.apply(GradlePlugins.KotlinKapt) 24 | plugins.apply(GradlePlugins.KotlinParcelize) 25 | } 26 | 27 | private fun Project.configureAndroid() = this.extensions.getByType(LibraryExtension::class).run { 28 | compileSdk = Configs.CompileSdk 29 | defaultConfig.apply { 30 | minSdk = Configs.MinSdk 31 | targetSdk = Configs.TargetSdk 32 | versionCode = Configs.VersionCode 33 | versionName = Configs.VersionName 34 | multiDexEnabled = true 35 | vectorDrawables.useSupportLibrary = true 36 | testInstrumentationRunner = Configs.AndroidJunitRunner 37 | //consumerProguardFiles("consumer-rules.pro") 38 | } 39 | 40 | compileOptions.apply { 41 | sourceCompatibility = JavaVersion.VERSION_11 42 | targetCompatibility = JavaVersion.VERSION_11 43 | } 44 | 45 | packagingOptions.apply { 46 | resources { 47 | setExcludes( 48 | setOf( 49 | "META-INF/metadata.kotlin_module", 50 | "META-INF/metadata.jvm.kotlin_module", 51 | "META-INF/AL2.0", 52 | "META-INF/LGPL2.1" 53 | ) 54 | ) 55 | } 56 | } 57 | 58 | dataBinding.apply { isEnabled = true } 59 | viewBinding.apply { isEnabled = true } 60 | 61 | buildTypes.apply { 62 | getByName("release") { 63 | isMinifyEnabled = true 64 | proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") 65 | buildConfigStringField("BASE_URL", Configs.Release.BaseUrl) 66 | } 67 | getByName("debug") { 68 | proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") 69 | buildConfigStringField("BASE_URL", Configs.Debug.BaseUrl) 70 | } 71 | } 72 | 73 | flavorDimensions(FlavorDimensions.DEFAULT) 74 | productFlavors { 75 | create(ProductFlavors.DEV) { 76 | dimension = FlavorDimensions.DEFAULT 77 | } 78 | create(ProductFlavors.INTERNAL) { 79 | dimension = FlavorDimensions.DEFAULT 80 | } 81 | create(ProductFlavors.PUBLIC) { 82 | dimension = FlavorDimensions.DEFAULT 83 | } 84 | } 85 | 86 | variantFilter { 87 | ignore = listOf("devRelease","internalDebug","publicDebug").contains(element = name) 88 | } 89 | 90 | } 91 | 92 | private fun BuildType.buildConfigStringField(name: String, value: String) { 93 | this.buildConfigField("String", name, "\"$value\"") 94 | } 95 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugins/KotlinLibraryPlugin.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | class KotlinLibraryPlugin : Plugin { 7 | override fun apply(target: Project) { 8 | target.applyPlugins() 9 | } 10 | 11 | private fun Project.applyPlugins() { 12 | plugins.apply(GradlePlugins.KotlinLibrary) 13 | } 14 | } -------------------------------------------------------------------------------- /core/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import plugins.AndroidLibraryPlugin 2 | import extensions.* 3 | 4 | apply() 5 | apply(plugin = GradlePlugins.Hilt) 6 | 7 | dependencies { 8 | 9 | implementation(Deps.Retrofit.Base) 10 | implementation(Deps.Retrofit.Moshi) 11 | implementation(Deps.Moshi.Kotlin) 12 | implementation(Deps.Moshi.LazyAdapter) 13 | implementation(Deps.Okhttp.Base) 14 | implementation(Deps.Okhttp.LoggingInterceptor) 15 | 16 | implementation(Deps.Hilt.Android) 17 | kapt(Deps.Hilt.AndroidCompiler) 18 | 19 | api(Deps.Timber) 20 | api(Deps.Coroutine.Core) 21 | api(Deps.Coroutine.Android) 22 | api(Deps.Test.Json) 23 | api(Deps.Test.Okhttp) 24 | api(Deps.Test.Coroutine) 25 | 26 | debugImplementation(Deps.Chucker.Library) 27 | releaseImplementation(Deps.Chucker.NoLibrary) 28 | 29 | testImplementation(Deps.Test.Hamcrest) 30 | testImplementation(Deps.Test.kluent) 31 | testImplementation(Deps.Test.Mockk) 32 | testImplementation(Deps.Test.TestRules) 33 | } -------------------------------------------------------------------------------- /core/data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/core/data/consumer-rules.pro -------------------------------------------------------------------------------- /core/data/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 -------------------------------------------------------------------------------- /core/data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/config/RemoteConfig.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.config 2 | 3 | import mohsen.soltanian.cleanarchitecture.core.data.BuildConfig 4 | import mohsen.soltanian.cleanarchitecture.core.data.enviroment.CoreEnvironment 5 | import javax.inject.Inject 6 | 7 | 8 | class RemoteConfig @Inject constructor() { 9 | 10 | private val timeOut: Long = 25 11 | 12 | fun environment(): CoreEnvironment { 13 | return if (isDev()) { 14 | CoreEnvironment.DEV 15 | } else { 16 | CoreEnvironment.PUBLIC 17 | } 18 | } 19 | 20 | fun baseUrl(): String { 21 | return when (environment()) { 22 | CoreEnvironment.DEV, -> { 23 | BuildConfig.BASE_URL 24 | } 25 | CoreEnvironment.INTERNAL, CoreEnvironment.PUBLIC -> { 26 | BuildConfig.BASE_URL 27 | } 28 | } 29 | } 30 | 31 | fun timeOut(): Long { 32 | return timeOut 33 | } 34 | 35 | fun isDev(): Boolean { 36 | return BuildConfig.DEBUG 37 | } 38 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/enviroment/CoreEnvironment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.enviroment 2 | 3 | enum class CoreEnvironment { 4 | DEV, 5 | INTERNAL, 6 | PUBLIC; 7 | } 8 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/extention/MoshiExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.extention 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.Types 6 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 7 | import mohsen.soltanian.cleanarchitecture.core.data.helper.NullToEmptyListAdapter 8 | import mohsen.soltanian.cleanarchitecture.core.data.helper.NullToEmptyStringAdapter 9 | 10 | val moshi: Moshi = Moshi.Builder() 11 | .add(NullToEmptyStringAdapter.FACTORY) 12 | .add(NullToEmptyListAdapter.FACTORY) 13 | .addLast(KotlinJsonAdapterFactory()) 14 | .build() 15 | 16 | inline fun String.fromJson(): T? { 17 | return try { 18 | val jsonAdapter = moshi.adapter(T::class.java) 19 | jsonAdapter.fromJson(this) 20 | } catch (ex: Exception) { 21 | null 22 | } 23 | } 24 | 25 | inline fun String.fromJsonList(): List? { 26 | return try { 27 | val type = Types.newParameterizedType(MutableList::class.java, T::class.java) 28 | val jsonAdapter: JsonAdapter> = moshi.adapter(type) 29 | jsonAdapter.fromJson(this) 30 | } catch (ex: Exception) { 31 | null 32 | } 33 | } 34 | 35 | inline fun T.toJson(): String { 36 | return try { 37 | val jsonAdapter = moshi.adapter(T::class.java).serializeNulls().lenient() 38 | jsonAdapter.toJson(this) 39 | } catch (ex: Exception) { 40 | "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/extention/Value.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.extention 2 | 3 | fun String.Companion.empty() = "" 4 | 5 | fun Long.Companion.empty() = 0L 6 | 7 | fun Double.Companion.empty() = 0.0 8 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/helper/NullToEmptyListAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.helper 2 | 3 | import com.squareup.moshi.* 4 | import java.io.IOException 5 | import java.lang.reflect.Type 6 | 7 | class NullToEmptyListAdapter(private val delegate: JsonAdapter>) : 8 | JsonAdapter?>() { 9 | @Throws(IOException::class) 10 | override fun fromJson(reader: JsonReader): List<*>? { 11 | if (reader.peek() == JsonReader.Token.NULL) { 12 | reader.skipValue() 13 | return emptyList() 14 | } 15 | return delegate.fromJson(reader) 16 | } 17 | 18 | @Throws(IOException::class) 19 | override fun toJson(writer: JsonWriter, value: List<*>?) { 20 | if(value == null) { 21 | delegate.toJson(writer, emptyList()) 22 | }else { 23 | delegate.toJson(writer, value) 24 | } 25 | } 26 | 27 | companion object { 28 | val FACTORY: Factory = object : Factory { 29 | override fun create( 30 | type: Type, 31 | annotations: Set, 32 | moshi: Moshi 33 | ): JsonAdapter<*>? { 34 | if (annotations.isNotEmpty()) { 35 | return null 36 | } 37 | if (Types.getRawType(type) != MutableList::class.java) { 38 | return null 39 | } 40 | val objectJsonAdapter = moshi.nextAdapter>(this, type, annotations) 41 | return NullToEmptyListAdapter(objectJsonAdapter) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/helper/NullToEmptyStringAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.helper 2 | 3 | import com.squareup.moshi.* 4 | import mohsen.soltanian.cleanarchitecture.core.data.extention.empty 5 | import java.io.IOException 6 | import java.lang.reflect.Type 7 | 8 | class NullToEmptyStringAdapter( 9 | val delegate: JsonAdapter) : 10 | JsonAdapter() { 11 | @Throws(IOException::class) 12 | override fun fromJson(reader: JsonReader): String? { 13 | if (reader.peek() == JsonReader.Token.NULL) { 14 | reader.skipValue() 15 | return String.empty() 16 | } 17 | return delegate.fromJson(reader) 18 | } 19 | 20 | @Throws(IOException::class) 21 | override fun toJson(writer: JsonWriter, value: String?) { 22 | if(value == null) { 23 | delegate.toJson(writer, String.empty()) 24 | }else { 25 | delegate.toJson(writer, value) 26 | } 27 | } 28 | 29 | companion object { 30 | val FACTORY: Factory = object : Factory { 31 | override fun create( 32 | type: Type, 33 | annotations: Set, 34 | moshi: Moshi 35 | ): JsonAdapter<*>? { 36 | if (annotations.isNotEmpty()) { 37 | return null 38 | } 39 | if (Types.getRawType(type) != String::class.java) { 40 | return null 41 | } 42 | val objectJsonAdapter = moshi.nextAdapter(this, type, annotations) 43 | return NullToEmptyStringAdapter(objectJsonAdapter) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/implClasses/ServiceImp.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.implClasses 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 5 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 6 | import mohsen.soltanian.cleanarchitecture.core.data.scopes.ServerService 7 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi 8 | import retrofit2.Retrofit 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | class ServiceImp 14 | @Inject constructor( 15 | @ServerService var retrofit: Retrofit) : RemoteApi { 16 | private val api by lazy { retrofit.create(RemoteApi::class.java) } 17 | 18 | override suspend fun getMovies(sortBy: String, page: Int, apiKey: String): MoviesResponse = 19 | api.getMovies(sortBy = sortBy, page = page, apiKey = apiKey) 20 | 21 | override suspend fun getCasts(movieId: String, apiKey: String): CastsResponse = 22 | api.getCasts(movieId = movieId, apiKey = apiKey) 23 | 24 | override suspend fun searchForMovies(query: String, apiKey: String): MoviesResponse = 25 | api.searchForMovies(query = query, apiKey = apiKey) 26 | 27 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/Cast.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class Cast( 8 | @Json(name= "character") 9 | val character: String?, 10 | @Json(name = "name") 11 | val name: String?, 12 | @Json(name = "profile_path") 13 | val profilePath: String? 14 | ) 15 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/CastsResponse.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import com.squareup.moshi.Json 4 | 5 | data class CastsResponse( 6 | @Json(name = "id") 7 | val id: Long?, 8 | @Json(name = "cast") 9 | val cast: List? 10 | ) { 11 | companion object { 12 | val default = CastsResponse(id = 0, cast = listOf()) 13 | } 14 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/Movie.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import android.os.Parcelable 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Parcelize 9 | @JsonClass(generateAdapter = true) 10 | class Movie( 11 | @Json(name = "adult") 12 | val adult: Boolean?, 13 | @Json(name = "backdrop_path") 14 | val movieBackdrop: String?, 15 | @Json(name = "genre_ids") 16 | val genreIds: List?, 17 | @Json(name = "id") 18 | val movieId: String?, 19 | @Json(name = "original_language") 20 | val movieLanguage: String?, 21 | @Json(name = "original_title") 22 | val originalTitle: String?, 23 | @Json(name = "overview") 24 | val movieDescription: String?, 25 | @Json(name = "popularity") 26 | val popularity: Double?, 27 | @Json(name = "poster_path") 28 | val moviePoster: String?, 29 | @Json(name = "release_date") 30 | val movieReleaseDate: String?, 31 | @Json(name = "title") 32 | val movieTitle: String?, 33 | @Json(name = "video") 34 | val video: Boolean?, 35 | @Json(name = "vote_average") 36 | val movieVote: Double?, 37 | @Json(name = "vote_count") 38 | val voteCount: Long? 39 | ) : Parcelable -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/MoviesResponse.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class MoviesResponse( 8 | @Json(name = "page") 9 | val page: Int?, 10 | @Json(name = "results") 11 | val results: List?, 12 | ) -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/network/HandleError.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.network 2 | 3 | import retrofit2.HttpException 4 | import java.io.IOException 5 | import java.net.SocketTimeoutException 6 | import java.net.UnknownHostException 7 | 8 | sealed class Failure : IOException() { 9 | object JsonError : Failure() 10 | object UnknownError : Failure() 11 | object UnknownHostError : Failure() 12 | object EmptyResponse : Failure() 13 | object ConnectivityError : Failure() 14 | object InternetError : Failure() 15 | object UnAuthorizedException : Failure() 16 | object ParsingDataError : Failure() 17 | object IgnorableError : Failure() 18 | data class TimeOutError(override var message: String) : Failure() 19 | data class ApiError(var code: Int = 0, override var message: String) : Failure() 20 | data class ServerError(var code: Int = 0, override var message: String) : Failure() 21 | data class NotFoundException(override var message: String) : Failure() 22 | data class SocketTimeoutError(override var message: String) : Failure() 23 | data class BusinessError(override var message: String, val stackTrace: String) : Failure() 24 | data class HttpError(var code: Int, override var message: String) : Failure() 25 | } 26 | 27 | fun Throwable.handleThrowable(): Failure { 28 | // Timber.e(this) 29 | return if (this is UnknownHostException) { 30 | Failure.ConnectivityError 31 | } else if (this is HttpException && this.code() == HttpStatusCode.Unauthorized.code) { 32 | Failure.UnAuthorizedException 33 | } else if (this is SocketTimeoutException) { 34 | Failure.SocketTimeoutError(this.message!!) 35 | } else if (this.message != null) { 36 | Failure.NotFoundException(this.message!!) 37 | } else { 38 | Failure.UnknownError 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/network/HttpStatusCode.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.network 2 | 3 | enum class HttpStatusCode(val code: Int) { 4 | 5 | Unknown(-1), 6 | 7 | // Client Errors 8 | BadRequest(400), 9 | Unauthorized(401), 10 | PaymentRequired(402), 11 | Forbidden(403), 12 | NotFound(404), 13 | MethodNotAllowed(405), 14 | NotAcceptable(406), 15 | ProxyAuthenticationRequired(407), 16 | RequestTimeout(408), 17 | Conflict(409), 18 | Gone(410), 19 | LengthRequired(411), 20 | PreconditionFailed(412), 21 | PayloadTooLarge(413), 22 | UriTooLong(414), 23 | UnsupportedMediaType(415), 24 | RangeNotSatisfiable(416), 25 | ExpectationFailed(417), 26 | ImATeapot(418), 27 | MisdirectedRequest(421), 28 | UnProcessableEntity(422), 29 | Locked(423), 30 | FailedDependency(424), 31 | UpgradeRequired(426), 32 | PreconditionRequired(428), 33 | TooManyRequests(429), 34 | RequestHeaderFieldsTooLarge(431), 35 | UnavailableForLegalReasons(451), 36 | 37 | // Server Errors 38 | InternalServerError(500), 39 | NotImplemented(501), 40 | BadGateway(502), 41 | ServiceUnavailable(503), 42 | GatewayTimeout(504), 43 | HttpVersionNotSupported(505), 44 | VariantAlsoNegates(506), 45 | InsufficientStorage(507), 46 | LoopDetected(508), 47 | NotExtended(510), 48 | NetworkAuthenticationRequired(511); 49 | } 50 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/network/interceptor/HttpRequestInterceptor.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.network.interceptor 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | import timber.log.Timber 6 | 7 | class HttpRequestInterceptor : Interceptor { 8 | override fun intercept(chain: Interceptor.Chain): Response { 9 | val originalRequest = chain.request() 10 | val request = originalRequest.newBuilder().url(originalRequest.url).build() 11 | Timber.d(request.toString()) 12 | return chain.proceed(request) 13 | } 14 | } -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/network/moshi/EnumValueJsonAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.network.moshi 2 | 3 | import com.squareup.moshi.* 4 | import java.io.IOException 5 | import kotlin.reflect.KClass 6 | 7 | class EnumValueJsonAdapter 8 | private constructor( 9 | private val enumType: Class, 10 | private val fallbackValue: T?, 11 | private val useFallbackValue: Boolean 12 | ) : JsonAdapter() { 13 | private val constants: Array = enumType.enumConstants as Array 14 | private val enumValues: HashMap = constants.associateByTo(hashMapOf()) { it.value } 15 | 16 | /** 17 | * Create a new adapter for this enum with a fallback value to use when the JSON value does not 18 | * match any of the enum's constants. Note that this value will not be used when the JSON value is 19 | * null, absent, or not a string. Also, the string values are case-sensitive, and this fallback 20 | * value will be used even on case mismatches. 21 | */ 22 | fun withUnknownFallback(fallbackValue: T?): EnumValueJsonAdapter = 23 | EnumValueJsonAdapter(enumType, fallbackValue, true) 24 | 25 | @Throws(IOException::class) 26 | override fun fromJson(reader: JsonReader): T? { 27 | val value = reader.nextInt() 28 | if (enumValues.containsKey(value.toString())) { 29 | return enumValues[value.toString()] 30 | } 31 | 32 | val path = reader.path 33 | if (!useFallbackValue) { 34 | throw JsonDataException("Unknown value of enum ${enumType.name} ($value) at path $path") 35 | } 36 | return fallbackValue 37 | } 38 | 39 | @Throws(IOException::class) 40 | override fun toJson(writer: JsonWriter, value: T?) { 41 | if (value == null) { 42 | throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.") 43 | } 44 | writer.value(value.value) 45 | } 46 | 47 | override fun toString() = "EnumJsonAdapter(" + enumType.name + ")" 48 | 49 | companion object { 50 | fun create( 51 | enumType: Class, 52 | unknownFallback: T? = null 53 | ): EnumValueJsonAdapter { 54 | val useFallbackValue = (unknownFallback != null) 55 | return EnumValueJsonAdapter(enumType, unknownFallback, useFallbackValue) 56 | } 57 | } 58 | } 59 | 60 | interface IValueEnum { 61 | val value: String 62 | } 63 | 64 | fun Moshi.Builder.addValueEnum( 65 | kClass: KClass, 66 | unknownFallback: T? = null 67 | ): Moshi.Builder = 68 | this.add(kClass.java, EnumValueJsonAdapter.create(kClass.java, unknownFallback).nullSafe()) 69 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/scopes/ServerService.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.scopes 2 | 3 | import javax.inject.Qualifier 4 | 5 | /** Annotation for Retrofit dependency. */ 6 | @Qualifier 7 | @Target( 8 | AnnotationTarget.FUNCTION, 9 | AnnotationTarget.PROPERTY_GETTER, 10 | AnnotationTarget.PROPERTY_SETTER, 11 | AnnotationTarget.VALUE_PARAMETER, 12 | AnnotationTarget.FIELD 13 | ) 14 | annotation class ServerService() 15 | -------------------------------------------------------------------------------- /core/data/src/main/java/mohsen/soltanian/cleanarchitecture/core/data/services/RemoteApi.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.services 2 | 3 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 4 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 5 | import retrofit2.http.GET 6 | import retrofit2.http.Path 7 | import retrofit2.http.Query 8 | 9 | interface RemoteApi { 10 | 11 | companion object { 12 | const val API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXX" 13 | } 14 | 15 | 16 | @GET("movie/{sort}") 17 | suspend fun getMovies( 18 | @Path("sort") sortBy: String, 19 | @Query("page") page: Int, 20 | @Query("api_key") apiKey: String 21 | ): MoviesResponse 22 | 23 | @GET("movie/{movieId}/credits") 24 | suspend fun getCasts( 25 | @Path("movieId") movieId: String, 26 | @Query("api_key") apiKey: String 27 | ): CastsResponse 28 | 29 | @GET("search/movie") 30 | suspend fun searchForMovies( 31 | @Query("query") query: String, 32 | @Query("api_key") apiKey: String 33 | ): MoviesResponse 34 | 35 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/DataUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.core 2 | 3 | import io.mockk.MockKAnnotations 4 | import org.junit.After 5 | import org.junit.Before 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.BlockJUnit4ClassRunner 8 | 9 | @RunWith(BlockJUnit4ClassRunner::class) 10 | open class DataUnitTest { 11 | 12 | open fun onSetUpTest() {} 13 | 14 | open fun onStopTest() {} 15 | 16 | @Before 17 | fun onSetup() { 18 | MockKAnnotations.init(this) 19 | onSetUpTest() 20 | } 21 | 22 | @After 23 | fun onTearDown() { 24 | onStopTest() 25 | } 26 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ModelTesting.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.core 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @kotlin.annotation.Target(AnnotationTarget.CLASS) 6 | annotation class ModelTesting(val clazz: KClass<*>, val modelFields: Array = []) 7 | -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ModelUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.core 2 | 3 | import android.annotation.SuppressLint 4 | import io.mockk.MockKAnnotations 5 | import mohsen.soltanian.cleanarchitecture.core.data.extention.toJson 6 | import org.hamcrest.CoreMatchers 7 | import org.hamcrest.MatcherAssert 8 | import org.hamcrest.Matchers 9 | import org.json.JSONException 10 | import org.json.JSONObject 11 | import org.junit.Before 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | import org.junit.runners.BlockJUnit4ClassRunner 15 | import java.lang.reflect.Constructor 16 | 17 | @RunWith(BlockJUnit4ClassRunner::class) 18 | abstract class ModelUnitTest { 19 | private val parameters = ArrayList() 20 | 21 | private val modelTesting: ModelTesting? = javaClass.getAnnotation(ModelTesting::class.java) 22 | 23 | @SuppressLint("NewApi") 24 | private fun getConstructor(clazz: Class<*>?): Any? { 25 | val constructors = clazz?.declaredConstructors 26 | if (constructors?.isNotEmpty() == true) { 27 | val nonConstructor = findNonConstructor(constructors) 28 | nonConstructor.isAccessible = true 29 | return try { 30 | nonConstructor.newInstance(*arrayOfNulls(nonConstructor.parameters.size)) 31 | } catch (ex: Exception) { 32 | ex.printStackTrace() 33 | null 34 | } 35 | } 36 | 37 | return null 38 | } 39 | 40 | private fun findNonConstructor(constructors: Array>): Constructor<*> { 41 | var originalConstructor = constructors.first() 42 | for (element in constructors) { 43 | if (!element.isSynthetic) { 44 | originalConstructor = element 45 | } 46 | } 47 | return originalConstructor 48 | } 49 | 50 | @SuppressLint("NewApi") 51 | @Throws(JSONException::class) 52 | @Before 53 | fun setUpTest() { 54 | MockKAnnotations.init(this) 55 | checkNotNull(modelTesting) { 56 | "you didn't use ModelTesting annotation." 57 | } 58 | val modelClass = modelTesting.clazz.javaObjectType 59 | val obj = getConstructor(modelClass) 60 | val responseJson = obj.toJson() 61 | val jsonObject = JSONObject(responseJson) 62 | val iterator = jsonObject.keys() 63 | parameters.clear() 64 | iterator.forEachRemaining { parameters.add(it) } 65 | } 66 | 67 | @Test 68 | fun `run test validate Model Fields`() { 69 | val list = modelTesting?.modelFields?.toList() 70 | val paramItems = Matchers.`is`(Matchers.`in`(parameters)) 71 | val listItems = Matchers.`is`(Matchers.`in`(list)) 72 | MatcherAssert.assertThat(list, CoreMatchers.everyItem(paramItems)) 73 | MatcherAssert.assertThat(parameters, CoreMatchers.everyItem(listItems)) 74 | } 75 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ServiceUnitTesting.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.core 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @kotlin.annotation.Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 6 | annotation class ServiceUnitTesting(val baseUrl: String, val clazz: KClass<*>) 7 | -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/core/ServicesUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.core 2 | 3 | 4 | import mohsen.soltanian.cleanarchitecture.core.data.extention.moshi 5 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import okhttp3.mockwebserver.MockResponse 9 | import okhttp3.mockwebserver.MockWebServer 10 | import okio.buffer 11 | import okio.source 12 | import retrofit2.Retrofit 13 | import retrofit2.converter.moshi.MoshiConverterFactory 14 | import kotlin.reflect.KClass 15 | 16 | abstract class ServicesUnitTest : DataUnitTest() { 17 | 18 | private var baseUrl: String = "" 19 | private lateinit var serviceClass: KClass<*> 20 | lateinit var mockWebServerInstance: MockWebServer 21 | private val config: ServiceUnitTesting = javaClass.getAnnotation(ServiceUnitTesting::class.java) as ServiceUnitTesting 22 | 23 | private var okhttpInstance: OkHttpClient = OkHttpClient.Builder() 24 | .followSslRedirects(true) 25 | .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) 26 | .build() 27 | 28 | override fun onSetUpTest() { 29 | super.onSetUpTest() 30 | baseUrl = config.baseUrl 31 | serviceClass = config.clazz 32 | 33 | mockWebServerInstance = MockWebServer().apply { 34 | start() 35 | } 36 | } 37 | 38 | protected fun serviceBuilder(isLive: Boolean = false): RemoteApi { 39 | return if(isLive) { 40 | Retrofit.Builder() 41 | .client(okhttpInstance) 42 | .baseUrl(baseUrl) 43 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 44 | .build() 45 | .create(serviceClass.javaObjectType) as RemoteApi 46 | } else { 47 | Retrofit.Builder() 48 | .client(okhttpInstance) 49 | .baseUrl(mockWebServerInstance.url("")) 50 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 51 | .build() 52 | .create(serviceClass.javaObjectType) as RemoteApi 53 | } 54 | } 55 | 56 | fun enqueueResponse(filePath: String) { 57 | val inputStream = javaClass.classLoader?.getResourceAsStream(filePath) 58 | val bufferSource = inputStream?.source()?.buffer() ?: return 59 | val mockResponse = MockResponse() 60 | 61 | mockWebServerInstance.enqueue( 62 | mockResponse.setBody( 63 | bufferSource.readString(Charsets.UTF_8) 64 | ) 65 | ) 66 | } 67 | 68 | override fun onStopTest() { 69 | super.onStopTest() 70 | mockWebServerInstance.shutdown() 71 | } 72 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/helper/NullToEmptyListAdapterTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.helper 2 | 3 | import com.squareup.moshi.* 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import io.mockk.InternalPlatformDsl.toArray 6 | import io.mockk.mockk 7 | import io.mockk.verify 8 | import mohsen.soltanian.cleanarchitecture.core.data.core.DataUnitTest 9 | import mohsen.soltanian.cleanarchitecture.core.data.extention.empty 10 | import mohsen.soltanian.cleanarchitecture.core.data.extention.toJson 11 | import org.amshove.kluent.`should be instance of` 12 | import org.amshove.kluent.`should equal` 13 | import org.junit.Test 14 | 15 | class NullToEmptyListAdapterTest : DataUnitTest() { 16 | 17 | 18 | @JsonClass(generateAdapter = true) 19 | data class ListModel( 20 | @Json(name = "keys") 21 | val keys: List? 22 | ) 23 | 24 | data class ToJsonListModel( 25 | val keys: List?= null 26 | ) 27 | 28 | val moshi: Moshi = Moshi.Builder().add(NullToEmptyListAdapter.FACTORY).addLast(KotlinJsonAdapterFactory()).build() 29 | 30 | 31 | @Test 32 | fun `verify fromJson method when All values are null`() { 33 | val json = "{\"keys\": null}".trimIndent() 34 | val jsonAdapter = moshi.adapter(ListModel::class.java).serializeNulls().lenient() 35 | val model = jsonAdapter.fromJson(json) 36 | model `should be instance of` ListModel::class.java 37 | model?.keys `should be instance of` List::class.java 38 | model?.keys?.size `should equal` 0 39 | 40 | } 41 | 42 | @Test 43 | fun `verify toJson method when All values are null`() { 44 | val jsonAdapter = moshi.adapter(ToJsonListModel::class.java).serializeNulls().lenient() 45 | val strModel = jsonAdapter.toJson(ToJsonListModel()) 46 | val model = jsonAdapter.fromJson(strModel) 47 | model `should be instance of` ToJsonListModel::class.java 48 | model?.keys `should be instance of` List::class.java 49 | model?.keys `should equal` listOf() 50 | 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/implClasses/ServiceImpTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.implClasses 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.impl.annotations.MockK 6 | import io.mockk.mockk 7 | import kotlinx.coroutines.test.runTest 8 | import mohsen.soltanian.cleanarchitecture.core.data.core.DataUnitTest 9 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 10 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 11 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 12 | import org.amshove.kluent.`should be instance of` 13 | import org.amshove.kluent.`should equal` 14 | import org.junit.Test 15 | 16 | class ServiceImpTest: DataUnitTest() { 17 | 18 | @MockK 19 | lateinit var serviceImp: ServiceImp 20 | 21 | @Test 22 | fun `get Movies list`() = runTest { 23 | val resModel = mockk(relaxed = true) 24 | coEvery { serviceImp.getMovies(sortBy = any(), page = any(), apiKey = any()) } returns resModel 25 | val response = serviceImp.getMovies(sortBy = "popular", page = 1, apiKey = API_KEY) 26 | response `should be instance of` MoviesResponse::class.java 27 | response.page `should equal` 0 28 | response.results `should equal` listOf() 29 | 30 | coVerify(exactly = 1) { serviceImp.getMovies(sortBy = any(), page = any(), apiKey = any()) } 31 | } 32 | 33 | @Test 34 | fun `get Casts list`() = runTest() { 35 | val resModel = mockk(relaxed = true) 36 | coEvery { serviceImp.getCasts(movieId = any(), apiKey = any()) } returns resModel 37 | val response = serviceImp.getCasts(movieId = "0", apiKey = API_KEY) 38 | response `should be instance of` CastsResponse::class.java 39 | response.id `should equal` 0L 40 | response.cast `should equal` listOf() 41 | 42 | coVerify(exactly = 1) { serviceImp.getCasts(movieId = any(), apiKey = any()) } 43 | 44 | } 45 | 46 | @Test 47 | fun `search movies by movie name`() = runTest() { 48 | val resModel = mockk(relaxed = true) 49 | coEvery { serviceImp.searchForMovies(any(), any()) } returns resModel 50 | val response = serviceImp.searchForMovies("memory", API_KEY) 51 | response `should be instance of` MoviesResponse::class.java 52 | response.page `should equal` 0 53 | response.results `should equal` listOf() 54 | 55 | coVerify(exactly = 1) { serviceImp.searchForMovies(query = any(), apiKey = any()) } 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/CastModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.verify 6 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelTesting 7 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelUnitTest 8 | import org.amshove.kluent.`should equal` 9 | import org.junit.Test 10 | 11 | @ModelTesting( 12 | clazz = Cast::class, 13 | modelFields = ["name", "character", "profile_path"]) 14 | class CastModelTest : ModelUnitTest() { 15 | 16 | @MockK 17 | lateinit var castModel: Cast 18 | 19 | @Test 20 | fun `run test for Cast Model`() { 21 | every { castModel.name } returns "mohsen" 22 | castModel.name `should equal` "mohsen" 23 | 24 | verify(exactly = 1) { castModel.name } 25 | } 26 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/CastsResponseModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import io.mockk.every 4 | import io.mockk.impl.annotations.MockK 5 | import io.mockk.verify 6 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelTesting 7 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelUnitTest 8 | import org.amshove.kluent.`should equal` 9 | import org.junit.Test 10 | 11 | @ModelTesting( 12 | clazz = CastsResponse::class, 13 | modelFields = ["id", "cast"]) 14 | class CastsResponseModelTest : ModelUnitTest() { 15 | 16 | @MockK 17 | lateinit var castsResponse: CastsResponse 18 | 19 | @Test 20 | fun test() { 21 | every { castsResponse.id } returns 0L 22 | every { castsResponse.cast } returns listOf() 23 | 24 | castsResponse.id `should equal` 0L 25 | castsResponse.cast `should equal` listOf() 26 | 27 | verify(exactly = 1) { 28 | castsResponse.id 29 | castsResponse.cast 30 | } 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/MovieModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import io.mockk.impl.annotations.MockK 4 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelTesting 5 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelUnitTest 6 | import mohsen.soltanian.cleanarchitecture.core.data.extention.empty 7 | import org.amshove.kluent.`should equal` 8 | import org.junit.Test 9 | 10 | @ModelTesting( 11 | clazz = Movie::class, 12 | modelFields = ["adult", "backdrop_path", "genre_ids", "id", "original_language", "original_title", 13 | "overview", "popularity", "poster_path", "release_date", "title", "video", "vote_average", "vote_count"]) 14 | class MovieModelTest : ModelUnitTest() { 15 | 16 | @MockK(relaxed = true) 17 | lateinit var movie: Movie 18 | 19 | @Test 20 | fun createResponse() { 21 | movie.movieId `should equal` String.empty() 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/models/response/MoviesResponseModelTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.models.response 2 | 3 | import io.mockk.mockk 4 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelTesting 5 | import mohsen.soltanian.cleanarchitecture.core.data.core.ModelUnitTest 6 | import org.amshove.kluent.`should be instance of` 7 | import org.amshove.kluent.`should equal` 8 | import org.junit.Test 9 | 10 | @ModelTesting( 11 | clazz = MoviesResponse::class, 12 | modelFields = ["page", "results"]) 13 | class MoviesResponseModelTest : ModelUnitTest() { 14 | 15 | @Test 16 | fun test() { 17 | val model = mockk(relaxed = true) 18 | 19 | model `should be instance of` MoviesResponse::class.java 20 | model.page `should equal` 0 21 | model.results `should equal` listOf() 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /core/data/src/test/java/mohsen/soltanian/cleanarchitecture/core/data/services/RemoteApiTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.data.services 2 | 3 | import io.mockk.verify 4 | import kotlinx.coroutines.test.runTest 5 | import mohsen.soltanian.cleanarchitecture.core.data.core.ServicesUnitTest 6 | import mohsen.soltanian.cleanarchitecture.core.data.core.ServiceUnitTesting 7 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 8 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 9 | import org.amshove.kluent.`should be instance of` 10 | import org.junit.Test 11 | 12 | @ServiceUnitTesting(baseUrl = "https://api.themoviedb.org/3/", clazz = RemoteApi::class) 13 | internal class RemoteApiTest: ServicesUnitTest() { 14 | 15 | @Test 16 | fun `run test by live service`() = runTest { 17 | val response = serviceBuilder(isLive = true).getCasts("550",API_KEY) 18 | response `should be instance of` CastsResponse::class.java 19 | } 20 | 21 | @Test 22 | fun `run test by mock service`() = runTest{ 23 | enqueueResponse("mock/get-casts.json") 24 | serviceBuilder(isLive = false).getCasts("550",API_KEY) 25 | mockWebServerInstance.takeRequest() 26 | verify(exactly = 1) { mockWebServerInstance.takeRequest() } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /core/domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import plugins.AndroidLibraryPlugin 2 | import extensions.* 3 | 4 | apply() 5 | apply(plugin = GradlePlugins.Hilt) 6 | 7 | 8 | dependencies { 9 | 10 | implementation(Deps.Hilt.Android) 11 | kapt(Deps.Hilt.AndroidCompiler) 12 | 13 | api(project(path= ":core:data")) 14 | api(Deps.AndroidX.Paging) 15 | 16 | testImplementation(Deps.Test.kluent) 17 | testImplementation(Deps.Test.Junit) 18 | testImplementation(Deps.Test.Mockk) 19 | testImplementation(Deps.Test.TestRules) 20 | 21 | androidTestImplementation(Deps.Test.JunitExt) 22 | androidTestImplementation(Deps.Test.EspressoCore) 23 | 24 | } -------------------------------------------------------------------------------- /core/domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/core/domain/consumer-rules.pro -------------------------------------------------------------------------------- /core/domain/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 -------------------------------------------------------------------------------- /core/domain/src/androidTest/java/mohsen/soltanian/cleanarchitecture/core/domain/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain 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("ir.stsepehr.hamrahcard.core.domain.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/base/BasePagingUseCase.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.base 2 | 3 | import androidx.paging.PagingData 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flowOn 7 | 8 | abstract class BasePagingUseCase where ReturnType : Any { 9 | 10 | protected abstract fun execute(params: Params): Flow> 11 | 12 | operator fun invoke(params: Params): Flow> { 13 | return execute(params).flowOn(Dispatchers.IO) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/base/BaseUseCase.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.base 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.flow.FlowCollector 5 | import kotlinx.coroutines.flow.flow 6 | import kotlinx.coroutines.flow.flowOn 7 | 8 | abstract class BaseUseCase where ReturnType : Any { 9 | 10 | protected abstract suspend fun FlowCollector.execute(params: Params) 11 | 12 | suspend operator fun invoke(params: Params) = flow { 13 | execute(params) 14 | }.flowOn(Dispatchers.IO) 15 | 16 | 17 | open class None 18 | } 19 | -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/pagingSource/MoviePagingSource.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.pagingSource 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import mohsen.soltanian.cleanarchitecture.core.data.implClasses.ServiceImp 6 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 7 | import mohsen.soltanian.cleanarchitecture.core.domain.usecase.MoviesUseCase 8 | import java.io.IOException 9 | 10 | class MoviePagingSource( 11 | private val serviceImp: ServiceImp, 12 | private val dataParams: MoviesUseCase.Params 13 | ) : PagingSource() { 14 | 15 | override fun getRefreshKey(state: PagingState): Int? { 16 | return state.anchorPosition?.let { anchorPosition -> 17 | val anchorPage = state.closestPageToPosition(anchorPosition) 18 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 19 | } 20 | } 21 | 22 | override suspend fun load(params: LoadParams): LoadResult { 23 | val page = params.key ?: 1 24 | return try { 25 | val response = serviceImp.getMovies(sortBy = dataParams.sortBy, page = page, apiKey = dataParams.apiKey) 26 | val list = response.results.orEmpty() 27 | 28 | LoadResult.Page( 29 | data = list, 30 | prevKey = if (page == 1) null else page - 1, 31 | nextKey = if (list.isEmpty()) null else page + 1 32 | ) 33 | } catch (exception: IOException) { 34 | // IOException for network failures. 35 | return LoadResult.Error(exception) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/MovieCastUseCase.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import kotlinx.coroutines.flow.FlowCollector 4 | import mohsen.soltanian.cleanarchitecture.core.data.implClasses.ServiceImp 5 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 6 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 7 | import mohsen.soltanian.cleanarchitecture.core.domain.base.BaseUseCase 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | class MovieCastUseCase @Inject constructor( 13 | private val serviceImp: ServiceImp 14 | ) : BaseUseCase(){ 15 | 16 | data class Params(val movieId: String, val apiKey: String = API_KEY) 17 | 18 | override suspend fun FlowCollector.execute(params: Params) = 19 | emit(value = serviceImp.getCasts(movieId = params.movieId, apiKey = params.apiKey)) 20 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/MoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import kotlinx.coroutines.flow.Flow 7 | import mohsen.soltanian.cleanarchitecture.core.data.implClasses.ServiceImp 8 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 9 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 10 | import mohsen.soltanian.cleanarchitecture.core.domain.base.BasePagingUseCase 11 | import mohsen.soltanian.cleanarchitecture.core.domain.pagingSource.MoviePagingSource 12 | import javax.inject.Inject 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | class MoviesUseCase @Inject constructor( 17 | private val serviceImp: ServiceImp 18 | ): BasePagingUseCase() { 19 | 20 | data class Params(val pagingConfig: PagingConfig, val sortBy: String, val apiKey: String = API_KEY) 21 | 22 | override fun execute(params: Params): Flow> { 23 | return Pager(config = params.pagingConfig, 24 | pagingSourceFactory = { MoviePagingSource(serviceImp= serviceImp, dataParams = params) } 25 | ).flow 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/SearchMovieUseCase.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import kotlinx.coroutines.flow.FlowCollector 4 | import mohsen.soltanian.cleanarchitecture.core.data.implClasses.ServiceImp 5 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 6 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 7 | import mohsen.soltanian.cleanarchitecture.core.domain.base.BaseUseCase 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | class SearchMovieUseCase @Inject constructor( 13 | private val serviceImp: ServiceImp 14 | ) : BaseUseCase() { 15 | 16 | data class Params(val query: String, val apikey: String = API_KEY) 17 | 18 | override suspend fun FlowCollector.execute(params: Params) = 19 | emit(value = serviceImp.searchForMovies(query = params.query, apiKey = params.apikey)) 20 | 21 | } -------------------------------------------------------------------------------- /core/domain/src/test/java/mohsen/soltanian/cleanarchitecture/core/domain/base/BaseUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.base 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.every 6 | import io.mockk.impl.annotations.MockK 7 | import io.mockk.spyk 8 | import kotlinx.coroutines.flow.FlowCollector 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.test.runTest 11 | import mohsen.soltanian.cleanarchitecture.core.domain.core.DomainUnitTest 12 | import org.amshove.kluent.`should be instance of` 13 | import org.amshove.kluent.`should be` 14 | import org.junit.Test 15 | import java.lang.reflect.Parameter 16 | 17 | class BaseUseCaseTest : DomainUnitTest() { 18 | 19 | @MockK lateinit var useCase: UseCaseSUT 20 | 21 | @MockK lateinit var params: Params 22 | @MockK lateinit var returnType: ReturnType 23 | 24 | @Test 25 | fun `running use case should return 'ReturnTypeClass'`() = runTest { 26 | every { params.name } returns "ParamTest" 27 | every { returnType.name } returns "mohsen" 28 | coEvery { useCase.invoke(params = any()) } returns flow { 29 | emit(value = returnType) 30 | } 31 | val response = useCase.invoke(params = params) 32 | response.collect { 33 | it `should be instance of` ReturnType::class.java 34 | it.name `should be` "mohsen" 35 | } 36 | 37 | coVerify(exactly = 1) { useCase.invoke(params = any()) } 38 | } 39 | 40 | @Test 41 | fun `running use case should return 'ReturnTypeClass2'`() = runTest { 42 | every { params.name } returns "ParamTest" 43 | 44 | val __useCase = spyk() 45 | val response = __useCase.invoke(params = params) 46 | response.collect { 47 | it `should be instance of` ReturnType::class.java 48 | it.name `should be` "mohsen" 49 | } 50 | 51 | coVerify(exactly = 1) { useCase.invoke(params = any()) } 52 | } 53 | 54 | data class Params(val name: String) 55 | data class ReturnType(val name: String) 56 | inner class UseCaseSUT : BaseUseCase() { 57 | override suspend fun FlowCollector.execute(params: Params) { 58 | emit(value = ReturnType(name = "mohsen")) 59 | } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /core/domain/src/test/java/mohsen/soltanian/cleanarchitecture/core/domain/core/DomainUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.core 2 | 3 | import io.mockk.MockKAnnotations 4 | import org.junit.After 5 | import org.junit.Before 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.BlockJUnit4ClassRunner 8 | 9 | @RunWith(BlockJUnit4ClassRunner::class) 10 | open class DomainUnitTest { 11 | 12 | open fun onSetUpTest() {} 13 | 14 | open fun onStopTest() {} 15 | 16 | @Before 17 | fun onSetup() { 18 | MockKAnnotations.init(this) 19 | onSetUpTest() 20 | } 21 | 22 | @After 23 | fun onTearDown() { 24 | onStopTest() 25 | } 26 | } -------------------------------------------------------------------------------- /core/domain/src/test/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/MovieCastUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.every 6 | import io.mockk.impl.annotations.InjectMockKs 7 | import io.mockk.impl.annotations.MockK 8 | import io.mockk.impl.annotations.SpyK 9 | import io.mockk.mockk 10 | import kotlinx.coroutines.flow.collect 11 | import kotlinx.coroutines.flow.flow 12 | import kotlinx.coroutines.test.runTest 13 | import mohsen.soltanian.cleanarchitecture.core.data.implClasses.ServiceImp 14 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Cast 15 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.CastsResponse 16 | import mohsen.soltanian.cleanarchitecture.core.data.services.RemoteApi.Companion.API_KEY 17 | import mohsen.soltanian.cleanarchitecture.core.domain.core.DomainUnitTest 18 | import org.amshove.kluent.`should be instance of` 19 | import org.amshove.kluent.`should equal` 20 | import org.junit.Test 21 | 22 | internal class MovieCastUseCaseTest: DomainUnitTest() { 23 | 24 | 25 | @MockK lateinit var uesCase: MovieCastUseCase 26 | 27 | @MockK(relaxed = true) lateinit var castsModel: CastsResponse 28 | @MockK(relaxed = true) lateinit var params: MovieCastUseCase.Params 29 | 30 | @Test 31 | fun `run test for given empty cats List`()= runTest { 32 | coEvery { uesCase.invoke(params = any()) } returns flow { 33 | emit(value = castsModel ) 34 | } 35 | val response = uesCase.invoke(params = params) 36 | response.collect { 37 | it `should be instance of` CastsResponse::class.java 38 | it.id `should equal` 0L 39 | it.cast `should equal` emptyList() 40 | } 41 | 42 | coVerify(exactly = 1) { uesCase.invoke(any()) } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /core/domain/src/test/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/MovieUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import androidx.paging.PagingData 4 | import androidx.paging.map 5 | import io.mockk.coEvery 6 | import io.mockk.coVerify 7 | import io.mockk.impl.annotations.MockK 8 | import kotlinx.coroutines.flow.flow 9 | import kotlinx.coroutines.test.runTest 10 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.Movie 11 | import mohsen.soltanian.cleanarchitecture.core.domain.core.DomainUnitTest 12 | import org.amshove.kluent.`should be instance of` 13 | import org.junit.Test 14 | 15 | internal class MovieUseCaseTest : DomainUnitTest() { 16 | 17 | @MockK lateinit var uesCase: MoviesUseCase 18 | 19 | @MockK(relaxed = true) lateinit var params: MoviesUseCase.Params 20 | 21 | @Test 22 | fun `given empty movies list`() = runTest { 23 | coEvery { uesCase.invoke(params = any()) } returns flow { 24 | emit(value = PagingData.empty()) 25 | } 26 | 27 | val response = uesCase.invoke(params = params) 28 | response.collect { 29 | it `should be instance of` PagingData::class.java 30 | it.map { model -> 31 | model `should be instance of` Movie::class.java 32 | } 33 | } 34 | 35 | coVerify(exactly = 1) { uesCase.invoke(params = any()) } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /core/domain/src/test/java/mohsen/soltanian/cleanarchitecture/core/domain/usecase/SearchMovieUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.core.domain.usecase 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.impl.annotations.MockK 6 | import kotlinx.coroutines.flow.flow 7 | import kotlinx.coroutines.test.runTest 8 | import mohsen.soltanian.cleanarchitecture.core.data.models.response.MoviesResponse 9 | import mohsen.soltanian.cleanarchitecture.core.domain.core.DomainUnitTest 10 | import org.amshove.kluent.`should be instance of` 11 | import org.amshove.kluent.`should equal` 12 | import org.junit.Test 13 | 14 | internal class SearchMovieUseCaseTest: DomainUnitTest() { 15 | 16 | @MockK lateinit var useCase: SearchMovieUseCase 17 | 18 | @MockK(relaxed = true) lateinit var params: SearchMovieUseCase.Params 19 | @MockK(relaxed = true) lateinit var moviesResponse: MoviesResponse 20 | 21 | @Test 22 | fun `run test`() = runTest() { 23 | coEvery { useCase.invoke(params = any()) } returns flow { 24 | emit(value = moviesResponse ) 25 | } 26 | val response = useCase.invoke(params) 27 | response.collect { 28 | it `should be instance of` MoviesResponse::class.java 29 | it.page `should equal` 0 30 | it.results `should equal` listOf() 31 | } 32 | 33 | coVerify(exactly = 1) { useCase.invoke(any()) } 34 | 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 24 01:33:45 PDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /keystore.properties: -------------------------------------------------------------------------------- 1 | storeFile = signing/appName-release.jks 2 | storePassword = store_123 3 | keyPassword = store_123 4 | keyAlias = key0 -------------------------------------------------------------------------------- /libraries/framework/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/framework/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import plugins.AndroidLibraryPlugin 2 | import extensions.* 3 | 4 | apply() 5 | apply(plugin = GradlePlugins.Hilt) 6 | 7 | dependencies { 8 | 9 | implementation(Deps.Hilt.Android) 10 | kapt(Deps.Hilt.AndroidCompiler) 11 | 12 | api(project(":core:domain")) 13 | api(Deps.AndroidX.AppCompat) 14 | api(Deps.LegacySupport.LegacySupport) 15 | api(Deps.AndroidX.CoreKtx) 16 | api(Deps.AndroidX.Material) 17 | api(Deps.AndroidX.ConstraintLayout) 18 | api(Deps.AndroidX.RecyclerView) 19 | api(Deps.Lifecycle.ViewModel) 20 | api(Deps.Lifecycle.LiveData) 21 | api(Deps.Lifecycle.Runtime) 22 | api(Deps.AndroidX.ActivityKtx) 23 | api(Deps.AndroidX.FragmentKtx) 24 | api(Deps.Navigation.NavigationComponent) 25 | api(Deps.Navigation.NavigationComponentUi) 26 | api(Deps.Glide.glide) 27 | api(Deps.FabCounter.fabCounter) 28 | 29 | kapt(Deps.Glide.compiler) 30 | 31 | testImplementation(Deps.Test.Junit) 32 | androidTestImplementation(Deps.Test.JunitExt) 33 | androidTestImplementation(Deps.Test.EspressoCore) 34 | } -------------------------------------------------------------------------------- /libraries/framework/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/libraries/framework/consumer-rules.pro -------------------------------------------------------------------------------- /libraries/framework/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 -------------------------------------------------------------------------------- /libraries/framework/src/androidTest/java/mohsen/soltanian/cleanarchitecture/libraries/framework/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework 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("mohsen.soltanian.cleanarchitecture.libraries.framework", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/binding/CommonBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.binding 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.view.View 5 | import android.widget.EditText 6 | import android.widget.ImageView 7 | import androidx.databinding.BindingAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.bumptech.glide.Glide 10 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicRecyclerAdapter 11 | 12 | object CommonBindingAdapter { 13 | @JvmStatic 14 | @BindingAdapter(value = ["imageUrl", "placeHolder"], requireAll = false) 15 | fun loadUrl(view: ImageView, url: String?, placeHolder: Drawable?) { 16 | Glide.with(view.context).load(url).placeholder(placeHolder).into(view) 17 | } 18 | 19 | @JvmStatic 20 | @BindingAdapter("data") 21 | fun setRecyclerViewProperties(recyclerView: RecyclerView, items: List) { 22 | if (recyclerView.adapter is BasicRecyclerAdapter<*, *>) 23 | (recyclerView.adapter as BasicRecyclerAdapter<*, *>).setItems(items as List) 24 | 25 | } 26 | 27 | @JvmStatic 28 | @BindingAdapter("visible") 29 | fun booleanToVisibility(view: View,value: Boolean?) = 30 | if (value == true) view.visibility = View.VISIBLE else view.visibility = View.GONE 31 | 32 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/component/ProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.component 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.drawable.ColorDrawable 7 | import android.view.Window 8 | import android.view.WindowManager 9 | import mohsen.soltanian.cleanarchitecture.libraries.framework.databinding.DialogProgressBinding 10 | 11 | class ProgressDialog(context: Context) : Dialog(context) { 12 | init { 13 | val binding = DialogProgressBinding.inflate(layoutInflater) 14 | requestWindowFeature(Window.FEATURE_NO_TITLE) 15 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 16 | window?.setLayout( 17 | WindowManager.LayoutParams.MATCH_PARENT, 18 | WindowManager.LayoutParams.WRAP_CONTENT 19 | ) 20 | 21 | setCanceledOnTouchOutside(false) 22 | setCancelable(true) 23 | setContentView(binding.root) 24 | } 25 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/adapter/BasicPagingRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.ViewDataBinding 6 | import androidx.paging.PagingDataAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | 9 | @SuppressWarnings("TooManyFunctions") 10 | abstract class BasicPagingRecyclerAdapter(diffCallback: DiffUtil.ItemCallback) : 11 | PagingDataAdapter>(diffCallback) { 12 | 13 | abstract fun createBinding(inflater: LayoutInflater, parent: ViewGroup): Binding? 14 | 15 | abstract fun bindView(binding: Binding, position: Int, item: T) 16 | 17 | /** Generates a ViewHolder from this Item with the given ViewDataBinding */ 18 | open fun getViewHolder(viewBinding: Binding): BasicViewHolder { 19 | return BasicViewHolder(viewBinding) 20 | } 21 | 22 | /** Generates a ViewHolder from this Item with the given parent */ 23 | private fun getViewHolder(parent: ViewGroup): BasicViewHolder? { 24 | return createBinding(LayoutInflater.from(parent.context), parent)?.let { getViewHolder(it) } 25 | } 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicViewHolder { 28 | return getViewHolder(parent)!! 29 | } 30 | 31 | override fun onBindViewHolder(holder: BasicViewHolder, position: Int) { 32 | holder.binding?.let { getItem(position)?.let { it1 -> bindView(it, position, item = it1) } } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/adapter/BasicViewHolder.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding.BindingViewHolder 5 | 6 | /** 7 | * A Simple [BasicViewHolder] providing easier support for ViewBinding 8 | */ 9 | class BasicViewHolder(binding: VB) : BindingViewHolder(binding) 10 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/adapter/paging/PagingLoadStateAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022, developersancho 3 | * All rights reserved. 4 | */ 5 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.paging 6 | 7 | import android.view.LayoutInflater 8 | import android.view.ViewGroup 9 | import androidx.core.view.isVisible 10 | import androidx.paging.LoadState 11 | import androidx.paging.LoadStateAdapter 12 | import androidx.paging.PagingDataAdapter 13 | import androidx.recyclerview.widget.RecyclerView 14 | import mohsen.soltanian.cleanarchitecture.libraries.framework.databinding.RowPagingLoadStateBinding 15 | 16 | class PagingLoadStateAdapter( 17 | private val adapter: PagingDataAdapter 18 | ) : LoadStateAdapter() { 19 | 20 | class PagingLoadStateViewHolder( 21 | private val binding: RowPagingLoadStateBinding, 22 | private val retryCallback: () -> Unit 23 | ) : RecyclerView.ViewHolder(binding.root) { 24 | init { 25 | binding.btnRetry.setOnClickListener { retryCallback() } 26 | } 27 | 28 | fun bind(loadState: LoadState) { 29 | binding.pbLoading.isVisible = loadState is LoadState.Loading 30 | binding.btnRetry.isVisible = loadState is LoadState.Error 31 | binding.tvErrorMessage.isVisible = 32 | !(loadState as? LoadState.Error)?.error?.message.isNullOrBlank() 33 | binding.tvErrorMessage.text = (loadState as? LoadState.Error)?.error?.message 34 | } 35 | } 36 | 37 | override fun onBindViewHolder(holder: PagingLoadStateViewHolder, loadState: LoadState) { 38 | holder.bind(loadState) 39 | } 40 | 41 | override fun onCreateViewHolder( 42 | parent: ViewGroup, 43 | loadState: LoadState 44 | ): PagingLoadStateViewHolder { 45 | return PagingLoadStateViewHolder( 46 | RowPagingLoadStateBinding.inflate( 47 | LayoutInflater.from(parent.context), parent, false 48 | ) 49 | ) { 50 | adapter.retry() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/ActivityAttribute.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation 2 | 3 | @kotlin.annotation.Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 4 | annotation class ActivityAttribute(val layoutId: Int = -1, val handleBackPress: Boolean = false, 5 | val handleDoubleBackPress: Boolean = false, val lockBackPress: Boolean = false, val checkNetworkConnection: Boolean = false) 6 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/FragmentAttribute.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation 2 | 3 | @kotlin.annotation.Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 4 | annotation class FragmentAttribute(val layoutId: Int = -1,val handleBackPress: Boolean = false, 5 | val handleDoubleBackPress: Boolean = false, val lockBackPress: Boolean = false, val checkNetworkConnection: Boolean = false) 6 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/annotation/Layout.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation 2 | 3 | 4 | @kotlin.annotation.Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 5 | annotation class Layout(val value: Int = -1) -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/AppInitializer.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | interface AppInitializer { 4 | fun init(application: CoreApplication<*>) 5 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/AppInitializerImpl.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | class AppInitializerImpl(private vararg val initializers: AppInitializer) : AppInitializer { 4 | override fun init(application: CoreApplication<*>) { 5 | initializers.forEach { 6 | it.init(application) 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/CoreApplication.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.os.Looper 6 | import android.widget.Toast 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleEventObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import kotlin.system.exitProcess 11 | 12 | abstract class CoreApplication : 13 | Application(), 14 | LifecycleEventObserver, 15 | CoreConfigProvider { 16 | var isAppInForeground: Boolean = true 17 | 18 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 19 | when (event) { 20 | Lifecycle.Event.ON_CREATE -> Unit 21 | Lifecycle.Event.ON_START -> onAppForegrounded() 22 | Lifecycle.Event.ON_RESUME -> Unit 23 | Lifecycle.Event.ON_PAUSE -> Unit 24 | Lifecycle.Event.ON_STOP -> onAppBackgrounded() 25 | Lifecycle.Event.ON_DESTROY -> Unit 26 | Lifecycle.Event.ON_ANY -> Unit 27 | } 28 | } 29 | 30 | open fun onAppBackgrounded() { 31 | isAppInForeground = false 32 | } 33 | 34 | open fun onAppForegrounded() { 35 | isAppInForeground = true 36 | } 37 | 38 | open fun handleSSLHandshakeException() {} 39 | 40 | private fun handleUncaughtException() { 41 | Thread.setDefaultUncaughtExceptionHandler { _, _ -> 42 | object : Thread() { 43 | override fun run() { 44 | Looper.prepare() 45 | Toast.makeText( 46 | applicationContext, 47 | appConfig().uncaughtExceptionMessage(), 48 | Toast.LENGTH_SHORT 49 | ).show() 50 | Looper.loop() 51 | } 52 | }.start() 53 | 54 | Thread.sleep(2000) 55 | 56 | val intent = Intent(this, appConfig().uncaughtExceptionPage()) 57 | // to custom behaviour, add extra params for intent 58 | intent.addFlags( 59 | Intent.FLAG_ACTIVITY_CLEAR_TOP 60 | or Intent.FLAG_ACTIVITY_CLEAR_TASK 61 | or Intent.FLAG_ACTIVITY_NEW_TASK 62 | ) 63 | startActivity(intent) 64 | try { 65 | exitProcess(2) 66 | } catch (e: Exception) { 67 | startActivity(intent) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/CoreConfig.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | abstract class CoreConfig { 4 | abstract fun appName(): String 5 | 6 | abstract fun environment(): CoreEnvironment 7 | 8 | open fun isDev() = false 9 | 10 | open fun uncaughtExceptionPage(): Class<*>? = null 11 | 12 | open fun uncaughtExceptionMessage(): String? = null 13 | } 14 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/CoreConfigProvider.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | interface CoreConfigProvider { 4 | fun appConfig(): T 5 | } 6 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/CoreEnvironment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | enum class CoreEnvironment { 4 | DEV, 5 | PROD; 6 | } 7 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/application/TimberInitializer.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.application 2 | 3 | import android.util.Log 4 | import timber.log.Timber 5 | 6 | class TimberInitializer : AppInitializer { 7 | override fun init(application: CoreApplication<*>) { 8 | if (application.appConfig().isDev()) { 9 | Timber.plant(Timber.DebugTree()) 10 | } else { 11 | Timber.plant(FireBaseCrashlyticsTree()) 12 | } 13 | } 14 | 15 | internal class FireBaseCrashlyticsTree : Timber.Tree() { 16 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 17 | when (priority) { 18 | Log.VERBOSE, Log.DEBUG -> return 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/binding/BindingViewHolder.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding 2 | 3 | import android.content.Context 4 | import androidx.databinding.ViewDataBinding 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | /** 8 | * A Simple [BindingViewHolder] providing easier support for ViewBinding 9 | */ 10 | open class BindingViewHolder(val binding: VB) : 11 | RecyclerView.ViewHolder(binding!!.root) { 12 | val context: Context = binding!!.root.context 13 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/binding/ViewBindingExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.binding 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.databinding.ViewDataBinding 7 | import androidx.viewbinding.ViewBinding 8 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicPagingRecyclerAdapter 9 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.adapter.BasicRecyclerAdapter 10 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.annotation.Layout 11 | import java.lang.reflect.ParameterizedType 12 | 13 | inline fun BasicRecyclerAdapter<*, *>.getDataBinding( 14 | layoutInflater: LayoutInflater,container: ViewGroup): V? { 15 | return if(this.javaClass.getAnnotation(Layout::class.java) == null) { 16 | throw Exception("layout id is Null") 17 | null 18 | }else { 19 | this.javaClass.getAnnotation(Layout::class.java) 20 | ?.let { layoutRes -> DataBindingUtil.inflate(layoutInflater, layoutRes.value,container,false) } 21 | 22 | } 23 | } 24 | 25 | inline fun BasicPagingRecyclerAdapter<*, *>.getDataBinding( 26 | layoutInflater: LayoutInflater,container: ViewGroup): V? { 27 | return if(this.javaClass.getAnnotation(Layout::class.java) == null) { 28 | throw Exception("layout id is Null") 29 | null 30 | }else { 31 | this.javaClass.getAnnotation(Layout::class.java) 32 | ?.let { layoutRes -> DataBindingUtil.inflate(layoutInflater, layoutRes.value,container,false) } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/mvi/MviActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmActivity 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.observeFlowStart 6 | 7 | abstract class MviActivity> : 8 | MvvmActivity() { 9 | 10 | abstract fun renderViewState(viewState: STATE) 11 | 12 | override fun observeUi() { 13 | super.observeUi() 14 | observeFlowStart(viewModel.stateFlow, ::renderViewState) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/mvi/MviFragment.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmFragment 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.observeFlowStart 6 | 7 | abstract class MviFragment> : 8 | MvvmFragment() { 9 | 10 | open fun renderViewState(viewState: STATE){} 11 | 12 | override fun observeUi() { 13 | super.observeUi() 14 | observeFlowStart(viewModel.stateFlow, ::renderViewState) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/mvi/MviViewModel.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvi 2 | 3 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm.MvvmViewModel 4 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.flow.MutableEventFlow 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.flow.asEventFlow 6 | 7 | abstract class MviViewModel : MvvmViewModel() { 8 | 9 | private val _stateFlow = MutableEventFlow() 10 | val stateFlow = _stateFlow.asEventFlow() 11 | 12 | abstract fun onTriggerEvent(eventType: EVENT) 13 | 14 | protected fun setState(state: STATE) = safeLaunch { 15 | _stateFlow.emit(state) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/mvvm/MvvmActivity.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm 2 | 3 | import androidx.databinding.DataBindingUtil 4 | import androidx.databinding.ViewDataBinding 5 | import mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.core.CoreActivity 6 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.observeFlowStart 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.observeLiveData 8 | 9 | abstract class MvvmActivity : CoreActivity() { 10 | 11 | lateinit var binding: VB 12 | 13 | abstract val viewModel: VM 14 | 15 | /** 16 | * Override for set binding variable 17 | * 18 | * @return variable id 19 | */ 20 | protected abstract fun bindingVariables(): HashMap 21 | 22 | open fun showProgress() {} 23 | 24 | open fun hideProgress() {} 25 | 26 | open fun showError(throwable: Throwable) {} 27 | 28 | protected fun getBinging(): VB = 29 | binding 30 | 31 | override fun performDataBinding(layoutID: Int) { 32 | super.performDataBinding(layoutID) 33 | if (::binding.isInitialized.not()) { 34 | binding = DataBindingUtil.setContentView(this, layoutID) 35 | for (item in bindingVariables()) { 36 | binding.setVariable(item.key, item.value) 37 | } 38 | binding.lifecycleOwner = this 39 | binding.executePendingBindings() 40 | } 41 | 42 | } 43 | 44 | override fun observeState() { 45 | super.observeState() 46 | observeConnectionState() 47 | observeProgress() 48 | observeError() 49 | } 50 | 51 | private fun observeProgress() { 52 | observeFlowStart(viewModel.progress) { state -> 53 | state?.let { 54 | when (it) { 55 | true -> { 56 | showProgress() 57 | } 58 | false -> { 59 | hideProgress() 60 | 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | private fun observeConnectionState() { 68 | observeFlowStart(viewModel.connectionState) { state -> 69 | state?.let { 70 | when(it) { 71 | true -> { 72 | networkAvailable() 73 | } 74 | false -> { 75 | networkNotAvailable() 76 | } 77 | } 78 | } 79 | 80 | } 81 | } 82 | 83 | private fun observeError() { 84 | observeLiveData(viewModel.error, ::showError) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/base/mvvm/MvvmViewModel.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.base.mvvm 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.CoroutineExceptionHandler 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.flow.* 10 | import kotlinx.coroutines.launch 11 | import timber.log.Timber 12 | 13 | open class MvvmViewModel : ViewModel() { 14 | 15 | 16 | private val _progress = MutableStateFlow(null) 17 | val progress get() = _progress.asStateFlow() 18 | 19 | private val _connectionState = MutableStateFlow(null) 20 | val connectionState get() = _connectionState.asStateFlow() 21 | 22 | private val _error = MutableLiveData() 23 | val error: LiveData get() = _error 24 | 25 | private val handler = CoroutineExceptionHandler { _, exception -> 26 | Timber.tag(COROUTINE_EXCEPTION_HANDLER_MESSAGE).e(exception) 27 | passError(exception) 28 | } 29 | 30 | protected fun safeLaunch(block: suspend CoroutineScope.() -> Unit) { 31 | viewModelScope.launch(handler, block = block) 32 | } 33 | 34 | open fun showProgress() { 35 | _progress.value = true 36 | } 37 | 38 | open fun hideProgress() { 39 | _progress.value = false 40 | } 41 | 42 | open fun activeConnection() { 43 | _connectionState.value = true 44 | } 45 | 46 | open fun inactiveConnection() { 47 | _connectionState.value = false 48 | } 49 | 50 | open fun passError(throwable: Throwable, showSystemError: Boolean = true) { 51 | if (showSystemError) { 52 | _error.value = throwable 53 | } 54 | } 55 | 56 | protected suspend fun call( 57 | callFlow: Flow, 58 | completionHandler: (collect: T) -> Unit = {} 59 | ) { 60 | callFlow 61 | .catch { 62 | passError(throwable = it) } 63 | .collect { 64 | completionHandler.invoke(it) 65 | } 66 | } 67 | 68 | protected suspend fun callWithProgress( 69 | callFlow: Flow, 70 | completionHandler: (collect: T) -> Unit = {} 71 | ) { 72 | callFlow 73 | .onStart { showProgress() } 74 | .onCompletion { hideProgress() } 75 | .catch { passError(throwable = it) } 76 | .collect { 77 | completionHandler.invoke(it) 78 | } 79 | } 80 | 81 | companion object { 82 | private const val COROUTINE_EXCEPTION_HANDLER_MESSAGE = "ExceptionHandler" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/core/flow/EventFlow.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.core.flow 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.FlowCollector 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | 8 | interface EventFlow : Flow { 9 | companion object { 10 | const val DEFAULT_REPLAY: Int = 3 11 | } 12 | } 13 | 14 | interface MutableEventFlow : EventFlow, FlowCollector 15 | 16 | @Suppress("FunctionName") 17 | fun MutableEventFlow( 18 | replay: Int = EventFlow.DEFAULT_REPLAY 19 | ): MutableEventFlow = EventFlowImpl(replay) 20 | 21 | fun MutableEventFlow.asEventFlow(): EventFlow = ReadOnlyEventFlow(this) 22 | 23 | private class ReadOnlyEventFlow(flow: EventFlow) : EventFlow by flow 24 | 25 | private class EventFlowImpl( 26 | replay: Int 27 | ) : MutableEventFlow { 28 | 29 | private val flow: MutableSharedFlow> = MutableSharedFlow(replay = replay) 30 | 31 | override suspend fun collect(collector: FlowCollector) = flow 32 | .collect { slot -> 33 | if (!slot.markConsumed()) { 34 | collector.emit(slot.value) 35 | } 36 | } 37 | 38 | override suspend fun emit(value: T) { 39 | flow.emit(EventFlowSlot(value)) 40 | } 41 | } 42 | 43 | private class EventFlowSlot(val value: T) { 44 | 45 | private val consumed: AtomicBoolean = AtomicBoolean(false) 46 | 47 | fun markConsumed(): Boolean = consumed.getAndSet(true) 48 | } 49 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/ActivityExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import androidx.activity.OnBackPressedCallback 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | 7 | fun AppCompatActivity.handleBackPress( block: () -> Unit) { 8 | val callback: OnBackPressedCallback = 9 | object : OnBackPressedCallback(true /* enabled by default */) { 10 | override fun handleOnBackPressed() { 11 | block() 12 | } 13 | } 14 | onBackPressedDispatcher.addCallback(this, callback) 15 | } 16 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/ContextExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.net.ConnectivityManager 6 | import androidx.annotation.ColorRes 7 | import androidx.annotation.DrawableRes 8 | import androidx.appcompat.content.res.AppCompatResources 9 | import androidx.core.content.ContextCompat 10 | 11 | fun Context.dp2px(value: Int): Int { 12 | val scale = resources.displayMetrics.density 13 | return (value.toFloat() * scale + 0.5f).toInt() 14 | } 15 | 16 | fun Context.drawable(@DrawableRes drawableRes: Int) = 17 | AppCompatResources.getDrawable(this@drawable, drawableRes) 18 | 19 | fun Context.color(@ColorRes colorRes: Int) = ContextCompat.getColor(this@color, colorRes) 20 | 21 | val Context.connectivityManager: ConnectivityManager 22 | get() = 23 | this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/DeviceExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.net.ConnectivityManager 7 | import android.net.NetworkCapabilities 8 | import android.os.Build 9 | import android.provider.Settings 10 | import androidx.annotation.RequiresApi 11 | import java.io.File 12 | 13 | @SuppressLint("MissingPermission") 14 | fun Context.isInternetAvailable(): Boolean { 15 | var result = false 16 | 17 | val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 18 | cm.run { 19 | cm.getNetworkCapabilities(cm.activeNetwork)?.run { 20 | result = when { 21 | hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true 22 | hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true 23 | hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true 24 | else -> false 25 | } 26 | } 27 | } 28 | 29 | return result 30 | } 31 | 32 | @SuppressLint("HardwareIds") 33 | fun Context.isEmulator(): Boolean { 34 | val androidId = Settings.Secure.getString(this.contentResolver, "android_id") 35 | return Build.PRODUCT.contains("sdk") || 36 | Build.HARDWARE.contains("goldfish") || 37 | Build.HARDWARE.contains("ranchu") || 38 | androidId == null 39 | } 40 | 41 | fun Context.isRooted(): Boolean { 42 | val isEmulator: Boolean = isEmulator() 43 | val buildTags = Build.TAGS 44 | return if (!isEmulator && buildTags != null && buildTags.contains("test-keys")) { 45 | true 46 | } else { 47 | var file = File("/system/app/Superuser.apk") 48 | if (file.exists()) { 49 | true 50 | } else { 51 | file = File("/system/xbin/su") 52 | !isEmulator && file.exists() 53 | } 54 | } 55 | } 56 | 57 | fun Context.appVersion(): String { 58 | return try { 59 | packageManager.getPackageInfo(packageName, 0).versionName 60 | } catch (ex: PackageManager.NameNotFoundException) { 61 | "" 62 | } 63 | } 64 | 65 | @RequiresApi(Build.VERSION_CODES.P) 66 | fun Context.appVersionCode(): Long { 67 | return try { 68 | packageManager.getPackageInfo(packageName, 0).longVersionCode 69 | } catch (ex: PackageManager.NameNotFoundException) { 70 | 0L 71 | } 72 | } 73 | 74 | fun Context.targetPlatform() = "Android" 75 | 76 | @SuppressLint("HardwareIds") 77 | fun Context.androidClientId(): String { 78 | val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) 79 | return if (androidId.isNullOrEmpty()) "" else androidId 80 | } 81 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/FragmentExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import androidx.activity.OnBackPressedCallback 4 | import androidx.fragment.app.Fragment 5 | 6 | fun Fragment.handleBackPress(block:() -> Unit) { 7 | val callback: OnBackPressedCallback = 8 | object : OnBackPressedCallback(true /* enabled by default */) { 9 | override fun handleOnBackPressed() { 10 | block() 11 | } 12 | } 13 | activity?.onBackPressedDispatcher?.addCallback(this, callback) 14 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/IntentExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | 8 | inline fun Activity.launchActivity( 9 | requestCode: Int = -1, 10 | options: Bundle? = null, 11 | noinline init: Intent.() -> Unit = {} 12 | ) { 13 | val intent = newIntent(this) 14 | intent.init() 15 | startActivityForResult(intent, requestCode, options) 16 | } 17 | 18 | inline fun Context.launchActivity( 19 | options: Bundle? = null, 20 | noinline init: Intent.() -> Unit = {} 21 | ) { 22 | val intent = newIntent(this) 23 | intent.init() 24 | startActivity(intent, options) 25 | } 26 | 27 | inline fun newIntent(context: Context): Intent = 28 | Intent(context, T::class.java) 29 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/KeyboardExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import androidx.core.view.ViewCompat 6 | import androidx.core.view.WindowInsetsCompat 7 | import androidx.core.view.doOnLayout 8 | import androidx.fragment.app.Fragment 9 | 10 | private fun Activity.findFocusedView() = currentFocus ?: window.decorView 11 | 12 | private fun Fragment.findFocusedView() = activity?.currentFocus ?: view 13 | 14 | fun View.showSoftKeyboard() = doOnLayout { 15 | it.takeIf { it.requestFocus() }?.post { 16 | ViewCompat.getWindowInsetsController(it)?.show(WindowInsetsCompat.Type.ime()) 17 | } 18 | } 19 | 20 | fun View.hideSoftKeyboard(clearFocus: Boolean = false) = doOnLayout { 21 | if (clearFocus) it.clearFocus() 22 | it.post { 23 | ViewCompat.getWindowInsetsController(it)?.hide(WindowInsetsCompat.Type.ime()) 24 | } 25 | } 26 | 27 | fun Activity.showSoftKeyboard() = findFocusedView().showSoftKeyboard() 28 | 29 | fun Fragment.showSoftKeyboard() = findFocusedView()?.showSoftKeyboard() 30 | 31 | fun Activity.hideSoftKeyboard(clearFocus: Boolean = false) = 32 | findFocusedView().hideSoftKeyboard(clearFocus) 33 | 34 | fun Fragment.hideSoftKeyboard(clearFocus: Boolean = false) = 35 | findFocusedView()?.hideSoftKeyboard(clearFocus) 36 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/LifecycleOwnerExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import androidx.lifecycle.* 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.launch 7 | 8 | fun LifecycleOwner.observeFlowStart(property: Flow, block: (T) -> Unit) { 9 | lifecycleScope.launch { 10 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 11 | property.collect { block(it) } 12 | } 13 | } 14 | } 15 | 16 | fun LifecycleOwner.observeLiveData(liveData: LiveData, block: (T) -> Unit) { 17 | liveData.observe(this, Observer { block(it) }) 18 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/SnackBarExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.annotation.IdRes 7 | import androidx.fragment.app.Fragment 8 | import com.google.android.material.snackbar.Snackbar 9 | 10 | fun Fragment.showSnackBar(view: View, message: String, @IdRes targetViewId: Int? = null) { 11 | Snackbar.make(view, message, Snackbar.LENGTH_LONG).apply { 12 | targetViewId?.let { 13 | anchorView = view.rootView.findViewById(it) 14 | } 15 | show() 16 | } 17 | } 18 | 19 | fun Activity.showSnackBar(view: View, message: String, @IdRes targetViewId: Int? = null) { 20 | Snackbar.make(view, message, Snackbar.LENGTH_LONG).apply { 21 | targetViewId?.let { 22 | anchorView = view.rootView.findViewById(it) 23 | } 24 | show() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/ToastExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.widget.Toast 6 | import androidx.fragment.app.Fragment 7 | 8 | fun Activity.exitToast(message: String): Toast { 9 | return Toast.makeText(this, message,Toast.LENGTH_LONG ) 10 | } 11 | fun Activity.toast(message: String) { 12 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 13 | } 14 | fun Activity.toastLong(message: String) { 15 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 16 | } 17 | 18 | fun Fragment.toast(message: String) { 19 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 20 | } 21 | 22 | fun Fragment.toastLong(message: String) { 23 | Toast.makeText(context, message, Toast.LENGTH_LONG).show() 24 | } 25 | 26 | fun Context.toast(message: String) { 27 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 28 | } 29 | 30 | fun Context.toastLong(message: String) { 31 | Toast.makeText(this, message, Toast.LENGTH_LONG).show() 32 | } 33 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/ViewExtension.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions 2 | 3 | import android.os.SystemClock 4 | import android.text.Editable 5 | import android.view.View 6 | import android.view.animation.AnimationUtils 7 | import android.widget.EditText 8 | import android.widget.ImageView 9 | import androidx.core.widget.addTextChangedListener 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.bumptech.glide.Glide 12 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 13 | import kotlinx.coroutines.channels.awaitClose 14 | import kotlinx.coroutines.flow.callbackFlow 15 | import mohsen.soltanian.cleanarchitecture.libraries.framework.R 16 | 17 | fun View.visible() { 18 | visibility = View.VISIBLE 19 | } 20 | 21 | fun View.gone() { 22 | visibility = View.GONE 23 | } 24 | 25 | fun View.invisible() { 26 | visibility = View.INVISIBLE 27 | } 28 | 29 | 30 | fun View.setSafeOnClickListener(debounceTime: Long = 600L, action: () -> Unit) { 31 | this.setOnClickListener(object : View.OnClickListener { 32 | private var lastClickTime: Long = 0 33 | 34 | override fun onClick(v: View) { 35 | if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return 36 | else action() 37 | 38 | lastClickTime = SystemClock.elapsedRealtime() 39 | } 40 | }) 41 | } 42 | 43 | fun ImageView.loadFromUrl(url: String) = 44 | Glide.with(this.context.applicationContext) 45 | .load(url) 46 | .transition(DrawableTransitionOptions.withCrossFade()) 47 | .into(this) 48 | 49 | 50 | fun EditText.asFlow() = callbackFlow { 51 | val afterTextChanged: (Editable?) -> Unit = { text -> 52 | this.trySend(text.toString()).isSuccess 53 | } 54 | 55 | val textChangedListener = 56 | addTextChangedListener(afterTextChanged = afterTextChanged) 57 | 58 | awaitClose { 59 | removeTextChangedListener(textChangedListener) 60 | } 61 | 62 | fun RecyclerView.runAnimation() { 63 | layoutAnimation = 64 | AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/extensions/helper/DividerItemDecorator.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.helper 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Rect 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import androidx.recyclerview.widget.DividerItemDecoration 8 | import androidx.recyclerview.widget.RecyclerView 9 | 10 | private val mBounds = Rect() 11 | 12 | class DividerItemDecorator(private val divider: Drawable, private val orientation: Int?) : 13 | RecyclerView.ItemDecoration() { 14 | 15 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 16 | if (parent.layoutManager != null) { 17 | if (this.orientation == DividerItemDecoration.VERTICAL) { 18 | drawVertical(c, parent) 19 | } else { 20 | drawHorizontal(c, parent) 21 | } 22 | } 23 | } 24 | 25 | private fun drawVertical(canvas: Canvas, parent: RecyclerView) { 26 | canvas.save() 27 | val left: Int 28 | val right: Int 29 | if (parent.clipToPadding) { 30 | left = parent.paddingLeft 31 | right = parent.width - parent.paddingRight 32 | canvas.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom) 33 | } else { 34 | left = 0 35 | right = parent.width 36 | } 37 | val childCount = parent.childCount 38 | for (i in 0 until childCount - 1) { 39 | val child = parent.getChildAt(i) 40 | parent.getDecoratedBoundsWithMargins(child, mBounds) 41 | val bottom: Int = mBounds.bottom // + child.translationY.roundToInt() 42 | val top = bottom - divider.intrinsicHeight 43 | divider.setBounds(left, top, right, bottom) 44 | divider.draw(canvas) 45 | } 46 | canvas.restore() 47 | } 48 | 49 | private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) { 50 | canvas.save() 51 | val top: Int 52 | val bottom: Int 53 | if (parent.clipToPadding) { 54 | top = parent.paddingTop 55 | bottom = parent.height - parent.paddingBottom 56 | canvas.clipRect(parent.paddingLeft, top, parent.width - parent.paddingRight, bottom) 57 | } else { 58 | top = 0 59 | bottom = parent.height 60 | } 61 | val childCount = parent.childCount 62 | for (i in 0 until childCount - 1) { 63 | val child = parent.getChildAt(i) 64 | parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds) 65 | val right: Int = mBounds.right // + child.translationX.roundToInt() 66 | val left = right - divider.intrinsicWidth 67 | divider.setBounds(left, top, right, bottom) 68 | divider.draw(canvas) 69 | } 70 | canvas.restore() 71 | } 72 | 73 | override fun getItemOffsets( 74 | outRect: Rect, 75 | view: View, 76 | parent: RecyclerView, 77 | state: RecyclerView.State 78 | ) { 79 | if (this.orientation == DividerItemDecoration.VERTICAL) { 80 | outRect[0, 0, 0] = divider.intrinsicHeight 81 | } else { 82 | outRect[0, 0, divider.intrinsicWidth] = 0 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/receivers/ConnectionCheck.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.receivers 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.isInternetAvailable 8 | 9 | class ConnectionCheck : BroadcastReceiver() { 10 | 11 | private var listener: ConnectivityReceiverListener? = null 12 | 13 | @SuppressLint("UnsafeProtectedBroadcastReceiver") 14 | override fun onReceive(context: Context, intent: Intent) { 15 | if (listener != null) listener!!.onNetworkConnectionChanged(context.isInternetAvailable()) 16 | } 17 | 18 | fun setConnectivityReceiverListener(listener: ConnectivityReceiverListener?) { 19 | this.listener = listener 20 | } 21 | 22 | interface ConnectivityReceiverListener { 23 | fun onNetworkConnectionChanged(isConnected: Boolean) 24 | } 25 | } -------------------------------------------------------------------------------- /libraries/framework/src/main/java/mohsen/soltanian/cleanarchitecture/libraries/framework/utils/NetworkHandler.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.NetworkCapabilities 6 | import android.os.Build 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import mohsen.soltanian.cleanarchitecture.libraries.framework.extensions.connectivityManager 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | /** 13 | * Injectable class which returns information about the network connection state. 14 | */ 15 | @Singleton 16 | class NetworkHandler 17 | @Inject constructor(@ApplicationContext private val context: Context) { 18 | @SuppressLint("MissingPermission") 19 | fun isNetworkAvailable(): Boolean { 20 | val connectivityManager = context.connectivityManager 21 | 22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 23 | val network = connectivityManager.activeNetwork ?: return false 24 | val activeNetwork = 25 | connectivityManager.getNetworkCapabilities(network) ?: return false 26 | 27 | return when { 28 | activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true 29 | activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true 30 | else -> false 31 | } 32 | } else { 33 | @Suppress("DEPRECATION") val networkInfo = 34 | connectivityManager.activeNetworkInfo ?: return false 35 | @Suppress("DEPRECATION") 36 | return networkInfo.isConnected 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_fall_down.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 13 | 14 | 22 | 23 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_fragment_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_fragment_in_from_pop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_fragment_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_fragment_out_from_pop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_scale_fragment_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 19 | 20 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_scale_fragment_in_from_pop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_scale_fragment_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_scale_fragment_out_from_pop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 19 | 20 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_vertical_fragment_in_from_pop_long.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_vertical_fragment_in_long.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_vertical_fragment_out_from_pop_long.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/anim_vertical_fragment_out_long.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/layout_animation_fall_down.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/drawable/bg_dialog_radius.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/drawable/ic_baseline_wifi_off_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/drawable/pb_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/layout/dialog_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/layout/row_paging_load_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 29 | 30 | 43 | 44 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #00000000 4 | 5 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6dp 4 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /libraries/framework/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Check your connection! 4 | Check your internet connection! 5 | Close 6 | Press back button again to exit 7 | 8 | -------------------------------------------------------------------------------- /libraries/framework/src/test/java/mohsen/soltanian/cleanarchitecture/libraries/framework/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mohsen.soltanian.cleanarchitecture.libraries.framework 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 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | jcenter() 6 | mavenCentral() 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | jcenter() 14 | mavenCentral() 15 | } 16 | } 17 | rootProject.name = "AndroidCleanArchitecture" 18 | include ':app' 19 | include ':core' 20 | include ':libraries' 21 | include ':libraries:framework' 22 | include ':core:data' 23 | include ':core:domain' 24 | -------------------------------------------------------------------------------- /signing/appName-release.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStarX/AndroidCleanArchitecture/dc4361d6bf1460d39b45a0a60dcd112703395eb2/signing/appName-release.jks --------------------------------------------------------------------------------