├── .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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------