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 @@ + + + +