diff --git a/README.md b/README.md
index 2fb9bcee..ca686de8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,21 @@
-# demo-android
-Demo app
+# REES46 Demo Android
+
+## Description
+
+REES46 Demo Android - application to demonstrate working with SDK.
+
+## Configure
+
+Versions:
+Java 22
+Kotlin 2.0.0
+Gradle 8.8
+Android Gradle Plugin 8.5.1
+
+Copy `google-services.json` file from [Firebase console](https://console.firebase.google.com/u/0/) to app module.
+
+## Documentation
+
+For detailed information on methods used from the SDK, please refer to the documentation available at the following link:
+
+[Official API references](https://reference.api.rees46.com/#introduction)
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..1ecc7546
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,59 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ id("com.google.gms.google-services")
+ id("androidx.navigation.safeargs.kotlin")
+}
+
+android {
+ namespace = "rees46.demo_android.app"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "rees46.demo_android"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_22
+ targetCompatibility = JavaVersion.VERSION_22
+ }
+ kotlinOptions {
+ jvmTarget = "22"
+ }
+ buildFeatures {
+ viewBinding = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.compat)
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.androidx.core.splashscreen)
+ implementation(project(":core"))
+ implementation(project(":feature"))
+ implementation(project(":navigation"))
+ implementation(project(":sdkRees46"))
+}
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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..44dac0a2
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/rees46/demo_android/app/DemoApplication.kt b/app/src/main/java/rees46/demo_android/app/DemoApplication.kt
new file mode 100644
index 00000000..601b6f94
--- /dev/null
+++ b/app/src/main/java/rees46/demo_android/app/DemoApplication.kt
@@ -0,0 +1,59 @@
+package rees46.demo_android.app
+
+import android.app.Application
+import com.personalizatio.SDK
+import org.koin.android.ext.android.getKoin
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
+import rees46.demo_android.app.di.navigatorModule
+import rees46.demo_android.core.di.sdkModule
+import rees46.demo_android.core.utils.SdkUtils
+import rees46.demo_android.feature.cart.di.cartModule
+import rees46.demo_android.feature.category.di.categoryModule
+import rees46.demo_android.feature.home.di.homeModule
+import rees46.demo_android.feature.productDetails.di.productDetailsModule
+import rees46.demo_android.feature.products.di.productsModule
+import rees46.demo_android.feature.recommendationBlock.di.recommendationBlockModule
+import rees46.demo_android.feature.search.di.searchModule
+import rees46.demo_android.feature.settings.di.settingsModule
+
+class DemoApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ startKoin {
+ androidContext(this@DemoApplication)
+
+ modules(
+ listOf(
+ navigatorModule,
+ sdkModule,
+ homeModule,
+ recommendationBlockModule,
+ cartModule,
+ categoryModule,
+ productDetailsModule,
+ productsModule,
+ settingsModule,
+ searchModule
+ )
+ )
+ }
+
+ initializeSdk()
+ }
+
+ private fun initializeSdk() {
+ val sdk = getKoin().get()
+ SdkUtils.initialize(
+ sdk = sdk,
+ context = this@DemoApplication,
+ shopId = SHOP_ID
+ )
+ }
+
+ companion object {
+ private const val SHOP_ID = "357382bf66ac0ce2f1722677c59511"
+ }
+}
diff --git a/app/src/main/java/rees46/demo_android/app/di/NavigatorModule.kt b/app/src/main/java/rees46/demo_android/app/di/NavigatorModule.kt
new file mode 100644
index 00000000..10106aa9
--- /dev/null
+++ b/app/src/main/java/rees46/demo_android/app/di/NavigatorModule.kt
@@ -0,0 +1,15 @@
+package rees46.demo_android.app.di
+
+import androidx.navigation.NavController
+import com.rees46.demo_android.navigation.Navigator
+import org.koin.dsl.module
+import rees46.demo_android.app.navigation.AppNavigator
+
+val navigatorModule = module {
+ single {
+ (navController: NavController) ->
+ AppNavigator(
+ navController = navController
+ )
+ }
+}
diff --git a/app/src/main/java/rees46/demo_android/app/navigation/AppNavigator.kt b/app/src/main/java/rees46/demo_android/app/navigation/AppNavigator.kt
new file mode 100644
index 00000000..aa6b16fa
--- /dev/null
+++ b/app/src/main/java/rees46/demo_android/app/navigation/AppNavigator.kt
@@ -0,0 +1,53 @@
+package rees46.demo_android.app.navigation
+
+import android.os.Bundle
+import androidx.annotation.IdRes
+import androidx.core.os.bundleOf
+import androidx.navigation.NavController
+import com.rees46.demo_android.navigation.Destination
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.ProductDetails
+import com.rees46.demo_android.navigation.ProductsDetails
+import rees46.demo_android.app.R
+import rees46.demo_android.core.settings.NavigationSettings
+
+class AppNavigator(private val navController: NavController) : Navigator {
+
+ override fun navigate(destination: Destination) {
+ when(destination) {
+ is ProductDetails -> {
+ val bundle = bundleOf(NavigationSettings.PRODUCT_ARGUMENT_FIELD to destination.navigationProduct)
+ navigate(
+ resId = R.id.productDetailsFragment,
+ args = bundle
+ )
+ }
+ is ProductsDetails -> {
+ val bundle = bundleOf(NavigationSettings.PRODUCTS_ARGUMENT_FIELD to destination.navigationProducts)
+ navigate(
+ resId = R.id.productsFragment,
+ args = bundle
+ )
+ }
+ else -> {}
+ }
+ }
+
+ override fun navigate(id: Int) {
+ navController.navigate(id)
+ }
+
+ override fun popBackStack() {
+ navController.popBackStack()
+ }
+
+ override fun getCurrentDestination() : Int? =
+ navController.currentDestination?.id
+
+ private fun navigate(@IdRes resId: Int, args: Bundle?) {
+ navController.navigate(
+ resId = resId,
+ args = args
+ )
+ }
+}
diff --git a/app/src/main/java/rees46/demo_android/app/presentation/MainActivity.kt b/app/src/main/java/rees46/demo_android/app/presentation/MainActivity.kt
new file mode 100644
index 00000000..0f21d010
--- /dev/null
+++ b/app/src/main/java/rees46/demo_android/app/presentation/MainActivity.kt
@@ -0,0 +1,114 @@
+package rees46.demo_android.app.presentation
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.MenuProvider
+import androidx.core.view.isVisible
+import androidx.lifecycle.LifecycleOwner
+import androidx.navigation.fragment.NavHostFragment
+import org.koin.android.ext.android.get
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.app.R
+import rees46.demo_android.app.databinding.ActivityMainBinding
+import com.rees46.demo_android.navigation.Navigator
+
+class MainActivity : AppCompatActivity(), LifecycleOwner {
+
+ private lateinit var binding: ActivityMainBinding
+
+ private val navigator by lazy {
+ get {
+ val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ parametersOf(navHostFragment.navController)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.getRoot())
+
+ setupTopAppBar()
+ setupBottomNavigationView()
+ setupPopBackStack()
+ createOptionMenu()
+ }
+
+ private fun setupTopAppBar() {
+ setSupportActionBar(binding.topAppBar)
+ }
+
+ private fun setupBottomNavigationView() {
+ binding.bottomNavigation.setOnItemSelectedListener {
+ when (it.itemId) {
+ R.id.home -> switchBottomTab(R.id.homeFragment)
+ R.id.category -> switchBottomTab(R.id.categoryFragment)
+ R.id.cart -> switchBottomTab(R.id.cartFragment)
+ R.id.settings -> switchBottomTab(R.id.settingsFragment)
+ }
+
+ true
+ }
+ }
+
+ private fun switchBottomTab(id: Int) {
+ if(navigator.getCurrentDestination() == id) return
+
+ navigator.navigate(id)
+ }
+
+ private fun setupPopBackStack() {
+ onBackPressedDispatcher.addCallback(
+ owner = this,
+ onBackPressedCallback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ navigator.popBackStack()
+
+ changeSelectedBottomItem()
+ }
+ }
+ )
+ }
+
+ private fun changeSelectedBottomItem() {
+ with(binding.bottomNavigation) {
+ when (navigator.getCurrentDestination()) {
+ R.id.homeFragment -> selectedItemId = R.id.home
+ R.id.categoryFragment -> selectedItemId = R.id.category
+ R.id.cartFragment -> selectedItemId = R.id.cart
+ R.id.settingsFragment -> selectedItemId = R.id.settingsFragment
+ }
+ }
+ }
+
+
+ private fun createOptionMenu() {
+ addMenuProvider(object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.main_top_app_bar_menu, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return when (menuItem.itemId) {
+ R.id.menu_top_app_cart -> {
+ binding.bottomNavigation.selectedItemId = R.id.cart
+ true
+ }
+ R.id.menu_top_app_search -> {
+ supportActionBar?.hide()
+ binding.bottomNavigation.isVisible = false
+ navigator.navigate(R.id.searchFragment)
+ true
+ }
+ else -> false
+ }
+ }
+ })
+ }
+}
diff --git a/app/src/main/res/drawable/ic_app.xml b/app/src/main/res/drawable/ic_app.xml
new file mode 100644
index 00000000..35af2543
--- /dev/null
+++ b/app/src/main/res/drawable/ic_app.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launch_screen.xml b/app/src/main/res/drawable/ic_launch_screen.xml
new file mode 100644
index 00000000..ba29ada5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launch_screen.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml
new file mode 100644
index 00000000..fa39383e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_cart.xml b/app/src/main/res/drawable/ic_navigation_cart.xml
new file mode 100644
index 00000000..4f0d4fb6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_cart.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_category.xml b/app/src/main/res/drawable/ic_navigation_category.xml
new file mode 100644
index 00000000..fe89eecd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_category.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_home.xml b/app/src/main/res/drawable/ic_navigation_home.xml
new file mode 100644
index 00000000..bedcc6ed
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_home.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_settings.xml b/app/src/main/res/drawable/ic_navigation_settings.xml
new file mode 100644
index 00000000..b9b72634
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_settings.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_top_bar_cart.xml b/app/src/main/res/drawable/ic_top_bar_cart.xml
new file mode 100644
index 00000000..50b6132f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_top_bar_cart.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_top_bar_search.xml b/app/src/main/res/drawable/ic_top_bar_search.xml
new file mode 100644
index 00000000..4e849953
--- /dev/null
+++ b/app/src/main/res/drawable/ic_top_bar_search.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_top_bar_settings.xml b/app/src/main/res/drawable/ic_top_bar_settings.xml
new file mode 100644
index 00000000..bd006da1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_top_bar_settings.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/font/inter.xml b/app/src/main/res/font/inter.xml
new file mode 100644
index 00000000..b2b0965a
--- /dev/null
+++ b/app/src/main/res/font/inter.xml
@@ -0,0 +1,7 @@
+
+
+
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..ea98bc47
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/main_navigation_menu.xml b/app/src/main/res/menu/main_navigation_menu.xml
new file mode 100644
index 00000000..78d78485
--- /dev/null
+++ b/app/src/main/res/menu/main_navigation_menu.xml
@@ -0,0 +1,22 @@
+
diff --git a/app/src/main/res/menu/main_top_app_bar_menu.xml b/app/src/main/res/menu/main_top_app_bar_menu.xml
new file mode 100644
index 00000000..113a11db
--- /dev/null
+++ b/app/src/main/res/menu/main_top_app_bar_menu.xml
@@ -0,0 +1,15 @@
+
diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml
new file mode 100644
index 00000000..bff9d8ce
--- /dev/null
+++ b/app/src/main/res/navigation/navigation.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/font_certs.xml b/app/src/main/res/values/font_certs.xml
new file mode 100644
index 00000000..d2226ac0
--- /dev/null
+++ b/app/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/app/src/main/res/values/preloaded_fonts.xml b/app/src/main/res/values/preloaded_fonts.xml
new file mode 100644
index 00000000..b093c8c0
--- /dev/null
+++ b/app/src/main/res/values/preloaded_fonts.xml
@@ -0,0 +1,6 @@
+
+
+
+ - @font/inter
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..13a3b1c0
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ DEMO STORE
+
+ home
+ category
+ cart
+ settings
+
+ cart
+ search
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..831e67a2
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..52775fb6
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,16 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+ alias(libs.plugins.android.library) apply false
+}
+
+buildscript {
+ repositories {
+ google()
+ }
+ dependencies {
+ classpath(libs.google.services)
+ classpath(libs.androidx.navigation.safe.args.gradle.plugin)
+ }
+}
\ No newline at end of file
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
new file mode 100644
index 00000000..9ab1d5ee
--- /dev/null
+++ b/core/build.gradle.kts
@@ -0,0 +1,47 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "rees46.demo_android.core"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_22
+ targetCompatibility = JavaVersion.VERSION_22
+ }
+ kotlinOptions {
+ jvmTarget = "22"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.material)
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.compat)
+ implementation(project(":sdkRees46"))
+}
diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/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/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8bdb7e14
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/core/src/main/java/rees46/demo_android/core/di/SdkModule.kt b/core/src/main/java/rees46/demo_android/core/di/SdkModule.kt
new file mode 100644
index 00000000..706ea517
--- /dev/null
+++ b/core/src/main/java/rees46/demo_android/core/di/SdkModule.kt
@@ -0,0 +1,10 @@
+package rees46.demo_android.core.di
+
+import com.personalizatio.SDK
+import org.koin.dsl.module
+
+var sdkModule = module {
+ single {
+ SDK()
+ }
+}
diff --git a/core/src/main/java/rees46/demo_android/core/settings/NavigationSettings.kt b/core/src/main/java/rees46/demo_android/core/settings/NavigationSettings.kt
new file mode 100644
index 00000000..40f8a750
--- /dev/null
+++ b/core/src/main/java/rees46/demo_android/core/settings/NavigationSettings.kt
@@ -0,0 +1,6 @@
+package rees46.demo_android.core.settings
+
+object NavigationSettings {
+ const val PRODUCT_ARGUMENT_FIELD = "product"
+ const val PRODUCTS_ARGUMENT_FIELD = "products"
+}
diff --git a/core/src/main/java/rees46/demo_android/core/settings/RecommendationSettings.kt b/core/src/main/java/rees46/demo_android/core/settings/RecommendationSettings.kt
new file mode 100644
index 00000000..d875fbf6
--- /dev/null
+++ b/core/src/main/java/rees46/demo_android/core/settings/RecommendationSettings.kt
@@ -0,0 +1,7 @@
+package rees46.demo_android.core.settings
+
+object RecommendationSettings {
+ const val SIMPLE_RECOMMENDED_CODE = "a043dbc2f852ffe18861a2cdfc364ef2"
+ const val CART_RECOMMENDED_CODE = "2dbebc39bee259b118bcc0ac3fa74a42"
+ const val ALSO_LIKE_RECOMMENDED_CODE = "a043dbc2f852ffe18861a2cdfc364ef2"
+}
diff --git a/core/src/main/java/rees46/demo_android/core/settings/SdkSettings.kt b/core/src/main/java/rees46/demo_android/core/settings/SdkSettings.kt
new file mode 100644
index 00000000..06deba3c
--- /dev/null
+++ b/core/src/main/java/rees46/demo_android/core/settings/SdkSettings.kt
@@ -0,0 +1,10 @@
+package rees46.demo_android.core.settings
+
+object SdkSettings {
+ const val API_URL = "https://api.rees46.ru/"
+ const val PREFERENCES_KEY = "demo android"
+ const val TAG = "DEMO TAG"
+ const val STREAM = "android"
+ const val NOTIFICATION_TYPE = "DEMO NOTIFICATION TYPE"
+ const val NOTIFICATION_ID = "DEMO NOTIFICATION ID"
+}
diff --git a/core/src/main/java/rees46/demo_android/core/utils/Sdk.utils.kt b/core/src/main/java/rees46/demo_android/core/utils/Sdk.utils.kt
new file mode 100644
index 00000000..35165b31
--- /dev/null
+++ b/core/src/main/java/rees46/demo_android/core/utils/Sdk.utils.kt
@@ -0,0 +1,45 @@
+package rees46.demo_android.core.utils
+
+import android.content.Context
+import com.personalizatio.SDK
+import com.personalizatio.api.OnApiCallbackListener
+import org.json.JSONObject
+import rees46.demo_android.core.settings.SdkSettings
+
+object SdkUtils {
+
+ fun initialize(
+ sdk: SDK,
+ context: Context,
+ shopId: String,
+ ) {
+ sdk.initialize(
+ context = context,
+ shopId = shopId,
+ apiUrl = SdkSettings.API_URL,
+ preferencesKey = SdkSettings.PREFERENCES_KEY,
+ tag = SdkSettings.TAG,
+ stream = SdkSettings.STREAM,
+ notificationType = SdkSettings.NOTIFICATION_TYPE,
+ notificationId = SdkSettings.NOTIFICATION_ID
+ )
+ }
+
+ fun createOnApiCallbackListener(onSuccess: () -> Unit): OnApiCallbackListener {
+ return object : OnApiCallbackListener() {
+ override fun onSuccess(response: JSONObject?) {
+ if(isResponseSuccess(response)) {
+ onSuccess()
+ }
+ }
+ }
+ }
+
+ private fun isResponseSuccess(response: JSONObject?): Boolean {
+ return response != null
+ && response.get(STATUS_RESPONSE_FIELD) == SUCCESS_RESPONSE_VALUE
+ }
+
+ private const val STATUS_RESPONSE_FIELD = "status"
+ private const val SUCCESS_RESPONSE_VALUE = "success"
+}
diff --git a/feature/.gitignore b/feature/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/build.gradle.kts b/feature/build.gradle.kts
new file mode 100644
index 00000000..84c3215b
--- /dev/null
+++ b/feature/build.gradle.kts
@@ -0,0 +1,64 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ id("androidx.navigation.safeargs.kotlin")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "rees46.demo_android"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_22
+ targetCompatibility = JavaVersion.VERSION_22
+ }
+ kotlinOptions {
+ jvmTarget = "22"
+ }
+ buildFeatures {
+ viewBinding = true
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.ui)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.androidx.legacy.support.v4)
+ implementation(libs.androidx.lifecycle.livedata.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.fragment.ktx)
+ implementation(libs.material)
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.compat)
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.messaging)
+ implementation(libs.gson)
+ implementation(libs.glide)
+ implementation(project(":sdkRees46"))
+ implementation(project(":core"))
+ implementation(project(":ui"))
+ implementation(project(":navigation"))
+}
diff --git a/feature/proguard-rules.pro b/feature/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/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/feature/src/main/AndroidManifest.xml b/feature/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e1000761
--- /dev/null
+++ b/feature/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/data/api/CartApi.kt b/feature/src/main/java/rees46/demo_android/feature/cart/data/api/CartApi.kt
new file mode 100644
index 00000000..955f435f
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/data/api/CartApi.kt
@@ -0,0 +1,41 @@
+package rees46.demo_android.feature.cart.data.api
+
+import com.personalizatio.Params
+import com.personalizatio.SDK
+import com.personalizatio.api.OnApiCallbackListener
+import com.personalizatio.api.params.ProductItemParams
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class CartApi(
+ private val sdk: SDK
+) {
+
+ fun addProduct(
+ product: Product,
+ quantity: Int,
+ listener: OnApiCallbackListener
+ ) {
+ val productItemParams = ProductItemParams(product.id)
+ .set(
+ column = ProductItemParams.PARAMETER.AMOUNT,
+ value = quantity
+ )
+
+ sdk.trackEventManager.track(
+ event = Params.TrackEvent.VIEW,
+ params = Params().put(productItemParams),
+ listener = listener
+ )
+ }
+
+ fun removeProduct(
+ productId: String,
+ listener: OnApiCallbackListener
+ ) {
+ sdk.trackEventManager.track(
+ event = Params.TrackEvent.REMOVE_FROM_CART,
+ params = Params().put(ProductItemParams(productId)),
+ listener = listener
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/data/models/Cart.kt b/feature/src/main/java/rees46/demo_android/feature/cart/data/models/Cart.kt
new file mode 100644
index 00000000..3199a32e
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/data/models/Cart.kt
@@ -0,0 +1,67 @@
+package rees46.demo_android.feature.cart.data.models
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+// TODO: removed after implementation getting cart in sdk
+class Cart {
+
+ val cartProductsFlow: MutableStateFlow> =
+ MutableStateFlow(mutableListOf())
+
+ var cartSumPrice: MutableStateFlow = MutableStateFlow(0.0)
+
+ fun getCartProduct(productId: String): CartProduct? {
+ return cartProductsFlow.value.find { product ->
+ product.product.id == productId
+ }
+ }
+
+ fun addProduct(
+ product: Product,
+ quantity: Int
+ ) {
+ val cartProduct = getCartProduct(product.id)
+ if (cartProduct == null) {
+ cartProductsFlow.update {
+ it.toMutableList().apply {
+ add(
+ CartProduct(
+ product = product,
+ quantity = quantity
+ )
+ )
+ }
+ }
+ } else {
+ cartProduct.quantity += quantity
+ }
+
+ updateCartSumPrice()
+ }
+
+ fun removeProduct(productId: String) {
+ cartProductsFlow.update {
+ it.toMutableList().apply {
+ removeIf { product ->
+ product.product.id == productId
+ }
+ }
+ }
+
+ updateCartSumPrice()
+ }
+
+ private fun updateCartSumPrice() {
+ cartSumPrice.update {
+ cartProductsFlow.value.sumOf { cartProduct ->
+ getCartProductPrice(cartProduct)
+ }
+ }
+ }
+
+ private fun getCartProductPrice(cartProduct: CartProduct) =
+ (cartProduct.product.price ?: 0.0) * cartProduct.quantity
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/data/repository/CartRepositoryImpl.kt b/feature/src/main/java/rees46/demo_android/feature/cart/data/repository/CartRepositoryImpl.kt
new file mode 100644
index 00000000..1fe3ea65
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/data/repository/CartRepositoryImpl.kt
@@ -0,0 +1,53 @@
+package rees46.demo_android.feature.cart.data.repository
+
+import kotlinx.coroutines.flow.StateFlow
+import rees46.demo_android.core.utils.SdkUtils
+import rees46.demo_android.feature.cart.data.api.CartApi
+import rees46.demo_android.feature.cart.data.models.Cart
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class CartRepositoryImpl(
+ private val cartApi: CartApi,
+ private val cart: Cart
+) : CartRepository {
+
+ override fun getCartProducts(): StateFlow> =
+ cart.cartProductsFlow
+
+ override fun getCartProduct(productId: String): CartProduct? =
+ cart.getCartProduct(productId)
+
+ override fun getSumPrice(): StateFlow =
+ cart.cartSumPrice
+
+ override fun addProduct(
+ product: Product,
+ quantity: Int
+ ) {
+ cartApi.addProduct(
+ product = product,
+ quantity = quantity,
+ listener = SdkUtils.createOnApiCallbackListener(
+ onSuccess = {
+ cart.addProduct(
+ product = product,
+ quantity = quantity
+ )
+ }
+ )
+ )
+ }
+
+ override fun removeProduct(productId: String) {
+ cartApi.removeProduct(
+ productId = productId,
+ listener = SdkUtils.createOnApiCallbackListener(
+ onSuccess = {
+ cart.removeProduct(productId)
+ }
+ )
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/di/CartModule.kt b/feature/src/main/java/rees46/demo_android/feature/cart/di/CartModule.kt
new file mode 100644
index 00000000..9a85f22d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/di/CartModule.kt
@@ -0,0 +1,59 @@
+package rees46.demo_android.feature.cart.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.feature.cart.data.api.CartApi
+import rees46.demo_android.feature.cart.data.models.Cart
+import rees46.demo_android.feature.cart.data.repository.CartRepositoryImpl
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+import rees46.demo_android.feature.cart.domain.usecase.GetCartProductsUseCase
+import rees46.demo_android.feature.cart.domain.usecase.GetCartSumPriceUseCase
+import rees46.demo_android.feature.cart.domain.usecase.RemoveProductFromCartUseCase
+import rees46.demo_android.feature.cart.presentation.mappers.CartProductItemMapper
+import rees46.demo_android.feature.cart.presentation.viewmodel.CartViewModel
+import kotlin.math.sin
+
+val cartModule = module {
+ viewModel {
+ CartViewModel(
+ getCartProductsUseCase = get(),
+ removeProductFromCartUseCase = get(),
+ getCartSumPriceUseCase = get(),
+ getRecommendationUseCase = get()
+ )
+ }
+ single {
+ CartProductItemMapper(
+ productItemMapper = get()
+ )
+ }
+ single {
+ GetCartProductsUseCase(
+ cartRepository = get()
+ )
+ }
+ single {
+ GetCartSumPriceUseCase(
+ cartRepository = get()
+ )
+ }
+ single {
+ RemoveProductFromCartUseCase(
+ cartRepository = get()
+ )
+ }
+ single {
+ CartApi(
+ sdk = get()
+ )
+ }
+ single {
+ Cart()
+ }
+ single {
+ CartRepositoryImpl(
+ cartApi = get(),
+ cart = get()
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/domain/models/CartProduct.kt b/feature/src/main/java/rees46/demo_android/feature/cart/domain/models/CartProduct.kt
new file mode 100644
index 00000000..80ddf007
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/domain/models/CartProduct.kt
@@ -0,0 +1,11 @@
+package rees46.demo_android.feature.cart.domain.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+@Parcelize
+data class CartProduct(
+ val product: Product,
+ var quantity: Int
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/domain/repository/CartRepository.kt b/feature/src/main/java/rees46/demo_android/feature/cart/domain/repository/CartRepository.kt
new file mode 100644
index 00000000..3556ee68
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/domain/repository/CartRepository.kt
@@ -0,0 +1,16 @@
+package rees46.demo_android.feature.cart.domain.repository
+
+import kotlinx.coroutines.flow.StateFlow
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+interface CartRepository {
+
+ fun getCartProducts() : StateFlow>
+ fun getCartProduct(productId: String): CartProduct?
+
+ fun addProduct(product: Product, quantity: Int)
+ fun removeProduct(productId: String)
+
+ fun getSumPrice(): StateFlow
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartProductsUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartProductsUseCase.kt
new file mode 100644
index 00000000..811ea4c9
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartProductsUseCase.kt
@@ -0,0 +1,13 @@
+package rees46.demo_android.feature.cart.domain.usecase
+
+import kotlinx.coroutines.flow.StateFlow
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+
+class GetCartProductsUseCase (
+ private val cartRepository: CartRepository
+) {
+
+ fun invoke(): StateFlow> =
+ cartRepository.getCartProducts()
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartSumPriceUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartSumPriceUseCase.kt
new file mode 100644
index 00000000..3362f71d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/GetCartSumPriceUseCase.kt
@@ -0,0 +1,12 @@
+package rees46.demo_android.feature.cart.domain.usecase
+
+import kotlinx.coroutines.flow.StateFlow
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+
+class GetCartSumPriceUseCase (
+ private val cartRepository: CartRepository
+) {
+
+ fun invoke(): StateFlow =
+ cartRepository.getSumPrice()
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/RemoveProductFromCartUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/RemoveProductFromCartUseCase.kt
new file mode 100644
index 00000000..ad4763fa
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/domain/usecase/RemoveProductFromCartUseCase.kt
@@ -0,0 +1,12 @@
+package rees46.demo_android.feature.cart.domain.usecase
+
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+
+class RemoveProductFromCartUseCase (
+ private val cartRepository: CartRepository
+) {
+
+ fun invoke(productId: String) {
+ cartRepository.removeProduct(productId)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/adapter/CartProductsAdapter.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/adapter/CartProductsAdapter.kt
new file mode 100644
index 00000000..0c655840
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/adapter/CartProductsAdapter.kt
@@ -0,0 +1,22 @@
+package rees46.demo_android.feature.cart.presentation.adapter
+
+import android.content.Context
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.feature.cart.presentation.models.CartProductRecyclerViewItem
+import rees46.demo_android.feature.cart.presentation.view.recyclerView.CartProductItemView
+
+class CartProductsAdapter(
+ private val context: Context,
+ cartProductItems: List,
+ listener: OnItemClickListener
+) : ListItemAdapter(
+ items = cartProductItems,
+ listener = listener
+) {
+
+ override fun createItemView(): CartProductItemView =
+ CartProductItemView(
+ context = context
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/mappers/CartProductItemMapper.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/mappers/CartProductItemMapper.kt
new file mode 100644
index 00000000..a332c9f8
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/mappers/CartProductItemMapper.kt
@@ -0,0 +1,29 @@
+package rees46.demo_android.feature.cart.presentation.mappers
+
+import rees46.demo_android.feature.cart.presentation.models.CartProductRecyclerViewItem
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+
+class CartProductItemMapper(
+ private val productItemMapper: ProductItemMapper
+) {
+
+ fun toCartProductItem(cartProduct: CartProduct): CartProductRecyclerViewItem =
+ with(cartProduct) {
+ CartProductRecyclerViewItem(
+ productItem = productItemMapper.toProductItem(product),
+ quantity = quantity
+ )
+ }
+
+ fun toCartProductItems(cartProducts: Collection): List =
+ cartProducts.map { toCartProductItem(it) }
+
+ fun toCartProduct(cartProductItem: CartProductRecyclerViewItem): CartProduct =
+ with(cartProductItem) {
+ CartProduct(
+ product = productItemMapper.toProduct(productItem),
+ quantity = quantity
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/models/CartProductRecyclerViewItem.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/models/CartProductRecyclerViewItem.kt
new file mode 100644
index 00000000..b19eb016
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/models/CartProductRecyclerViewItem.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.cart.presentation.models
+
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+
+data class CartProductRecyclerViewItem(
+ val productItem: ProductRecyclerViewItem,
+ var quantity: Int
+) : RecyclerViewItem() {
+
+ override fun areItemsTheSame(anotherItem: RecyclerViewItem): Boolean {
+ val cartProductItem = anotherItem as CartProductRecyclerViewItem
+
+ return productItem.id == cartProductItem.productItem.id
+ }
+
+ override fun areContentsTheSame(anotherItem: RecyclerViewItem): Boolean =
+ this == anotherItem
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/CartFragment.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/CartFragment.kt
new file mode 100644
index 00000000..4fb8cc67
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/CartFragment.kt
@@ -0,0 +1,136 @@
+package rees46.demo_android.feature.cart.presentation.view
+
+import android.os.Bundle
+import android.os.Handler
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.feature.cart.presentation.models.CartProductRecyclerViewItem
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.databinding.FragmentCartBinding
+import rees46.demo_android.feature.cart.presentation.viewmodel.CartViewModel
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.ProductDetails
+import com.rees46.demo_android.navigation.ProductsDetails
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.cart.presentation.mappers.CartProductItemMapper
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+
+class CartFragment : Fragment(), OnItemClickListener {
+
+ private val viewModel: CartViewModel by viewModel()
+ private val productItemMapper: ProductItemMapper by inject()
+ private val cartProductItemMapper: CartProductItemMapper by inject()
+ private val navigationProductMapper: NavigationProductMapper by inject()
+
+ private lateinit var binding: FragmentCartBinding
+
+ private val navigator by lazy {
+ get {
+ parametersOf(findNavController())
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentCartBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupViews()
+ setupViewModels()
+ }
+
+ private fun setupViews() {
+ binding.cartProductsRecyclerView.setup(this)
+
+ setupRecommendationBlockView()
+ }
+
+ private fun setupViewModels() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.cartProductsFlow.collectLatest(::updateCart)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.sumPriceFlow.collectLatest {
+ binding.totalValueText.text = "$it"
+ }
+ }
+ }
+
+ private fun updateCart(cartProducts: MutableList) {
+ updateCartView(cartProducts.isEmpty())
+
+ viewLifecycleOwner.lifecycleScope.launch {
+
+ }
+ val cartProductItems = cartProductItemMapper.toCartProductItems(cartProducts)
+ Handler(requireContext().mainLooper).post {
+ binding.cartProductsRecyclerView.updateItems(cartProductItems)
+ }
+ }
+
+ private fun updateCartView(isEmpty: Boolean) {
+ binding.apply {
+ cartLayout.isVisible = !isEmpty
+ headerText.isVisible = !isEmpty
+ emptyCartText.isVisible = isEmpty
+ recyclerContainer.isVisible = !isEmpty
+ }
+ }
+
+ private fun removeProduct(cartProduct: CartProduct) {
+ viewModel.removeProduct(cartProduct)
+ }
+
+ private fun setupRecommendationBlockView() {
+ binding.recommendationBlock.apply {
+ setup(
+ productItemMapper = productItemMapper,
+ onCardProductClick = ::navigateProductFragment,
+ onShowAllClick = ::navigateProductsFragment
+ )
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.recommendationFlow.collect(::update)
+ }
+ }
+ }
+
+ private fun navigateProductFragment(product: Product) {
+ val navigationProduct = navigationProductMapper.toNavigationProduct(product)
+ navigator.navigate(ProductDetails(navigationProduct))
+ }
+
+ private fun navigateProductsFragment(products: List) {
+ val navigationProducts = navigationProductMapper.toNavigationProducts(products)
+ navigator.navigate(ProductsDetails(navigationProducts))
+ }
+
+ override fun onItemClick(item: RecyclerViewItem) {
+ val cartProduct = cartProductItemMapper.toCartProduct(item as CartProductRecyclerViewItem)
+ removeProduct(cartProduct)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/CheckoutButton.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/CheckoutButton.kt
new file mode 100644
index 00000000..9e46c1bf
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/CheckoutButton.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.cart.presentation.view.button
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.button.view.BaseButton
+import com.rees46.ui.R
+
+open class CheckoutButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = rees46.demo_android.R.string.checkout,
+ backgroundColorRes = R.color.background_color_opposite_primary,
+ textColorRes = R.color.text_color_opposite_primary
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/ContinueShoppingButton.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/ContinueShoppingButton.kt
new file mode 100644
index 00000000..a1a8755d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/button/ContinueShoppingButton.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.cart.presentation.view.button
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.button.view.BaseButton
+import com.rees46.ui.R
+
+open class ContinueShoppingButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = rees46.demo_android.R.string.continue_shopping,
+ backgroundColorRes = R.color.text_color_opposite_primary,
+ textColorRes = R.color.text_color_primary
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductItemView.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductItemView.kt
new file mode 100644
index 00000000..3edee6c8
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductItemView.kt
@@ -0,0 +1,42 @@
+package rees46.demo_android.feature.cart.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.rees46.demo_android.ui.extensions.updateImage
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.databinding.ViewCartProductItemBinding
+import rees46.demo_android.feature.cart.presentation.models.CartProductRecyclerViewItem
+
+class CartProductItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : RecyclerItemView(
+ context = context,
+ attrs = attrs
+) {
+
+ private var binding: ViewCartProductItemBinding =
+ ViewCartProductItemBinding.inflate(LayoutInflater.from(context), this, true)
+
+ override fun bind(item: RecyclerViewItem, listener: OnItemClickListener) {
+ with(binding) {
+ removeButton.setOnClickListener {
+ listener.onItemClick(item)
+ }
+
+ val cartProductItem = item as CartProductRecyclerViewItem
+
+ with(cartProductItem.productItem) {
+ productImageView.updateImage(pictureUrl)
+
+ productNameText.text = name
+ producerNameText.text = producerName
+ priceText.text = (price?.times(cartProductItem.quantity)).toString()
+ productQuantity.text = "x${cartProductItem.quantity}"
+ }
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductsRecyclerView.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductsRecyclerView.kt
new file mode 100644
index 00000000..6d837cd4
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/view/recyclerView/CartProductsRecyclerView.kt
@@ -0,0 +1,35 @@
+package rees46.demo_android.feature.cart.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.rees46.demo_android.ui.recyclerView.base.view.ListRecyclerView
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.feature.cart.presentation.models.CartProductRecyclerViewItem
+
+class CartProductsRecyclerView @JvmOverloads constructor(
+ private val context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ListRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+) {
+
+ override fun createAdapter(
+ listener: OnItemClickListener
+ ): ListItemAdapter =
+ rees46.demo_android.feature.cart.presentation.adapter.CartProductsAdapter(
+ context = context,
+ cartProductItems = items,
+ listener = listener
+ )
+
+ override fun createLayoutManager(): LayoutManager =
+ LinearLayoutManager(context)
+ .apply {
+ orientation = VERTICAL
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/cart/presentation/viewmodel/CartViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/viewmodel/CartViewModel.kt
new file mode 100644
index 00000000..28887f34
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/cart/presentation/viewmodel/CartViewModel.kt
@@ -0,0 +1,43 @@
+package rees46.demo_android.feature.cart.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import rees46.demo_android.core.settings.RecommendationSettings
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.cart.domain.usecase.GetCartProductsUseCase
+import rees46.demo_android.feature.cart.domain.usecase.GetCartSumPriceUseCase
+import rees46.demo_android.feature.cart.domain.usecase.RemoveProductFromCartUseCase
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+import rees46.demo_android.feature.recommendationBlock.domain.usecase.GetRecommendationUseCase
+
+class CartViewModel(
+ getCartProductsUseCase: GetCartProductsUseCase,
+ private val removeProductFromCartUseCase: RemoveProductFromCartUseCase,
+ getCartSumPriceUseCase: GetCartSumPriceUseCase,
+ getRecommendationUseCase: GetRecommendationUseCase
+) : ViewModel() {
+
+ val cartProductsFlow: StateFlow> = getCartProductsUseCase.invoke()
+
+ private val _recommendationFlow = MutableStateFlow(Recommendation("", emptyList()))
+ val recommendationFlow: StateFlow = _recommendationFlow.asStateFlow()
+
+ val sumPriceFlow: StateFlow = getCartSumPriceUseCase.invoke()
+
+ init {
+ getRecommendationUseCase.invoke(
+ recommenderCode = RecommendationSettings.CART_RECOMMENDED_CODE,
+ onGetRecommendation = {
+ viewModelScope.launch { _recommendationFlow.emit(it) }
+ }
+ )
+ }
+
+ fun removeProduct(cartProduct: CartProduct) {
+ removeProductFromCartUseCase.invoke(cartProduct.product.id)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/category/di/CategoryModule.kt b/feature/src/main/java/rees46/demo_android/feature/category/di/CategoryModule.kt
new file mode 100644
index 00000000..6bfe1fab
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/category/di/CategoryModule.kt
@@ -0,0 +1,11 @@
+package rees46.demo_android.feature.category.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.feature.category.presentation.viewmodel.CategoryViewModel
+
+val categoryModule = module {
+ viewModel {
+ CategoryViewModel()
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/category/presentation/view/CategoryFragment.kt b/feature/src/main/java/rees46/demo_android/feature/category/presentation/view/CategoryFragment.kt
new file mode 100644
index 00000000..e53a5a74
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/category/presentation/view/CategoryFragment.kt
@@ -0,0 +1,22 @@
+package rees46.demo_android.feature.category.presentation.view
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import rees46.demo_android.databinding.FragmentCategoryBinding
+
+class CategoryFragment : Fragment() {
+
+ private lateinit var binding: FragmentCategoryBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentCategoryBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/category/presentation/viewmodel/CategoryViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/category/presentation/viewmodel/CategoryViewModel.kt
new file mode 100644
index 00000000..71fc9d93
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/category/presentation/viewmodel/CategoryViewModel.kt
@@ -0,0 +1,5 @@
+package rees46.demo_android.feature.category.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class CategoryViewModel : ViewModel()
diff --git a/feature/src/main/java/rees46/demo_android/feature/home/di/HomeModule.kt b/feature/src/main/java/rees46/demo_android/feature/home/di/HomeModule.kt
new file mode 100644
index 00000000..ccc44fa7
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/home/di/HomeModule.kt
@@ -0,0 +1,15 @@
+package rees46.demo_android.feature.home.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.core.settings.RecommendationSettings
+import rees46.demo_android.feature.home.presentation.viewmodel.HomeViewModel
+
+val homeModule = module {
+ viewModel {
+ HomeViewModel(
+ getRecommendationUseCase = get(),
+ recommenderCode = RecommendationSettings.SIMPLE_RECOMMENDED_CODE
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/home/presentation/view/HomeFragment.kt b/feature/src/main/java/rees46/demo_android/feature/home/presentation/view/HomeFragment.kt
new file mode 100644
index 00000000..c74cf144
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/home/presentation/view/HomeFragment.kt
@@ -0,0 +1,93 @@
+package rees46.demo_android.feature.home.presentation.view
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.personalizatio.SDK
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.databinding.FragmentHomeBinding
+import rees46.demo_android.feature.recommendationBlock.presentation.view.RecommendationBlockView
+import rees46.demo_android.feature.home.presentation.viewmodel.HomeViewModel
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.ProductDetails
+import com.rees46.demo_android.navigation.ProductsDetails
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+
+class HomeFragment : Fragment() {
+
+ private val viewModel: HomeViewModel by viewModel()
+ private val productItemMapper: ProductItemMapper by inject()
+ private val navigationProductMapper: NavigationProductMapper by inject()
+
+ private lateinit var binding: FragmentHomeBinding
+
+ private val sdk: SDK by inject()
+
+ private val navigator by lazy {
+ get {
+ parametersOf(findNavController())
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentHomeBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ with(binding.storiesView) {
+ sdk.initializeStoriesView(this)
+ settings.icon_size = 80
+ settings.label_font_size = 0
+ }
+
+ with(binding) {
+ setupRecommendationBlockView(newArrivalsRecommendationBlockView)
+ setupRecommendationBlockView(topTrendsRecommendationBlockView)
+ setupRecommendationBlockView(youLikeRecommendationBlockView)
+ }
+ }
+
+ private fun setupRecommendationBlockView(recommendationBlockView: RecommendationBlockView) {
+ recommendationBlockView.apply {
+ setup(
+ productItemMapper = productItemMapper,
+ onCardProductClick = ::navigateProductFragment,
+ onShowAllClick = ::navigateProductsFragment
+ )
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.recommendationFlow.collectLatest(::update)
+ }
+ }
+ }
+
+ private fun navigateProductFragment(product: Product) {
+ val navigationProduct = navigationProductMapper.toNavigationProduct(product)
+ navigator.navigate(ProductDetails(navigationProduct))
+ }
+
+ private fun navigateProductsFragment(products: List) {
+ val navigationProducts = navigationProductMapper.toNavigationProducts(products)
+ navigator.navigate(ProductsDetails(navigationProducts))
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/home/presentation/viewmodel/HomeViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/home/presentation/viewmodel/HomeViewModel.kt
new file mode 100644
index 00000000..75d5b5a6
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/home/presentation/viewmodel/HomeViewModel.kt
@@ -0,0 +1,28 @@
+package rees46.demo_android.feature.home.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+import rees46.demo_android.feature.recommendationBlock.domain.usecase.GetRecommendationUseCase
+
+class HomeViewModel(
+ getRecommendationUseCase: GetRecommendationUseCase,
+ recommenderCode: String
+) : ViewModel() {
+
+ private val _recommendationFlow: MutableStateFlow = MutableStateFlow(Recommendation("", emptyList()))
+ val recommendationFlow: StateFlow = _recommendationFlow.asStateFlow()
+
+ init {
+ getRecommendationUseCase.invoke(
+ recommenderCode = recommenderCode,
+ onGetRecommendation = {
+ viewModelScope.launch { _recommendationFlow.emit(it) }
+ }
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/data/mappers/ProductMapper.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/data/mappers/ProductMapper.kt
new file mode 100644
index 00000000..89a04e40
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/data/mappers/ProductMapper.kt
@@ -0,0 +1,27 @@
+package rees46.demo_android.feature.productDetails.data.mappers
+
+import rees46.demo_android.feature.productDetails.data.models.ProductDto
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class ProductMapper {
+
+ fun toProduct(productDto: ProductDto): Product =
+ with(productDto) {
+ Product(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toProducts(productDtoList: List): List =
+ productDtoList.map { toProduct(it) }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/data/models/ProductDto.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/data/models/ProductDto.kt
new file mode 100644
index 00000000..7bdd63a9
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/data/models/ProductDto.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.productDetails.data.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ProductDto(
+ val id: String,
+ val name: String,
+ val producerName: String,
+ val price: Double?,
+ val priceFormatted: String,
+ val priceFull: Double?,
+ val priceFullFormatted: String?,
+ val pictureUrl: String,
+ val description: String,
+ val rating: Float,
+ val sale: Int
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/di/ProductDetailsModule.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/di/ProductDetailsModule.kt
new file mode 100644
index 00000000..18f2a475
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/di/ProductDetailsModule.kt
@@ -0,0 +1,44 @@
+package rees46.demo_android.feature.productDetails.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.core.settings.RecommendationSettings
+import rees46.demo_android.feature.productDetails.data.mappers.ProductMapper
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.productDetails.domain.usecase.AddProductToCartUseCase
+import rees46.demo_android.feature.productDetails.domain.usecase.GetCartProductUseCase
+import rees46.demo_android.feature.productDetails.domain.usecase.GetRecommendationForProductUseCase
+import rees46.demo_android.feature.productDetails.presentation.viewmodel.ProductDetailsViewModel
+
+val productDetailsModule = module {
+ viewModel { product ->
+ ProductDetailsViewModel(
+ addProductToCartUseCase = get(),
+ getCartProductUseCase = get(),
+ getRecommendationForProductUseCase = get(),
+ recommendedCode = RecommendationSettings.ALSO_LIKE_RECOMMENDED_CODE,
+ product = product.get()
+ )
+ }
+ single {
+ ProductMapper()
+ }
+ single {
+ NavigationProductMapper()
+ }
+ single {
+ AddProductToCartUseCase(
+ cartRepository = get()
+ )
+ }
+ single {
+ GetCartProductUseCase(
+ cartRepository = get()
+ )
+ }
+ single {
+ GetRecommendationForProductUseCase(
+ recommendationRepository = get()
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/mappers/NavigationProductMapper.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/mappers/NavigationProductMapper.kt
new file mode 100644
index 00000000..35261548
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/mappers/NavigationProductMapper.kt
@@ -0,0 +1,85 @@
+package rees46.demo_android.feature.productDetails.domain.mappers
+
+import com.rees46.demo_android.navigation.models.NavigationProduct
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class NavigationProductMapper {
+
+ fun toNavigationProduct(product: Product): NavigationProduct =
+ with(product) {
+ NavigationProduct(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toNavigationProducts(productList: List): List =
+ productList.map { toNavigationProduct(it) }
+
+ fun toNavigationProduct(productItem: ProductRecyclerViewItem): NavigationProduct =
+ with(productItem) {
+ NavigationProduct(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toProduct(navigationProduct: NavigationProduct?): Product? {
+ if (navigationProduct == null) return null
+
+ with(navigationProduct) {
+ return Product(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+ }
+
+ fun toProductItem(navigationProduct: NavigationProduct): ProductRecyclerViewItem =
+ with(navigationProduct) {
+ ProductRecyclerViewItem(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toProductItems(navigationProducts: Collection): List =
+ navigationProducts.map { toProductItem(it) }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/models/Product.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/models/Product.kt
new file mode 100644
index 00000000..449d1926
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/models/Product.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.productDetails.domain.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class Product(
+ val id: String,
+ val name: String,
+ val producerName: String,
+ val price: Double?,
+ val priceFormatted: String,
+ val priceFull: Double?,
+ val priceFullFormatted: String?,
+ val pictureUrl: String,
+ val description: String,
+ val rating: Float,
+ val sale: Int
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/AddProductToCartUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/AddProductToCartUseCase.kt
new file mode 100644
index 00000000..db1e07d1
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/AddProductToCartUseCase.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.productDetails.domain.usecase
+
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class AddProductToCartUseCase (
+ private val cartRepository: CartRepository
+) {
+
+ fun invoke(
+ product: Product,
+ quantity: Int
+ ) {
+ cartRepository.addProduct(
+ product = product,
+ quantity = quantity
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetCartProductUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetCartProductUseCase.kt
new file mode 100644
index 00000000..873e07e4
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetCartProductUseCase.kt
@@ -0,0 +1,14 @@
+package rees46.demo_android.feature.productDetails.domain.usecase
+
+import rees46.demo_android.feature.cart.domain.models.CartProduct
+import rees46.demo_android.feature.cart.domain.repository.CartRepository
+
+class GetCartProductUseCase (
+ private val cartRepository: CartRepository
+) {
+
+ fun invoke(
+ productId: String
+ ): CartProduct? =
+ cartRepository.getCartProduct(productId)
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetRecommendationForProductUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetRecommendationForProductUseCase.kt
new file mode 100644
index 00000000..7cc2b428
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/domain/usecase/GetRecommendationForProductUseCase.kt
@@ -0,0 +1,21 @@
+package rees46.demo_android.feature.productDetails.domain.usecase
+
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+import rees46.demo_android.feature.recommendationBlock.domain.repository.RecommendationRepository
+
+class GetRecommendationForProductUseCase (
+ private val recommendationRepository: RecommendationRepository
+) {
+
+ fun invoke(
+ recommenderCode: String,
+ productId: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ ) {
+ recommendationRepository.getRecommendationForProduct(
+ recommenderCode = recommenderCode,
+ productId = productId,
+ onGetRecommendation = onGetRecommendation
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/ProductAction.enum.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/ProductAction.enum.kt
new file mode 100644
index 00000000..3434aab0
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/ProductAction.enum.kt
@@ -0,0 +1,7 @@
+package rees46.demo_android.feature.productDetails.presentation
+
+enum class ProductAction {
+ ADD,
+ DECREASE,
+ INCREASE
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/ProductDetailsFragment.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/ProductDetailsFragment.kt
new file mode 100644
index 00000000..7d662863
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/ProductDetailsFragment.kt
@@ -0,0 +1,127 @@
+package rees46.demo_android.feature.productDetails.presentation.view
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.rees46.demo_android.ui.extensions.updateImage
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.core.settings.NavigationSettings
+import rees46.demo_android.databinding.FragmentProductDetailsBinding
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.models.NavigationProduct
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.productDetails.presentation.ProductAction
+import rees46.demo_android.feature.productDetails.presentation.viewmodel.ProductDetailsViewModel
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+
+class ProductDetailsFragment : Fragment() {
+
+ private val viewModel: ProductDetailsViewModel by viewModel {
+ val navigationProduct = arguments?.getParcelable(NavigationSettings.PRODUCT_ARGUMENT_FIELD)
+ parametersOf(navigationProductMapper.toProduct(navigationProduct))
+ }
+
+ private lateinit var binding: FragmentProductDetailsBinding
+
+ private val productItemMapper: ProductItemMapper by inject()
+ private val navigationProductMapper: NavigationProductMapper by inject()
+
+ private val navigator by lazy {
+ get {
+ parametersOf(findNavController())
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentProductDetailsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupViews()
+ setupViewModel()
+ }
+
+ private fun setupViewModel() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.currentProductFlow.collect(::updateCardProductView)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.countCartProductFlow.collect(::updateCount)
+ }
+ }
+
+ private fun setupViews() {
+ setupCardAction(viewModel::proceedCartAction)
+
+ setupRecommendationBlockView()
+ }
+
+ private fun setupRecommendationBlockView() {
+ binding.recommendationBlock.apply {
+ setup(
+ productItemMapper = productItemMapper,
+ onCardProductClick = ::updateProduct,
+ onShowAllClick = ::navigateProductsFragment
+ )
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.recommendationFlow.collectLatest(::update)
+ }
+ }
+ }
+
+ private fun setupCardAction(onCardActionClick: (ProductAction) -> Unit) {
+ binding.apply {
+ addToCartButton.setOnClickListener { onCardActionClick.invoke(ProductAction.ADD) }
+ countCard.setOnClickListener(
+ onMinusClick = { onCardActionClick.invoke(ProductAction.DECREASE) },
+ onPlusClick = { onCardActionClick.invoke(ProductAction.INCREASE) }
+ )
+ }
+ }
+
+ private fun updateProduct(product: Product?) {
+ viewModel.updateProduct(product)
+ }
+
+ private fun updateCardProductView(product: Product?) {
+ product?.let {
+ viewModel.updateRecommendationBlock(product.id)
+
+ binding.apply {
+ productImage.updateImage(product.pictureUrl)
+
+ binding.productDetailsView.setProduct(product)
+ }
+ }
+ }
+
+ private fun updateCount(count: Int) {
+ binding.countCard.setCount(count)
+ }
+
+ private fun navigateProductsFragment(products: List) {
+ val navigationProducts = navigationProductMapper.toNavigationProducts(products)
+ navigator.navigate(com.rees46.demo_android.navigation.ProductsDetails(navigationProducts))
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/button/AddToCartButton.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/button/AddToCartButton.kt
new file mode 100644
index 00000000..66e5d965
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/button/AddToCartButton.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.productDetails.presentation.view.button
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.button.view.BaseButton
+import com.rees46.ui.R
+
+open class AddToCartButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = rees46.demo_android.R.string.add_to_cart,
+ backgroundColorRes = R.color.background_color_opposite_primary,
+ textColorRes = R.color.text_color_opposite_primary
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/countCard/CountCardView.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/countCard/CountCardView.kt
new file mode 100644
index 00000000..46733f5a
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/countCard/CountCardView.kt
@@ -0,0 +1,33 @@
+package rees46.demo_android.feature.productDetails.presentation.view.countCard
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.google.android.material.card.MaterialCardView
+import rees46.demo_android.databinding.ViewCountCardBinding
+
+@SuppressLint("ViewConstructor")
+open class CountCardView @JvmOverloads constructor(
+ context: Context,
+ val attrs: AttributeSet? = null,
+ val defStyleAttr: Int = 0
+) : MaterialCardView(context, attrs, defStyleAttr) {
+
+ private var binding: ViewCountCardBinding =
+ ViewCountCardBinding.inflate(LayoutInflater.from(context), this, true)
+
+ fun setCount(count: Int) {
+ binding.valueText.text = count.toString()
+ }
+
+ fun setOnClickListener(
+ onMinusClick: () -> Unit,
+ onPlusClick: () -> Unit
+ ) {
+ with(binding) {
+ minusButton.setOnClickListener { onMinusClick.invoke() }
+ plusButton.setOnClickListener { onPlusClick.invoke() }
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsDescriptionView.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsDescriptionView.kt
new file mode 100644
index 00000000..eb0bcbe6
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsDescriptionView.kt
@@ -0,0 +1,30 @@
+package rees46.demo_android.feature.productDetails.presentation.view.description
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import rees46.demo_android.databinding.ViewProductDetailsDescriptionBinding
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+class ProductDetailsDescriptionView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+ private var binding: ViewProductDetailsDescriptionBinding =
+ ViewProductDetailsDescriptionBinding.inflate(LayoutInflater.from(context), this, true)
+
+ fun setProduct(product: Product) {
+ binding.apply {
+ productNameText.text = product.name
+ producerNameText.text = product.producerName
+ priceText.text = product.priceFormatted
+ descriptionText.text = product.description
+ productRatingBar.rating = product.rating
+ oldPriceText.updateText(product.priceFullFormatted.toString())
+ saleCardView.setValue(product.sale)
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsSaleCardView.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsSaleCardView.kt
new file mode 100644
index 00000000..01a9049a
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/description/ProductDetailsSaleCardView.kt
@@ -0,0 +1,23 @@
+package rees46.demo_android.feature.productDetails.presentation.view.description
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.google.android.material.card.MaterialCardView
+import rees46.demo_android.databinding.ViewProductDetailsSaleCardBinding
+
+class ProductDetailsSaleCardView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : MaterialCardView(context, attrs, defStyleAttr) {
+
+ private var binding: ViewProductDetailsSaleCardBinding =
+ ViewProductDetailsSaleCardBinding.inflate(LayoutInflater.from(context), this, true)
+
+ fun setValue(value: Int) {
+ binding.apply {
+ valueText.text = "-${value}%"
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/text/OldPriceText.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/text/OldPriceText.kt
new file mode 100644
index 00000000..819d496d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/view/text/OldPriceText.kt
@@ -0,0 +1,18 @@
+package rees46.demo_android.feature.productDetails.presentation.view.text
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import rees46.demo_android.R
+
+@SuppressLint("ViewConstructor")
+class OldPriceText @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : com.rees46.demo_android.ui.text.view.OldPriceText(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textSizeRes = R.dimen.text_size_product_details_old_price,
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/viewmodel/ProductDetailsViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/viewmodel/ProductDetailsViewModel.kt
new file mode 100644
index 00000000..1047c67e
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/productDetails/presentation/viewmodel/ProductDetailsViewModel.kt
@@ -0,0 +1,74 @@
+package rees46.demo_android.feature.productDetails.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import rees46.demo_android.feature.productDetails.domain.usecase.AddProductToCartUseCase
+import rees46.demo_android.feature.productDetails.domain.usecase.GetCartProductUseCase
+import rees46.demo_android.feature.productDetails.domain.usecase.GetRecommendationForProductUseCase
+import rees46.demo_android.feature.productDetails.presentation.ProductAction
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+
+class ProductDetailsViewModel(
+ private val addProductToCartUseCase: AddProductToCartUseCase,
+ private val getCartProductUseCase: GetCartProductUseCase,
+ private val getRecommendationForProductUseCase: GetRecommendationForProductUseCase,
+ private val recommendedCode: String,
+ product: Product?
+) : ViewModel() {
+
+ private val _recommendationFlow: MutableStateFlow = MutableStateFlow(Recommendation("", emptyList()))
+ val recommendationFlow: StateFlow = _recommendationFlow.asStateFlow()
+
+ private val _currentProductFlow: MutableStateFlow = MutableStateFlow(product)
+ val currentProductFlow: StateFlow = _currentProductFlow.asStateFlow()
+
+ private var _countCartProductFlow: MutableStateFlow =
+ MutableStateFlow(if(product != null) getCartProductUseCase.invoke(product.id)?.quantity ?: 1 else 0)
+ var countCartProductFlow: StateFlow = _countCartProductFlow.asStateFlow()
+
+ fun updateProduct(product: Product?) {
+ _countCartProductFlow.update { product?.let { getCartProductUseCase.invoke(product.id)?.quantity ?: 1 } ?:0 }
+ _currentProductFlow.update { product }
+ }
+
+ fun updateRecommendationBlock(productId: String) {
+ getRecommendationForProductUseCase.invoke(
+ recommenderCode = recommendedCode,
+ productId = productId,
+ onGetRecommendation = {
+ viewModelScope.launch { _recommendationFlow.emit(it) }
+ }
+ )
+ }
+
+ fun proceedCartAction(action: ProductAction) {
+ when (action) {
+ ProductAction.ADD -> addToCart()
+ ProductAction.INCREASE -> increaseCount()
+ ProductAction.DECREASE -> decreaseCount()
+ }
+ }
+
+ private fun addToCart() {
+ _currentProductFlow.value?.let { product ->
+ addProductToCartUseCase.invoke(
+ product = product,
+ quantity = _countCartProductFlow.value
+ )
+ }
+ }
+
+ private fun increaseCount() = _countCartProductFlow.update { it.inc() }
+
+ private fun decreaseCount() {
+ if (_countCartProductFlow.value > 1) {
+ _countCartProductFlow.update { it.dec() }
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/di/ProductsModule.kt b/feature/src/main/java/rees46/demo_android/feature/products/di/ProductsModule.kt
new file mode 100644
index 00000000..35ac31c2
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/di/ProductsModule.kt
@@ -0,0 +1,15 @@
+package rees46.demo_android.feature.products.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+import rees46.demo_android.feature.products.presentation.viewmodel.ProductsViewModel
+
+val productsModule = module {
+ viewModel {
+ ProductsViewModel()
+ }
+ single {
+ ProductItemMapper()
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/adapter/ScrollProductsAdapter.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/adapter/ScrollProductsAdapter.kt
new file mode 100644
index 00000000..36fadf20
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/adapter/ScrollProductsAdapter.kt
@@ -0,0 +1,23 @@
+package rees46.demo_android.feature.products.presentation.adapter
+
+import android.content.Context
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.adapter.ProductsAdapter
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.products.presentation.view.recyclerView.ScrollProductItemView
+
+class ScrollProductsAdapter(
+ private val context: Context,
+ productItems: List,
+ listener: OnItemClickListener
+) : ProductsAdapter(
+ items = productItems,
+ listener = listener
+) {
+
+ override fun createItemView(): RecyclerItemView =
+ ScrollProductItemView(
+ context = context
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/mappers/ProductItemMapper.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/mappers/ProductItemMapper.kt
new file mode 100644
index 00000000..2231bc10
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/mappers/ProductItemMapper.kt
@@ -0,0 +1,47 @@
+package rees46.demo_android.feature.products.presentation.mappers
+
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+
+class ProductItemMapper {
+
+ fun toProductItem(product: Product): ProductRecyclerViewItem =
+ with(product) {
+ ProductRecyclerViewItem(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toProductItems(products: Collection): List =
+ products.map { toProductItem(it) }
+
+ fun toProduct(productItem: ProductRecyclerViewItem): Product =
+ with(productItem) {
+ Product(
+ id = id,
+ name = name,
+ producerName = producerName,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = pictureUrl,
+ description = description,
+ rating = rating,
+ sale = sale
+ )
+ }
+
+ fun toProducts(productItems: Collection): List =
+ productItems.map { toProduct(it) }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/ProductsFragment.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/ProductsFragment.kt
new file mode 100644
index 00000000..d2d71f4d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/ProductsFragment.kt
@@ -0,0 +1,81 @@
+package rees46.demo_android.feature.products.presentation.view
+
+import android.os.Bundle
+import android.os.Handler
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.databinding.FragmentProductsBinding
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.ProductDetails
+import com.rees46.demo_android.navigation.models.NavigationProduct
+import rees46.demo_android.feature.products.presentation.viewmodel.ProductsViewModel
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.core.settings.NavigationSettings
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+
+class ProductsFragment : Fragment(), OnItemClickListener {
+
+ private val viewModel: ProductsViewModel by viewModel()
+ private val productItemMapper: ProductItemMapper by inject()
+ private val navigationProductMapper: NavigationProductMapper by inject()
+
+ private lateinit var binding: FragmentProductsBinding
+
+ private val navigator by lazy {
+ get {
+ parametersOf(findNavController())
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentProductsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupViews()
+ }
+
+ private fun setupViews() {
+ binding.productsRecyclerView.apply {
+ setup(this@ProductsFragment)
+ val navigationProducts = arguments?.getParcelableArrayList(NavigationSettings.PRODUCTS_ARGUMENT_FIELD)
+ navigationProducts?.let {
+ Handler(requireContext().mainLooper).post {
+ val productItems = navigationProductMapper.toProductItems(navigationProducts)
+ updateItems(productItems)
+ }
+ }
+ }
+ }
+
+ private fun navigateProductFragment(product: Product) {
+ val navigationProduct = navigationProductMapper.toNavigationProduct(product)
+ navigator.navigate(ProductDetails(navigationProduct))
+ }
+
+ override fun onItemClick(item: RecyclerViewItem) {
+ val product = productItemMapper.toProduct(item as ProductRecyclerViewItem)
+ navigateProductFragment(product)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductItemView.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductItemView.kt
new file mode 100644
index 00000000..098fb487
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductItemView.kt
@@ -0,0 +1,20 @@
+package rees46.demo_android.feature.products.presentation.view.recyclerView
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.recyclerView.products.view.ProductItemView
+import rees46.demo_android.R
+
+@SuppressLint("ViewConstructor")
+class ScrollProductItemView(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ProductItemView(
+ context = context,
+ attrs = attrs
+) {
+
+ override var isShopVisible: Boolean = true
+ override var layoutWidthRes: Int = R.dimen.width_products_layout
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductsRecyclerView.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductsRecyclerView.kt
new file mode 100644
index 00000000..ef8cd735
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/recyclerView/ScrollProductsRecyclerView.kt
@@ -0,0 +1,33 @@
+package rees46.demo_android.feature.products.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.GridLayoutManager
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.view.ProductsRecyclerView
+import rees46.demo_android.feature.products.presentation.adapter.ScrollProductsAdapter
+
+class ScrollProductsRecyclerView @JvmOverloads constructor(
+ private val context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ProductsRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+) {
+
+ override fun createAdapter(listener: OnItemClickListener): ScrollProductsAdapter =
+ ScrollProductsAdapter(
+ context = context,
+ productItems = items,
+ listener = listener
+ )
+
+ override fun createLayoutManager(): LayoutManager =
+ GridLayoutManager(context, GRID_LAYOUT_COUNT)
+
+ companion object {
+ private const val GRID_LAYOUT_COUNT = 2
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/text/OldPriceText.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/text/OldPriceText.kt
new file mode 100644
index 00000000..c9038a39
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/view/text/OldPriceText.kt
@@ -0,0 +1,18 @@
+package rees46.demo_android.feature.products.presentation.view.text
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import rees46.demo_android.R
+
+@SuppressLint("ViewConstructor")
+class OldPriceText @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : com.rees46.demo_android.ui.text.view.OldPriceText(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textSizeRes = R.dimen.text_size_product_item_old_Price
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/products/presentation/viewmodel/ProductsViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/products/presentation/viewmodel/ProductsViewModel.kt
new file mode 100644
index 00000000..332165eb
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/products/presentation/viewmodel/ProductsViewModel.kt
@@ -0,0 +1,5 @@
+package rees46.demo_android.feature.products.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class ProductsViewModel : ViewModel()
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/api/RecommendationApi.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/api/RecommendationApi.kt
new file mode 100644
index 00000000..c05a46ae
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/api/RecommendationApi.kt
@@ -0,0 +1,33 @@
+package rees46.demo_android.feature.recommendationBlock.data.api
+
+import com.personalizatio.Params
+import com.personalizatio.SDK
+import com.personalizatio.api.responses.recommendation.GetExtendedRecommendationResponse
+import rees46.demo_android.feature.recommendationBlock.data.models.RecommendationDto
+import rees46.demo_android.feature.search.data.api.toProducts
+
+class RecommendationApi(
+ private val sdk: SDK
+) {
+
+ fun getRecommendation(
+ recommenderCode: String,
+ params: Params,
+ onGetRecommendation: (RecommendationDto) -> Unit
+ ) {
+ sdk.recommendationManager.getExtendedRecommendation(
+ recommenderCode = recommenderCode,
+ params = params,
+ onGetExtendedRecommendation = {
+ onGetRecommendation.invoke(it.toRecommendation())
+ }
+ )
+ }
+
+ private fun GetExtendedRecommendationResponse.toRecommendation(): RecommendationDto {
+ return RecommendationDto(
+ title = title,
+ products = products.toProducts()
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/mappers/RecommendationMapper.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/mappers/RecommendationMapper.kt
new file mode 100644
index 00000000..f912fad4
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/mappers/RecommendationMapper.kt
@@ -0,0 +1,17 @@
+package rees46.demo_android.feature.recommendationBlock.data.mappers
+
+import rees46.demo_android.feature.productDetails.data.mappers.ProductMapper
+import rees46.demo_android.feature.recommendationBlock.data.models.RecommendationDto
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+
+class RecommendationMapper (
+ private val productMapper: ProductMapper
+) {
+ fun toRecommendation(recommendationDto: RecommendationDto): Recommendation =
+ with(recommendationDto) {
+ Recommendation(
+ title = title,
+ products = productMapper.toProducts(products)
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/models/RecommendationDto.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/models/RecommendationDto.kt
new file mode 100644
index 00000000..03b6e3a3
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/models/RecommendationDto.kt
@@ -0,0 +1,11 @@
+package rees46.demo_android.feature.recommendationBlock.data.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import rees46.demo_android.feature.productDetails.data.models.ProductDto
+
+@Parcelize
+data class RecommendationDto(
+ val title: String,
+ val products: List
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/repository/RecommendationRepositoryImpl.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/repository/RecommendationRepositoryImpl.kt
new file mode 100644
index 00000000..31d55124
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/data/repository/RecommendationRepositoryImpl.kt
@@ -0,0 +1,58 @@
+package rees46.demo_android.feature.recommendationBlock.data.repository
+
+import com.personalizatio.Params
+import rees46.demo_android.feature.recommendationBlock.data.api.RecommendationApi
+import rees46.demo_android.feature.recommendationBlock.data.mappers.RecommendationMapper
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+import rees46.demo_android.feature.recommendationBlock.domain.repository.RecommendationRepository
+
+class RecommendationRepositoryImpl (
+ private val recommendationApi: RecommendationApi,
+ private val recommendationMapper: RecommendationMapper
+) : RecommendationRepository {
+
+ override fun getRecommendation(
+ recommenderCode: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ ) {
+ getRecommendation(
+ recommenderCode = recommenderCode,
+ params = Params(),
+ onGetRecommendation = onGetRecommendation
+ )
+ }
+
+ override fun getRecommendationForProduct(
+ recommenderCode: String,
+ productId: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ ) {
+ getRecommendation(
+ recommenderCode = recommenderCode,
+ params = createRecommendationParams(productId),
+ onGetRecommendation = onGetRecommendation
+ )
+ }
+
+ private fun getRecommendation(
+ recommenderCode: String,
+ params: Params,
+ onGetRecommendation: (Recommendation) -> Unit
+ ) {
+ recommendationApi.getRecommendation(
+ recommenderCode = recommenderCode,
+ params = params,
+ onGetRecommendation = {
+ onGetRecommendation.invoke(recommendationMapper.toRecommendation(it))
+ }
+ )
+ }
+
+ private fun createRecommendationParams(
+ productId: String,
+ parameter: Params.Parameter = Params.Parameter.ITEM
+ ) = Params().put(
+ param = parameter,
+ value = productId
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/di/RecommendationBlockModule.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/di/RecommendationBlockModule.kt
new file mode 100644
index 00000000..61dc0ab6
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/di/RecommendationBlockModule.kt
@@ -0,0 +1,32 @@
+package rees46.demo_android.feature.recommendationBlock.di
+
+import org.koin.dsl.module
+import rees46.demo_android.feature.recommendationBlock.data.api.RecommendationApi
+import rees46.demo_android.feature.recommendationBlock.data.mappers.RecommendationMapper
+import rees46.demo_android.feature.recommendationBlock.data.repository.RecommendationRepositoryImpl
+import rees46.demo_android.feature.recommendationBlock.domain.repository.RecommendationRepository
+import rees46.demo_android.feature.recommendationBlock.domain.usecase.GetRecommendationUseCase
+
+val recommendationBlockModule = module {
+ single {
+ GetRecommendationUseCase(
+ recommendationRepository = get()
+ )
+ }
+ single {
+ RecommendationApi(
+ sdk = get()
+ )
+ }
+ single {
+ RecommendationMapper(
+ productMapper = get()
+ )
+ }
+ single {
+ RecommendationRepositoryImpl(
+ recommendationApi = get(),
+ recommendationMapper = get()
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/models/Recommendation.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/models/Recommendation.kt
new file mode 100644
index 00000000..7feedb64
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/models/Recommendation.kt
@@ -0,0 +1,11 @@
+package rees46.demo_android.feature.recommendationBlock.domain.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+@Parcelize
+data class Recommendation(
+ val title: String,
+ val products: List
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/repository/RecommendationRepository.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/repository/RecommendationRepository.kt
new file mode 100644
index 00000000..963cd4cf
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/repository/RecommendationRepository.kt
@@ -0,0 +1,17 @@
+package rees46.demo_android.feature.recommendationBlock.domain.repository
+
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+
+interface RecommendationRepository {
+
+ fun getRecommendation(
+ recommenderCode: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ )
+
+ fun getRecommendationForProduct(
+ recommenderCode: String,
+ productId: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/usecase/GetRecommendationUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/usecase/GetRecommendationUseCase.kt
new file mode 100644
index 00000000..ec4d6e4a
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/domain/usecase/GetRecommendationUseCase.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.recommendationBlock.domain.usecase
+
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+import rees46.demo_android.feature.recommendationBlock.domain.repository.RecommendationRepository
+
+class GetRecommendationUseCase (
+ private val recommendationRepository: RecommendationRepository
+) {
+
+ fun invoke(
+ recommenderCode: String,
+ onGetRecommendation: (Recommendation) -> Unit
+ ) {
+ recommendationRepository.getRecommendation(
+ recommenderCode = recommenderCode,
+ onGetRecommendation = onGetRecommendation
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/adapter/RecommendationProductsAdapter.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/adapter/RecommendationProductsAdapter.kt
new file mode 100644
index 00000000..e99632ad
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/adapter/RecommendationProductsAdapter.kt
@@ -0,0 +1,23 @@
+package rees46.demo_android.feature.recommendationBlock.presentation.adapter
+
+import android.content.Context
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.adapter.ProductsAdapter
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.recommendationBlock.presentation.view.recyclerView.RecommendationProductItemView
+
+class RecommendationProductsAdapter(
+ private val context: Context,
+ productItems: List,
+ listener: OnItemClickListener
+) : ProductsAdapter(
+ items = productItems,
+ listener = listener
+) {
+
+ override fun createItemView(): RecyclerItemView =
+ RecommendationProductItemView(
+ context = context
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/RecommendationBlockView.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/RecommendationBlockView.kt
new file mode 100644
index 00000000..c3ef6d6d
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/RecommendationBlockView.kt
@@ -0,0 +1,97 @@
+package rees46.demo_android.feature.recommendationBlock.presentation.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Handler
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.R
+import rees46.demo_android.feature.productDetails.domain.models.Product
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.recommendationBlock.presentation.view.recyclerView.RecommendationProductsRecyclerView
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+import rees46.demo_android.feature.recommendationBlock.domain.models.Recommendation
+
+class RecommendationBlockView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ConstraintLayout(context, attrs), OnItemClickListener {
+
+ private lateinit var headerTextView: TextView
+ private lateinit var showAllTextView: TextView
+ private lateinit var productsRecyclerView: RecommendationProductsRecyclerView
+
+ private var onCardProductClick: (Product) -> Unit = { }
+ private var onShowAllClick: (List) -> Unit = { }
+
+ private lateinit var productItemMapper: ProductItemMapper
+
+ init {
+ inflate(context, R.layout.view_recommendation_block, this)
+
+ initViews()
+
+ changeView(false)
+ }
+
+ internal fun setup(
+ productItemMapper: ProductItemMapper,
+ onCardProductClick: (Product) -> Unit = { },
+ onShowAllClick: (List) -> Unit = { }
+ ) {
+ this.productItemMapper = productItemMapper
+ this.onCardProductClick = onCardProductClick
+ this.onShowAllClick = onShowAllClick
+
+ setupViews()
+ }
+
+ private fun initViews() {
+ headerTextView = findViewById(R.id.header_text)
+ showAllTextView = findViewById(R.id.show_all_text)
+ productsRecyclerView = findViewById(R.id.products_recycler_view)
+ }
+
+ private fun setupViews() {
+ productsRecyclerView.setup(this)
+
+ showAllTextView.setOnClickListener {
+ onShowAllClick.invoke(productItemMapper.toProducts(productsRecyclerView.items))
+ }
+ }
+
+ fun update(recommendation: Recommendation) {
+ addCardProducts(recommendation.products)
+
+ setHeaderText(recommendation.title)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun addCardProducts(product: Collection) {
+ Handler(context.mainLooper).post {
+ val productItems = productItemMapper.toProductItems(product)
+ productsRecyclerView.updateItems(productItems)
+ }
+
+ changeView(true)
+ }
+
+ private fun setHeaderText(text: String) {
+ headerTextView.text = text
+ }
+
+ private fun changeView(show: Boolean) {
+ headerTextView.isVisible = show
+ showAllTextView.isVisible = show
+ productsRecyclerView.isVisible = show
+ }
+
+ override fun onItemClick(item: RecyclerViewItem) {
+ val product = productItemMapper.toProduct(item as ProductRecyclerViewItem)
+ onCardProductClick.invoke(product)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductItemView.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductItemView.kt
new file mode 100644
index 00000000..bdcd3200
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductItemView.kt
@@ -0,0 +1,20 @@
+package rees46.demo_android.feature.recommendationBlock.presentation.view.recyclerView
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.recyclerView.products.view.ProductItemView
+import rees46.demo_android.R
+
+@SuppressLint("ViewConstructor")
+class RecommendationProductItemView(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ProductItemView(
+ context = context,
+ attrs = attrs
+) {
+
+ override var isShopVisible: Boolean = false
+ override var layoutWidthRes: Int = R.dimen.width_recommendation_block_layout
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductsRecyclerView.kt b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductsRecyclerView.kt
new file mode 100644
index 00000000..a9920f9c
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/recommendationBlock/presentation/view/recyclerView/RecommendationProductsRecyclerView.kt
@@ -0,0 +1,31 @@
+package rees46.demo_android.feature.recommendationBlock.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.view.ProductsRecyclerView
+
+class RecommendationProductsRecyclerView @JvmOverloads constructor(
+ private val context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ProductsRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+) {
+
+ override fun createAdapter(listener: OnItemClickListener): rees46.demo_android.feature.recommendationBlock.presentation.adapter.RecommendationProductsAdapter =
+ rees46.demo_android.feature.recommendationBlock.presentation.adapter.RecommendationProductsAdapter(
+ context = context,
+ productItems = items,
+ listener = listener
+ )
+
+ override fun createLayoutManager(): LayoutManager =
+ LinearLayoutManager(context)
+ .apply {
+ orientation = HORIZONTAL
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/data/api/SearchApi.kt b/feature/src/main/java/rees46/demo_android/feature/search/data/api/SearchApi.kt
new file mode 100644
index 00000000..f5bc1c6f
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/data/api/SearchApi.kt
@@ -0,0 +1,82 @@
+package rees46.demo_android.feature.search.data.api
+
+import com.personalizatio.SDK
+import com.personalizatio.api.responses.product.Product
+import com.personalizatio.api.responses.search.Category
+import rees46.demo_android.feature.productDetails.data.models.ProductDto
+import rees46.demo_android.feature.search.data.models.SearchDto
+import rees46.demo_android.feature.search.data.models.CategoryDto
+import kotlin.random.Random
+
+class SearchApi(
+ private val sdk: SDK
+) {
+
+ fun search(
+ query: String,
+ onSearch: (SearchDto) -> Unit
+ ) {
+ sdk.searchManager.searchInstant(
+ query = query,
+ onSearchInstant = { searchInstantEntity ->
+ val searchDto = SearchDto(
+ products = searchInstantEntity.products.toProducts(),
+ categories = searchInstantEntity.categories.toCategories()
+ )
+ onSearch.invoke(searchDto)
+ }
+ )
+ }
+
+ fun searchBlank(onSearch: (SearchDto) -> Unit) {
+ sdk.searchManager.searchBlank(
+ onSearchBlank = { searchBlankEntity ->
+ val searchDto = SearchDto(
+ products = searchBlankEntity.products.toProducts(),
+ categories = listOf()
+ )
+ onSearch.invoke(searchDto)
+ }
+ )
+ }
+}
+
+fun Product.toProduct(): ProductDto {
+ return ProductDto(
+ id = id,
+ name = name,
+ producerName = brand,
+ price = price,
+ priceFormatted = priceFormatted,
+ priceFull = priceFull,
+ priceFullFormatted = priceFullFormatted,
+ pictureUrl = picture,
+ description = description,
+ rating = getRating(),
+ sale = getSale()
+ )
+}
+
+fun List.toProducts(): List =
+ map { it.toProduct() }
+
+// TODO: replaced by real data
+private fun getRating() =
+ Random.nextFloat() * 5
+
+// TODO: replaced by real data
+private fun getSale() =
+ (1 + Random.nextFloat() * 50).toInt()
+
+private fun Category.toCategory(): CategoryDto {
+ return CategoryDto(
+ id = id,
+ name = name,
+ parent = parent,
+ url = url,
+ count = count
+ )
+}
+
+fun List.toCategories(): List =
+ map { it.toCategory() }
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/data/mappers/SearchMapper.kt b/feature/src/main/java/rees46/demo_android/feature/search/data/mappers/SearchMapper.kt
new file mode 100644
index 00000000..baa030ef
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/data/mappers/SearchMapper.kt
@@ -0,0 +1,32 @@
+package rees46.demo_android.feature.search.data.mappers
+
+import rees46.demo_android.feature.productDetails.data.mappers.ProductMapper
+import rees46.demo_android.feature.search.data.models.CategoryDto
+import rees46.demo_android.feature.search.data.models.SearchDto
+import rees46.demo_android.feature.search.domain.models.Category
+import rees46.demo_android.feature.search.domain.models.Search
+
+class SearchMapper(
+ private val productMapper: ProductMapper
+) {
+
+ fun toSearch(searchDto: SearchDto): Search =
+ Search(
+ products = productMapper.toProducts(searchDto.products),
+ categories = toCategories(searchDto.categories)
+ )
+
+ private fun toCategory(categoryDto: CategoryDto): Category =
+ with(categoryDto) {
+ Category(
+ id = id,
+ name = name,
+ parent = parent,
+ url = url,
+ count = count
+ )
+ }
+
+ private fun toCategories(categoriesDto: List): List =
+ categoriesDto.map { toCategory(it) }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/data/models/CategoryDto.kt b/feature/src/main/java/rees46/demo_android/feature/search/data/models/CategoryDto.kt
new file mode 100644
index 00000000..c6e5ea62
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/data/models/CategoryDto.kt
@@ -0,0 +1,13 @@
+package rees46.demo_android.feature.search.data.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class CategoryDto(
+ val id: String,
+ val name: String,
+ val parent: String?,
+ val url: String,
+ val count: Int
+) : Parcelable
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/data/models/SearchDto.kt b/feature/src/main/java/rees46/demo_android/feature/search/data/models/SearchDto.kt
new file mode 100644
index 00000000..2c4b2fd8
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/data/models/SearchDto.kt
@@ -0,0 +1,8 @@
+package rees46.demo_android.feature.search.data.models
+
+import rees46.demo_android.feature.productDetails.data.models.ProductDto
+
+data class SearchDto(
+ val products: List,
+ val categories: List
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/data/repository/SearchRepositoryImpl.kt b/feature/src/main/java/rees46/demo_android/feature/search/data/repository/SearchRepositoryImpl.kt
new file mode 100644
index 00000000..e4c0fe22
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/data/repository/SearchRepositoryImpl.kt
@@ -0,0 +1,34 @@
+package rees46.demo_android.feature.search.data.repository
+
+import rees46.demo_android.feature.search.data.api.SearchApi
+import rees46.demo_android.feature.search.data.mappers.SearchMapper
+import rees46.demo_android.feature.search.domain.repository.SearchRepository
+import rees46.demo_android.feature.search.domain.models.Search
+
+class SearchRepositoryImpl (
+ private val productApi: SearchApi,
+ private val searchMapper: SearchMapper
+) : SearchRepository {
+
+ override fun searchProducts(
+ query: String,
+ onGetSearch: (Search) -> Unit
+ ) {
+ productApi.search(
+ query = query,
+ onSearch = { searchDto ->
+ onGetSearch.invoke(searchMapper.toSearch(searchDto))
+ }
+ )
+ }
+
+ override fun searchRecommendedProducts(
+ onGetSearch: (Search) -> Unit
+ ) {
+ productApi.searchBlank(
+ onSearch = { searchDto ->
+ onGetSearch(searchMapper.toSearch(searchDto))
+ }
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/di/SearchModule.kt b/feature/src/main/java/rees46/demo_android/feature/search/di/SearchModule.kt
new file mode 100644
index 00000000..a8c096f8
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/di/SearchModule.kt
@@ -0,0 +1,52 @@
+package rees46.demo_android.feature.search.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.feature.search.data.api.SearchApi
+import rees46.demo_android.feature.search.data.mappers.SearchMapper
+import rees46.demo_android.feature.search.data.repository.SearchRepositoryImpl
+import rees46.demo_android.feature.search.domain.repository.SearchRepository
+import rees46.demo_android.feature.search.presentation.viewmodel.SearchViewModel
+import rees46.demo_android.feature.search.domain.usecase.SearchProductsUseCase
+import rees46.demo_android.feature.search.domain.usecase.SearchRecommendedProductsUseCase
+import rees46.demo_android.feature.search.presentation.mappers.SearchItemMapper
+
+val searchModule = module {
+ viewModel {
+ SearchViewModel(
+ searchProductsUseCase = get(),
+ searchRecommendedProductsUseCase = get()
+ )
+ }
+ single {
+ SearchApi(
+ sdk = get()
+ )
+ }
+ single {
+ SearchMapper(
+ productMapper = get()
+ )
+ }
+ single {
+ SearchItemMapper(
+ productItemMapper = get()
+ )
+ }
+ single {
+ SearchRepositoryImpl(
+ productApi = get(),
+ searchMapper = get()
+ )
+ }
+ single {
+ SearchProductsUseCase(
+ searchRepository = get()
+ )
+ }
+ single {
+ SearchRecommendedProductsUseCase(
+ searchRepository = get()
+ )
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Category.kt b/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Category.kt
new file mode 100644
index 00000000..ec3c0c95
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Category.kt
@@ -0,0 +1,9 @@
+package rees46.demo_android.feature.search.domain.models
+
+data class Category(
+ val id: String,
+ val name: String,
+ val parent: String?,
+ val url: String,
+ val count: Int
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Search.kt b/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Search.kt
new file mode 100644
index 00000000..f66f615c
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/domain/models/Search.kt
@@ -0,0 +1,8 @@
+package rees46.demo_android.feature.search.domain.models
+
+import rees46.demo_android.feature.productDetails.domain.models.Product
+
+data class Search(
+ val products: List,
+ val categories: List
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/domain/repository/SearchRepository.kt b/feature/src/main/java/rees46/demo_android/feature/search/domain/repository/SearchRepository.kt
new file mode 100644
index 00000000..98a98da2
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/domain/repository/SearchRepository.kt
@@ -0,0 +1,15 @@
+package rees46.demo_android.feature.search.domain.repository
+
+import rees46.demo_android.feature.search.domain.models.Search
+
+interface SearchRepository {
+
+ fun searchProducts(
+ query: String,
+ onGetSearch: (Search) -> Unit
+ )
+
+ fun searchRecommendedProducts(
+ onGetSearch: (Search) -> Unit
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchProductsUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchProductsUseCase.kt
new file mode 100644
index 00000000..9d6541b6
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchProductsUseCase.kt
@@ -0,0 +1,16 @@
+package rees46.demo_android.feature.search.domain.usecase
+
+import rees46.demo_android.feature.search.domain.repository.SearchRepository
+import rees46.demo_android.feature.search.domain.models.Search
+
+class SearchProductsUseCase (
+ private val searchRepository: SearchRepository
+) {
+
+ fun invoke(
+ query: String,
+ onGetSearch: (Search) -> Unit
+ ) {
+ searchRepository.searchProducts(query, onGetSearch)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchRecommendedProductsUseCase.kt b/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchRecommendedProductsUseCase.kt
new file mode 100644
index 00000000..8fb7f49e
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/domain/usecase/SearchRecommendedProductsUseCase.kt
@@ -0,0 +1,15 @@
+package rees46.demo_android.feature.search.domain.usecase
+
+import rees46.demo_android.feature.search.domain.models.Search
+import rees46.demo_android.feature.search.domain.repository.SearchRepository
+
+class SearchRecommendedProductsUseCase (
+ private val searchRepository: SearchRepository
+) {
+
+ fun invoke(
+ onGetSearch: (Search) -> Unit
+ ) {
+ searchRepository.searchRecommendedProducts(onGetSearch)
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchCategoryAdapter.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchCategoryAdapter.kt
new file mode 100644
index 00000000..efc88390
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchCategoryAdapter.kt
@@ -0,0 +1,22 @@
+package rees46.demo_android.feature.search.presentation.adapter
+
+import android.content.Context
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.feature.search.presentation.models.CategoryRecyclerViewItem
+import rees46.demo_android.feature.search.presentation.view.recyclerView.SearchCategoryItemView
+
+class SearchCategoryAdapter(
+ private val context: Context,
+ items: List,
+ listener: OnItemClickListener
+) : ListItemAdapter(
+ items = items,
+ listener = listener
+) {
+
+ override fun createItemView(): SearchCategoryItemView =
+ SearchCategoryItemView(
+ context = context
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchProductAdapter.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchProductAdapter.kt
new file mode 100644
index 00000000..9ba4b4ab
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/adapter/SearchProductAdapter.kt
@@ -0,0 +1,22 @@
+package rees46.demo_android.feature.search.presentation.adapter
+
+import android.content.Context
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.search.presentation.view.recyclerView.SearchProductItemView
+
+class SearchProductAdapter(
+ private val context: Context,
+ items: List,
+ listener: OnItemClickListener
+) : ListItemAdapter(
+ items = items,
+ listener = listener
+) {
+
+ override fun createItemView(): SearchProductItemView =
+ SearchProductItemView(
+ context = context
+ )
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/mappers/SearchItemMapper.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/mappers/SearchItemMapper.kt
new file mode 100644
index 00000000..89c56a23
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/mappers/SearchItemMapper.kt
@@ -0,0 +1,32 @@
+package rees46.demo_android.feature.search.presentation.mappers
+
+import rees46.demo_android.feature.search.presentation.models.CategoryRecyclerViewItem
+import rees46.demo_android.feature.search.presentation.models.SearchRecyclerViewItem
+import rees46.demo_android.feature.products.presentation.mappers.ProductItemMapper
+import rees46.demo_android.feature.search.domain.models.Category
+import rees46.demo_android.feature.search.domain.models.Search
+
+class SearchItemMapper(
+ private val productItemMapper: ProductItemMapper
+) {
+
+ fun toSearchItem(search: Search) =
+ SearchRecyclerViewItem(
+ productItems = productItemMapper.toProductItems(search.products),
+ categoryItems = toCategoryItems(search.categories)
+ )
+
+ private fun toCategoryItem(category: Category): CategoryRecyclerViewItem =
+ with(category) {
+ CategoryRecyclerViewItem(
+ id = id,
+ name = name,
+ parent = parent,
+ url = url,
+ count = count
+ )
+ }
+
+ private fun toCategoryItems(categories: Collection): List =
+ categories.map { toCategoryItem(it) }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/CategoryRecyclerViewItem.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/CategoryRecyclerViewItem.kt
new file mode 100644
index 00000000..5a52dd68
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/CategoryRecyclerViewItem.kt
@@ -0,0 +1,21 @@
+package rees46.demo_android.feature.search.presentation.models
+
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+
+data class CategoryRecyclerViewItem(
+ val id: String,
+ val name: String,
+ val parent: String?,
+ val url: String,
+ val count: Int
+): RecyclerViewItem() {
+
+ override fun areItemsTheSame(anotherItem: RecyclerViewItem): Boolean {
+ val categoryItem = anotherItem as CategoryRecyclerViewItem
+
+ return id == categoryItem.id
+ }
+
+ override fun areContentsTheSame(anotherItem: RecyclerViewItem): Boolean =
+ this == anotherItem
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/SearchRecyclerViewItem.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/SearchRecyclerViewItem.kt
new file mode 100644
index 00000000..bdf08b0f
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/models/SearchRecyclerViewItem.kt
@@ -0,0 +1,31 @@
+package rees46.demo_android.feature.search.presentation.models
+
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+
+data class SearchRecyclerViewItem(
+ val productItems: List,
+ val categoryItems: List
+): RecyclerViewItem() {
+
+ override fun areItemsTheSame(anotherItem: RecyclerViewItem): Boolean {
+ val searchItem = anotherItem as SearchRecyclerViewItem
+
+ return areItemsTheSame(productItems, searchItem.productItems)
+ && areItemsTheSame(categoryItems, anotherItem.categoryItems)
+ }
+
+ override fun areContentsTheSame(anotherItem: RecyclerViewItem): Boolean =
+ this == anotherItem
+
+ private fun areItemsTheSame(items: List, anotherItems: List): Boolean {
+ for (i in 0..items.size) {
+ if(i >= anotherItems.size) return false
+ if (!items[i].areItemsTheSame(categoryItems[i])) {
+ return false
+ }
+ }
+
+ return true
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/SearchFragment.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/SearchFragment.kt
new file mode 100644
index 00000000..179688ea
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/SearchFragment.kt
@@ -0,0 +1,104 @@
+package rees46.demo_android.feature.search.presentation.view
+
+import android.os.Bundle
+import android.os.Handler
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.rees46.demo_android.ui.extensions.backPressedInvoke
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import rees46.demo_android.R
+import rees46.demo_android.databinding.FragmentSearchBinding
+import com.rees46.demo_android.navigation.Navigator
+import com.rees46.demo_android.navigation.ProductDetails
+import rees46.demo_android.feature.productDetails.domain.mappers.NavigationProductMapper
+import rees46.demo_android.feature.search.presentation.mappers.SearchItemMapper
+import rees46.demo_android.feature.search.presentation.viewmodel.SearchViewModel
+
+class SearchFragment : Fragment(), OnItemClickListener {
+
+ private val viewModel: SearchViewModel by viewModel()
+
+ private val searchItemMapper: SearchItemMapper by inject()
+ private val navigationProductMapper: NavigationProductMapper by inject()
+
+ private lateinit var binding: FragmentSearchBinding
+
+ private val navigator by lazy {
+ get {
+ parametersOf(findNavController())
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentSearchBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupView()
+ }
+
+ private fun setupView() {
+ binding.cancelButton.setOnClickListener {
+ backPressedInvoke()
+ }
+
+ setupSearch()
+ }
+
+ private fun setupSearch() {
+ binding.textInput.addTextChangedListener {
+ viewModel.searchProduct(query = it?.toString() ?: "")
+ }
+
+ setupSearchResultView()
+ }
+
+ private fun setupSearchResultView() {
+ binding.searchResultRecyclerView.setup(this)
+ binding.searchResultCategoriesRecyclerView.setup(this)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.searchResultItems.collectLatest {
+ val resString = if(it.products.isEmpty()) R.string.suitable_products_not_found else R.string.suitable_products
+ binding.suitableProductsText.text = getString(resString)
+
+ binding.suitableCategoriesText.isVisible = it.categories.isEmpty()
+
+ Handler(requireContext().mainLooper).post {
+ val searchItem = searchItemMapper.toSearchItem(it)
+ binding.searchResultRecyclerView.updateItems(searchItem.productItems)
+ binding.searchResultCategoriesRecyclerView.updateItems(searchItem.categoryItems)
+ }
+ }
+ }
+ }
+
+ override fun onItemClick(item: RecyclerViewItem) {
+ val navigationProduct = navigationProductMapper.toNavigationProduct(item as ProductRecyclerViewItem)
+ navigator.navigate(ProductDetails(navigationProduct))
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoriesRecyclerView.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoriesRecyclerView.kt
new file mode 100644
index 00000000..b4a1abdf
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoriesRecyclerView.kt
@@ -0,0 +1,36 @@
+package rees46.demo_android.feature.search.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.rees46.demo_android.ui.recyclerView.base.view.ListRecyclerView
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.feature.search.presentation.adapter.SearchCategoryAdapter
+import rees46.demo_android.feature.search.presentation.models.CategoryRecyclerViewItem
+
+class SearchCategoriesRecyclerView @JvmOverloads constructor(
+ private val context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ListRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+) {
+
+ override fun createAdapter(
+ listener: OnItemClickListener
+ ): ListItemAdapter =
+ SearchCategoryAdapter(
+ context = context,
+ items = items,
+ listener = listener
+ )
+
+ override fun createLayoutManager(): LayoutManager =
+ LinearLayoutManager(context)
+ .apply {
+ orientation = VERTICAL
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoryItemView.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoryItemView.kt
new file mode 100644
index 00000000..d566e6e9
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchCategoryItemView.kt
@@ -0,0 +1,33 @@
+package rees46.demo_android.feature.search.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import rees46.demo_android.databinding.ViewSearchCategoryItemBinding
+import rees46.demo_android.feature.search.presentation.models.CategoryRecyclerViewItem
+
+class SearchCategoryItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : RecyclerItemView(
+ context = context,
+ attrs = attrs
+) {
+ private var binding: ViewSearchCategoryItemBinding =
+ ViewSearchCategoryItemBinding.inflate(LayoutInflater.from(context), this, true)
+
+ override fun bind(item: RecyclerViewItem, listener: OnItemClickListener) {
+ with(binding) {
+ with(item as CategoryRecyclerViewItem) {
+ categoryName.text = name
+
+ rootView.setOnClickListener{
+ listener.onItemClick(item)
+ }
+ }
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductItemView.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductItemView.kt
new file mode 100644
index 00000000..24dc4608
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductItemView.kt
@@ -0,0 +1,37 @@
+package rees46.demo_android.feature.search.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.rees46.demo_android.ui.extensions.updateImage
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.databinding.ViewSearchProductItemBinding
+
+class SearchProductItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : RecyclerItemView(
+ context = context,
+ attrs = attrs
+) {
+
+ private var binding: ViewSearchProductItemBinding =
+ ViewSearchProductItemBinding.inflate(LayoutInflater.from(context), this, true)
+
+ override fun bind(item: RecyclerViewItem, listener: OnItemClickListener) {
+ with(binding) {
+ with(item as ProductRecyclerViewItem) {
+ productNameText.text = name
+ priceText.text = priceFormatted
+ productImageView.updateImage(pictureUrl)
+
+ rootView.setOnClickListener{
+ listener.onItemClick(item)
+ }
+ }
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductsRecyclerView.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductsRecyclerView.kt
new file mode 100644
index 00000000..0eb24be3
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/view/recyclerView/SearchProductsRecyclerView.kt
@@ -0,0 +1,36 @@
+package rees46.demo_android.feature.search.presentation.view.recyclerView
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.rees46.demo_android.ui.recyclerView.base.view.ListRecyclerView
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import rees46.demo_android.feature.search.presentation.adapter.SearchProductAdapter
+
+class SearchProductsRecyclerView @JvmOverloads constructor(
+ private val context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ListRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+) {
+
+ override fun createAdapter(
+ listener: OnItemClickListener
+ ): ListItemAdapter =
+ SearchProductAdapter(
+ context = context,
+ items = items,
+ listener = listener
+ )
+
+ override fun createLayoutManager(): LayoutManager =
+ LinearLayoutManager(context)
+ .apply {
+ orientation = VERTICAL
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/search/presentation/viewmodel/SearchViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/search/presentation/viewmodel/SearchViewModel.kt
new file mode 100644
index 00000000..fb04c39e
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/search/presentation/viewmodel/SearchViewModel.kt
@@ -0,0 +1,45 @@
+package rees46.demo_android.feature.search.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import rees46.demo_android.feature.search.domain.models.Search
+import rees46.demo_android.feature.search.domain.usecase.SearchProductsUseCase
+import rees46.demo_android.feature.search.domain.usecase.SearchRecommendedProductsUseCase
+
+class SearchViewModel(
+ private val searchProductsUseCase: SearchProductsUseCase,
+ private val searchRecommendedProductsUseCase: SearchRecommendedProductsUseCase
+) : ViewModel() {
+
+ private val _searchResultItems: MutableStateFlow = MutableStateFlow(Search(listOf(), listOf()))
+ val searchResultItems: Flow = _searchResultItems
+
+ fun searchProduct(query: String = "") {
+ if (query.isEmpty()) {
+ emptySearch()
+ }
+ else {
+ searchProductsUseCase.invoke(
+ query = query,
+ onGetSearch = { handleSearchResult(it) }
+ )
+ }
+ }
+
+ private fun emptySearch() {
+ searchRecommendedProductsUseCase.invoke(
+ onGetSearch = {
+ handleSearchResult(it)
+ }
+ )
+ }
+
+ private fun handleSearchResult(search: Search) {
+ viewModelScope.launch {
+ _searchResultItems.emit(search)
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/settings/di/SettingsModule.kt b/feature/src/main/java/rees46/demo_android/feature/settings/di/SettingsModule.kt
new file mode 100644
index 00000000..da9607b4
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/settings/di/SettingsModule.kt
@@ -0,0 +1,11 @@
+package rees46.demo_android.feature.settings.di
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+import rees46.demo_android.feature.category.presentation.viewmodel.CategoryViewModel
+
+val settingsModule = module {
+ viewModel {
+ CategoryViewModel()
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/SettingsFragment.kt b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/SettingsFragment.kt
new file mode 100644
index 00000000..29cde866
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/SettingsFragment.kt
@@ -0,0 +1,51 @@
+package rees46.demo_android.feature.settings.presentation.view
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.personalizatio.SDK
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import rees46.demo_android.core.utils.SdkUtils
+import rees46.demo_android.databinding.FragmentSettingsBinding
+import rees46.demo_android.feature.settings.presentation.viewmodel.SettingsViewModel
+
+class SettingsFragment : Fragment() {
+
+ private val viewModel: SettingsViewModel by viewModel()
+
+ private lateinit var binding: FragmentSettingsBinding
+
+ private val sdk: SDK by inject()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentSettingsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupViews()
+ }
+
+ private fun setupViews() {
+ binding.uploadButton.setOnClickListener {
+ val storeId = binding.storeKeyTextInput.text?.toString() ?: ""
+ SdkUtils.initialize(
+ sdk = sdk,
+ context = requireContext(),
+ shopId = storeId
+ )
+ }
+ }
+}
diff --git a/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/button/UploadButton.kt b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/button/UploadButton.kt
new file mode 100644
index 00000000..c2020005
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/view/button/UploadButton.kt
@@ -0,0 +1,19 @@
+package rees46.demo_android.feature.settings.presentation.view.button
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.button.view.BaseButton
+import com.rees46.ui.R
+
+open class UploadButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = rees46.demo_android.R.string.upload,
+ backgroundColorRes = R.color.background_color_opposite_primary,
+ textColorRes = R.color.text_color_opposite_primary
+)
diff --git a/feature/src/main/java/rees46/demo_android/feature/settings/presentation/viewmodel/SettingsViewModel.kt b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/viewmodel/SettingsViewModel.kt
new file mode 100644
index 00000000..8be1e61a
--- /dev/null
+++ b/feature/src/main/java/rees46/demo_android/feature/settings/presentation/viewmodel/SettingsViewModel.kt
@@ -0,0 +1,5 @@
+package rees46.demo_android.feature.settings.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class SettingsViewModel: ViewModel()
diff --git a/feature/src/main/res/color/main_bottom_navigation_item_color.xml b/feature/src/main/res/color/main_bottom_navigation_item_color.xml
new file mode 100644
index 00000000..2a62228f
--- /dev/null
+++ b/feature/src/main/res/color/main_bottom_navigation_item_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/feature/src/main/res/drawable/ic_arrow_right.xml b/feature/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 00000000..4904368f
--- /dev/null
+++ b/feature/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/feature/src/main/res/drawable/ic_cross.xml b/feature/src/main/res/drawable/ic_cross.xml
new file mode 100644
index 00000000..b8ba0401
--- /dev/null
+++ b/feature/src/main/res/drawable/ic_cross.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/src/main/res/drawable/ic_cross_circle_fill.xml b/feature/src/main/res/drawable/ic_cross_circle_fill.xml
new file mode 100644
index 00000000..24ea9898
--- /dev/null
+++ b/feature/src/main/res/drawable/ic_cross_circle_fill.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/feature/src/main/res/drawable/ic_minus.xml b/feature/src/main/res/drawable/ic_minus.xml
new file mode 100644
index 00000000..9fdb0865
--- /dev/null
+++ b/feature/src/main/res/drawable/ic_minus.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/feature/src/main/res/drawable/ic_plus.xml b/feature/src/main/res/drawable/ic_plus.xml
new file mode 100644
index 00000000..3831006b
--- /dev/null
+++ b/feature/src/main/res/drawable/ic_plus.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/src/main/res/layout/fragment_cart.xml b/feature/src/main/res/layout/fragment_cart.xml
new file mode 100644
index 00000000..66205146
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_cart.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/fragment_category.xml b/feature/src/main/res/layout/fragment_category.xml
new file mode 100644
index 00000000..188da83f
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_category.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/feature/src/main/res/layout/fragment_home.xml b/feature/src/main/res/layout/fragment_home.xml
new file mode 100644
index 00000000..1a87043f
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/fragment_product_details.xml b/feature/src/main/res/layout/fragment_product_details.xml
new file mode 100644
index 00000000..c9e5315a
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_product_details.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/fragment_products.xml b/feature/src/main/res/layout/fragment_products.xml
new file mode 100644
index 00000000..93432d50
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_products.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/fragment_search.xml b/feature/src/main/res/layout/fragment_search.xml
new file mode 100644
index 00000000..e98cff74
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/fragment_settings.xml b/feature/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 00000000..0310dde7
--- /dev/null
+++ b/feature/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/view_cart_product_item.xml b/feature/src/main/res/layout/view_cart_product_item.xml
new file mode 100644
index 00000000..0905a378
--- /dev/null
+++ b/feature/src/main/res/layout/view_cart_product_item.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/view_count_card.xml b/feature/src/main/res/layout/view_count_card.xml
new file mode 100644
index 00000000..39fd82ed
--- /dev/null
+++ b/feature/src/main/res/layout/view_count_card.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/src/main/res/layout/view_product_details_description.xml b/feature/src/main/res/layout/view_product_details_description.xml
new file mode 100644
index 00000000..5e370dff
--- /dev/null
+++ b/feature/src/main/res/layout/view_product_details_description.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/src/main/res/layout/view_product_details_sale_card.xml b/feature/src/main/res/layout/view_product_details_sale_card.xml
new file mode 100644
index 00000000..828e563f
--- /dev/null
+++ b/feature/src/main/res/layout/view_product_details_sale_card.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/feature/src/main/res/layout/view_recommendation_block.xml b/feature/src/main/res/layout/view_recommendation_block.xml
new file mode 100644
index 00000000..e70c03d3
--- /dev/null
+++ b/feature/src/main/res/layout/view_recommendation_block.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/view_search_category_item.xml b/feature/src/main/res/layout/view_search_category_item.xml
new file mode 100644
index 00000000..7f640177
--- /dev/null
+++ b/feature/src/main/res/layout/view_search_category_item.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/src/main/res/layout/view_search_product_item.xml b/feature/src/main/res/layout/view_search_product_item.xml
new file mode 100644
index 00000000..5a6e2de2
--- /dev/null
+++ b/feature/src/main/res/layout/view_search_product_item.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/src/main/res/values/dimens.xml b/feature/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..84e8192d
--- /dev/null
+++ b/feature/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 64dp
+
diff --git a/feature/src/main/res/values/dimens_margin.xml b/feature/src/main/res/values/dimens_margin.xml
new file mode 100644
index 00000000..a94ac5b2
--- /dev/null
+++ b/feature/src/main/res/values/dimens_margin.xml
@@ -0,0 +1,6 @@
+
+
+ 20dp
+ 20dp
+ 12dp
+
diff --git a/feature/src/main/res/values/dimens_size.xml b/feature/src/main/res/values/dimens_size.xml
new file mode 100644
index 00000000..bac14777
--- /dev/null
+++ b/feature/src/main/res/values/dimens_size.xml
@@ -0,0 +1,7 @@
+
+
+ 32dp
+
+ 171dp
+ 140dp
+
diff --git a/feature/src/main/res/values/dimens_text.xml b/feature/src/main/res/values/dimens_text.xml
new file mode 100644
index 00000000..43eab785
--- /dev/null
+++ b/feature/src/main/res/values/dimens_text.xml
@@ -0,0 +1,11 @@
+
+
+ 32sp
+ 24sp
+ 18sp
+ 12sp
+ 24sp
+ 14sp
+ 16sp
+ 16sp
+
diff --git a/feature/src/main/res/values/strings.xml b/feature/src/main/res/values/strings.xml
new file mode 100644
index 00000000..0a854d5a
--- /dev/null
+++ b/feature/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+ demo-android
+ DEMOSTORE
+ Add to cart
+ Stories
+ Shopping cart
+ Your shopping cart is empty
+ Shipping:
+ Total:
+ Continue shopping
+ SUITABLE CATEGORIES
+ SUITABLE PRODUCTS
+ Products not found
+ ]]>
+ Store settings
+ Try your own products
+ If you want to try a demo store with your products, you can put here your store key. To get the key, sign up on rees46.ru.
+ This process may take some time.
+ You story key
+ Upload
+ Shop
+ 46 reviews
+ Checkout
+
\ No newline at end of file
diff --git a/feature/src/main/res/values/styles.xml b/feature/src/main/res/values/styles.xml
new file mode 100644
index 00000000..22d17dcb
--- /dev/null
+++ b/feature/src/main/res/values/styles.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/feature/src/main/res/values/themes.xml b/feature/src/main/res/values/themes.xml
new file mode 100644
index 00000000..7dd594db
--- /dev/null
+++ b/feature/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/feature/src/main/res/xml/backup_rules.xml b/feature/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..fa0f996d
--- /dev/null
+++ b/feature/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..20e2a015
--- /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. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-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/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..727793b5
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,64 @@
+[versions]
+agp = "8.5.1"
+coreSplashscreen = "1.2.0-alpha01"
+databindingRuntime = "8.5.1"
+firebaseBom = "33.1.1"
+firebaseMessaging = "24.0.0"
+glide = "4.16.0"
+googleServices = "4.4.2"
+gson = "2.11.0"
+kotlin = "2.0.0"
+coreKtx = "1.13.1"
+appcompat = "1.7.0"
+material = "1.12.0"
+activity = "1.9.0"
+constraintlayout = "2.1.4"
+koin = "3.5.0"
+media3Exoplayer = "1.3.1"
+navigationFragmentKtx = "2.7.7"
+navigationUiKtx = "2.7.7"
+legacySupportV4 = "1.0.0"
+lifecycleLivedataKtx = "2.8.3"
+lifecycleViewmodelKtx = "2.8.3"
+fragmentKtx = "1.8.1"
+navigationFragment = "2.7.7"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+navigationRuntimeKtx = "2.7.7"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-databinding-runtime = { module = "androidx.databinding:databinding-runtime", version.ref = "databindingRuntime" }
+androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
+androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Exoplayer" }
+androidx-navigation-safe-args-gradle-plugin = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigationFragmentKtx" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessaging" }
+glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
+google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
+koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
+koin-compat = { group = "io.insert-koin", name = "koin-android-compat", version.ref = "koin" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
+androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
+androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
+androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
+androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+
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..28fb6e3f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 14 14:44:38 MSK 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
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/navigation/.gitignore b/navigation/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/navigation/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts
new file mode 100644
index 00000000..66ee1c42
--- /dev/null
+++ b/navigation/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "com.rees46.demo_android.navigation"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_22
+ targetCompatibility = JavaVersion.VERSION_22
+ }
+ kotlinOptions {
+ jvmTarget = "22"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ implementation(libs.koin.compat)
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(project(":core"))
+}
diff --git a/navigation/consumer-rules.pro b/navigation/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/navigation/proguard-rules.pro b/navigation/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/navigation/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/navigation/src/main/AndroidManifest.xml b/navigation/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e1000761
--- /dev/null
+++ b/navigation/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/navigation/src/main/java/com/rees46/demo_android/navigation/Destination.kt b/navigation/src/main/java/com/rees46/demo_android/navigation/Destination.kt
new file mode 100644
index 00000000..7fe34d9b
--- /dev/null
+++ b/navigation/src/main/java/com/rees46/demo_android/navigation/Destination.kt
@@ -0,0 +1,7 @@
+package com.rees46.demo_android.navigation
+
+import com.rees46.demo_android.navigation.models.NavigationProduct
+
+sealed interface Destination
+class ProductDetails(val navigationProduct: NavigationProduct): Destination
+class ProductsDetails(val navigationProducts: Collection): Destination
diff --git a/navigation/src/main/java/com/rees46/demo_android/navigation/Navigator.kt b/navigation/src/main/java/com/rees46/demo_android/navigation/Navigator.kt
new file mode 100644
index 00000000..0a7a72e7
--- /dev/null
+++ b/navigation/src/main/java/com/rees46/demo_android/navigation/Navigator.kt
@@ -0,0 +1,10 @@
+package com.rees46.demo_android.navigation
+
+interface Navigator {
+ fun navigate(destination: Destination)
+
+ fun navigate(id: Int)
+
+ fun popBackStack()
+ fun getCurrentDestination() : Int?
+}
diff --git a/navigation/src/main/java/com/rees46/demo_android/navigation/models/NavigationProduct.kt b/navigation/src/main/java/com/rees46/demo_android/navigation/models/NavigationProduct.kt
new file mode 100644
index 00000000..b2c58e6d
--- /dev/null
+++ b/navigation/src/main/java/com/rees46/demo_android/navigation/models/NavigationProduct.kt
@@ -0,0 +1,19 @@
+package com.rees46.demo_android.navigation.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class NavigationProduct(
+ val id: String,
+ val name: String,
+ val producerName: String,
+ val price: Double?,
+ val priceFormatted: String,
+ val priceFull: Double?,
+ val priceFullFormatted: String?,
+ val pictureUrl: String,
+ val description: String,
+ val rating: Float,
+ val sale: Int
+) : Parcelable
diff --git a/sdkRees46/build.gradle.kts b/sdkRees46/build.gradle.kts
new file mode 100644
index 00000000..6eb6adef
--- /dev/null
+++ b/sdkRees46/build.gradle.kts
@@ -0,0 +1,2 @@
+configurations.maybeCreate("default")
+artifacts.add("default", file("personalizatio-sdk-rees46-release.aar"))
diff --git a/sdkRees46/personalizatio-sdk-rees46-release.aar b/sdkRees46/personalizatio-sdk-rees46-release.aar
new file mode 100644
index 00000000..3eeebd57
Binary files /dev/null and b/sdkRees46/personalizatio-sdk-rees46-release.aar differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..d2be3f9d
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,31 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "demo-android"
+
+include(":sdkRees46")
+include(":feature")
+include(":data")
+include(":domain")
+include(":core")
+include(":app")
+include(":ui")
+include(":navigation")
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
new file mode 100644
index 00000000..d5a03f69
--- /dev/null
+++ b/ui/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "com.rees46.ui"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_22
+ targetCompatibility = JavaVersion.VERSION_22
+ }
+ kotlinOptions {
+ jvmTarget = "22"
+ }
+ viewBinding {
+ enable = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(project(":core"))
+ implementation(libs.glide)
+}
diff --git a/ui/consumer-rules.pro b/ui/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/proguard-rules.pro b/ui/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/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/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/ui/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/button/view/BaseButton.kt b/ui/src/main/java/com/rees46/demo_android/ui/button/view/BaseButton.kt
new file mode 100644
index 00000000..7ebfe0a2
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/button/view/BaseButton.kt
@@ -0,0 +1,49 @@
+package com.rees46.demo_android.ui.button.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import androidx.annotation.ColorRes
+import androidx.annotation.DimenRes
+import androidx.annotation.StringRes
+import com.google.android.material.button.MaterialButton
+import com.rees46.demo_android.ui.extensions.convertDimenResToDp
+import com.rees46.demo_android.ui.extensions.setBackgroundColor
+import com.rees46.demo_android.ui.extensions.setTextColor
+import com.rees46.ui.R
+
+@SuppressLint("ViewConstructor")
+open class BaseButton @JvmOverloads constructor(
+ context: Context,
+ val attrs: AttributeSet? = null,
+ val defStyleAttr: Int = 0,
+ @DimenRes private val textSizeRes: Int = R.dimen.text_size_default_button,
+ @StringRes private val textRes: Int,
+ @ColorRes private val backgroundColorRes: Int,
+ @ColorRes private val textColorRes: Int
+) : MaterialButton(context, attrs, defStyleAttr) {
+
+ init {
+ setupView()
+ }
+
+ private fun setupView() {
+ setText(textRes)
+ textSize = context.convertDimenResToDp(textSizeRes)
+ setTypeface(null, Typeface.BOLD)
+ textAlignment = View.TEXT_ALIGNMENT_CENTER
+ setTextColor(context, textColorRes)
+
+ gravity = Gravity.CENTER
+
+ setCornerRadiusResource(R.dimen.corner_radius_default_button)
+
+ setBackgroundColor(context, backgroundColorRes)
+
+ setStrokeWidthResource(R.dimen.stroke_width_default_button)
+ setStrokeColorResource(R.color.color_primary)
+ }
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/button/view/ErrorGoHomeButton.kt b/ui/src/main/java/com/rees46/demo_android/ui/button/view/ErrorGoHomeButton.kt
new file mode 100644
index 00000000..43623937
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/button/view/ErrorGoHomeButton.kt
@@ -0,0 +1,18 @@
+package com.rees46.demo_android.ui.button.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.ui.R
+
+open class ErrorGoHomeButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = R.string.go_home,
+ backgroundColorRes = R.color.background_color_opposite_primary,
+ textColorRes = R.color.text_color_opposite_primary
+)
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/button/view/ProductShopButton.kt b/ui/src/main/java/com/rees46/demo_android/ui/button/view/ProductShopButton.kt
new file mode 100644
index 00000000..d33904b5
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/button/view/ProductShopButton.kt
@@ -0,0 +1,19 @@
+package com.rees46.demo_android.ui.button.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.ui.R
+
+class ProductShopButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : BaseButton(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr,
+ textRes = R.string.shop,
+ backgroundColorRes = R.color.background_color_opposite_primary,
+ textColorRes = R.color.text_color_opposite_primary,
+ textSizeRes = R.dimen.text_size_product_shop_button
+)
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/extensions/Button.extensions.kt b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Button.extensions.kt
new file mode 100644
index 00000000..dd5a9885
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Button.extensions.kt
@@ -0,0 +1,25 @@
+package com.rees46.demo_android.ui.extensions
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.widget.Button
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+
+fun Button.setBackgroundColor(
+ context: Context,
+ @ColorRes colorRes: Int
+) {
+ val color = ContextCompat.getColor(context, colorRes)
+
+ backgroundTintList = ColorStateList.valueOf(color)
+}
+
+fun Button.setTextColor(
+ context: Context,
+ @ColorRes colorRes: Int
+) {
+ val color = ContextCompat.getColor(context, colorRes)
+
+ setTextColor(ColorStateList.valueOf(color))
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/extensions/Context.extensions.kt b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Context.extensions.kt
new file mode 100644
index 00000000..5e176a14
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Context.extensions.kt
@@ -0,0 +1,17 @@
+package com.rees46.demo_android.ui.extensions
+
+import android.content.Context
+import androidx.annotation.DimenRes
+
+fun Context.convertPxToDp(px: Float): Float {
+ return px / resources.displayMetrics.density
+}
+
+fun Context.convertDimenResToPx(@DimenRes dimenRes: Int): Float {
+ return resources.getDimension(dimenRes)
+}
+
+fun Context.convertDimenResToDp(@DimenRes dimenRes: Int): Float {
+ val px = convertDimenResToPx(dimenRes)
+ return convertPxToDp(px)
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/extensions/Fragment.extensions.kt b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Fragment.extensions.kt
new file mode 100644
index 00000000..c9aa2291
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/extensions/Fragment.extensions.kt
@@ -0,0 +1,7 @@
+package com.rees46.demo_android.ui.extensions
+
+import androidx.fragment.app.Fragment
+
+fun Fragment.backPressedInvoke() {
+ requireActivity().onBackPressedDispatcher.onBackPressed()
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/extensions/ImageView.extensions.kt b/ui/src/main/java/com/rees46/demo_android/ui/extensions/ImageView.extensions.kt
new file mode 100644
index 00000000..33a93b1d
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/extensions/ImageView.extensions.kt
@@ -0,0 +1,11 @@
+package com.rees46.demo_android.ui.extensions
+
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+
+fun ImageView.updateImage(imageUrl: String) {
+ Glide.with(rootView)
+ .load(imageUrl)
+ .centerCrop()
+ .into(this)
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/layout/view/ErrorLayout.kt b/ui/src/main/java/com/rees46/demo_android/ui/layout/view/ErrorLayout.kt
new file mode 100644
index 00000000..9e215a66
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/layout/view/ErrorLayout.kt
@@ -0,0 +1,18 @@
+package com.rees46.demo_android.ui.layout.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import com.rees46.ui.databinding.ViewErrorBinding
+
+class ErrorLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+ private var binding: ViewErrorBinding =
+ ViewErrorBinding.inflate(LayoutInflater.from(context), this, true)
+
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/adapter/ListItemAdapter.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/adapter/ListItemAdapter.kt
new file mode 100644
index 00000000..da73e9a0
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/adapter/ListItemAdapter.kt
@@ -0,0 +1,56 @@
+package com.rees46.demo_android.ui.recyclerView.base.adapter
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemViewHolder
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+
+abstract class ListItemAdapter (
+ val items: List,
+ private val listener: OnItemClickListener
+) : ListAdapter(AsyncDifferConfig.Builder(DiffCallback()).build()) {
+
+ abstract fun createItemView(): RecyclerItemView
+
+ override fun onCreateViewHolder(
+ viewGroup: ViewGroup,
+ viewType: Int
+ ): RecyclerItemViewHolder {
+ val itemView = createItemView()
+ .apply {
+ setup()
+ }
+
+ return RecyclerItemViewHolder(
+ view = itemView,
+ listener = listener
+ )
+ }
+
+ override fun onBindViewHolder(
+ viewHolder: RecyclerItemViewHolder,
+ position: Int
+ ) {
+ viewHolder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: I,
+ newItem: I
+ ): Boolean = newItem.areItemsTheSame(oldItem)
+
+ override fun areContentsTheSame(
+ oldItem: I,
+ newItem: I
+ ): Boolean = oldItem.areContentsTheSame(newItem)
+ }
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/listener/OnItemClickListener.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/listener/OnItemClickListener.kt
new file mode 100644
index 00000000..28b71eb7
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/listener/OnItemClickListener.kt
@@ -0,0 +1,8 @@
+package com.rees46.demo_android.ui.recyclerView.base.listener
+
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+
+interface OnItemClickListener {
+
+ fun onItemClick(item: RecyclerViewItem)
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/models/RecyclerViewItem.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/models/RecyclerViewItem.kt
new file mode 100644
index 00000000..1cb387bf
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/models/RecyclerViewItem.kt
@@ -0,0 +1,7 @@
+package com.rees46.demo_android.ui.recyclerView.base.models
+
+abstract class RecyclerViewItem {
+
+ abstract fun areItemsTheSame(anotherItem: RecyclerViewItem): Boolean
+ abstract fun areContentsTheSame(anotherItem: RecyclerViewItem): Boolean
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/ListRecyclerView.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/ListRecyclerView.kt
new file mode 100644
index 00000000..2a70f70d
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/ListRecyclerView.kt
@@ -0,0 +1,42 @@
+package com.rees46.demo_android.ui.recyclerView.base.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+
+abstract class ListRecyclerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : androidx.recyclerview.widget.RecyclerView(context, attrs, defStyleAttr) {
+
+ private var listAdapter: ListItemAdapter? = null
+
+ val items: MutableList = arrayListOf()
+
+ fun setup(
+ listener: OnItemClickListener
+ ) {
+ listAdapter = createAdapter(
+ listener = listener
+ )
+
+ this.adapter = listAdapter
+ this.layoutManager = createLayoutManager()
+ }
+
+ abstract fun createAdapter(
+ listener: OnItemClickListener
+ ): ListItemAdapter
+
+ abstract fun createLayoutManager(): LayoutManager
+
+ fun updateItems(items: List) {
+ this.items.clear()
+ this.items.addAll(items)
+
+ listAdapter?.submitList(items)
+ }
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemView.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemView.kt
new file mode 100644
index 00000000..2e48a76f
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemView.kt
@@ -0,0 +1,19 @@
+package com.rees46.demo_android.ui.recyclerView.base.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+
+@SuppressLint("ViewConstructor")
+abstract class RecyclerItemView(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ConstraintLayout(context, attrs) {
+
+ open fun setup() { }
+
+ abstract fun bind(item: RecyclerViewItem, listener: OnItemClickListener)
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemViewHolder.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemViewHolder.kt
new file mode 100644
index 00000000..d492043f
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/base/view/RecyclerItemViewHolder.kt
@@ -0,0 +1,15 @@
+package com.rees46.demo_android.ui.recyclerView.base.view
+
+import androidx.recyclerview.widget.RecyclerView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+
+class RecyclerItemViewHolder(
+ private val view: RecyclerItemView,
+ private val listener: OnItemClickListener
+) : RecyclerView.ViewHolder(view) {
+
+ fun bind(item: RecyclerViewItem) {
+ view.bind(item, listener)
+ }
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/adapter/ProductsAdapter.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/adapter/ProductsAdapter.kt
new file mode 100644
index 00000000..7f5f3403
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/adapter/ProductsAdapter.kt
@@ -0,0 +1,14 @@
+package com.rees46.demo_android.ui.recyclerView.products.adapter
+
+import com.rees46.demo_android.ui.recyclerView.base.adapter.ListItemAdapter
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.products.view.ProductItemView
+
+abstract class ProductsAdapter(
+ items: List,
+ listener: OnItemClickListener
+) : ListItemAdapter(
+ items = items,
+ listener = listener
+)
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/models/ProductRecyclerViewItem.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/models/ProductRecyclerViewItem.kt
new file mode 100644
index 00000000..5c06243b
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/models/ProductRecyclerViewItem.kt
@@ -0,0 +1,27 @@
+package com.rees46.demo_android.ui.recyclerView.products.models
+
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+
+data class ProductRecyclerViewItem(
+ val id: String,
+ val name: String,
+ val producerName: String,
+ val price: Double?,
+ val priceFormatted: String,
+ val priceFull: Double?,
+ val priceFullFormatted: String?,
+ val pictureUrl: String,
+ val description: String,
+ val rating: Float,
+ val sale: Int
+) : RecyclerViewItem() {
+
+ override fun areItemsTheSame(anotherItem: RecyclerViewItem): Boolean {
+ val productItem = anotherItem as ProductRecyclerViewItem
+
+ return id == productItem.id
+ }
+
+ override fun areContentsTheSame(anotherItem: RecyclerViewItem): Boolean =
+ this == anotherItem
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductItemView.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductItemView.kt
new file mode 100644
index 00000000..301f5fdd
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductItemView.kt
@@ -0,0 +1,63 @@
+package com.rees46.demo_android.ui.recyclerView.products.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.annotation.DimenRes
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.rees46.demo_android.ui.extensions.convertDimenResToPx
+import com.rees46.demo_android.ui.extensions.updateImage
+import com.rees46.demo_android.ui.recyclerView.base.models.RecyclerViewItem
+import com.rees46.demo_android.ui.recyclerView.base.view.RecyclerItemView
+import com.rees46.demo_android.ui.recyclerView.base.listener.OnItemClickListener
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+import com.rees46.ui.databinding.ViewProductItemBinding
+
+@SuppressLint("ViewConstructor")
+abstract class ProductItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : RecyclerItemView(
+ context = context,
+ attrs = attrs
+) {
+
+ abstract var isShopVisible: Boolean
+ @get:DimenRes
+ abstract var layoutWidthRes: Int
+
+ private var binding: ViewProductItemBinding =
+ ViewProductItemBinding.inflate(LayoutInflater.from(context), this, true)
+
+ override fun setup() {
+ with(binding) {
+ shopButton.isVisible = isShopVisible
+
+ productImage.updateLayoutParams {
+ width = context.convertDimenResToPx(layoutWidthRes).toInt()
+ }
+ }
+ }
+
+ override fun bind(item: RecyclerViewItem, listener: OnItemClickListener) {
+ setOnClickListener {
+ listener.onItemClick(item)
+ }
+
+ val productItem = item as ProductRecyclerViewItem
+
+ with(binding) {
+ with(productItem) {
+ productImage.updateImage(pictureUrl)
+
+ productNameText.text = name
+ producerNameText.text = producerName
+ priceText.text = priceFormatted
+ productRatingBar.rating = rating
+ oldPriceText.updateText(priceFullFormatted.toString())
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductsRecyclerView.kt b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductsRecyclerView.kt
new file mode 100644
index 00000000..13c9a4b4
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/recyclerView/products/view/ProductsRecyclerView.kt
@@ -0,0 +1,16 @@
+package com.rees46.demo_android.ui.recyclerView.products.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.rees46.demo_android.ui.recyclerView.base.view.ListRecyclerView
+import com.rees46.demo_android.ui.recyclerView.products.models.ProductRecyclerViewItem
+
+abstract class ProductsRecyclerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ListRecyclerView(
+ context = context,
+ attrs = attrs,
+ defStyleAttr = defStyleAttr
+)
diff --git a/ui/src/main/java/com/rees46/demo_android/ui/text/view/OldPriceText.kt b/ui/src/main/java/com/rees46/demo_android/ui/text/view/OldPriceText.kt
new file mode 100644
index 00000000..e1e7aeb4
--- /dev/null
+++ b/ui/src/main/java/com/rees46/demo_android/ui/text/view/OldPriceText.kt
@@ -0,0 +1,32 @@
+package com.rees46.demo_android.ui.text.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Paint
+import android.util.AttributeSet
+import androidx.annotation.DimenRes
+import com.rees46.demo_android.ui.extensions.convertDimenResToDp
+import com.rees46.ui.R
+
+@SuppressLint("ViewConstructor")
+open class OldPriceText @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ @DimenRes private val textSizeRes: Int = R.dimen.text_size_default_old_price
+) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
+
+ init {
+ setupView()
+ }
+
+ private fun setupView() {
+ paintFlags += Paint.STRIKE_THRU_TEXT_FLAG
+
+ textSize = context.convertDimenResToDp(textSizeRes)
+ }
+
+ fun updateText(value: String) {
+ text = value
+ }
+}
diff --git a/ui/src/main/res/layout/view_base_button.xml b/ui/src/main/res/layout/view_base_button.xml
new file mode 100644
index 00000000..6e9e976c
--- /dev/null
+++ b/ui/src/main/res/layout/view_base_button.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/ui/src/main/res/layout/view_error.xml b/ui/src/main/res/layout/view_error.xml
new file mode 100644
index 00000000..e18a68c9
--- /dev/null
+++ b/ui/src/main/res/layout/view_error.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/main/res/layout/view_old_price_text.xml b/ui/src/main/res/layout/view_old_price_text.xml
new file mode 100644
index 00000000..c9e87133
--- /dev/null
+++ b/ui/src/main/res/layout/view_old_price_text.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/ui/src/main/res/layout/view_product_item.xml b/ui/src/main/res/layout/view_product_item.xml
new file mode 100644
index 00000000..5d452403
--- /dev/null
+++ b/ui/src/main/res/layout/view_product_item.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/main/res/values/colors.xml b/ui/src/main/res/values/colors.xml
new file mode 100644
index 00000000..26fa76f8
--- /dev/null
+++ b/ui/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #FF000000
+ #FF737373
+ #FFFFFFFF
+
+ #FFFFBE17
+
diff --git a/ui/src/main/res/values/colors_background.xml b/ui/src/main/res/values/colors_background.xml
new file mode 100644
index 00000000..7a5089d1
--- /dev/null
+++ b/ui/src/main/res/values/colors_background.xml
@@ -0,0 +1,5 @@
+
+
+ #FFFFFFFF
+ #FF000000
+
diff --git a/ui/src/main/res/values/colors_text.xml b/ui/src/main/res/values/colors_text.xml
new file mode 100644
index 00000000..bef4bcff
--- /dev/null
+++ b/ui/src/main/res/values/colors_text.xml
@@ -0,0 +1,6 @@
+
+
+ #FF000000
+ #FF737373
+ #FFFFFFFF
+
diff --git a/ui/src/main/res/values/dimens.xml b/ui/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..af2dd740
--- /dev/null
+++ b/ui/src/main/res/values/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 6dp
+
+ 1dp
+
diff --git a/ui/src/main/res/values/dimens_margin.xml b/ui/src/main/res/values/dimens_margin.xml
new file mode 100644
index 00000000..ab08678b
--- /dev/null
+++ b/ui/src/main/res/values/dimens_margin.xml
@@ -0,0 +1,7 @@
+
+
+ 16dp
+ 8dp
+ 4dp
+ 20dp
+
diff --git a/ui/src/main/res/values/dimens_padding.xml b/ui/src/main/res/values/dimens_padding.xml
new file mode 100644
index 00000000..4a80414a
--- /dev/null
+++ b/ui/src/main/res/values/dimens_padding.xml
@@ -0,0 +1,6 @@
+
+
+ 16dp
+ 8dp
+ 4dp
+
diff --git a/ui/src/main/res/values/dimens_text.xml b/ui/src/main/res/values/dimens_text.xml
new file mode 100644
index 00000000..7ab061b4
--- /dev/null
+++ b/ui/src/main/res/values/dimens_text.xml
@@ -0,0 +1,16 @@
+
+
+ 16sp
+ 12sp
+ 12sp
+ 16sp
+ 16sp
+ 12sp
+ 16sp
+ 14sp
+ 16sp
+ 24sp
+ 13sp
+ 13sp
+ 12sp
+
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e2b5f69c
--- /dev/null
+++ b/ui/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+ Something \nwent wrong.
+ Page not found.
+ Go home
+ shop
+
diff --git a/ui/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml
new file mode 100644
index 00000000..3a96a7a0
--- /dev/null
+++ b/ui/src/main/res/values/styles.xml
@@ -0,0 +1,6 @@
+
+
+
+