diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..64438a58 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..da76ab4d --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..9b7a17a3 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..9ef2be38 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 8bb0c38d..ba4eaed4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +![Diagram](diagram.jpg?raw=true "Diagram") + +Here is some sample pictures of the App : + +![Sample](sample.jpg?raw=true "Sample") + + # Work sample - Application developer ## Assignment diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..d69e4f52 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "ir.sass.daresay" + minSdk 21 + targetSdk 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + dataBinding true + } +} + +dependencies { + implementation lib.get("hilt") + kapt kaptLibs.get("hilt") + + implementation project(":core:base_ui") + api project(":core:navigator") + + implementation project(":core:shared_domain") + implementation project(":features:movie:ui") + implementation project(":features:home:ui") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/ir/sass/daresay/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ir/sass/daresay/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..ade5a996 --- /dev/null +++ b/app/src/androidTest/java/ir/sass/daresay/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ir.sass.daresay + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ir.sass.daresay", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e2c592be --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/loading.json b/app/src/main/assets/loading.json new file mode 100644 index 00000000..588a47d8 --- /dev/null +++ b/app/src/main/assets/loading.json @@ -0,0 +1 @@ +{"v":"5.7.11","fr":60,"ip":0,"op":81,"w":1920,"h":1080,"nm":"Loading Dots","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Dot4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":55,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[1142,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[1142,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":55,"s":[1142,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":39,"s":[75,75,100]},{"t":55,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.858823529412,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Dot3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[100]},{"t":47,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[1022,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":31,"s":[1022,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":47,"s":[1022,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":17,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[75,75,100]},{"t":47,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.858823529412,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Dot2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[100]},{"t":39,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[902,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[902,500,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[902,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[75,75,100]},{"t":39,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.858823529412,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Dot1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14,"s":[100]},{"t":30,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[782,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[782,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":30,"s":[782,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[75,75,100]},{"t":30,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.858823529412,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/java/ir/sass/daresay/App.kt b/app/src/main/java/ir/sass/daresay/App.kt new file mode 100644 index 00000000..28b493b6 --- /dev/null +++ b/app/src/main/java/ir/sass/daresay/App.kt @@ -0,0 +1,7 @@ +package ir.sass.daresay + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/ir/sass/daresay/MainActivity.kt b/app/src/main/java/ir/sass/daresay/MainActivity.kt new file mode 100644 index 00000000..8f0e9dd6 --- /dev/null +++ b/app/src/main/java/ir/sass/daresay/MainActivity.kt @@ -0,0 +1,88 @@ +package ir.sass.daresay + +import android.os.Bundle +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import dagger.hilt.android.AndroidEntryPoint +import ir.sass.daresay.databinding.ActivityMainBinding +import ir.sass.navigator.flow.NavigationAction +import ir.sass.navigator.flow.features.NavcontrollerHelper +import ir.sass.navigator.flow.features.home.HomeNavigationAction +import ir.sass.navigator.flow.features.movie.MovieNavigationAction +import ir.sass.navigator.navigator.navigateToFlow +import ir.sass.shared_domain.MovieListType + +@AndroidEntryPoint +class MainActivity : AppCompatActivity(), NavcontrollerHelper { + + lateinit var dataBinding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + dataBinding = ActivityMainBinding.inflate( + LayoutInflater.from(this) + ) + setContentView(dataBinding.root) + + dataBinding.bottomNavigationView.setOnNavigationItemSelectedListener { + when(it.itemId){ + R.id.home->{ + navigateToFlow(this, HomeNavigationAction()) + } + R.id.popular-> { + navigateToFlow(this,MovieNavigationAction(MovieListType.POPULAR.ordinal)) + } + R.id.favorite-> { + navigateToFlow(this,MovieNavigationAction(MovieListType.FAVORITE.ordinal)) + } + R.id.top_rated-> { + navigateToFlow(this,MovieNavigationAction(MovieListType.TOP_RATED.ordinal)) + } + } + true + } + + } + + + override fun navigate(action: NavigationAction) { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val controller = navHostFragment.navController + action.navigate(controller) + } + + +/* here we have a policy, if we are in any kind of MovieListFragment + and we click back, we must return to Home and the stack has to be cleared + the reason is MovieListFragment is one the roots so if we click back on any root + we should return to the first step which is Home + if we pressed back on home even though the back stack is not empty we must + close the program + + if we are in neither of those situation we should just pop the stack*/ + + override fun onBack() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val controller = navHostFragment.navController + + when(controller.currentDestination?.label ){ + "MovieListFragment"->{ + controller.setGraph(ir.sass.navigator.R.navigation.app_navigation) + dataBinding.bottomNavigationView.menu.findItem(R.id.home).setChecked(true) + } + + "HomeFragment"->{ + finish() + } + + else->{ + controller.popBackStack() + } + } + + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..1976a2c9 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 00000000..e248cf4e --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..bdb738fc --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + daresay + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..35658ee8 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/ir/sass/daresay/ExampleUnitTest.kt b/app/src/test/java/ir/sass/daresay/ExampleUnitTest.kt new file mode 100644 index 00000000..10dd9304 --- /dev/null +++ b/app/src/test/java/ir/sass/daresay/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package ir.sass.daresay + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..da5e6eed --- /dev/null +++ b/build.gradle @@ -0,0 +1,112 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.2" + classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +plugins { + id 'com.android.application' version '7.1.2' apply false + id 'com.android.library' version '7.1.2' apply false + id 'org.jetbrains.kotlin.android' version '1.5.30' apply false + id 'org.jetbrains.kotlin.jvm' version '1.5.30' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext{ + lib_version = [ + hilt:"2.38.1", + retrofit:"2.9.0", + coroutines:"1.3.9", + appcompat:"1.4.1", + androidCore:"1.7.0", + material:"1.5.0", + navigation:"2.4.1", + lifecycle:"2.4.0-alpha03", + inject:"1", + fragment:"1.3.0", + lottie:"3.7.1", + glide:"4.12.0", + robolectric:"4.7.3", + junit:"4.+", + androidTest:"1.1.3", + mockitoTest:"3.0.0", + nhaarmanMockito:"2.1.0", + coroutinesTest:"1.6.0", + coreTest:"2.1.0", + turbine:"0.7.0", + fragmentTest:"1.4.1", + espresso:"3.4.0", + room:"2.3.0", + coreTestExt:"1.4.0", + junitExt:"1.1.3" + ] + lib = [ + appcompat:"androidx.appcompat:appcompat:${lib_version.appcompat}", + androidCore:"androidx.core:core-ktx:${lib_version.androidCore}", + hilt:"com.google.dagger:hilt-android:${lib_version.hilt}", + retrofit:[ + "com.squareup.retrofit2:retrofit:${lib_version.retrofit}", + "com.squareup.retrofit2:converter-gson:${lib_version.retrofit}" + ], + material:"com.google.android.material:material:${lib_version.material}", + navigation:[ + "androidx.navigation:navigation-fragment-ktx:${lib_version.navigation}", + "androidx.navigation:navigation-ui-ktx:${lib_version.navigation}" + ], + coroutines:"org.jetbrains.kotlinx:kotlinx-coroutines-android:${lib_version.coroutines}", + lifecycle:[ + "androidx.lifecycle:lifecycle-viewmodel-ktx:${lib_version.lifecycle}", + "androidx.lifecycle:lifecycle-livedata-ktx:${lib_version.lifecycle}", + "androidx.lifecycle:lifecycle-runtime-ktx:${lib_version.lifecycle}", + "androidx.lifecycle:lifecycle-viewmodel-savedstate:${lib_version.lifecycle}" + ], + inject:"javax.inject:javax.inject:${lib_version.inject}", + fragment:"androidx.fragment:fragment-ktx:${lib_version.fragment}", + lottie:"com.airbnb.android:lottie:${lib_version.lottie}", + glide:"com.github.bumptech.glide:glide:${lib_version.glide}", + room:[ + "androidx.room:room-runtime:${lib_version.room}", + "androidx.room:room-ktx:${lib_version.room}" + ] + ] + kaptLibs = [ + glide:"com.github.bumptech.glide:compiler:${lib_version.glide}", + hilt:"com.google.dagger:hilt-compiler:${lib_version.hilt}", + room:"androidx.room:room-compiler:${lib_version.room}" + ] + + testLib = [ + hiltTest:"com.google.dagger:hilt-android-testing:${lib_version.hilt}", + robolectric:"org.robolectric:robolectric:${lib_version.robolectric}", + junit:"junit:junit:${lib_version.junit}", + junitExt:"androidx.test.ext:junit-ktx:${lib_version.junitExt}", + androidTest:"androidx.test.ext:junit-ktx:${lib_version.androidTest}", + coroutinesTest:"org.jetbrains.kotlinx:kotlinx-coroutines-test:${lib_version.coroutinesTest}", + mockito:[ + "org.mockito:mockito-inline:${lib_version.mockitoTest}", + "com.nhaarman.mockitokotlin2:mockito-kotlin:${lib_version.nhaarmanMockito}" + ], + coreTest:"androidx.arch.core:core-testing:${lib_version.coreTest}", + coreTestExt:"androidx.test:core-ktx:${lib_version.coreTestExt}", + turbine:"app.cash.turbine:turbine:${lib_version.turbine}", + fragmentTest:"androidx.fragment:fragment-testing:${lib_version.fragmentTest}", + espresso:"androidx.test.espresso:espresso-core:${lib_version.espresso}" + + ] + katpTestLib = [ + hiltTest:"com.google.dagger:hilt-android-compiler:${lib_version.hilt}" + ] +} diff --git a/core/base_data/.gitignore b/core/base_data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/base_data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/base_data/build.gradle b/core/base_data/build.gradle new file mode 100644 index 00000000..e6e7a609 --- /dev/null +++ b/core/base_data/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api lib.get("androidCore") + api lib.get("appcompat") + api lib.get("coroutines") + api lib.get("retrofit") + implementation project(":core:base_domain") + + implementation testLib.get("mockito") + implementation testLib.get("coroutinesTest") + implementation testLib.get("coreTest") +} \ No newline at end of file diff --git a/core/base_data/consumer-rules.pro b/core/base_data/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/base_data/proguard-rules.pro b/core/base_data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/base_data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/base_data/src/main/AndroidManifest.xml b/core/base_data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2f70658f --- /dev/null +++ b/core/base_data/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/base_data/src/main/java/ir/sass/base_data/model/Mapper.kt b/core/base_data/src/main/java/ir/sass/base_data/model/Mapper.kt new file mode 100644 index 00000000..28f8d7c8 --- /dev/null +++ b/core/base_data/src/main/java/ir/sass/base_data/model/Mapper.kt @@ -0,0 +1,20 @@ +package ir.sass.base_data.model + +import com.google.gson.Gson + +/*this methods is used when we want to navigate between fragments +the reason that we need these methods is that we can not use parcelable +in our domain layers, using serialize is not a good option too +so we cast the data to a json and re-cast it again in our destination +we can use these methods for other things too*/ + +fun toJsonString(input: T): String = Gson().toJson(input).toString() + +inline fun toReal(input: String): T? { + return try { + Gson().fromJson(input, T::class.java) + } catch (e: Exception) { + null + } +} + diff --git a/core/base_data/src/main/java/ir/sass/base_data/model/safeApi.kt b/core/base_data/src/main/java/ir/sass/base_data/model/safeApi.kt new file mode 100644 index 00000000..66154696 --- /dev/null +++ b/core/base_data/src/main/java/ir/sass/base_data/model/safeApi.kt @@ -0,0 +1,17 @@ +package ir.sass.base_data.model + +import ir.sass.base_domain.model.Mapper + +/*safe api is a mediator function, it will invoke the service request +right now on catch section it returs the Throwable without any changes +but at furthers steps we can expand this function for having multiple options +for example specific message for any type of error*/ + +suspend fun > safeApi(req: suspend () -> T): Result { + return try { + val data = req.invoke().cast() + Result.success(data) + } catch (e: Throwable) { + Result.failure(e) + } +} diff --git a/core/base_data/src/main/java/ir/sass/base_data/test/BaseDataTest.kt b/core/base_data/src/main/java/ir/sass/base_data/test/BaseDataTest.kt new file mode 100644 index 00000000..19f35455 --- /dev/null +++ b/core/base_data/src/main/java/ir/sass/base_data/test/BaseDataTest.kt @@ -0,0 +1,22 @@ +package ir.sass.base_data.test + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import org.junit.Rule + +/*this is the parent class for any unit test that exist on data layers +we can not put this class on test folder because of the framework policy +so I added this class here intentionally*/ + +open class BaseDataTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @ExperimentalCoroutinesApi + private val testDispatcher = StandardTestDispatcher() + + @ExperimentalCoroutinesApi + protected val testScope = TestScope(testDispatcher) +} \ No newline at end of file diff --git a/core/base_data/src/test/java/ir/sass/base_data/MapperTest.kt b/core/base_data/src/test/java/ir/sass/base_data/MapperTest.kt new file mode 100644 index 00000000..e220d282 --- /dev/null +++ b/core/base_data/src/test/java/ir/sass/base_data/MapperTest.kt @@ -0,0 +1,30 @@ +package ir.sass.base_data + +import ir.sass.base_data.model.toJsonString +import ir.sass.base_data.model.toReal +import org.junit.Test + +class MapperTest { + data class A(val number: Int, val word: String) + + @Test + fun `check if the mapper can cast class A to jsonString`() { + val a = A(1, "google") + val jsonString = toJsonString(a) + assert(jsonString.equals("{\"number\":1,\"word\":\"google\"}")) + } + + @Test + fun `check if the mapper can cast json string to the type of A`() { + val json = "{\"number\":1,\"word\":\"google\"}" + val casted = toReal(json) + assert(casted!!.number == 1 && casted.word == "google") + } + + @Test + fun `check if the mapper return null when there is a problem in casting json String`() { + val json = "{wtsdgshgshsfh}" + val casted = toReal(json) + assert(casted == null) + } +} \ No newline at end of file diff --git a/core/base_data/src/test/java/ir/sass/base_data/SafeApiTest.kt b/core/base_data/src/test/java/ir/sass/base_data/SafeApiTest.kt new file mode 100644 index 00000000..478ba37d --- /dev/null +++ b/core/base_data/src/test/java/ir/sass/base_data/SafeApiTest.kt @@ -0,0 +1,30 @@ +package ir.sass.base_data + +import ir.sass.base_data.test.BaseDataTest +import ir.sass.base_domain.model.Domain +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SafeApiTest : BaseDataTest() { + + @ExperimentalCoroutinesApi + @Test + fun `check if the safeApi can return the correct data when the request is success`() = + testScope.runTest { + val data: Domain.Data = helperSuccess() + assert(data.data.isSuccess && data.data.getOrThrow() == 1) + } + + @ExperimentalCoroutinesApi + @Test + fun `check if the safeApi can return the correct data when the request is not success`() = + testScope.runTest { + val data: Domain.Data = helperFailed() + assert(data.data.isFailure && data.data.exceptionOrNull()!!.message!! == "Error") + } + + suspend fun helperSuccess(): Domain.Data = Domain.Data(Result.success(1)) + + suspend fun helperFailed(): Domain.Data = Domain.Data(Result.failure(Throwable("Error"))) +} \ No newline at end of file diff --git a/core/base_domain/.gitignore b/core/base_domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/base_domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/base_domain/build.gradle b/core/base_domain/build.gradle new file mode 100644 index 00000000..28a9b648 --- /dev/null +++ b/core/base_domain/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} + +dependencies { + api lib.get("coroutines") + api lib.get("inject") +} \ No newline at end of file diff --git a/core/base_domain/src/main/java/ir/sass/base_domain/model/Domain.kt b/core/base_domain/src/main/java/ir/sass/base_domain/model/Domain.kt new file mode 100644 index 00000000..8fc88d98 --- /dev/null +++ b/core/base_domain/src/main/java/ir/sass/base_domain/model/Domain.kt @@ -0,0 +1,11 @@ +package ir.sass.base_domain.model + +/*we are having clean mvvm, so we have a repository which send data into viewModel +this is a wrapper class for holding data, if we are on loading stage the repository +will send Domain.Progress, after that repository will send Domain.Data, the data is +a Result class so it can be a success request or failure */ + +sealed class Domain { + class Progress(val message: String = "Loading") : Domain() + class Data(val data: kotlin.Result) : Domain() +} \ No newline at end of file diff --git a/core/base_domain/src/main/java/ir/sass/base_domain/model/Mapper.kt b/core/base_domain/src/main/java/ir/sass/base_domain/model/Mapper.kt new file mode 100644 index 00000000..9255baf8 --- /dev/null +++ b/core/base_domain/src/main/java/ir/sass/base_domain/model/Mapper.kt @@ -0,0 +1,8 @@ +package ir.sass.base_domain.model + +/*this is a mapper interface for casting dto objects into domain objects +we can use this interface for other usages too*/ + +interface Mapper { + fun cast(): Cast +} \ No newline at end of file diff --git a/core/base_domain/src/main/java/ir/sass/base_domain/usecase/MotherUseCase.kt b/core/base_domain/src/main/java/ir/sass/base_domain/usecase/MotherUseCase.kt new file mode 100644 index 00000000..f5ed6336 --- /dev/null +++ b/core/base_domain/src/main/java/ir/sass/base_domain/usecase/MotherUseCase.kt @@ -0,0 +1,27 @@ +package ir.sass.base_domain.usecase + +import ir.sass.base_domain.model.Domain +import kotlinx.coroutines.flow.Flow + +/*we have 3 different type of use-case sometimes we have an action which has an input +but it won't return any value and sometimes it has output without any input +sometimes it has both and sometimes it has not neither of them*/ + +abstract class MotherUseCase { + abstract operator fun invoke(input: Input): Flow> +} + +abstract class MotherUseCaseWithOnlyInput() { + abstract operator fun invoke(input: Input) +} + +abstract class MotherUseCaseWithOnlyOutput { + abstract operator fun invoke(): Flow> +} + +abstract class MotherUseCaseEmpty { + abstract operator fun invoke() +} + + + diff --git a/core/base_ui/.gitignore b/core/base_ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/base_ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/base_ui/build.gradle b/core/base_ui/build.gradle new file mode 100644 index 00000000..d5086f56 --- /dev/null +++ b/core/base_ui/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + dataBinding true + } +} + +dependencies { + api lib.get("androidCore") + api lib.get("appcompat") + api lib.get("material") + api lib.get("coroutines") + api project(":core:base_domain") + api lib.get("hilt") + api lib.get("lifecycle") + api lib.get("fragment") + api project(":core:navigator") + + implementation lib.get("lottie") + implementation lib.get("glide") + +} \ No newline at end of file diff --git a/core/base_ui/consumer-rules.pro b/core/base_ui/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/base_ui/proguard-rules.pro b/core/base_ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/base_ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/base_ui/src/main/AndroidManifest.xml b/core/base_ui/src/main/AndroidManifest.xml new file mode 100644 index 00000000..02fa3eaa --- /dev/null +++ b/core/base_ui/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/base_ui/src/main/assets/loading.json b/core/base_ui/src/main/assets/loading.json new file mode 100644 index 00000000..fbdd9327 --- /dev/null +++ b/core/base_ui/src/main/assets/loading.json @@ -0,0 +1 @@ +{"v":"5.7.11","fr":60,"ip":0,"op":81,"w":1920,"h":1080,"nm":"Loading Dots","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Dot4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":55,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[1142,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[1142,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":55,"s":[1142,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":39,"s":[75,75,100]},{"t":55,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.383944462795,0.858823529412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Dot3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[100]},{"t":47,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[1022,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":31,"s":[1022,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":47,"s":[1022,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":17,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[75,75,100]},{"t":47,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.383944462795,0.858823529412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Dot2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[100]},{"t":39,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[902,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[902,500,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[902,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[75,75,100]},{"t":39,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.383944462795,0.858823529412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Dot1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14,"s":[100]},{"t":30,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[782,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[782,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":30,"s":[782,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[75,75,100]},{"t":30,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.383944462795,0.858823529412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/AlertPop.kt b/core/base_ui/src/main/java/ir/sass/base_ui/AlertPop.kt new file mode 100644 index 00000000..5e035606 --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/AlertPop.kt @@ -0,0 +1,39 @@ +package ir.sass.base_ui + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import ir.sass.base_ui.databinding.DialogAlertBinding + +class AlertPop( + context: Context, val setting : AlertPopSetting) : Dialog(context) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = DialogAlertBinding.inflate( + LayoutInflater.from(context),null,false + ) + + setContentView(binding.root) + + binding.action = { + setting.buttonAction.invoke() + dismiss() + } + binding.message = setting.message + binding.button = setting.buttonText + + window?.setBackgroundDrawableResource(android.R.color.transparent) + } +} + +fun showAlertDialog(context: Context,setting: AlertPopSetting){ + val alert = AlertPop(context,setting) + alert.show() +} + +class AlertPopSetting( + val message : String, + val buttonText : String = "Ok", + val buttonAction : () -> Unit = {} +) \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/MotherAdapter.kt b/core/base_ui/src/main/java/ir/sass/base_ui/MotherAdapter.kt new file mode 100644 index 00000000..696b94d2 --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/MotherAdapter.kt @@ -0,0 +1,120 @@ +package ir.sass.base_ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.ScaleAnimation +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView + + +/** + * this is the base class for all adapters + * @param wrapper is for setting of the adapter, it holds layout and an alternate function for onBindViewHolder + * @param DB is generic type for dataBinding + * @param DataType is generic type for data type + * + * @property changeList will replace the current list with a new list + */ + +class MotherAdapter ( + private val wrapper: RecyclerItemWrapper +) : RecyclerView.Adapter>() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MotherAdapterVH = + MotherAdapterVH( + DataBindingUtil.inflate(LayoutInflater.from(parent.context), wrapper.layout, parent, false) + ) + + private var lastPos = -1 + override fun onBindViewHolder( + holder: MotherAdapterVH, + position: Int + ) { + if(position > lastPos){ + val anim = ScaleAnimation( + 0.0f, + 1.0f, + 0.0f, + 1.0f, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.5f + ).apply { + duration = 500 + } + holder.binding.root.startAnimation(anim) + } + lastPos = lastPos.coerceAtLeast(position) + + wrapper.bindingFun.invoke(holder.binding,wrapper.list[position],position) + if(position == wrapper.list.size - 1) + wrapper.ended.invoke() + } + + override fun getItemCount(): Int = wrapper.list.size + + + fun changeList(list: List){ + val diffCallback = DiffCallback(wrapper.list, list) + val diffResult = DiffUtil.calculateDiff(diffCallback) + wrapper.list.clear() + wrapper.list.addAll(list) + diffResult.dispatchUpdatesTo(this) + notifyDataSetChanged() // I have to call this , if I won't I get crash + } + + fun addToList(list: List){ + val newList = mutableListOf() + newList.addAll(wrapper.list) + newList.addAll(list) + val diffCallback = DiffCallback(wrapper.list, newList) + val diffResult = DiffUtil.calculateDiff(diffCallback) + wrapper.list.clear() + wrapper.list.addAll(list) + diffResult.dispatchUpdatesTo(this) + notifyDataSetChanged() // I have to call this , if I won't I get crash + } + + override fun getItemId(position: Int): Long = position.toLong() + + override fun getItemViewType(position: Int): Int = position + + class DiffCallback(private val old : List, private val new : List) : DiffUtil.Callback() { + override fun getOldListSize(): Int = old.size + + override fun getNewListSize(): Int = new.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + old[oldItemPosition] === new[newItemPosition] + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + old[oldItemPosition] == new[newItemPosition] + } + + class MotherAdapterVH(var binding : DB) : RecyclerView.ViewHolder(binding.root) + +} + +/** + * this is a wrapper class for holding layout and an alternate function for onBindViewHolder + * @param layout is layout reference + * @param bindingFun is an alternate function for onBindViewHolder + * @param ended is a callback and it will invoked when you scroll to the end + */ + +data class RecyclerItemWrapper( + @LayoutRes + var layout : Int, + val ended : () -> Unit = {}, + val bindingFun : (binding : DB,item : DataType,pos : Int) -> Unit + +){ + internal val list = mutableListOf() +} + + diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/MotherFragment.kt b/core/base_ui/src/main/java/ir/sass/base_ui/MotherFragment.kt new file mode 100644 index 00000000..12af5f4a --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/MotherFragment.kt @@ -0,0 +1,124 @@ +package ir.sass.base_ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import ir.sass.base_ui.databinding.FragmentBaseBinding +import ir.sass.base_ui.utils.toast +import ir.sass.navigator.flow.features.NavcontrollerHelper +import kotlinx.coroutines.launch + +/** + * this is the base class for all fragments + * @param setting is for setting of the fragment, it holds layout and title + * @param DataBinding is generic type for dataBinding + * @param DataType is generic type for data type + * + * @property connectViewModelForError is a function which connect fragment to MotherViewModel for collecting errors + * @property connectViewModelForLoading is a function which connect fragment to MotherViewModel for collecting loading states + * @property connectViewModelForLoadingAndError is a wrapper function for doing both above functions + * @property binding you will write your code here, you must override this function + * @property loadingOn will manually show loading + * @property loadingOff will manually dismiss loading + * @property coroutinesLauncher you can handle your suspend functions here,it takes one parameter which is named +state and that's from Lifecycle.State, you will pass your action and the +action will be invoked + */ + +abstract class MotherFragment( + private val setting: MotherFragmentSetting +) : Fragment(R.layout.fragment_base) { + + private lateinit var dataBindingOuter: FragmentBaseBinding + protected lateinit var dataBinding: DataBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + dataBindingOuter = FragmentBaseBinding.inflate(inflater) + dataBindingOuter.title = setting.title + return dataBindingOuter.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dataBinding = DataBindingUtil.inflate( + LayoutInflater.from(requireContext()), setting.layout, dataBindingOuter.linAdd, true + ) + binding() + + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + (requireActivity() as NavcontrollerHelper).onBack() + } + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) + } + + fun connectViewModelForLoadingAndError(viewModel: MotherViewModel) { + connectViewModelForError(viewModel) + connectViewModelForLoading(viewModel) + } + + fun connectViewModelForError(viewModel: MotherViewModel) { + coroutinesLauncher(Lifecycle.State.STARTED) { + viewModel.error.collect { + showAlertDialog(requireContext(),AlertPopSetting(it)) + } + } + } + + fun connectViewModelForLoading(viewModel: MotherViewModel) { + coroutinesLauncher(Lifecycle.State.RESUMED) { + viewModel.loading.collect { + if (it) loadingOn() else loadingOff() + } + } + } + + abstract fun binding() + + + fun loadingOn() { + dataBindingOuter.loading = true + } + + fun loadingOff() { + dataBindingOuter.loading = false + } + + fun coroutinesLauncher(state: Lifecycle.State, action: suspend () -> Unit) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(state) { + action.invoke() + } + } + } + + fun getParentNavControllerHelper() = (requireActivity() as NavcontrollerHelper) + + +} + +/** + * this is the setting class for MotherFragment + * @param layout is layout reference + * @param title is title of the fragment + */ + +class MotherFragmentSetting( + @LayoutRes + val layout: Int, + val title: String +) \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/MotherViewModel.kt b/core/base_ui/src/main/java/ir/sass/base_ui/MotherViewModel.kt new file mode 100644 index 00000000..51eedc6f --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/MotherViewModel.kt @@ -0,0 +1,63 @@ +package ir.sass.base_ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ir.sass.base_domain.model.Domain +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch + +/** + * this is the base class for all viewModels + * @property _loading is for emitting the loading state + * @property loading is for collecting the loading state + * @property _error is for emitting the error messages + * @property error is for collecting the error messages + * @property action is a helper function which can run a request and it can + * handle if the request fail or not, it will emit an error if something is going wrong + * @property handleError is a function for emitting error + * @property loading is a function for changing loading state + */ + +open class MotherViewModel : ViewModel() { + private val _loading: MutableSharedFlow = MutableSharedFlow() + val loading: SharedFlow = _loading + + private val _error: MutableSharedFlow = MutableSharedFlow() + val error: SharedFlow = _error + + fun action(result: Flow>, loading: Boolean, action: (T) -> Unit) { + viewModelScope.launch { + result.collect { it -> + if (loading && it is Domain.Progress) { + loading(true) + } else { + loading(false) + if (it is Domain.Data) { + if (it.data.isSuccess) { + it.data.getOrNull()?.let { + action.invoke(it) + } + } else { + handleError(it.data.exceptionOrNull() ?: Throwable("Error")) + } + } + } + } + } + } + + protected fun handleError(t: Throwable) { + viewModelScope.launch { + _error.emit(t.message ?: "Error") + } + } + + private fun loading(active: Boolean) { + viewModelScope.launch { + _loading.emit(active) + } + } + +} \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/utils/Context.kt b/core/base_ui/src/main/java/ir/sass/base_ui/utils/Context.kt new file mode 100644 index 00000000..7434d49f --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/utils/Context.kt @@ -0,0 +1,12 @@ +package ir.sass.base_ui.utils + +import android.content.Context +import android.widget.Toast + +/* +extension function for showing toasts +*/ + +fun Context.toast(msg: String) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() +} \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/utils/View.kt b/core/base_ui/src/main/java/ir/sass/base_ui/utils/View.kt new file mode 100644 index 00000000..d78153f5 --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/utils/View.kt @@ -0,0 +1,40 @@ +package ir.sass.base_ui.utils + +import android.view.View +import androidx.databinding.BindingAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +/* +extension function for clicking in dataBinding +*/ + +@BindingAdapter("ext:click") +fun setClick(view: View, action: () -> Unit) { + view.setOnClickListener { + CoroutineScope(Dispatchers.Main).launch { + it.isClickable = false + action.invoke() + delay(200) + it.isClickable = true + } + } +} + +/* +extension function for making a view disabled in dataBinding +*/ + +@BindingAdapter("ext:active") +fun active(view: View, active: Boolean) { + if (active) { + view.isEnabled = true + view.alpha = 1f + } else { + view.isEnabled = false + view.alpha = 0.7f + } +} \ No newline at end of file diff --git a/core/base_ui/src/main/java/ir/sass/base_ui/view/ImageView.kt b/core/base_ui/src/main/java/ir/sass/base_ui/view/ImageView.kt new file mode 100644 index 00000000..c08843e3 --- /dev/null +++ b/core/base_ui/src/main/java/ir/sass/base_ui/view/ImageView.kt @@ -0,0 +1,84 @@ +package ir.sass.base_ui.view + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.airbnb.lottie.LottieAnimationView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import ir.sass.base_ui.R +import ir.sass.base_ui.databinding.LottieImageViewBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class LottieImageView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + val binding: LottieImageViewBinding = LottieImageViewBinding.inflate( + LayoutInflater.from(context), this, true + ) +} + +@BindingAdapter("ext:url") +fun setImageUrl(img: LottieImageView, url: String?) { + + url?.let { + img.binding.lottie.playAnimation() + + img.setBackgroundResource(R.drawable.back_img) + + Glide.with(img.context) + .load(url) + .centerCrop() + .addListener(imageLoadingListener(img.binding.lottie, img.binding.img)) + .into(img.binding.img) + } +} + + +fun imageLoadingListener( + pendingImage: LottieAnimationView, + img: ImageView +): RequestListener { + return object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: com.bumptech.glide.request.target.Target?, + isFirstResource: Boolean + ): Boolean { + pendingImage.pauseAnimation() + pendingImage.visibility = View.GONE + CoroutineScope(Main).launch { + delay(30) + img.setImageResource(R.drawable.broken_image) + } + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: com.bumptech.glide.request.target.Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + pendingImage.pauseAnimation() + pendingImage.visibility = View.GONE + return false + } + } +} \ No newline at end of file diff --git a/core/base_ui/src/main/res/drawable/back_img.xml b/core/base_ui/src/main/res/drawable/back_img.xml new file mode 100644 index 00000000..28c33f61 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/back_img.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/base_ui/src/main/res/drawable/bookmark.xml b/core/base_ui/src/main/res/drawable/bookmark.xml new file mode 100644 index 00000000..bd429a2f --- /dev/null +++ b/core/base_ui/src/main/res/drawable/bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/bookmark_book.xml b/core/base_ui/src/main/res/drawable/bookmark_book.xml new file mode 100644 index 00000000..066aeff0 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/bookmark_book.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/broken_image.xml b/core/base_ui/src/main/res/drawable/broken_image.xml new file mode 100644 index 00000000..db39e3d5 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/broken_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/film.xml b/core/base_ui/src/main/res/drawable/film.xml new file mode 100644 index 00000000..e9702e3b --- /dev/null +++ b/core/base_ui/src/main/res/drawable/film.xml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/base_ui/src/main/res/drawable/home.xml b/core/base_ui/src/main/res/drawable/home.xml new file mode 100644 index 00000000..977dc1f1 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/home.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/movie.xml b/core/base_ui/src/main/res/drawable/movie.xml new file mode 100644 index 00000000..edefe307 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/movie.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/movie_popluar.xml b/core/base_ui/src/main/res/drawable/movie_popluar.xml new file mode 100644 index 00000000..3d980059 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/movie_popluar.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/movie_star.xml b/core/base_ui/src/main/res/drawable/movie_star.xml new file mode 100644 index 00000000..3d980059 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/movie_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/not_bookmark.xml b/core/base_ui/src/main/res/drawable/not_bookmark.xml new file mode 100644 index 00000000..c3ec5180 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/not_bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/base_ui/src/main/res/drawable/people.xml b/core/base_ui/src/main/res/drawable/people.xml new file mode 100644 index 00000000..fdaaf3a2 --- /dev/null +++ b/core/base_ui/src/main/res/drawable/people.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/core/base_ui/src/main/res/layout/dialog_alert.xml b/core/base_ui/src/main/res/layout/dialog_alert.xml new file mode 100644 index 00000000..2bebdaa2 --- /dev/null +++ b/core/base_ui/src/main/res/layout/dialog_alert.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/base_ui/src/main/res/layout/fragment_base.xml b/core/base_ui/src/main/res/layout/fragment_base.xml new file mode 100644 index 00000000..64353a1d --- /dev/null +++ b/core/base_ui/src/main/res/layout/fragment_base.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/base_ui/src/main/res/layout/lottie_image_view.xml b/core/base_ui/src/main/res/layout/lottie_image_view.xml new file mode 100644 index 00000000..c1d4aebd --- /dev/null +++ b/core/base_ui/src/main/res/layout/lottie_image_view.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core/base_ui/src/main/res/values/colors.xml b/core/base_ui/src/main/res/values/colors.xml new file mode 100644 index 00000000..8a3537a8 --- /dev/null +++ b/core/base_ui/src/main/res/values/colors.xml @@ -0,0 +1,12 @@ + + + #FFFFFF + #000000 + #151E27 + #1D2733 + #0F161E + #546A77 + #30546A77 + #00000000 + #D24A16 + \ No newline at end of file diff --git a/core/navigator/.gitignore b/core/navigator/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/navigator/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/navigator/build.gradle b/core/navigator/build.gradle new file mode 100644 index 00000000..cc746695 --- /dev/null +++ b/core/navigator/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'androidx.navigation.safeargs.kotlin' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api lib.get("navigation") + implementation project(":core:base_data") +} \ No newline at end of file diff --git a/core/navigator/consumer-rules.pro b/core/navigator/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/navigator/proguard-rules.pro b/core/navigator/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/navigator/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/navigator/src/main/AndroidManifest.xml b/core/navigator/src/main/AndroidManifest.xml new file mode 100644 index 00000000..259eb937 --- /dev/null +++ b/core/navigator/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/navigator/src/main/java/ir/sass/navigator/flow/NavigationAction.kt b/core/navigator/src/main/java/ir/sass/navigator/flow/NavigationAction.kt new file mode 100644 index 00000000..1518c061 --- /dev/null +++ b/core/navigator/src/main/java/ir/sass/navigator/flow/NavigationAction.kt @@ -0,0 +1,14 @@ +package ir.sass.navigator.flow + +import androidx.navigation.NavController + +/** + * this is the base class for all navigation actions + * once you want to create a navigation you will extend this class (you can see home and movie) + * + * @property navigate must be overridden and you must pass a NavController to it + */ + +abstract class NavigationAction{ + abstract fun navigate(navController: NavController) +} \ No newline at end of file diff --git a/core/navigator/src/main/java/ir/sass/navigator/flow/features/NavcontrollerHelper.kt b/core/navigator/src/main/java/ir/sass/navigator/flow/features/NavcontrollerHelper.kt new file mode 100644 index 00000000..87f6c69b --- /dev/null +++ b/core/navigator/src/main/java/ir/sass/navigator/flow/features/NavcontrollerHelper.kt @@ -0,0 +1,18 @@ +package ir.sass.navigator.flow.features + +import ir.sass.navigator.flow.NavigationAction + +/** + * you will implement you activity with this interface + * then you can write a proper code for navigation between modules + * + * @property navigate is the function that you will override + * and write navigation codes there depending on you architecture + + */ + +interface NavcontrollerHelper { + fun navigate(action: NavigationAction) + + fun onBack() +} \ No newline at end of file diff --git a/core/navigator/src/main/java/ir/sass/navigator/flow/features/home/HomeNavigationAction.kt b/core/navigator/src/main/java/ir/sass/navigator/flow/features/home/HomeNavigationAction.kt new file mode 100644 index 00000000..6e54e6d5 --- /dev/null +++ b/core/navigator/src/main/java/ir/sass/navigator/flow/features/home/HomeNavigationAction.kt @@ -0,0 +1,12 @@ +package ir.sass.navigator.flow.features.home + +import androidx.navigation.NavController +import ir.sass.navigator.R +import ir.sass.navigator.flow.NavigationAction + +class HomeNavigationAction( +) : NavigationAction() { + override fun navigate(navController: NavController) { + navController.navigate(R.id.navigate_to_home) + } +} \ No newline at end of file diff --git a/core/navigator/src/main/java/ir/sass/navigator/flow/features/movie/MovieNavigationAction.kt b/core/navigator/src/main/java/ir/sass/navigator/flow/features/movie/MovieNavigationAction.kt new file mode 100644 index 00000000..eef24335 --- /dev/null +++ b/core/navigator/src/main/java/ir/sass/navigator/flow/features/movie/MovieNavigationAction.kt @@ -0,0 +1,16 @@ +package ir.sass.navigator.flow.features.movie + +import androidx.core.os.bundleOf +import androidx.navigation.NavController +import ir.sass.navigator.R +import ir.sass.navigator.flow.NavigationAction + +class MovieNavigationAction( + val type : Int +) : NavigationAction() { + override fun navigate(navController: NavController) { + navController.navigate(R.id.navigate_to_movie, bundleOf().apply { + this.putInt("type",type) + }) + } +} \ No newline at end of file diff --git a/core/navigator/src/main/java/ir/sass/navigator/navigator/NavigatorSystem.kt b/core/navigator/src/main/java/ir/sass/navigator/navigator/NavigatorSystem.kt new file mode 100644 index 00000000..59bb2aa5 --- /dev/null +++ b/core/navigator/src/main/java/ir/sass/navigator/navigator/NavigatorSystem.kt @@ -0,0 +1,16 @@ +package ir.sass.navigator.navigator + +import ir.sass.navigator.flow.NavigationAction +import ir.sass.navigator.flow.features.NavcontrollerHelper + +/** + * this is a function for invoking navigation + * + * @param navController read NavcontrollerHelper for further explanations + * @param action read NavigationAction for further explanations + + */ + +fun navigateToFlow(navController: NavcontrollerHelper, action: NavigationAction){ + navController.navigate(action) +} diff --git a/core/navigator/src/main/res/navigation/app_navigation.xml b/core/navigator/src/main/res/navigation/app_navigation.xml new file mode 100644 index 00000000..becdcb84 --- /dev/null +++ b/core/navigator/src/main/res/navigation/app_navigation.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core/navigator/src/main/res/navigation/home_navigation.xml b/core/navigator/src/main/res/navigation/home_navigation.xml new file mode 100644 index 00000000..8b3ecbc2 --- /dev/null +++ b/core/navigator/src/main/res/navigation/home_navigation.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/navigator/src/main/res/navigation/movie_navigation.xml b/core/navigator/src/main/res/navigation/movie_navigation.xml new file mode 100644 index 00000000..241ff04b --- /dev/null +++ b/core/navigator/src/main/res/navigation/movie_navigation.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/navigator/src/main/res/values/navigation_id.xml b/core/navigator/src/main/res/values/navigation_id.xml new file mode 100644 index 00000000..0ae62c4f --- /dev/null +++ b/core/navigator/src/main/res/values/navigation_id.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/shared_data/.gitignore b/core/shared_data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/shared_data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/shared_data/build.gradle b/core/shared_data/build.gradle new file mode 100644 index 00000000..dee474c4 --- /dev/null +++ b/core/shared_data/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api project(":core:base_data") + api project(":core:base_domain") + + + implementation lib.get("room") + kapt kaptLibs.get("room") + + implementation lib.get("hilt") + kapt kaptLibs.get("hilt") +} \ No newline at end of file diff --git a/core/shared_data/consumer-rules.pro b/core/shared_data/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/shared_data/proguard-rules.pro b/core/shared_data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/shared_data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/shared_data/src/androidTest/java/ir/sass/shared_data/ExampleInstrumentedTest.kt b/core/shared_data/src/androidTest/java/ir/sass/shared_data/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..853e38a7 --- /dev/null +++ b/core/shared_data/src/androidTest/java/ir/sass/shared_data/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ir.sass.shared_data + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ir.sass.shared_data.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/shared_data/src/main/AndroidManifest.xml b/core/shared_data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dd87e58e --- /dev/null +++ b/core/shared_data/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/shared_data/src/main/java/ir/sass/shared_data/db/AppDatabase.kt b/core/shared_data/src/main/java/ir/sass/shared_data/db/AppDatabase.kt new file mode 100644 index 00000000..16172966 --- /dev/null +++ b/core/shared_data/src/main/java/ir/sass/shared_data/db/AppDatabase.kt @@ -0,0 +1,33 @@ +package ir.sass.shared_data.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import ir.sass.shared_data.db.model.ResultEntity + +@Database(entities = [ResultEntity::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun movieDao(): MovieDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + private const val DB_NAME = "DARESAY" + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context, + AppDatabase::class.java, DB_NAME + ) + .build() + INSTANCE = instance + instance + } + } + } + + +} \ No newline at end of file diff --git a/core/shared_data/src/main/java/ir/sass/shared_data/db/MovieDao.kt b/core/shared_data/src/main/java/ir/sass/shared_data/db/MovieDao.kt new file mode 100644 index 00000000..65127939 --- /dev/null +++ b/core/shared_data/src/main/java/ir/sass/shared_data/db/MovieDao.kt @@ -0,0 +1,23 @@ +package ir.sass.shared_data.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import ir.sass.shared_data.db.model.RESULT_TABLE_NAME +import ir.sass.shared_data.db.model.ResultEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface MovieDao { + @Query("SELECT * FROM $RESULT_TABLE_NAME") + fun getAllResults(): Flow> + + @Insert + fun insertNewResult(input: ResultEntity) + + @Query("DELETE FROM $RESULT_TABLE_NAME WHERE id = :id") + fun deleteAResult(id: Int) + + @Query("SELECT EXISTS (SELECT 1 FROM $RESULT_TABLE_NAME WHERE id = :id)") + fun exists(id: Int): Boolean +} \ No newline at end of file diff --git a/core/shared_data/src/main/java/ir/sass/shared_data/db/model/ResultEntity.kt b/core/shared_data/src/main/java/ir/sass/shared_data/db/model/ResultEntity.kt new file mode 100644 index 00000000..2e74d371 --- /dev/null +++ b/core/shared_data/src/main/java/ir/sass/shared_data/db/model/ResultEntity.kt @@ -0,0 +1,36 @@ +package ir.sass.shared_data.db.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +const val RESULT_TABLE_NAME = "result_entity" + +@Entity(tableName = RESULT_TABLE_NAME) +data class ResultEntity( + @ColumnInfo(name = "adult") + val adult: Boolean, + @PrimaryKey + @ColumnInfo(name = "id") + val id: Int, + @ColumnInfo(name = "original_language") + val original_language: String, + @ColumnInfo(name = "original_title") + val original_title: String, + @ColumnInfo(name = "overview") + val overview: String, + @ColumnInfo(name = "popularity") + val popularity: Double, + @ColumnInfo(name = "poster_path") + val poster_path : String, + @ColumnInfo(name = "release_date") + val release_date: String, + @ColumnInfo(name = "title") + val title: String, + @ColumnInfo(name = "vote_average") + val vote_average: Double, + @ColumnInfo(name = "vote_count") + val vote_count: Int, + @ColumnInfo(name = "backdrop_path") + val backdrop_path : String +) \ No newline at end of file diff --git a/core/shared_data/src/main/java/ir/sass/shared_data/di/DbModule.kt b/core/shared_data/src/main/java/ir/sass/shared_data/di/DbModule.kt new file mode 100644 index 00000000..3708666d --- /dev/null +++ b/core/shared_data/src/main/java/ir/sass/shared_data/di/DbModule.kt @@ -0,0 +1,20 @@ +package ir.sass.shared_data.di + +import android.app.Application +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ir.sass.shared_data.db.AppDatabase +import ir.sass.shared_data.db.MovieDao +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DbModule { + @Provides + @Singleton + fun provideMovieDao(application: Application): MovieDao { + return AppDatabase.getDatabase(application).movieDao() + } +} \ No newline at end of file diff --git a/core/shared_domain/.gitignore b/core/shared_domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/shared_domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/shared_domain/build.gradle b/core/shared_domain/build.gradle new file mode 100644 index 00000000..fa2872dc --- /dev/null +++ b/core/shared_domain/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} \ No newline at end of file diff --git a/core/shared_domain/src/main/java/ir/sass/shared_domain/MovieListType.kt b/core/shared_domain/src/main/java/ir/sass/shared_domain/MovieListType.kt new file mode 100644 index 00000000..da681d03 --- /dev/null +++ b/core/shared_domain/src/main/java/ir/sass/shared_domain/MovieListType.kt @@ -0,0 +1,7 @@ +package ir.sass.shared_domain + +enum class MovieListType { + FAVORITE, + POPULAR, + TOP_RATED +} \ No newline at end of file diff --git a/diagram.jpg b/diagram.jpg new file mode 100644 index 00000000..8fa8526d Binary files /dev/null and b/diagram.jpg differ diff --git a/features/home/ui/.gitignore b/features/home/ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/features/home/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/home/ui/build.gradle b/features/home/ui/build.gradle new file mode 100644 index 00000000..e89af8f6 --- /dev/null +++ b/features/home/ui/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'androidx.navigation.safeargs.kotlin' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + dataBinding true + } +} + +dependencies { + api project(":core:base_ui") + implementation lib.get("hilt") + kapt kaptLibs.get("hilt") + implementation project(":core:shared_domain") +} \ No newline at end of file diff --git a/features/home/ui/consumer-rules.pro b/features/home/ui/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/features/home/ui/proguard-rules.pro b/features/home/ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/features/home/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/home/ui/src/main/AndroidManifest.xml b/features/home/ui/src/main/AndroidManifest.xml new file mode 100644 index 00000000..34aa4433 --- /dev/null +++ b/features/home/ui/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/features/home/ui/src/main/java/ir/sass/home/ui/home/HomeFragment.kt b/features/home/ui/src/main/java/ir/sass/home/ui/home/HomeFragment.kt new file mode 100644 index 00000000..db155ae8 --- /dev/null +++ b/features/home/ui/src/main/java/ir/sass/home/ui/home/HomeFragment.kt @@ -0,0 +1,17 @@ +package ir.sass.home.ui.home + +import ir.sass.base_ui.MotherFragment +import ir.sass.base_ui.MotherFragmentSetting +import ir.sass.home.ui.R +import ir.sass.home.ui.databinding.FragmentHomeBinding +import ir.sass.navigator.flow.features.movie.MovieNavigationAction +import ir.sass.navigator.navigator.navigateToFlow +import ir.sass.shared_domain.MovieListType + +class HomeFragment : MotherFragment( + MotherFragmentSetting(R.layout.fragment_home, "Home") +) { + override fun binding() { + + } +} \ No newline at end of file diff --git a/features/home/ui/src/main/res/layout/fragment_home.xml b/features/home/ui/src/main/res/layout/fragment_home.xml new file mode 100644 index 00000000..d09586c4 --- /dev/null +++ b/features/home/ui/src/main/res/layout/fragment_home.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/home/ui/src/main/res/navigation/home_navigation.xml b/features/home/ui/src/main/res/navigation/home_navigation.xml new file mode 100644 index 00000000..61ef4a4d --- /dev/null +++ b/features/home/ui/src/main/res/navigation/home_navigation.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/features/movie/data/.gitignore b/features/movie/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/features/movie/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/movie/data/build.gradle b/features/movie/data/build.gradle new file mode 100644 index 00000000..326c5019 --- /dev/null +++ b/features/movie/data/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + api project(":core:base_data") + api project(":core:base_domain") + implementation project(":core:shared_data") + + implementation project(":features:movie:domain") + + implementation lib.get("hilt") + kapt kaptLibs.get("hilt") + + implementation lib.get("room") + kapt kaptLibs.get("room") + + testImplementation testLib.get("mockito") + testImplementation testLib.get("mockito") + testImplementation testLib.get("coroutinesTest") + testImplementation testLib.get("junitExt") + testImplementation testLib.get("coreTestExt") + testImplementation testLib.get("robolectric") +} \ No newline at end of file diff --git a/features/movie/data/consumer-rules.pro b/features/movie/data/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/features/movie/data/proguard-rules.pro b/features/movie/data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/features/movie/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/movie/data/src/androidTest/java/ir/sass/movie/data/MovieRepositoryImpTest.kt b/features/movie/data/src/androidTest/java/ir/sass/movie/data/MovieRepositoryImpTest.kt new file mode 100644 index 00000000..44c9567c --- /dev/null +++ b/features/movie/data/src/androidTest/java/ir/sass/movie/data/MovieRepositoryImpTest.kt @@ -0,0 +1,95 @@ +package ir.sass.movie.data + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nhaarman.mockitokotlin2.mock +import ir.sass.base_data.test.BaseDataTest +import ir.sass.base_domain.model.Domain +import ir.sass.movie.data.datasource.remote.DiscoverMovieApi +import ir.sass.movie.data.repository.MovieRepositoryImp +import ir.sass.shared_data.db.AppDatabase +import ir.sass.shared_data.db.MovieDao +import ir.sass.shared_data.db.model.ResultEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MovieRepositoryImpTest : BaseDataTest() { + + private val apiService: DiscoverMovieApi = mock() + private lateinit var movieDao: MovieDao + private lateinit var repository: MovieRepositoryImp + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java + ).build() + movieDao = db.movieDao() + + repository = MovieRepositoryImp(apiService, movieDao) + } + + @After + fun closeDb() { + db.close() + } + + @ExperimentalCoroutinesApi + @Test + fun if_we_call_saveToLocal_from_repository_then_there_must_be_a_data_in_db() = + testScope.runTest { + val data = insertAStubIntoDb() + val response = movieDao.getAllResults() + response.collect { + assertThat(it[0], equalTo(data)) + } + } + + @ExperimentalCoroutinesApi + @Test + fun if_we_call_deleteFromLocal_from_repository_then_that_row_should_be_deleted_from_db() = + testScope.runTest { + val data = insertAStubIntoDb() + movieDao.deleteAResult(data.id) + val response = movieDao.getAllResults() + response.collect { + assert(it.isEmpty()) + } + } + + @ExperimentalCoroutinesApi + @Test + fun if_we_call_discoverMoviesFromLocal_from_repository_then_we_must_get_a_flow_of_DiscoverMovieModel_with_Domain_wrapper() = + testScope.runTest { + val data = insertAStubIntoDb() + repository.discoverMoviesFromLocal().collect { + assert(it is Domain.Data) + assert((it as Domain.Data).data.isSuccess) + assert((it as Domain.Data).data.getOrThrow().results!!.size == 1) + } + } + + private fun insertAStubIntoDb(): ResultEntity { + val data = createEntityStub() + movieDao.insertNewResult(createEntityStub()) + return data + } + + private fun createEntityStub() = ResultEntity( + true, 1, "1", "1", "1", 1.0, + "1", "1", "1", 1.0, 1, "1" + ) + + +} \ No newline at end of file diff --git a/features/movie/data/src/main/AndroidManifest.xml b/features/movie/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..805001f9 --- /dev/null +++ b/features/movie/data/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/datasource/remote/DiscoverMovieApi.kt b/features/movie/data/src/main/java/ir/sass/movie/data/datasource/remote/DiscoverMovieApi.kt new file mode 100644 index 00000000..f15a4099 --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/datasource/remote/DiscoverMovieApi.kt @@ -0,0 +1,13 @@ +package ir.sass.movie.data.datasource.remote + +import ir.sass.movie.data.model.movie.DiscoverMovieDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface DiscoverMovieApi { + @GET("/3/movie/popular") + suspend fun discoverPopularMovies(@Query("page") page : Int): DiscoverMovieDto + + @GET("/3/movie/top_rated") + suspend fun discoverTopMovies(@Query("page") page : Int): DiscoverMovieDto +} \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/di/DataSourceModule.kt b/features/movie/data/src/main/java/ir/sass/movie/data/di/DataSourceModule.kt new file mode 100644 index 00000000..613748fe --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/di/DataSourceModule.kt @@ -0,0 +1,47 @@ +package ir.sass.movie.data.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ir.sass.movie.data.datasource.remote.DiscoverMovieApi +import ir.sass.movie.data.utils.API_BASE_URL +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataSourceModule { + @Provides + fun provideOkHttpClient(): OkHttpClient { + val okClient = OkHttpClient.Builder() + .addInterceptor { chain -> + var request: Request = chain.request() + val url: HttpUrl = request.url().newBuilder() + .addQueryParameter("api_key", "500b07a7f284efab4476a03d5fc384e1") + .build() + request = request.newBuilder().url(url).build() + chain.proceed(request) + } + .build() + return okClient + } + + @Provides + @Singleton + fun provideRetrofit(httpClient: OkHttpClient): Retrofit = Retrofit.Builder() + .baseUrl(API_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient) + .build() + + @Provides + @Singleton + fun provideDiscoverMovieApi(retrofit: Retrofit): DiscoverMovieApi = retrofit + .create(DiscoverMovieApi::class.java) + +} \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/di/RepositoryModule.kt b/features/movie/data/src/main/java/ir/sass/movie/data/di/RepositoryModule.kt new file mode 100644 index 00000000..a493ecf3 --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/di/RepositoryModule.kt @@ -0,0 +1,15 @@ +package ir.sass.movie.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ir.sass.domain.repository.MovieRepository +import ir.sass.movie.data.repository.MovieRepositoryImp + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + abstract fun bindMovieRepository(movieRepositoryImp: MovieRepositoryImp): MovieRepository +} \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/DiscoverMovieDto.kt b/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/DiscoverMovieDto.kt new file mode 100644 index 00000000..566e30af --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/DiscoverMovieDto.kt @@ -0,0 +1,98 @@ +package ir.sass.movie.data.model.movie + +import com.google.gson.annotations.SerializedName +import ir.sass.base_domain.model.Mapper +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.model.ResultModel +import ir.sass.movie.data.utils.BACKGROUND_IMAGE_BASE_URL +import ir.sass.movie.data.utils.IMAGE_BASE_URL +import ir.sass.movie.data.utils.SAMPLE_MOVIE_IMAGE_URL + +data class DiscoverMovieDto( + @SerializedName("page") + val page: Int?, + @SerializedName("results") + val results: List?, + @SerializedName("total_pages") + val total_pages: Int?, + @SerializedName("total_results") + val total_results: Int?, + @SerializedName("status_code") + val status_code: Int?, + @SerializedName("status_message") + val status_message: String?, + @SerializedName("success") + val success: Boolean? +) : Mapper { + override fun cast(): DiscoverMovieModel = DiscoverMovieModel( + page, results?.map { + it.cast() + }, total_pages, total_results, status_code, status_message, success ?: true + ) + + +} + +data class ResultDto( + @SerializedName("adult") + val adult: Boolean, + @SerializedName("backdrop_path") + val backdrop_path: String?, + @SerializedName("genre_ids") + val genre_ids: List, + @SerializedName("id") + val id: Int, + @SerializedName("original_language") + val original_language: String, + @SerializedName("original_title") + val original_title: String, + @SerializedName("overview") + val overview: String, + @SerializedName("popularity") + val popularity: Double, + @SerializedName("poster_path") + val poster_path: String?, + @SerializedName("release_date") + val release_date: String, + @SerializedName("title") + val title: String, + @SerializedName("video") + val video: Boolean, + @SerializedName("vote_average") + val vote_average: Double, + @SerializedName("vote_count") + val vote_count: Int +) : Mapper { + override fun cast(): ResultModel { + val finalUrlForImage = if (poster_path == null) { + SAMPLE_MOVIE_IMAGE_URL + } else { + IMAGE_BASE_URL + poster_path + } + + + val finalBackdropPath = if (backdrop_path == null) { + SAMPLE_MOVIE_IMAGE_URL + } else { + BACKGROUND_IMAGE_BASE_URL + backdrop_path + } + + return ResultModel( + adult, + finalBackdropPath, + genre_ids, + id, + original_language, + original_title, + overview, + popularity, + finalUrlForImage, + release_date, + title, + video, + vote_average, + vote_count + ) + } + +} diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/ResultEntityExt.kt b/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/ResultEntityExt.kt new file mode 100644 index 00000000..5e3e6bdf --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/model/movie/ResultEntityExt.kt @@ -0,0 +1,15 @@ +package ir.sass.movie.data.model.movie + +import ir.sass.domain.model.ResultModel +import ir.sass.shared_data.db.model.ResultEntity + +fun ResultEntity.cast(): ResultModel = ResultModel( + adult, backdrop_path, listOf(), id, + original_language, original_title, overview, popularity, poster_path,release_date, title, + false, vote_average, 1 +) + +fun ResultModel.castToEntity(): ResultEntity = ResultEntity( + adult, id, original_language, original_title, overview, popularity, + poster_path, release_date, title, vote_average, vote_count,backdrop_path +) \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/repository/MovieRepositoryImp.kt b/features/movie/data/src/main/java/ir/sass/movie/data/repository/MovieRepositoryImp.kt new file mode 100644 index 00000000..92eea423 --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/repository/MovieRepositoryImp.kt @@ -0,0 +1,80 @@ +package ir.sass.movie.data.repository + +import ir.sass.base_data.model.safeApi +import ir.sass.base_domain.model.Domain +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.model.ResultModel +import ir.sass.domain.repository.MovieRepository +import ir.sass.movie.data.datasource.remote.DiscoverMovieApi +import ir.sass.movie.data.model.movie.cast +import ir.sass.movie.data.model.movie.castToEntity +import ir.sass.shared_data.db.MovieDao +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class MovieRepositoryImp @Inject constructor( + private val discoverMovieApi: DiscoverMovieApi, + private val dao: MovieDao +) : MovieRepository { + override fun discoverPopularMovies(page : Int): Flow> = flow { + emit(Domain.Progress()) + val response = safeApi { discoverMovieApi.discoverPopularMovies(page) } + + if (response.isSuccess && response.getOrNull() != null && response.getOrThrow().results != null) { + emit(Domain.Data(response)) + } else { + val errorMessage = response.getOrNull()?.status_message ?: "Error" + emit(Domain.Data(Result.failure(Throwable(errorMessage)))) + } + }.flowOn(IO) + + override fun discoverTopMovies(page : Int): Flow> = flow { + emit(Domain.Progress()) + val response = safeApi { discoverMovieApi.discoverTopMovies(page) } + + if (response.isSuccess && response.getOrNull() != null && response.getOrThrow().results != null) { + emit(Domain.Data(response)) + } else { + val errorMessage = response.getOrNull()?.status_message ?: "Error" + emit(Domain.Data(Result.failure(Throwable(errorMessage)))) + } + }.flowOn(IO) + + override fun discoverMoviesFromLocal(): Flow> = + dao.getAllResults().map { + Domain.Data( + Result.success( + DiscoverMovieModel(1, + it.map { + it.cast() + }, 1, 1, 200, "", true + ) + + ) + ) + } + + override fun saveToLocal(model: ResultModel) { + CoroutineScope(IO).launch { + dao.insertNewResult(model.castToEntity()) + } + } + + override fun deleteFromLocal(model: ResultModel) { + CoroutineScope(IO).launch { + dao.deleteAResult(model.id) + } + } + + override fun isExistOnDb(model: ResultModel): Flow> = flow> { + emit(Domain.Data(Result.success( + dao.exists(model.id) + ))) + }.flowOn(IO) +} \ No newline at end of file diff --git a/features/movie/data/src/main/java/ir/sass/movie/data/utils/URLs.kt b/features/movie/data/src/main/java/ir/sass/movie/data/utils/URLs.kt new file mode 100644 index 00000000..734a9ee6 --- /dev/null +++ b/features/movie/data/src/main/java/ir/sass/movie/data/utils/URLs.kt @@ -0,0 +1,6 @@ +package ir.sass.movie.data.utils + +const val IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w300/" +const val BACKGROUND_IMAGE_BASE_URL = "https://www.themoviedb.org/t/p/w533_and_h300_bestv2/" +const val API_BASE_URL = "https://api.themoviedb.org/" +const val SAMPLE_MOVIE_IMAGE_URL = "https://lajoyalink.com/wp-content/uploads/2018/03/Movie.jpg" diff --git a/features/movie/data/src/test/java/ir/sass/movie/data/repository/MovieRepositoryImpTest.kt b/features/movie/data/src/test/java/ir/sass/movie/data/repository/MovieRepositoryImpTest.kt new file mode 100644 index 00000000..328c234f --- /dev/null +++ b/features/movie/data/src/test/java/ir/sass/movie/data/repository/MovieRepositoryImpTest.kt @@ -0,0 +1,77 @@ +package ir.sass.movie.data.repository + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import ir.sass.base_data.test.BaseDataTest +import ir.sass.base_domain.model.Domain +import ir.sass.movie.data.datasource.remote.DiscoverMovieApi +import ir.sass.movie.data.model.movie.DiscoverMovieDto +import ir.sass.shared_data.db.MovieDao +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + + +class MovieRepositoryImpTest : BaseDataTest() { + + private val apiService: DiscoverMovieApi = mock() + private val discoverMovieDto : DiscoverMovieDto = mock() + private val movieDao : MovieDao = mock() + private val repository = MovieRepositoryImp(apiService,movieDao) + + + @ExperimentalCoroutinesApi + @Test + fun `api service must call discoverPopularMovies 1 time when we call discoverPopularMovies from Repository`() = testScope.runTest{ + whenever(apiService.discoverPopularMovies(1)).thenReturn(discoverMovieDto) + repository.discoverPopularMovies(1).collect { + verify(apiService, times(1)).discoverPopularMovies(1) + } + } + + @ExperimentalCoroutinesApi + @Test + fun `api service must call discoverTopMovies 1 time when we call discoverPopularMovies from Repository`() = testScope.runTest{ + whenever(apiService.discoverTopMovies(1)).thenReturn(discoverMovieDto) + repository.discoverTopMovies(1).collect { + verify(apiService, times(1)).discoverTopMovies(1) + } + } + + @ExperimentalCoroutinesApi + @Test + fun `when you call discoverPopularMovies ,repository must emit a Progress from Domain before emitting the actual data then it must emit Result`() = testScope.runTest{ + var flagHelper = "None" + repository.discoverPopularMovies(1).collect { + when(flagHelper){ + "None"->{ + assert(it is Domain.Progress) + flagHelper = "Progress" + } + "Progress"->{ + assert(it is Domain.Data) + } + } + } + } + + @ExperimentalCoroutinesApi + @Test + fun `when you call discoverTopMovies ,repository must emit a Progress from Domain before emitting the actual data then it must emit Result`() = testScope.runTest{ + var flagHelper = "None" + repository.discoverTopMovies(1).collect { + when(flagHelper){ + "None"->{ + assert(it is Domain.Progress) + flagHelper = "Progress" + } + "Progress"->{ + assert(it is Domain.Data) + } + } + } + } + +} \ No newline at end of file diff --git a/features/movie/domain/.gitignore b/features/movie/domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/features/movie/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/movie/domain/build.gradle b/features/movie/domain/build.gradle new file mode 100644 index 00000000..adc25c47 --- /dev/null +++ b/features/movie/domain/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} + + +dependencies { + implementation project(":core:base_domain") +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/model/DiscoverMovieModel.kt b/features/movie/domain/src/main/java/ir/sass/domain/model/DiscoverMovieModel.kt new file mode 100644 index 00000000..c311b900 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/model/DiscoverMovieModel.kt @@ -0,0 +1,29 @@ +package ir.sass.domain.model + + +data class DiscoverMovieModel( + val page: Int?, + val results: List?, + val total_pages: Int?, + val total_results: Int?, + val status_code: Int?, + val status_message: String?, + val success: Boolean +) + +data class ResultModel( + val adult: Boolean, + val backdrop_path: String, + val genre_ids: List, + val id: Int, + val original_language: String, + val original_title: String, + val overview: String, + val popularity: Double, + val poster_path: String, + val release_date: String, + val title: String, + val video: Boolean, + val vote_average: Double, + val vote_count: Int +) diff --git a/features/movie/domain/src/main/java/ir/sass/domain/repository/MovieRepository.kt b/features/movie/domain/src/main/java/ir/sass/domain/repository/MovieRepository.kt new file mode 100644 index 00000000..a5451b32 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/repository/MovieRepository.kt @@ -0,0 +1,20 @@ +package ir.sass.domain.repository + +import ir.sass.base_domain.model.Domain +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.model.ResultModel +import kotlinx.coroutines.flow.Flow + +interface MovieRepository { + fun discoverPopularMovies(page : Int): Flow> + + fun discoverTopMovies(page : Int): Flow> + + fun discoverMoviesFromLocal(): Flow> + + fun saveToLocal(model: ResultModel) + + fun deleteFromLocal(model: ResultModel) + + fun isExistOnDb(model: ResultModel) : Flow> +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/CheckIfAMovieIsInDataBaseUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/CheckIfAMovieIsInDataBaseUseCase.kt new file mode 100644 index 00000000..774aa525 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/CheckIfAMovieIsInDataBaseUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.model.Domain +import ir.sass.base_domain.usecase.MotherUseCase +import ir.sass.domain.model.ResultModel +import ir.sass.domain.repository.MovieRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class CheckIfAMovieIsInDataBaseUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCase() { + override fun invoke(input: ResultModel): Flow> = repository.isExistOnDb(input) +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/DeleteMovieFromLocalUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DeleteMovieFromLocalUseCase.kt new file mode 100644 index 00000000..93685dd2 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DeleteMovieFromLocalUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.usecase.MotherUseCaseWithOnlyInput +import ir.sass.domain.model.ResultModel +import ir.sass.domain.repository.MovieRepository +import javax.inject.Inject + +class DeleteMovieFromLocalUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCaseWithOnlyInput() { + override fun invoke(input: ResultModel) { + repository.deleteFromLocal(input) + } +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverMyFavoriteMoviesOfflineUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverMyFavoriteMoviesOfflineUseCase.kt new file mode 100644 index 00000000..1e2473d7 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverMyFavoriteMoviesOfflineUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.model.Domain +import ir.sass.base_domain.usecase.MotherUseCaseWithOnlyOutput +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.repository.MovieRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DiscoverMyFavoriteMoviesOfflineUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCaseWithOnlyOutput() { + override fun invoke(): Flow> = repository.discoverMoviesFromLocal() +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverPopularMovieUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverPopularMovieUseCase.kt new file mode 100644 index 00000000..a90dbf4e --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverPopularMovieUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.model.Domain +import ir.sass.base_domain.usecase.MotherUseCase +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.repository.MovieRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DiscoverPopularMovieUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCase() { + override fun invoke(page: Int): Flow> = repository.discoverPopularMovies(page) +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverTopMovieUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverTopMovieUseCase.kt new file mode 100644 index 00000000..86304c5c --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/DiscoverTopMovieUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.model.Domain +import ir.sass.base_domain.usecase.MotherUseCase +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.repository.MovieRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DiscoverTopMovieUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCase() { + override fun invoke(page : Int): Flow> = repository.discoverTopMovies(page) +} \ No newline at end of file diff --git a/features/movie/domain/src/main/java/ir/sass/domain/usecase/SaveMovieToLocalUseCase.kt b/features/movie/domain/src/main/java/ir/sass/domain/usecase/SaveMovieToLocalUseCase.kt new file mode 100644 index 00000000..dd540ab1 --- /dev/null +++ b/features/movie/domain/src/main/java/ir/sass/domain/usecase/SaveMovieToLocalUseCase.kt @@ -0,0 +1,14 @@ +package ir.sass.domain.usecase + +import ir.sass.base_domain.usecase.MotherUseCaseWithOnlyInput +import ir.sass.domain.model.ResultModel +import ir.sass.domain.repository.MovieRepository +import javax.inject.Inject + +class SaveMovieToLocalUseCase @Inject constructor( + private val repository: MovieRepository +) : MotherUseCaseWithOnlyInput() { + override fun invoke(input: ResultModel) { + repository.saveToLocal(input) + } +} \ No newline at end of file diff --git a/features/movie/ui/.gitignore b/features/movie/ui/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/features/movie/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/movie/ui/build.gradle b/features/movie/ui/build.gradle new file mode 100644 index 00000000..4036b671 --- /dev/null +++ b/features/movie/ui/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'androidx.navigation.safeargs.kotlin' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 21 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + dataBinding true + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + api project(":core:base_ui") + implementation project(":core:shared_domain") + implementation project(":features:movie:data") + implementation project(":features:movie:domain") + implementation project(":core:shared_data") + implementation lib.get("hilt") + implementation 'androidx.room:room-ktx:2.4.2' + kapt kaptLibs.get("hilt") + + testImplementation testLib.get("mockito") + testImplementation testLib.get("coroutinesTest") + testImplementation testLib.get("coreTest") + testImplementation testLib.get("turbine") + testImplementation testLib.get("junit") + testImplementation testLib.get("robolectric") + testImplementation testLib.get("hiltTest") + kaptTest katpTestLib.get("hiltTest") + debugImplementation testLib.get("fragmentTest") + testImplementation testLib.get("espresso") +} \ No newline at end of file diff --git a/features/movie/ui/consumer-rules.pro b/features/movie/ui/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/features/movie/ui/proguard-rules.pro b/features/movie/ui/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/features/movie/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/movie/ui/src/debug/AndroidManifest.xml b/features/movie/ui/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..0dae98fe --- /dev/null +++ b/features/movie/ui/src/debug/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/features/movie/ui/src/debug/java/ir/sass/movie/ui/HiltAndroidActivityTest.kt b/features/movie/ui/src/debug/java/ir/sass/movie/ui/HiltAndroidActivityTest.kt new file mode 100644 index 00000000..55a66ecd --- /dev/null +++ b/features/movie/ui/src/debug/java/ir/sass/movie/ui/HiltAndroidActivityTest.kt @@ -0,0 +1,18 @@ +package ir.sass.movie.ui + +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint +import ir.sass.navigator.flow.NavigationAction +import ir.sass.navigator.flow.features.NavcontrollerHelper + +@AndroidEntryPoint +class HiltAndroidActivityTest : AppCompatActivity(), NavcontrollerHelper { + + override fun navigate(action: NavigationAction) { + // nothing + } + + override fun onBack() { + // nothing + } +} \ No newline at end of file diff --git a/features/movie/ui/src/main/AndroidManifest.xml b/features/movie/ui/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fdf58639 --- /dev/null +++ b/features/movie/ui/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/features/movie/ui/src/main/java/ir/sass/movie/ui/di/MovieModule.kt b/features/movie/ui/src/main/java/ir/sass/movie/ui/di/MovieModule.kt new file mode 100644 index 00000000..7ddbc0fe --- /dev/null +++ b/features/movie/ui/src/main/java/ir/sass/movie/ui/di/MovieModule.kt @@ -0,0 +1,16 @@ +package ir.sass.movie.ui.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent +import ir.sass.domain.repository.MovieRepository +import ir.sass.domain.usecase.DiscoverPopularMovieUseCase + +@Module +@InstallIn(FragmentComponent::class) +object MovieModule { + @Provides + fun provideDiscoverMovieUseCase(repository: MovieRepository) = + DiscoverPopularMovieUseCase(repository) +} \ No newline at end of file diff --git a/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragment.kt b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragment.kt new file mode 100644 index 00000000..8b7fb63b --- /dev/null +++ b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragment.kt @@ -0,0 +1,48 @@ +package ir.sass.movie.ui.fragments.detail + +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import dagger.hilt.android.AndroidEntryPoint +import ir.sass.base_data.model.toReal +import ir.sass.base_ui.MotherFragment +import ir.sass.base_ui.MotherFragmentSetting +import ir.sass.base_ui.utils.toast +import ir.sass.domain.model.ResultModel +import ir.sass.movie.ui.R +import ir.sass.movie.ui.databinding.FragmentMovieDetailBinding +import ir.sass.shared_domain.MovieListType + +@AndroidEntryPoint +class MovieDetailFragment : MotherFragment( + MotherFragmentSetting( + R.layout.fragment_movie_detail, + "Detail" + ) +) { + + private val args by navArgs() + + private val viewModel: MovieDetailFragmentViewModel by viewModels() + + override fun binding() { + toReal(args.data)?.let { + dataBinding.data = it + viewModel.resultModel = it + viewModel.type = MovieListType.values()[args.type] + } + + dataBinding.viewModel = viewModel + dataBinding.lifecycleOwner = viewLifecycleOwner + + viewModel.checkItemExists() + + + coroutinesLauncher(Lifecycle.State.STARTED) { + viewModel.message.collect { + requireContext().toast(it) + } + } + } +} \ No newline at end of file diff --git a/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModel.kt b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModel.kt new file mode 100644 index 00000000..aad954a4 --- /dev/null +++ b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModel.kt @@ -0,0 +1,65 @@ +package ir.sass.movie.ui.fragments.detail + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import ir.sass.base_ui.MotherViewModel +import ir.sass.domain.model.ResultModel +import ir.sass.domain.usecase.CheckIfAMovieIsInDataBaseUseCase +import ir.sass.domain.usecase.DeleteMovieFromLocalUseCase +import ir.sass.domain.usecase.SaveMovieToLocalUseCase +import ir.sass.shared_domain.MovieListType +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MovieDetailFragmentViewModel @Inject constructor( + private val saveMovieToLocalUseCase: SaveMovieToLocalUseCase, + private val deleteMovieFromLocalUseCase: DeleteMovieFromLocalUseCase, + private val checkIfAMovieIsInDataBaseUseCase : CheckIfAMovieIsInDataBaseUseCase +) : MotherViewModel() { + + lateinit var resultModel: ResultModel + lateinit var type : MovieListType + + private val _message: MutableSharedFlow = MutableSharedFlow() + val message: SharedFlow = _message + + private val _bookmarked: MutableStateFlow = MutableStateFlow(false) + val bookmarked: StateFlow = _bookmarked + + fun checkItemExists(){ + action(checkIfAMovieIsInDataBaseUseCase(resultModel),false){ + viewModelScope.launch { + _bookmarked.emit(it) + } + } + } + + private fun saveToLocal(input: ResultModel) { + saveMovieToLocalUseCase(input) + } + + private fun deleteFromLocal(input: ResultModel) { + deleteMovieFromLocalUseCase(input) + } + + fun saveOrDelete() { + val message = if (_bookmarked.value) { + deleteFromLocal(resultModel) + "Deleted !" + } else { + saveToLocal(resultModel) + "Saved !" + } + + viewModelScope.launch { + _message.emit(message) + _bookmarked.emit(!_bookmarked.value) + } + } + +} \ No newline at end of file diff --git a/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragment.kt b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragment.kt new file mode 100644 index 00000000..67797fa0 --- /dev/null +++ b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragment.kt @@ -0,0 +1,73 @@ +package ir.sass.movie.ui.fragments.mainList + +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import ir.sass.base_data.model.toJsonString +import ir.sass.base_ui.* +import ir.sass.domain.model.ResultModel +import ir.sass.movie.ui.R +import ir.sass.movie.ui.databinding.FragmentMovieListBinding +import ir.sass.movie.ui.databinding.ItemMovieListBinding +import ir.sass.shared_domain.MovieListType + +@AndroidEntryPoint +class MovieListFragment : MotherFragment( + MotherFragmentSetting( + R.layout.fragment_movie_list, + "Movies" + ) +) { + private val viewModel: MovieListFragmentViewModel by viewModels() + + private val args by navArgs() + lateinit var type : MovieListType + + private val adapter = MotherAdapter( + RecyclerItemWrapper(R.layout.item_movie_list, + ended = { + viewModel.getMovies(type) + } + ) { binding, item, pos -> + binding.movie = item + binding.txtTitle.isSelected = true + binding.navigate = { + findNavController().navigate( + MovieListFragmentDirections.actionMovieListFragmentToMovieDetailFragment( + toJsonString(item), type.ordinal + ) + ) + } + } + ) + + override fun binding() { + type = MovieListType.values()[args.type] + connectViewModelForLoadingAndError(viewModel) + + coroutinesLauncher(Lifecycle.State.CREATED){ + viewModel.getMovies(type) + } + + + dataBinding.adapter = adapter + dataBinding.lifecycleOwner = viewLifecycleOwner + dataBinding.recyclerview.setHasFixedSize(true) + dataBinding.recyclerview.layoutManager = GridLayoutManager(requireContext(),2) + + + coroutinesLauncher(Lifecycle.State.STARTED) { + viewModel.movies.collect { + it?.let { + it.results?.let { + dataBinding.emptyState = it.isEmpty() && (adapter.itemCount == 0 || MovieListType.values()[args.type] == MovieListType.FAVORITE) + adapter.addToList(it) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModel.kt b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModel.kt new file mode 100644 index 00000000..557732a8 --- /dev/null +++ b/features/movie/ui/src/main/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModel.kt @@ -0,0 +1,75 @@ +package ir.sass.movie.ui.fragments.mainList + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import ir.sass.base_ui.MotherViewModel +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.model.ResultModel +import ir.sass.domain.usecase.DiscoverPopularMovieUseCase +import ir.sass.domain.usecase.DiscoverMyFavoriteMoviesOfflineUseCase +import ir.sass.domain.usecase.DiscoverTopMovieUseCase +import ir.sass.shared_domain.MovieListType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MovieListFragmentViewModel @Inject constructor( + private val discoverPopularMovieUseCase: DiscoverPopularMovieUseCase, + private val discoverTopMovieUseCase : DiscoverTopMovieUseCase, + private val discoverMyFavoriteMoviesOfflineUseCase: DiscoverMyFavoriteMoviesOfflineUseCase +) : MotherViewModel() { + + private val _movies: MutableStateFlow = MutableStateFlow(null) + val movies: StateFlow = _movies + + private var page = 0 + + fun getMovies(type: MovieListType) { + when(type){ + MovieListType.FAVORITE->{ + action(discoverMyFavoriteMoviesOfflineUseCase(), true) { + viewModelScope.launch { + _movies.emit(it) + } + } + } + MovieListType.TOP_RATED->{ + action(discoverTopMovieUseCase(++page), true) { + viewModelScope.launch { + val newList = mutableListOf() + _movies.value?.results?.let { + newList.addAll(it) + } + it.results?.let { + newList.addAll(it) + } + val copy = it.copy( + results = newList + ) + _movies.emit(copy) + } + } + } + MovieListType.POPULAR->{ + action(discoverPopularMovieUseCase(++page), true) { + viewModelScope.launch { + val newList = mutableListOf() + _movies.value?.results?.let { + newList.addAll(it) + } + it.results?.let { + newList.addAll(it) + } + val copy = it.copy( + results = newList + ) + _movies.emit(copy) + } + } + } + } + } + +} \ No newline at end of file diff --git a/features/movie/ui/src/main/res/layout/fragment_movie_detail.xml b/features/movie/ui/src/main/res/layout/fragment_movie_detail.xml new file mode 100644 index 00000000..9f047ff1 --- /dev/null +++ b/features/movie/ui/src/main/res/layout/fragment_movie_detail.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/movie/ui/src/main/res/layout/fragment_movie_list.xml b/features/movie/ui/src/main/res/layout/fragment_movie_list.xml new file mode 100644 index 00000000..e117c61d --- /dev/null +++ b/features/movie/ui/src/main/res/layout/fragment_movie_list.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/movie/ui/src/main/res/layout/item_movie_list.xml b/features/movie/ui/src/main/res/layout/item_movie_list.xml new file mode 100644 index 00000000..a1c50d95 --- /dev/null +++ b/features/movie/ui/src/main/res/layout/item_movie_list.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/movie/ui/src/main/res/navigation/movie_navigation.xml b/features/movie/ui/src/main/res/navigation/movie_navigation.xml new file mode 100644 index 00000000..57b10e31 --- /dev/null +++ b/features/movie/ui/src/main/res/navigation/movie_navigation.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/base/BaseViewModelTest.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/BaseViewModelTest.kt new file mode 100644 index 00000000..579f1467 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/BaseViewModelTest.kt @@ -0,0 +1,27 @@ +package ir.sass.movie.ui.base + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Rule + +open class BaseViewModelTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @ExperimentalCoroutinesApi + val testDispatcher = StandardTestDispatcher() + + @ExperimentalCoroutinesApi + protected val testScope = TestScope(testDispatcher) + + @ExperimentalCoroutinesApi + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/base/RecyclerViewMatcher.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/RecyclerViewMatcher.kt new file mode 100644 index 00000000..4f52ce58 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/RecyclerViewMatcher.kt @@ -0,0 +1,60 @@ +package ir.sass.movie.ui.base + +import android.content.res.Resources +import android.content.res.Resources.NotFoundException +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +class RecyclerViewMatcher(private val recyclerViewId: Int) { + fun atPosition(position: Int): Matcher { + return atPositionOnView(position, -1) + } + + fun atPositionOnView(position: Int, targetViewId: Int): Matcher { + return object : TypeSafeMatcher() { + var resources: Resources? = null + var childView: View? = null + override fun describeTo(description: Description) { + var idDescription = Integer.toString(recyclerViewId) + if (resources != null) { + idDescription = try { + resources!!.getResourceName(recyclerViewId) + } catch (var4: NotFoundException) { + String.format( + "%s (resource name not found)", + *arrayOf(Integer.valueOf(recyclerViewId)) + ) + } + } + description.appendText("with id: $idDescription") + } + + public override fun matchesSafely(view: View): Boolean { + resources = view.resources + if (childView == null) { + val recyclerView = view.rootView.findViewById( + recyclerViewId + ) as RecyclerView + childView = if (recyclerView != null && recyclerView.id == recyclerViewId) { + recyclerView.findViewHolderForAdapterPosition(position)!!.itemView + } else { + return false + } + } + return if (targetViewId == -1) { + view === childView + } else { + val targetView = childView!!.findViewById(targetViewId) + view === targetView + } + } + } + } +} + +fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher { + return RecyclerViewMatcher(recyclerViewId) +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/base/launchFragmentInHiltContainer.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/launchFragmentInHiltContainer.kt new file mode 100644 index 00000000..22f05cee --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/base/launchFragmentInHiltContainer.kt @@ -0,0 +1,34 @@ +package ir.sass.movie.ui.base + +import android.content.ComponentName +import android.content.Intent +import android.os.Bundle +import androidx.core.util.Preconditions +import androidx.fragment.app.Fragment +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import ir.sass.movie.ui.HiltAndroidActivityTest + +inline fun launchFragmentInHiltContainer( + fragmentArgs: Bundle? = null, + crossinline action: Fragment.() -> Unit = {} +) { + val startActivityIntent = Intent.makeMainActivity( + ComponentName( + ApplicationProvider.getApplicationContext(), + HiltAndroidActivityTest::class.java + ) + ) + ActivityScenario.launch(startActivityIntent).onActivity { activity -> + val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate( + Preconditions.checkNotNull(T::class.java.classLoader), + T::class.java.name + ) + fragment.arguments = fragmentArgs + activity.supportFragmentManager + .beginTransaction() + .add(android.R.id.content, fragment, "") + .commitNow() + fragment.action() + } +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/di/MovieTestModule.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/di/MovieTestModule.kt new file mode 100644 index 00000000..7f9b3b42 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/di/MovieTestModule.kt @@ -0,0 +1,114 @@ +package ir.sass.movie.ui.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import ir.sass.base_domain.model.Domain +import ir.sass.domain.model.DiscoverMovieModel +import ir.sass.domain.model.ResultModel +import ir.sass.domain.repository.MovieRepository +import ir.sass.domain.usecase.DiscoverPopularMovieUseCase +import ir.sass.domain.usecase.DiscoverMyFavoriteMoviesOfflineUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [MovieModule::class] +) +class MovieTestModule { + @Provides + fun provideSearchAmongSongUseCase() = + DiscoverPopularMovieUseCase(fakeRepoSuccess()) + + @Provides + fun provideDiscoverMyFavoriteMoviesOfflineUseCase() = + DiscoverMyFavoriteMoviesOfflineUseCase(fakeRepoSuccess()) +} + +fun fakeRepoSuccess() = object : MovieRepository { + + + + override fun discoverPopularMovies(page: Int): Flow> = flow { + emit( + Domain.Data( + Result.success( + DiscoverMovieModel( + 1, + listOf( + ResultModel( + true, "fake popular path", listOf(), 1, + "En", "fake popular title", "fake popular overview", 1.0, + "fake popular path", "fake popular path", "fake popular date", true,1.0, 1 + ) + ), 1, 1, 200, null, true + ) + ) + ) + ) + } + + override fun discoverTopMovies(page: Int): Flow> = flow { + emit( + Domain.Data( + Result.success( + DiscoverMovieModel( + 1, + listOf( + ResultModel( + true, "fake top path", listOf(), 1, + "En", "fake top title", "fake top overview", 1.0, + "fake top path", "fake path", "fake top date", true,1.0, 1 + ) + ), 1, 1, 200, null, true + ) + ) + ) + ) + } + + override fun discoverMoviesFromLocal(): Flow> = flow { + emit( + Domain.Data( + Result.success( + DiscoverMovieModel( + 1, + listOf( + ResultModel( + true, + "fake db path", + listOf(), + 1, + "En", + "fake db title", + "fake db overview", + 1.0, + "fake db path", + "fake db path", + "fake db date", + true, + 1.0, + 1 + ) + ), 1, 1, 200, null, true + ) + ) + ) + ) + } + + override fun saveToLocal(model: ResultModel) { + // just make sure this method is called + } + + override fun deleteFromLocal(model: ResultModel) { + // just make sure this method is called + } + + override fun isExistOnDb(model: ResultModel): Flow> = flow { + emit(Domain.Data(Result.success(true))) + } +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentTest.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentTest.kt new file mode 100644 index 00000000..2bc3c3f0 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentTest.kt @@ -0,0 +1,163 @@ +package ir.sass.movie.ui.fragments.detail + +import android.os.Build +import androidx.core.os.bundleOf +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.* +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import ir.sass.base_data.model.toJsonString +import ir.sass.domain.model.ResultModel +import ir.sass.movie.ui.R +import ir.sass.movie.ui.base.launchFragmentInHiltContainer +import ir.sass.shared_domain.MovieListType +import org.hamcrest.Matchers.not +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +@Config( + manifest = Config.NONE, + sdk = [Build.VERSION_CODES.P], + application = HiltTestApplication::class, + instrumentedPackages = [ + // required to access final members on androidx.loader.content.ModernAsyncTask + "androidx.loader.content" + ]) +class MovieDetailFragmentTest { + @get:Rule + val hiltAndroidRule = HiltAndroidRule(this) + + @Test + fun `every element in UI must have value in TOP_RATED mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModelWhenItsForJustAdults() + )) + putInt("type",MovieListType.TOP_RATED.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + assertionHandlerForEveryElementInUIHasValue() + } + } + } + + @Test + fun `every element in UI must have value in FAVORITE mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModelWhenItsForJustAdults() + )) + putInt("type",MovieListType.FAVORITE.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + assertionHandlerForEveryElementInUIHasValue() + } + } + } + + @Test + fun `every element in UI must have value in POPLUAR mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModelWhenItsForJustAdults() + )) + putInt("type",MovieListType.POPULAR.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + assertionHandlerForEveryElementInUIHasValue() + } + } + } + + + private fun assertionHandlerForEveryElementInUIHasValue(){ + onView(withId(R.id.txt_title)).check(matches(withText("fake title"))) + onView(withId(R.id.txt_date)).check(matches(withText("fake date"))) + onView(withId(R.id.txt_lang)).check(matches(withText("En"))) + onView(withId(R.id.txt_adult)).check(matches(withText("No"))) + onView(withId(R.id.txt_vote_count)).check(matches(withText("1"))) + onView(withId(R.id.txt_overview)).check(matches(withText("fake overview"))) + } + + @Test + fun `check the proper text is shown when adult flag is false in FAVORITE mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModel() + )) + putInt("type",MovieListType.TOP_RATED.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + onView(withId(R.id.txt_adult)).check(matches(withText("Yes"))) + } + } + } + + @Test + fun `check the proper text is shown when adult flag is false in POPULAR mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModel() + )) + putInt("type",MovieListType.POPULAR.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + onView(withId(R.id.txt_adult)).check(matches(withText("Yes"))) + } + } + } + + @Test + fun `check the proper text is shown when adult flag is false in TOP_RATED mode`(){ + launchFragmentInHiltContainer( + bundleOf().apply { + putString("data", toJsonString( + getStubModel() + )) + putInt("type",MovieListType.TOP_RATED.ordinal) + } + ){ + (this as MovieDetailFragment).apply { + onView(withId(R.id.txt_adult)).check(matches(withText("Yes"))) + } + } + } + + + + private fun getStubModelWhenItsForJustAdults() = ResultModel( + true,"fake path", listOf(),1, + "En","fake title","fake overview",1.0, + "fake path","fake date","fake title",false,1.0, + 1 + ) + + private fun getStubModel() = ResultModel( + false,"fake path", listOf(),1, + "En","fake title","fake overview",1.0, + "fake path","fake date","fake title",false,1.0, + 1 + ) +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModelTest.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModelTest.kt new file mode 100644 index 00000000..285cc4a8 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/detail/MovieDetailFragmentViewModelTest.kt @@ -0,0 +1,70 @@ +package ir.sass.movie.ui.fragments.detail + +import app.cash.turbine.test +import ir.sass.domain.model.ResultModel +import ir.sass.domain.usecase.CheckIfAMovieIsInDataBaseUseCase +import ir.sass.domain.usecase.DeleteMovieFromLocalUseCase +import ir.sass.domain.usecase.SaveMovieToLocalUseCase +import ir.sass.movie.ui.base.BaseViewModelTest +import ir.sass.movie.ui.di.fakeRepoSuccess +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MovieDetailFragmentViewModelTest : BaseViewModelTest() { + + @ExperimentalCoroutinesApi + @Test + fun `check if call checkItemExists then _bookmarked must emit true, at first it must be false`() = testScope.runTest{ + val viewModel = provideViewModel() + viewModel.checkItemExists() + var helper = "First" + viewModel.bookmarked.test { + val response = this.awaitItem() + if(helper == "First"){ + helper = "Not First" + assert(!response) + } + else + assert(response) + } + } + + @ExperimentalCoroutinesApi + @Test + fun `check if call saveOrDelete movie then _message must emit Saved ! after that if call that again the message must be Deleted !`() = testScope.runTest{ + val viewModel = provideViewModel() + delay(5) + viewModel.saveOrDelete() + var helper = "First" + viewModel.message.test { + val response = this.awaitItem() + if(helper == "First"){ + assert(response == "Saved !") + helper = "Not First" + viewModel.saveOrDelete() + }else + assert(response == "Deleted !") + } + } + + + private fun provideViewModel() : MovieDetailFragmentViewModel{ + val saveMovieToLocalUseCase: SaveMovieToLocalUseCase = + SaveMovieToLocalUseCase(fakeRepoSuccess()) + val deleteMovieFromLocalUseCase: DeleteMovieFromLocalUseCase = + DeleteMovieFromLocalUseCase(fakeRepoSuccess()) + val checkIfAMovieIsInDataBaseUseCase : CheckIfAMovieIsInDataBaseUseCase = + CheckIfAMovieIsInDataBaseUseCase(fakeRepoSuccess()) + return MovieDetailFragmentViewModel(saveMovieToLocalUseCase, + deleteMovieFromLocalUseCase,checkIfAMovieIsInDataBaseUseCase).apply { + resultModel = ResultModel( + true,"fake path", listOf(),1,"En","fake title", + "fake overview",1.0,"fake path", + "fake date","fake title",false,1.0,1 + ) + } + } + +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentTest.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentTest.kt new file mode 100644 index 00000000..7f449f0c --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentTest.kt @@ -0,0 +1,109 @@ +package ir.sass.movie.ui.fragments.mainList + +import android.os.Build +import androidx.core.os.bundleOf +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import ir.sass.movie.ui.R +import ir.sass.movie.ui.base.launchFragmentInHiltContainer +import ir.sass.movie.ui.base.withRecyclerView +import ir.sass.shared_domain.MovieListType +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +@Config( + manifest = Config.NONE, + sdk = [Build.VERSION_CODES.P], + application = HiltTestApplication::class, + instrumentedPackages = [ + // required to access final members on androidx.loader.content.ModernAsyncTask + "androidx.loader.content" + ] +) +class MovieListFragmentTest { + @get:Rule + val hiltAndroidRule = HiltAndroidRule(this) + + + /* actually if the items have value in list then we can be sure the list has item too + so we don't need two write two test for that*/ + @Test + fun `in online popular mode test if all text in list has values and make sure the list has items`() { + launchFragmentInHiltContainer( + bundleOf().apply { + putInt("type", MovieListType.POPULAR.ordinal) + } + ) { + (this as MovieListFragment).apply { + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_title) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Title : fake popular title"))) + + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_date) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Release date : fake popular date"))) + } + } + } + + @Test + fun `in online top rated mode test if all text in list has values and make sure the list has items`() { + launchFragmentInHiltContainer( + bundleOf().apply { + putInt("type", MovieListType.TOP_RATED.ordinal) + } + ) { + (this as MovieListFragment).apply { + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_title) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Title : fake top title"))) + + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_date) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Release date : fake top date"))) + } + } + } + + @Test + fun `in offline mode test if all text in list has values and make sure the list has items`() { + launchFragmentInHiltContainer( + bundleOf().apply { + putInt("type", MovieListType.FAVORITE.ordinal) + } + ) { + (this as MovieListFragment).apply { + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_title) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Title : fake db title"))) + + onView( + withRecyclerView(R.id.recyclerview) + .atPositionOnView(0, R.id.txt_date) + ) + .check(ViewAssertions.matches(ViewMatchers.withText("Release date : fake db date"))) + } + } + } + +} \ No newline at end of file diff --git a/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModelTest.kt b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModelTest.kt new file mode 100644 index 00000000..aeaac463 --- /dev/null +++ b/features/movie/ui/src/test/java/ir/sass/movie/ui/fragments/mainList/MovieListFragmentViewModelTest.kt @@ -0,0 +1,67 @@ +package ir.sass.movie.ui.fragments.mainList + +import app.cash.turbine.test +import com.nhaarman.mockitokotlin2.mock +import ir.sass.domain.usecase.DiscoverPopularMovieUseCase +import ir.sass.domain.usecase.DiscoverMyFavoriteMoviesOfflineUseCase +import ir.sass.domain.usecase.DiscoverTopMovieUseCase +import ir.sass.movie.ui.base.BaseViewModelTest +import ir.sass.movie.ui.di.fakeRepoSuccess +import ir.sass.shared_domain.MovieListType +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MovieListFragmentViewModelTest : BaseViewModelTest() { + + @ExperimentalCoroutinesApi + @Test + fun `in online popular mode check if calling getMovies() method would get data from usecase`() = + testScope.runTest { + val discoverPopularMovieUseCase = DiscoverPopularMovieUseCase(fakeRepoSuccess()) + val discoverTopMovieUseCase : DiscoverTopMovieUseCase = mock() + val discoverMyFavoriteMoviesOfflineUseCase : DiscoverMyFavoriteMoviesOfflineUseCase = mock() + val viewModel = MovieListFragmentViewModel(discoverPopularMovieUseCase, + discoverTopMovieUseCase,discoverMyFavoriteMoviesOfflineUseCase) + viewModel.getMovies(MovieListType.POPULAR) + viewModel.movies.test { + delay(1) + assert(expectMostRecentItem()!!.results!![0].title == "fake popular title") + } + } + + @ExperimentalCoroutinesApi + @Test + fun `in online top rated mode check if calling getMovies() method would get data from usecase`() = + testScope.runTest { + val discoverPopularMovieUseCase : DiscoverPopularMovieUseCase = mock() + val discoverTopMovieUseCase = DiscoverTopMovieUseCase(fakeRepoSuccess()) + val discoverMyFavoriteMoviesOfflineUseCase : DiscoverMyFavoriteMoviesOfflineUseCase = mock() + val viewModel = MovieListFragmentViewModel(discoverPopularMovieUseCase, + discoverTopMovieUseCase,discoverMyFavoriteMoviesOfflineUseCase) + viewModel.getMovies(MovieListType.TOP_RATED) + viewModel.movies.test { + delay(1) + assert(expectMostRecentItem()!!.results!![0].title == "fake top title") + } + } + + @ExperimentalCoroutinesApi + @Test + fun `in offline mode check if calling getMovies() method would get data from usecase`() = + testScope.runTest { + val discoverPopularMovieUseCase : DiscoverPopularMovieUseCase = mock() + val discoverTopMovieUseCase : DiscoverTopMovieUseCase = mock() + val discoverMyFavoriteMoviesOfflineUseCase = DiscoverMyFavoriteMoviesOfflineUseCase(fakeRepoSuccess()) + val viewModel = MovieListFragmentViewModel(discoverPopularMovieUseCase, + discoverTopMovieUseCase,discoverMyFavoriteMoviesOfflineUseCase) + viewModel.getMovies(MovieListType.FAVORITE) + viewModel.movies.test { + delay(1) + assert(expectMostRecentItem()!!.results!![0].title == "fake db title") + } + } + + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..cd0519bb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e77fd91a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri May 20 18:43:00 IRDT 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample.jpg b/sample.jpg new file mode 100644 index 00000000..43394151 Binary files /dev/null and b/sample.jpg differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..71ecb155 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "daresay" +include ':app' +include ':core' +include ':core:base_ui' +include ':core:base_data' +include ':core:base_domain' +include ':core:navigator' +include ':features' +include ':features:home' +include ':features:movie' +include ':features:movie:domain' +include ':features:movie:data' +include ':features:movie:ui' +include ':features:home:ui' +include ':core:shared_data' +include ':core:shared_domain'