├── .gitignore
├── .idea
└── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── maruchin
│ │ └── androidnavigation
│ │ ├── MainActivity.kt
│ │ ├── SampleApplication.kt
│ │ ├── navigationbar
│ │ ├── NavigationBar.kt
│ │ ├── NavigationBarHost.kt
│ │ └── NavigationBarState.kt
│ │ └── root
│ │ └── RootHost.kt
│ └── res
│ ├── values
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build-logic
├── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── AppModuleConventions.kt
│ ├── DataModuleConventions.kt
│ ├── FeatureModuleConventions.kt
│ ├── UiModuleConventions.kt
│ └── internal
│ ├── Android.kt
│ ├── Compose.kt
│ ├── Hilt.kt
│ ├── Navigation.kt
│ ├── ProjectExtension.kt
│ └── UnitTests.kt
├── build.gradle.kts
├── core
├── forms
│ └── build.gradle.kts
├── intent
│ └── build.gradle.kts
└── ui
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── res
│ └── values
│ └── strings.xml
├── data
├── addresses
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── maruchin
│ │ └── data
│ │ └── addresses
│ │ ├── Address.kt
│ │ ├── AddressesRepository.kt
│ │ ├── SampleData.kt
│ │ └── internal
│ │ ├── DataAddressesModule.kt
│ │ └── FakeAddressesRepository.kt
├── cart
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── maruchin
│ │ └── data
│ │ └── cart
│ │ ├── Cart.kt
│ │ ├── CartProduct.kt
│ │ ├── CartRepository.kt
│ │ ├── SampleData.kt
│ │ └── internal
│ │ ├── DataCartModule.kt
│ │ └── FakeCartRepository.kt
├── categories
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── maruchin
│ │ └── data
│ │ └── categories
│ │ ├── CategoriesRepository.kt
│ │ ├── Category.kt
│ │ ├── SampleData.kt
│ │ └── internal
│ │ ├── DataCategoriesModule.kt
│ │ └── FakeCategoriesRepository.kt
├── deliveries
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── data
│ │ │ └── deliveries
│ │ │ ├── DeliveriesRepository.kt
│ │ │ ├── Delivery.kt
│ │ │ ├── SampleData.kt
│ │ │ └── internal
│ │ │ ├── DataDeliveriesModule.kt
│ │ │ └── FakeDeliveriesRepository.kt
│ │ └── res
│ │ └── drawable
│ │ ├── dhl_logo.jpeg
│ │ ├── gls_logo.jpeg
│ │ └── ups_logo.png
├── order
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── maruchin
│ │ └── data
│ │ └── order
│ │ ├── Order.kt
│ │ ├── OrderProduct.kt
│ │ ├── OrderRepository.kt
│ │ ├── SampleData.kt
│ │ └── internal
│ │ ├── DataOrderModule.kt
│ │ └── FakeOrderRepository.kt
├── payments
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── data
│ │ │ └── payments
│ │ │ ├── Payment.kt
│ │ │ ├── PaymentsRepository.kt
│ │ │ ├── SampleData.kt
│ │ │ └── internal
│ │ │ ├── DataPaymentsModule.kt
│ │ │ └── FakePaymentsRepository.kt
│ │ └── res
│ │ └── drawable
│ │ ├── google_pay_logo.png
│ │ ├── mastercard_logo.png
│ │ ├── paypal_logo.png
│ │ └── visa_logo.jpeg
├── products
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── data
│ │ │ └── products
│ │ │ ├── Product.kt
│ │ │ ├── ProductFilters.kt
│ │ │ ├── ProductFiltersRepository.kt
│ │ │ ├── ProductsRepository.kt
│ │ │ ├── Rating.kt
│ │ │ ├── SampleData.kt
│ │ │ └── internal
│ │ │ ├── DataProductsModule.kt
│ │ │ ├── FakeProductFiltersRepository.kt
│ │ │ └── FakeProductsRepository.kt
│ │ └── res
│ │ └── drawable
│ │ └── product_image.jpg
├── promotions
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── maruchin
│ │ └── data
│ │ └── promotions
│ │ ├── Promotion.kt
│ │ ├── PromotionsRepository.kt
│ │ ├── SampleData.kt
│ │ └── internal
│ │ ├── DataPromotionsModule.kt
│ │ └── FakePromotionsRepository.kt
└── user
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── java
│ └── com
│ └── maruchin
│ └── data
│ └── user
│ ├── ClubData.kt
│ ├── ClubLevel.kt
│ ├── PersonalData.kt
│ ├── SampleData.kt
│ ├── User.kt
│ ├── UserRepository.kt
│ ├── ValidateEmail.kt
│ ├── ValidatePassword.kt
│ └── internal
│ ├── DataUserModule.kt
│ └── FakeUserRepository.kt
├── features
├── cart
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── cart
│ │ │ ├── CartGraph.kt
│ │ │ ├── CartNavigation.kt
│ │ │ ├── CartScreen.kt
│ │ │ └── CartViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── category-browser
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── categorybrowser
│ │ │ ├── CategoryBrowserGraph.kt
│ │ │ ├── categorylist
│ │ │ ├── CategoryList.kt
│ │ │ ├── CategoryListNavigation.kt
│ │ │ ├── CategoryListScreen.kt
│ │ │ └── CategoryListViewModel.kt
│ │ │ └── subcategorylist
│ │ │ ├── SubcategoryListNavigation.kt
│ │ │ ├── SubcategoryListScreen.kt
│ │ │ └── SubcategoryListViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── favorites
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── favorites
│ │ │ ├── FavoritesGraph.kt
│ │ │ ├── FavoritesNavigation.kt
│ │ │ ├── FavoritesScreen.kt
│ │ │ └── FavoritesViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── home
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── home
│ │ │ ├── HomeGraph.kt
│ │ │ ├── HomeNavigation.kt
│ │ │ ├── HomeScreen.kt
│ │ │ └── HomeViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── login
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── login
│ │ │ ├── LoginGraph.kt
│ │ │ ├── changepassword
│ │ │ ├── ChangePasswordNavigation.kt
│ │ │ ├── ChangePasswordScreen.kt
│ │ │ └── ChangePasswordViewModel.kt
│ │ │ ├── forgotpassword
│ │ │ ├── EmailSentDialog.kt
│ │ │ ├── ForgotPasswordNavigation.kt
│ │ │ ├── ForgotPasswordScreen.kt
│ │ │ └── ForgotPasswordViewModel.kt
│ │ │ └── login
│ │ │ ├── LoginNavigation.kt
│ │ │ ├── LoginScreen.kt
│ │ │ └── LoginViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── my-data
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── mydata
│ │ │ ├── MyDataGraph.kt
│ │ │ ├── addaddress
│ │ │ ├── AddAddressNavigation.kt
│ │ │ ├── AddAddressScreen.kt
│ │ │ └── AddAddressViewModel.kt
│ │ │ ├── changepassword
│ │ │ ├── ChangePasswordNavigation.kt
│ │ │ ├── ChangePasswordScreen.kt
│ │ │ └── ChangePasswordViewModel.kt
│ │ │ ├── deleteaccount
│ │ │ ├── DeleteAccountNavigation.kt
│ │ │ ├── DeleteAccountScreen.kt
│ │ │ └── DeleteAccountViewModel.kt
│ │ │ ├── editaddress
│ │ │ ├── EditAddressNavigation.kt
│ │ │ ├── EditAddressScreen.kt
│ │ │ └── EditAddressViewModel.kt
│ │ │ ├── editmydata
│ │ │ ├── EditMyDataNavigation.kt
│ │ │ ├── EditMyDataScreen.kt
│ │ │ └── EditMyDataViewModel.kt
│ │ │ ├── myaddresses
│ │ │ ├── MyAddressesNavigation.kt
│ │ │ ├── MyAddressesScreen.kt
│ │ │ └── MyAddressesViewModel.kt
│ │ │ └── mydata
│ │ │ ├── MyDataNavigation.kt
│ │ │ ├── MyDataScreen.kt
│ │ │ └── MyDataViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── order
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── order
│ │ │ ├── OrderGraph.kt
│ │ │ ├── address
│ │ │ ├── AddressNavigation.kt
│ │ │ ├── AddressScreen.kt
│ │ │ └── AddressViewModel.kt
│ │ │ ├── confirmation
│ │ │ ├── ConfirmationNavigation.kt
│ │ │ ├── ConfirmationScreen.kt
│ │ │ └── ConfirmationViewModel.kt
│ │ │ ├── delivery
│ │ │ ├── DeliveryNavigation.kt
│ │ │ ├── DeliveryScreen.kt
│ │ │ └── DeliveryViewModel.kt
│ │ │ ├── payment
│ │ │ ├── PaymentNavigation.kt
│ │ │ ├── PaymentScreen.kt
│ │ │ └── PaymentViewModel.kt
│ │ │ └── summary
│ │ │ ├── SummaryNavigation.kt
│ │ │ ├── SummaryScreen.kt
│ │ │ └── SummaryViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── product-browser
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── productbrowser
│ │ │ ├── ProductBrowserGraph.kt
│ │ │ ├── filters
│ │ │ ├── FiltersNavigation.kt
│ │ │ ├── FiltersScreen.kt
│ │ │ └── FiltersViewModel.kt
│ │ │ └── productlist
│ │ │ ├── ProductListNavigation.kt
│ │ │ ├── ProductListScreen.kt
│ │ │ └── ProductListViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── product-card
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── productcard
│ │ │ ├── ProductCardGraph.kt
│ │ │ ├── card
│ │ │ ├── CardNavigation.kt
│ │ │ ├── CardScreen.kt
│ │ │ └── CardViewModel.kt
│ │ │ └── gallery
│ │ │ ├── GalleryNavigation.kt
│ │ │ ├── GalleryScreen.kt
│ │ │ └── GalleryViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── profile
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── maruchin
│ │ │ └── features
│ │ │ └── profile
│ │ │ ├── ProfileGraph.kt
│ │ │ ├── club
│ │ │ ├── ClubPage.kt
│ │ │ └── ClubViewModel.kt
│ │ │ ├── clubauth
│ │ │ └── ClubAuthPage.kt
│ │ │ ├── findoutmore
│ │ │ ├── FindOutMoreNavigation.kt
│ │ │ ├── FindOutMorePagerState.kt
│ │ │ └── FindOutMoreScreen.kt
│ │ │ ├── mydata
│ │ │ └── MyDataPage.kt
│ │ │ ├── myorders
│ │ │ ├── MyOrdersNavigation.kt
│ │ │ └── MyOrdersScreen.kt
│ │ │ ├── profile
│ │ │ ├── ProfileNavigation.kt
│ │ │ ├── ProfileScreen.kt
│ │ │ ├── ProfileTabsState.kt
│ │ │ └── ProfileViewModel.kt
│ │ │ ├── promotion
│ │ │ ├── PromotionNavigation.kt
│ │ │ ├── PromotionScreen.kt
│ │ │ └── PromotionsViewModel.kt
│ │ │ ├── promotions
│ │ │ ├── PromotionsPage.kt
│ │ │ └── PromotionsViewModel.kt
│ │ │ ├── purchasehistory
│ │ │ ├── PurchaseHistoryNavigation.kt
│ │ │ └── PurchaseHistoryScreen.kt
│ │ │ └── returns
│ │ │ ├── ReturnsNavigation.kt
│ │ │ └── ReturnsScreen.kt
│ │ └── res
│ │ ├── drawable
│ │ └── club_auth_cover.jpg
│ │ └── values
│ │ └── strings.xml
└── registration
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── java
│ └── com
│ │ └── maruchin
│ │ └── features
│ │ └── registration
│ │ ├── RegistrationGraph.kt
│ │ ├── birthdate
│ │ ├── BirthDateNavigation.kt
│ │ ├── BirthDateScreen.kt
│ │ └── BirthDateViewModel.kt
│ │ └── registrationform
│ │ ├── RegistrationFormNavigation.kt
│ │ ├── RegistrationFormScreen.kt
│ │ └── RegistrationViewModel.kt
│ └── res
│ └── values
│ └── strings.xml
├── forms
├── build.gradle.kts
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── maruchin
│ │ └── forms
│ │ ├── addressform
│ │ ├── AddressForm.kt
│ │ └── AddressFormState.kt
│ │ ├── changepasswordform
│ │ ├── ChangePasswordForm.kt
│ │ └── ChangePasswordFormState.kt
│ │ ├── datefield
│ │ ├── DateField.kt
│ │ └── DateFieldState.kt
│ │ ├── emailfield
│ │ ├── EmailField.kt
│ │ └── EmailFieldState.kt
│ │ ├── loginform
│ │ ├── LoginForm.kt
│ │ └── LoginFormState.kt
│ │ ├── passwordfield
│ │ ├── PasswordField.kt
│ │ └── PasswordFieldState.kt
│ │ ├── passwordsform
│ │ ├── PasswordsForm.kt
│ │ └── PasswordsFormState.kt
│ │ ├── personaldataform
│ │ ├── PersonalDataForm.kt
│ │ └── PersonalDataFormState.kt
│ │ ├── registrationform
│ │ ├── RegistrationForm.kt
│ │ └── RegistrationFormState.kt
│ │ └── textfield
│ │ ├── TextField.kt
│ │ └── TextFieldState.kt
│ └── res
│ └── values
│ └── strings.xml
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── ui
├── build.gradle.kts
└── src
└── main
├── java
└── com
│ └── maruchin
│ └── ui
│ ├── AddressItem.kt
│ ├── AllProductsButton.kt
│ ├── Deeplink.kt
│ ├── DeliveryItem.kt
│ ├── NavigationTransitions.kt
│ ├── OpenEmailApp.kt
│ ├── OpenWebsite.kt
│ ├── OrderProductItem.kt
│ ├── PaymentItem.kt
│ ├── ProductGrid.kt
│ ├── ProductItem.kt
│ └── ScreenContentPlaceholder.kt
└── res
└── values
└── strings.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | build/
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | .idea/**/*
16 | local.properties
17 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.appmodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.androidnavigation"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 |
12 | implementation(project(":features:home"))
13 | implementation(project(":features:category-browser"))
14 | implementation(project(":features:product-browser"))
15 | implementation(project(":features:product-card"))
16 | implementation(project(":features:login"))
17 | implementation(project(":features:profile"))
18 | implementation(project(":features:my-data"))
19 | implementation(project(":features:registration"))
20 | implementation(project(":features:cart"))
21 | implementation(project(":features:order"))
22 | implementation(project(":features:favorites"))
23 | }
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/maruchin/androidnavigation/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.androidnavigation
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.material3.MaterialTheme
7 | import com.maruchin.androidnavigation.root.RootHost
8 | import dagger.hilt.android.AndroidEntryPoint
9 |
10 | @AndroidEntryPoint
11 | class MainActivity : ComponentActivity() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContent {
16 | MaterialTheme {
17 | RootHost()
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/maruchin/androidnavigation/SampleApplication.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.androidnavigation
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class SampleApplication : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/maruchin/androidnavigation/navigationbar/NavigationBarState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.androidnavigation.navigationbar
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.remember
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraph.Companion.findStartDestination
8 | import com.maruchin.features.cart.CART_GRAPH_ROUTE
9 | import com.maruchin.features.categorybrowser.CATEGORY_BROWSER_GRAPH_ROUTE
10 | import com.maruchin.features.favorites.FAVORITES_GRAPH_ROUTE
11 | import com.maruchin.features.home.HOME_GRAPH_ROUTE
12 | import com.maruchin.features.profile.PROFILE_GRAPH_ROUTE
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.map
15 |
16 | @Stable
17 | internal class NavigationBarState(private val navController: NavController) {
18 | private val navigationBarRoutes = listOf(
19 | HOME_GRAPH_ROUTE,
20 | CATEGORY_BROWSER_GRAPH_ROUTE,
21 | FAVORITES_GRAPH_ROUTE,
22 | CART_GRAPH_ROUTE,
23 | PROFILE_GRAPH_ROUTE,
24 | )
25 |
26 | fun isRouteSelected(route: String): Flow {
27 | return navController.currentBackStack.map { backStack ->
28 | backStack
29 | .map { it.destination.route }
30 | .lastOrNull { navigationBarRoutes.contains(it) }
31 | .let { it == route }
32 | }
33 | }
34 |
35 | fun openRoute(route: String) {
36 | navController.navigate(route) {
37 | popUpTo(navController.graph.findStartDestination().id) {
38 | saveState = true
39 | }
40 | launchSingleTop = true
41 | restoreState = true
42 | }
43 | }
44 | }
45 |
46 | @Composable
47 | internal fun rememberNavigationBarState(navController: NavController): NavigationBarState {
48 | return remember(navController) {
49 | NavigationBarState(navController)
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | android-navigation
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build-logic/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | group = "com.maruchin.buildlogic"
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_17
9 | targetCompatibility = JavaVersion.VERSION_17
10 | }
11 |
12 | dependencies {
13 | compileOnly(libs.android.tools.build.gradle.plugin)
14 | compileOnly(libs.kotlin.gradle.plugin)
15 | }
16 |
17 | gradlePlugin {
18 | plugins {
19 | register("dataModule") {
20 | id = "buildlogic.datamodule"
21 | implementationClass = "DataModuleConventions"
22 | }
23 | register("featureModule") {
24 | id = "buildlogic.featuremodule"
25 | implementationClass = "FeatureModuleConventions"
26 | }
27 | register("uiModule") {
28 | id = "buildlogic.uimodule"
29 | implementationClass = "UiModuleConventions"
30 | }
31 | register("appModule") {
32 | id = "buildlogic.appmodule"
33 | implementationClass = "AppModuleConventions"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel=true
2 | org.gradle.caching=true
3 | org.gradle.configureondemand=true
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | versionCatalogs {
7 | create("libs") {
8 | from(files("../gradle/libs.versions.toml"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/AppModuleConventions.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
2 | import internal.configureAndroid
3 | import internal.configureCompose
4 | import internal.configureHilt
5 | import internal.configureNavigation
6 | import internal.configureUnitTest
7 | import org.gradle.api.Plugin
8 | import org.gradle.api.Project
9 | import org.gradle.kotlin.dsl.configure
10 |
11 | class AppModuleConventions : Plugin {
12 | override fun apply(target: Project) = with(target) {
13 | with(pluginManager) {
14 | apply("com.android.application")
15 | }
16 |
17 | extensions.configure {
18 | defaultConfig {
19 | applicationId = "com.maruchin.androidnavigation"
20 | targetSdk = 34
21 | versionCode = 1
22 | versionName = "1.0"
23 |
24 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
25 | vectorDrawables {
26 | useSupportLibrary = true
27 | }
28 | }
29 | buildTypes {
30 | release {
31 | isMinifyEnabled = false
32 | proguardFiles(
33 | getDefaultProguardFile("proguard-android-optimize.txt"),
34 | "proguard-rules.pro"
35 | )
36 | }
37 | }
38 | packaging {
39 | resources {
40 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
41 | }
42 | }
43 | }
44 |
45 | configureAndroid()
46 | configureHilt()
47 | configureCompose()
48 | configureNavigation()
49 | configureUnitTest()
50 | }
51 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/DataModuleConventions.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.LibraryExtension
2 | import internal.configureAndroid
3 | import internal.configureHilt
4 | import internal.configureUnitTest
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | class DataModuleConventions : Plugin {
9 | override fun apply(target: Project) = with(target) {
10 | with(pluginManager) {
11 | apply("com.android.library")
12 | }
13 |
14 | configureAndroid()
15 | configureHilt()
16 | configureUnitTest()
17 | }
18 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/FeatureModuleConventions.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.LibraryExtension
2 | import internal.configureAndroid
3 | import internal.configureCompose
4 | import internal.configureHilt
5 | import internal.configureNavigation
6 | import internal.configureUnitTest
7 | import org.gradle.api.Plugin
8 | import org.gradle.api.Project
9 |
10 | class FeatureModuleConventions : Plugin {
11 | override fun apply(target: Project) = with(target) {
12 | with(pluginManager) {
13 | apply("com.android.library")
14 | }
15 |
16 | configureAndroid()
17 | configureHilt()
18 | configureUnitTest()
19 | configureCompose()
20 | configureNavigation()
21 | }
22 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/UiModuleConventions.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.LibraryExtension
2 | import internal.configureAndroid
3 | import internal.configureCompose
4 | import internal.configureNavigation
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | class UiModuleConventions : Plugin {
9 | override fun apply(target: Project) = with(target) {
10 | with(pluginManager) {
11 | apply("com.android.library")
12 | }
13 |
14 | configureAndroid()
15 | configureCompose()
16 | configureNavigation()
17 | }
18 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/Android.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import com.android.build.api.dsl.CommonExtension
4 | import org.gradle.api.JavaVersion
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.configure
7 | import org.gradle.kotlin.dsl.getByType
8 | import org.jetbrains.kotlin.gradle.plugin.KaptExtension
9 |
10 | internal inline fun > Project.configureAndroid() {
11 | with(pluginManager) {
12 | apply("org.jetbrains.kotlin.android")
13 | apply("org.jetbrains.kotlin.kapt")
14 | }
15 |
16 | extensions.configure {
17 | compileSdk = 34
18 |
19 | defaultConfig {
20 | minSdk = 26
21 | }
22 |
23 | compileOptions {
24 | sourceCompatibility = JavaVersion.VERSION_17
25 | targetCompatibility = JavaVersion.VERSION_17
26 | }
27 | }
28 |
29 | val kaptExtension = extensions.getByType()
30 | kaptExtension.apply {
31 | correctErrorTypes = true
32 | }
33 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/Compose.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import com.android.build.api.dsl.CommonExtension
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.configure
6 | import org.gradle.kotlin.dsl.dependencies
7 |
8 | internal inline fun > Project.configureCompose() {
9 | extensions.configure {
10 | buildFeatures {
11 | compose = true
12 | }
13 | composeOptions {
14 | kotlinCompilerExtensionVersion = "1.4.3"
15 | }
16 | }
17 |
18 | dependencies {
19 | add("implementation", platform(getLibrary("compose.bom")))
20 | add("implementation", getLibrary("compose.ui"))
21 | add("implementation", getLibrary("compose.graphics"))
22 | add("implementation", getLibrary("compose.preview"))
23 | add("implementation", getLibrary("compose.material"))
24 | add("implementation", getLibrary("compose.icons"))
25 | add("implementation", getLibrary("coil.compose"))
26 | add("implementation", getLibrary("androidx.lifecycle"))
27 | add("implementation", getLibrary("androidx.activity"))
28 | add("implementation", getLibrary("androidx.browser"))
29 | add("debugImplementation", getLibrary("compose.ui.tooling"))
30 | }
31 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/Hilt.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | internal fun Project.configureHilt() {
7 | with(pluginManager) {
8 | apply("dagger.hilt.android.plugin")
9 | }
10 |
11 | dependencies {
12 | add("implementation", getLibrary("hilt"))
13 | add("kapt", getLibrary("hilt.compiler"))
14 | }
15 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/Navigation.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | internal fun Project.configureNavigation() {
7 | dependencies {
8 | add("implementation", getLibrary("hilt.navigation.compose"))
9 | add("implementation",getLibrary("compose.navigation"))
10 | }
11 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/ProjectExtension.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalog
5 | import org.gradle.api.artifacts.VersionCatalogsExtension
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | internal val Project.libs
9 | get(): VersionCatalog = extensions.getByType().named("libs")
10 |
11 | internal fun Project.getLibrary(name: String) = libs.findLibrary(name).get()
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/internal/UnitTests.kt:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | internal fun Project.configureUnitTest() {
7 | dependencies {
8 | add("testImplementation", getLibrary("junit"))
9 | add("testImplementation", getLibrary("kotlinx.coroutines.test"))
10 | add("testImplementation", getLibrary("turbine"))
11 | }
12 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.android.library) apply false
5 | alias(libs.plugins.kotlin.android) apply false
6 | alias(libs.plugins.hilt) apply false
7 | alias(libs.plugins.serialization) apply false
8 | }
--------------------------------------------------------------------------------
/core/forms/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | alias(libs.plugins.android.library)
4 | alias(libs.plugins.kotlin.android)
5 | }
6 |
7 | android {
8 | namespace = "com.maruchin.core.forms"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | minSdk = 26
13 | }
14 |
15 | buildTypes {
16 | release {
17 | isMinifyEnabled = false
18 | }
19 | }
20 | compileOptions {
21 | sourceCompatibility = JavaVersion.VERSION_17
22 | targetCompatibility = JavaVersion.VERSION_17
23 | }
24 | kotlinOptions {
25 | jvmTarget = "17"
26 | }
27 | buildFeatures {
28 | compose = true
29 | }
30 | composeOptions {
31 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
32 | }
33 | }
34 |
35 | dependencies {
36 | api(project(":data:addresses"))
37 | api(project(":data:user"))
38 |
39 | implementation(platform(libs.compose.bom))
40 | implementation(libs.bundles.ui)
41 | implementation(libs.bundles.navigation)
42 |
43 | debugImplementation(libs.compose.ui.tooling)
44 | }
--------------------------------------------------------------------------------
/core/intent/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | alias(libs.plugins.android.library)
4 | alias(libs.plugins.kotlin.android)
5 | }
6 |
7 | android {
8 | namespace = "com.maruchin.core.intent"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | minSdk = 26
13 | }
14 |
15 | buildTypes {
16 | release {
17 | isMinifyEnabled = false
18 | }
19 | }
20 | compileOptions {
21 | sourceCompatibility = JavaVersion.VERSION_17
22 | targetCompatibility = JavaVersion.VERSION_17
23 | }
24 | kotlinOptions {
25 | jvmTarget = "17"
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation(libs.androidx.core)
31 | implementation(libs.androidx.browser)
32 | }
--------------------------------------------------------------------------------
/core/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | alias(libs.plugins.android.library)
4 | alias(libs.plugins.kotlin.android)
5 | }
6 |
7 | android {
8 | namespace = "com.maruchin.core.ui"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | minSdk = 26
13 | }
14 |
15 | buildTypes {
16 | release {
17 | isMinifyEnabled = false
18 | }
19 | }
20 | compileOptions {
21 | sourceCompatibility = JavaVersion.VERSION_17
22 | targetCompatibility = JavaVersion.VERSION_17
23 | }
24 | kotlinOptions {
25 | jvmTarget = "17"
26 | }
27 |
28 | buildFeatures {
29 | compose = true
30 | }
31 |
32 | composeOptions {
33 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
34 | }
35 | }
36 |
37 | dependencies {
38 | implementation(project(":data:products"))
39 | implementation(project(":data:deliveries"))
40 | implementation(project(":data:payments"))
41 | implementation(project(":data:user"))
42 | implementation(project(":data:addresses"))
43 |
44 | api(platform(libs.compose.bom))
45 | api(libs.bundles.ui)
46 | api(libs.bundles.navigation)
47 |
48 | debugApi(libs.compose.ui.tooling)
49 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | New password
4 | Repeat new password
5 | Current password
6 | First name
7 | Last name
8 | Phone number
9 | Password
10 | Repeat password
11 | City
12 | Postal code
13 | Apartment
14 | House
15 | Street
16 | Email
17 |
--------------------------------------------------------------------------------
/data/addresses/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.addresses"
7 | }
8 |
--------------------------------------------------------------------------------
/data/addresses/src/main/java/com/maruchin/data/addresses/Address.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.addresses
2 |
3 | data class Address(
4 | val id: String,
5 | val firstName: String,
6 | val lastName: String,
7 | val street: String,
8 | val house: String,
9 | val apartment: String?,
10 | val postalCode: String,
11 | val city: String,
12 | )
13 |
--------------------------------------------------------------------------------
/data/addresses/src/main/java/com/maruchin/data/addresses/AddressesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.addresses
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface AddressesRepository {
6 |
7 | fun getAll(): Flow>
8 |
9 | fun getById(id: String): Flow
10 |
11 | suspend fun save(address: Address)
12 | }
--------------------------------------------------------------------------------
/data/addresses/src/main/java/com/maruchin/data/addresses/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.addresses
2 |
3 | val sampleAddress = Address(
4 | id = "1",
5 | firstName = "John",
6 | lastName = "Doe",
7 | street = "Main Street",
8 | house = "1",
9 | apartment = null,
10 | postalCode = "12-345",
11 | city = "New York",
12 | )
--------------------------------------------------------------------------------
/data/addresses/src/main/java/com/maruchin/data/addresses/internal/DataAddressesModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.addresses.internal
2 |
3 | import com.maruchin.data.addresses.AddressesRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataAddressesModule {
12 |
13 | @Binds
14 | fun bindAddressesRepository(impl: FakeAddressesRepository): AddressesRepository
15 | }
--------------------------------------------------------------------------------
/data/addresses/src/main/java/com/maruchin/data/addresses/internal/FakeAddressesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.addresses.internal
2 |
3 | import com.maruchin.data.addresses.Address
4 | import com.maruchin.data.addresses.AddressesRepository
5 | import com.maruchin.data.addresses.sampleAddress
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.coroutines.flow.update
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | internal class FakeAddressesRepository @Inject constructor() : AddressesRepository {
15 |
16 | private val addresses = MutableStateFlow(listOf(sampleAddress))
17 |
18 | override fun getAll(): Flow> {
19 | return addresses
20 | }
21 |
22 | override fun getById(id: String): Flow {
23 | return addresses.map { list ->
24 | list.find { it.id == id }
25 | }
26 | }
27 |
28 | override suspend fun save(address: Address) {
29 | val existingAddress = addresses.value.find { it.id == address.id }
30 | if (existingAddress == null) {
31 | addresses.update {
32 | it + address
33 | }
34 | } else {
35 | addresses.update { list ->
36 | list.map { if (it.id == address.id) address else it }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/data/cart/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.cart"
7 | }
8 |
9 | dependencies {
10 | api(project(":data:products"))
11 | }
--------------------------------------------------------------------------------
/data/cart/src/main/java/com/maruchin/data/cart/Cart.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.cart
2 |
3 | data class Cart(
4 | val products: List = emptyList()
5 | ) {
6 |
7 | val totalPrice: Double
8 | get() = products.sumOf { it.product.price.toDouble() }
9 | }
10 |
--------------------------------------------------------------------------------
/data/cart/src/main/java/com/maruchin/data/cart/CartProduct.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.cart
2 |
3 | import com.maruchin.data.products.Product
4 |
5 | data class CartProduct(
6 | val product: Product,
7 | val quantity: Int,
8 | )
9 |
--------------------------------------------------------------------------------
/data/cart/src/main/java/com/maruchin/data/cart/CartRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.cart
2 |
3 | import com.maruchin.data.products.Product
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface CartRepository {
7 |
8 | fun get(): Flow
9 |
10 | suspend fun addProduct(product: Product)
11 |
12 | suspend fun removeProduct(productId: String)
13 |
14 | suspend fun increaseProductQuantity(productId: String)
15 |
16 | suspend fun decreaseProductQuantity(productId: String)
17 | }
--------------------------------------------------------------------------------
/data/cart/src/main/java/com/maruchin/data/cart/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.cart
2 |
3 | import com.maruchin.data.products.sampleProducts
4 |
5 | val sampleCart = Cart(
6 | products = listOf(
7 | CartProduct(
8 | product = sampleProducts[0],
9 | quantity = 1,
10 | ),
11 | CartProduct(
12 | product = sampleProducts[1],
13 | quantity = 2,
14 | ),
15 | )
16 | )
17 |
18 | val sampleCartWithFixedPrice = Cart(
19 | products = listOf(
20 | CartProduct(
21 | product = sampleProducts[0].copy(price = 10.0),
22 | quantity = 1,
23 | ),
24 | CartProduct(
25 | product = sampleProducts[1].copy(price = 20.0),
26 | quantity = 2,
27 | ),
28 | )
29 | )
30 |
--------------------------------------------------------------------------------
/data/cart/src/main/java/com/maruchin/data/cart/internal/DataCartModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.cart.internal
2 |
3 | import com.maruchin.data.cart.CartRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataCartModule {
12 |
13 | @Binds
14 | fun bindCartRepository(impl: FakeCartRepository): CartRepository
15 | }
--------------------------------------------------------------------------------
/data/categories/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.categories"
7 | }
8 |
--------------------------------------------------------------------------------
/data/categories/src/main/java/com/maruchin/data/categories/CategoriesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.categories
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface CategoriesRepository {
6 |
7 | fun getAll(): Flow>
8 |
9 | fun getRecommended(): Flow>
10 |
11 | fun getById(id: String): Flow
12 | }
--------------------------------------------------------------------------------
/data/categories/src/main/java/com/maruchin/data/categories/Category.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.categories
2 |
3 | data class Category(
4 | val id: String,
5 | val name: String,
6 | val subcategories: List,
7 | ) {
8 |
9 | val isFinal: Boolean
10 | get() = subcategories.isEmpty()
11 | }
12 |
13 | fun List.flatten(): List {
14 | return flatMap { category ->
15 | val subcategories = category.subcategories
16 | listOf(category) + subcategories.flatten()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/data/categories/src/main/java/com/maruchin/data/categories/internal/DataCategoriesModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.categories.internal
2 |
3 | import com.maruchin.data.categories.CategoriesRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataCategoriesModule {
12 |
13 | @Binds
14 | fun categoriesRepository(impl: FakeCategoriesRepository): CategoriesRepository
15 | }
--------------------------------------------------------------------------------
/data/categories/src/main/java/com/maruchin/data/categories/internal/FakeCategoriesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.categories.internal
2 |
3 | import com.maruchin.data.categories.CategoriesRepository
4 | import com.maruchin.data.categories.Category
5 | import com.maruchin.data.categories.flatten
6 | import com.maruchin.data.categories.sampleCategories
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.map
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | internal class FakeCategoriesRepository @Inject constructor() : CategoriesRepository {
15 |
16 | private val categories = MutableStateFlow(sampleCategories)
17 |
18 | override fun getAll(): Flow> {
19 | return categories
20 | }
21 |
22 | override fun getRecommended(): Flow> {
23 | return categories.map { categories ->
24 | categories.flatten().filter { it.isFinal }
25 | }
26 | }
27 |
28 | override fun getById(id: String): Flow {
29 | return categories.map { categories ->
30 | categories.flatten().find { it.id == id }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/data/deliveries/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.deliveries"
7 | }
8 |
--------------------------------------------------------------------------------
/data/deliveries/src/main/java/com/maruchin/data/deliveries/DeliveriesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.deliveries
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface DeliveriesRepository {
6 |
7 | fun getAll(): Flow>
8 |
9 | fun getById(deliveryId: String): Flow
10 | }
--------------------------------------------------------------------------------
/data/deliveries/src/main/java/com/maruchin/data/deliveries/Delivery.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.deliveries
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class Delivery(
6 | val id: String,
7 | @DrawableRes
8 | val logo: Int,
9 | val name: String,
10 | val price: Float,
11 | )
12 |
--------------------------------------------------------------------------------
/data/deliveries/src/main/java/com/maruchin/data/deliveries/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.deliveries
2 |
3 | val sampleDeliveries = listOf(
4 | Delivery(
5 | id = "1",
6 | logo = R.drawable.dhl_logo,
7 | name = "DHL",
8 | price = 5f,
9 | ),
10 | Delivery(
11 | id = "2",
12 | logo = R.drawable.gls_logo,
13 | name = "GLS",
14 | price = 7f,
15 | ),
16 | Delivery(
17 | id = "3",
18 | logo = R.drawable.ups_logo,
19 | name = "UPS",
20 | price = 3f,
21 | ),
22 | )
--------------------------------------------------------------------------------
/data/deliveries/src/main/java/com/maruchin/data/deliveries/internal/DataDeliveriesModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.deliveries.internal
2 |
3 | import com.maruchin.data.deliveries.DeliveriesRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataDeliveriesModule {
12 |
13 | @Binds
14 | fun bindDeliveriesRepository(impl: FakeDeliveriesRepository): DeliveriesRepository
15 | }
--------------------------------------------------------------------------------
/data/deliveries/src/main/java/com/maruchin/data/deliveries/internal/FakeDeliveriesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.deliveries.internal
2 |
3 | import com.maruchin.data.deliveries.DeliveriesRepository
4 | import com.maruchin.data.deliveries.Delivery
5 | import com.maruchin.data.deliveries.sampleDeliveries
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.flowOf
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | internal class FakeDeliveriesRepository @Inject constructor() : DeliveriesRepository {
13 |
14 | override fun getAll(): Flow> {
15 | return flowOf(sampleDeliveries)
16 | }
17 |
18 | override fun getById(deliveryId: String): Flow {
19 | return flowOf(sampleDeliveries.first { it.id == deliveryId })
20 | }
21 | }
--------------------------------------------------------------------------------
/data/deliveries/src/main/res/drawable/dhl_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/deliveries/src/main/res/drawable/dhl_logo.jpeg
--------------------------------------------------------------------------------
/data/deliveries/src/main/res/drawable/gls_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/deliveries/src/main/res/drawable/gls_logo.jpeg
--------------------------------------------------------------------------------
/data/deliveries/src/main/res/drawable/ups_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/deliveries/src/main/res/drawable/ups_logo.png
--------------------------------------------------------------------------------
/data/order/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.order"
7 | }
8 |
9 | dependencies {
10 | api(project(":data:products"))
11 | api(project(":data:deliveries"))
12 | api(project(":data:cart"))
13 | api(project(":data:addresses"))
14 | api(project(":data:payments"))
15 | }
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/Order.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order
2 |
3 | import com.maruchin.data.addresses.Address
4 | import com.maruchin.data.deliveries.Delivery
5 | import com.maruchin.data.payments.Payment
6 |
7 | sealed interface Order {
8 |
9 | object None : Order
10 |
11 | data class InProgress(
12 | val products: List,
13 | val delivery: Delivery? = null,
14 | val address: Address? = null,
15 | val payment: Payment? = null,
16 | ) : Order {
17 |
18 | val totalPrice: Double
19 | get() {
20 | val productsPrice = products.sumOf { it.product.price.toDouble() * it.quantity }
21 | val deliveryPrice = delivery?.price ?: 0f
22 | return productsPrice + deliveryPrice
23 | }
24 | }
25 |
26 | data class Submitted(val orderNumber: String) : Order
27 | }
28 |
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/OrderProduct.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order
2 |
3 | import com.maruchin.data.products.Product
4 |
5 | data class OrderProduct(
6 | val product: Product,
7 | val quantity: Int,
8 | )
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/OrderRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order
2 |
3 | import com.maruchin.data.addresses.Address
4 | import com.maruchin.data.cart.CartProduct
5 | import com.maruchin.data.deliveries.Delivery
6 | import com.maruchin.data.payments.Payment
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | interface OrderRepository {
10 |
11 | fun get(): Flow
12 |
13 | suspend fun createNew(products: List)
14 |
15 | suspend fun selectDelivery(delivery: Delivery)
16 |
17 | suspend fun selectAddress(address: Address)
18 |
19 | suspend fun selectPayment(payment: Payment)
20 |
21 | suspend fun submit()
22 | }
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order
2 |
3 | import com.maruchin.data.addresses.sampleAddress
4 | import com.maruchin.data.deliveries.sampleDeliveries
5 | import com.maruchin.data.payments.samplePayments
6 | import com.maruchin.data.products.sampleProducts
7 |
8 | val sampleInProgressOrder = Order.InProgress(
9 | products = listOf(
10 | OrderProduct(product = sampleProducts[0], quantity = 1),
11 | OrderProduct(product = sampleProducts[1], quantity = 2),
12 | ),
13 | delivery = sampleDeliveries[0],
14 | address = sampleAddress,
15 | payment = samplePayments[0],
16 | )
17 |
18 | val sampleSubmittedOrder = Order.Submitted(orderNumber = "1234567890")
19 |
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/internal/DataOrderModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order.internal
2 |
3 | import com.maruchin.data.order.OrderRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataOrderModule {
12 |
13 | @Binds
14 | fun bindOrderRepository(impl: FakeOrderRepository): OrderRepository
15 | }
--------------------------------------------------------------------------------
/data/order/src/main/java/com/maruchin/data/order/internal/FakeOrderRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.order.internal
2 |
3 | import com.maruchin.data.addresses.Address
4 | import com.maruchin.data.cart.CartProduct
5 | import com.maruchin.data.deliveries.Delivery
6 | import com.maruchin.data.order.Order
7 | import com.maruchin.data.order.OrderProduct
8 | import com.maruchin.data.order.OrderRepository
9 | import com.maruchin.data.payments.Payment
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import javax.inject.Inject
13 | import javax.inject.Singleton
14 |
15 | @Singleton
16 | internal class FakeOrderRepository @Inject constructor() : OrderRepository {
17 |
18 | private val order = MutableStateFlow(Order.None)
19 |
20 | override fun get(): Flow {
21 | return order
22 | }
23 |
24 | override suspend fun createNew(products: List) {
25 | val orderProducts = products.map {
26 | OrderProduct(product = it.product, quantity = it.quantity)
27 | }
28 | order.value = Order.InProgress(products = orderProducts)
29 | }
30 |
31 | override suspend fun selectDelivery(delivery: Delivery) {
32 | val inProgressOrder = order.value as? Order.InProgress ?: return
33 | order.value = inProgressOrder.copy(delivery = delivery)
34 | }
35 |
36 | override suspend fun selectAddress(address: Address) {
37 | val inProgressOrder = order.value as? Order.InProgress ?: return
38 | order.value = inProgressOrder.copy(address = address)
39 | }
40 |
41 | override suspend fun selectPayment(payment: Payment) {
42 | val inProgressOrder = order.value as? Order.InProgress ?: return
43 | order.value = inProgressOrder.copy(payment = payment)
44 | }
45 |
46 | override suspend fun submit() {
47 | order.value = Order.Submitted(orderNumber = "123456789")
48 | }
49 | }
--------------------------------------------------------------------------------
/data/payments/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.payments"
7 | }
8 |
--------------------------------------------------------------------------------
/data/payments/src/main/java/com/maruchin/data/payments/Payment.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.payments
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class Payment(
6 | @DrawableRes
7 | val logo: Int,
8 | val name: String,
9 | )
--------------------------------------------------------------------------------
/data/payments/src/main/java/com/maruchin/data/payments/PaymentsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.payments
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface PaymentsRepository {
6 |
7 | fun getAll(): Flow>
8 | }
--------------------------------------------------------------------------------
/data/payments/src/main/java/com/maruchin/data/payments/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.payments
2 |
3 | val samplePayments = listOf(
4 | Payment(
5 | logo = R.drawable.google_pay_logo,
6 | name = "Google Pay",
7 | ),
8 | Payment(
9 | logo = R.drawable.paypal_logo,
10 | name = "PayPal",
11 | ),
12 | Payment(
13 | logo = R.drawable.visa_logo,
14 | name = "Visa",
15 | ),
16 | Payment(
17 | logo = R.drawable.mastercard_logo,
18 | name = "Mastercard",
19 | ),
20 | )
--------------------------------------------------------------------------------
/data/payments/src/main/java/com/maruchin/data/payments/internal/DataPaymentsModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.payments.internal
2 |
3 | import com.maruchin.data.payments.PaymentsRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataPaymentsModule {
12 |
13 | @Binds
14 | fun bindPaymentsRepository(impl: FakePaymentsRepository): PaymentsRepository
15 | }
--------------------------------------------------------------------------------
/data/payments/src/main/java/com/maruchin/data/payments/internal/FakePaymentsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.payments.internal
2 |
3 | import com.maruchin.data.payments.Payment
4 | import com.maruchin.data.payments.PaymentsRepository
5 | import com.maruchin.data.payments.samplePayments
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.flowOf
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | internal class FakePaymentsRepository @Inject constructor() : PaymentsRepository {
13 |
14 | override fun getAll(): Flow> {
15 | return flowOf(samplePayments)
16 | }
17 | }
--------------------------------------------------------------------------------
/data/payments/src/main/res/drawable/google_pay_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/payments/src/main/res/drawable/google_pay_logo.png
--------------------------------------------------------------------------------
/data/payments/src/main/res/drawable/mastercard_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/payments/src/main/res/drawable/mastercard_logo.png
--------------------------------------------------------------------------------
/data/payments/src/main/res/drawable/paypal_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/payments/src/main/res/drawable/paypal_logo.png
--------------------------------------------------------------------------------
/data/payments/src/main/res/drawable/visa_logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/payments/src/main/res/drawable/visa_logo.jpeg
--------------------------------------------------------------------------------
/data/products/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.products"
7 | }
8 |
9 | dependencies {
10 | api(project(":data:categories"))
11 | }
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/Product.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | data class Product(
4 | val id: String,
5 | val name: String,
6 | val description: String,
7 | val price: Double,
8 | val images: List,
9 | val categoryId: String,
10 | val rating: Rating,
11 | val isFavorite: Boolean,
12 | )
13 |
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/ProductFilters.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | data class ProductFilters(
4 | val sorting: Sorting = Sorting.ALPHABETICALLY,
5 | val price: Price = Price(min = null, max = null),
6 | ) {
7 |
8 | operator fun invoke(products: List): List {
9 | return products
10 | .let(sorting::invoke)
11 | .let(price::invoke)
12 | }
13 |
14 | enum class Sorting {
15 | ALPHABETICALLY, PRICE_FROM_THE_LOWEST, PRICE_FROM_THE_HIGHEST;
16 |
17 | operator fun invoke(products: List): List {
18 | return when (this) {
19 | ALPHABETICALLY -> products.sortedBy { it.name }
20 | PRICE_FROM_THE_LOWEST -> products.sortedBy { it.price }
21 | PRICE_FROM_THE_HIGHEST -> products.sortedByDescending { it.price }
22 | }
23 | }
24 | }
25 |
26 | data class Price(val min: Float?, val max: Float?) {
27 |
28 | operator fun invoke(products: List): List {
29 | return products.filter { product ->
30 | product.price in (min ?: Float.MIN_VALUE)..(max ?: Float.MAX_VALUE)
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/ProductFiltersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface ProductFiltersRepository {
6 |
7 | fun get(): Flow
8 |
9 | suspend fun updateSorting(sorting: ProductFilters.Sorting)
10 |
11 | suspend fun updatePrice(price: ProductFilters.Price)
12 | }
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/ProductsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface ProductsRepository {
6 |
7 | fun getForCategory(categoryId: String, filters: ProductFilters? = null): Flow>
8 |
9 | fun getRecommendedForCategory(categoryId: String): Flow>
10 |
11 | fun getFavorites(): Flow>
12 |
13 | fun findByTitle(title: String): Flow>
14 |
15 | fun getById(id: String): Flow
16 |
17 | suspend fun updateIsFavorite(id: String, isFavorite: Boolean)
18 | }
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/Rating.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | data class Rating(val rate: Float, val count: Int) {
4 |
5 | val stars: List
6 | get() = (1..MAX_RATE).map { index ->
7 | when {
8 | index <= rate -> STAR.STAR
9 | index - 1 < rate -> STAR.HALF_STAR
10 | else -> STAR.EMPTY_STAR
11 | }
12 | }
13 |
14 | enum class STAR {
15 | STAR, HALF_STAR, EMPTY_STAR
16 | }
17 |
18 | companion object {
19 | const val MAX_RATE = 5
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products
2 |
3 | import com.maruchin.data.categories.flatten
4 | import com.maruchin.data.categories.sampleCategories
5 | import kotlin.random.Random
6 |
7 | val categoriesIds = sampleCategories.flatten().filter { it.isFinal }.map { it.id }
8 |
9 | val sampleProducts = (0..1_000).map { index ->
10 | Product(
11 | id = index.toString(),
12 | name = "Lorem ipsum dolor sit amet",
13 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
14 | price = (1..2_500).random().toDouble(),
15 | images = (1..5).map { R.drawable.product_image },
16 | categoryId = categoriesIds.random(),
17 | rating = Rating(rate = 3.5f, count = 128),
18 | isFavorite = Random.nextBoolean(),
19 | )
20 | }
21 |
22 | val sampleFavoriteProducts = sampleProducts.filter { it.isFavorite }
23 |
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/internal/DataProductsModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products.internal
2 |
3 | import com.maruchin.data.products.ProductFiltersRepository
4 | import com.maruchin.data.products.ProductsRepository
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Module
11 | @InstallIn(SingletonComponent::class)
12 | internal interface DataProductsModule {
13 |
14 | @Binds
15 | fun productsRepository(impl: FakeProductsRepository): ProductsRepository
16 |
17 | @Binds
18 | fun productFiltersRepository(impl: FakeProductFiltersRepository): ProductFiltersRepository
19 | }
--------------------------------------------------------------------------------
/data/products/src/main/java/com/maruchin/data/products/internal/FakeProductFiltersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.products.internal
2 |
3 | import com.maruchin.data.products.ProductFilters
4 | import com.maruchin.data.products.ProductFiltersRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | internal class FakeProductFiltersRepository @Inject constructor() : ProductFiltersRepository {
12 |
13 | private val productFilters = MutableStateFlow(ProductFilters())
14 |
15 | override fun get(): Flow {
16 | return productFilters
17 | }
18 |
19 | override suspend fun updateSorting(sorting: ProductFilters.Sorting) {
20 | productFilters.value
21 | .copy(sorting = sorting)
22 | .let { productFilters.emit(it) }
23 | }
24 |
25 | override suspend fun updatePrice(price: ProductFilters.Price) {
26 | productFilters.value
27 | .copy(price = price)
28 | .let { productFilters.emit(it) }
29 | }
30 | }
--------------------------------------------------------------------------------
/data/products/src/main/res/drawable/product_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/data/products/src/main/res/drawable/product_image.jpg
--------------------------------------------------------------------------------
/data/promotions/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.promotions"
7 | }
8 |
--------------------------------------------------------------------------------
/data/promotions/src/main/java/com/maruchin/data/promotions/Promotion.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.promotions
2 |
3 | import java.net.URL
4 |
5 | data class Promotion(
6 | val id: String,
7 | val image: URL,
8 | val title: String,
9 | val description: String,
10 | val promoCode: String
11 | )
12 |
--------------------------------------------------------------------------------
/data/promotions/src/main/java/com/maruchin/data/promotions/PromotionsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.promotions
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface PromotionsRepository {
6 |
7 | fun getAvailable(): Flow>
8 |
9 | fun getById(id: String): Flow
10 | }
--------------------------------------------------------------------------------
/data/promotions/src/main/java/com/maruchin/data/promotions/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.promotions
2 |
3 | import java.net.URL
4 |
5 | val samplePromotions = (0 until 10).map {
6 | Promotion(
7 | id = it.toString(),
8 | image = URL("https://img.freepik.com/free-psd/new-collection-fashion-sale-web-banner-template_120329-1507.jpg"),
9 | title = "Lorem ipsum dolor sit amet",
10 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
11 | promoCode = "PROMO$it"
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/data/promotions/src/main/java/com/maruchin/data/promotions/internal/DataPromotionsModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.promotions.internal
2 |
3 | import com.maruchin.data.promotions.PromotionsRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataPromotionsModule {
12 |
13 | @Binds
14 | fun bindPromotionsRepository(fake: FakePromotionsRepository): PromotionsRepository
15 | }
--------------------------------------------------------------------------------
/data/promotions/src/main/java/com/maruchin/data/promotions/internal/FakePromotionsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.promotions.internal
2 |
3 | import com.maruchin.data.promotions.Promotion
4 | import com.maruchin.data.promotions.PromotionsRepository
5 | import com.maruchin.data.promotions.samplePromotions
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | internal class FakePromotionsRepository @Inject constructor() : PromotionsRepository {
14 |
15 | private val promotions = MutableStateFlow(samplePromotions)
16 |
17 | override fun getAvailable(): Flow> {
18 | return promotions
19 | }
20 |
21 | override fun getById(id: String): Flow {
22 | return promotions.map { promotions ->
23 | promotions.first { it.id == id }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/data/user/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.datamodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.data.user"
7 | }
8 |
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/ClubData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | import java.net.URL
4 | import java.time.LocalDate
5 |
6 | data class ClubData(
7 | val cardBarCode: URL,
8 | val clubLevel: ClubLevel,
9 | val birthDate: LocalDate?,
10 | )
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/ClubLevel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | enum class ClubLevel {
4 | STANDARD, SILVER, GOLD
5 | }
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/PersonalData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | data class PersonalData(
4 | val firstName: String,
5 | val lastName: String,
6 | val email: String,
7 | val phoneNumber: String,
8 | )
9 |
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/SampleData.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | import java.net.URL
4 |
5 | val sampleLoggedUser = User.LoggedIn(
6 | clubData = ClubData(
7 | cardBarCode = URL("https://static01.nyt.com/images/2013/01/06/magazine/WMT-UPC/WMT-UPC-articleLarge.png?quality=75&auto=webp&disable=upscale"),
8 | clubLevel = ClubLevel.STANDARD,
9 | birthDate = null,
10 | ),
11 | personalData = PersonalData(
12 | firstName = "John",
13 | lastName = "Doe",
14 | email = "john.doe@gmail.com",
15 | phoneNumber = "111222333",
16 | ),
17 | )
18 |
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/User.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | sealed interface User {
4 |
5 | object LoggedOut : User
6 |
7 | data class LoggedIn(
8 | val clubData: ClubData,
9 | val personalData: PersonalData,
10 | ) : User
11 | }
12 |
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import java.time.LocalDate
5 |
6 | interface UserRepository {
7 |
8 | fun get(): Flow
9 |
10 | suspend fun login(email: String, password: String)
11 |
12 | suspend fun register(personalData: PersonalData, password: String)
13 |
14 | suspend fun logout()
15 |
16 | suspend fun changePasswordWithToken(newPassword: String, token: String)
17 |
18 | suspend fun changePassword(currentPassword: String, newPassword: String)
19 |
20 | suspend fun deleteAccount()
21 |
22 | suspend fun updatePersonalData(personalData: PersonalData)
23 |
24 | suspend fun updateBirthDate(birthDate: LocalDate)
25 | }
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/ValidateEmail.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | private const val EMAIL_FORMAT = "^[A-Za-z0-9+_.-]+@(.+)\$"
4 |
5 | fun validateEmail(email: String): EmailValidationResult {
6 | return when {
7 | email.isEmpty() -> EmailValidationResult.EMPTY
8 | !email.matches(Regex(EMAIL_FORMAT)) -> EmailValidationResult.INVALID_FORMAT
9 | else -> EmailValidationResult.VALID
10 | }
11 |
12 | }
13 |
14 | fun isEmailValid(email: String): Boolean {
15 | return validateEmail(email) == EmailValidationResult.VALID
16 | }
17 |
18 | enum class EmailValidationResult {
19 | VALID, EMPTY, INVALID_FORMAT
20 | }
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/ValidatePassword.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user
2 |
3 | fun validatePassword(value: String): PasswordValidationResult {
4 | return when {
5 | value.isEmpty() -> PasswordValidationResult.EMPTY
6 | else -> PasswordValidationResult.VALID
7 | }
8 | }
9 |
10 | fun isPasswordValid(value: String): Boolean {
11 | return validatePassword(value) == PasswordValidationResult.VALID
12 | }
13 |
14 | fun arePasswordsValid(first: String, second: String): Boolean {
15 | return isPasswordValid(first) && isPasswordValid(second) && first == second
16 | }
17 |
18 | enum class PasswordValidationResult {
19 | VALID, EMPTY
20 | }
21 |
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/internal/DataUserModule.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user.internal
2 |
3 | import com.maruchin.data.user.UserRepository
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 |
9 | @Module
10 | @InstallIn(SingletonComponent::class)
11 | internal interface DataUserModule {
12 |
13 | @Binds
14 | fun userRepository(impl: FakeUserRepository): UserRepository
15 | }
--------------------------------------------------------------------------------
/data/user/src/main/java/com/maruchin/data/user/internal/FakeUserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.data.user.internal
2 |
3 | import com.maruchin.data.user.PersonalData
4 | import com.maruchin.data.user.User
5 | import com.maruchin.data.user.UserRepository
6 | import com.maruchin.data.user.sampleLoggedUser
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import java.time.LocalDate
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | internal class FakeUserRepository @Inject constructor() : UserRepository {
15 |
16 | private val user = MutableStateFlow(User.LoggedOut)
17 |
18 | override fun get(): Flow {
19 | return user
20 | }
21 |
22 | override suspend fun login(email: String, password: String) {
23 | user.emit(sampleLoggedUser)
24 | }
25 |
26 | override suspend fun register(personalData: PersonalData, password: String) {
27 | user.emit(sampleLoggedUser)
28 | }
29 |
30 | override suspend fun logout() {
31 | user.emit(User.LoggedOut)
32 | }
33 |
34 | override suspend fun changePasswordWithToken(newPassword: String, token: String) = Unit
35 |
36 | override suspend fun changePassword(currentPassword: String, newPassword: String) = Unit
37 |
38 | override suspend fun deleteAccount() {
39 | user.emit(User.LoggedOut)
40 | }
41 |
42 | override suspend fun updatePersonalData(personalData: PersonalData) {
43 | val loggedUser = user.value as? User.LoggedIn ?: return
44 | user.value = loggedUser.copy(
45 | personalData = personalData
46 | )
47 | }
48 |
49 | override suspend fun updateBirthDate(birthDate: LocalDate) {
50 | val loggedUser = user.value as? User.LoggedIn ?: return
51 | user.value = loggedUser.copy(
52 | clubData = loggedUser.clubData.copy(
53 | birthDate = birthDate
54 | )
55 | )
56 | }
57 | }
--------------------------------------------------------------------------------
/features/cart/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.cart"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:cart"))
12 | implementation(project(":data:order"))
13 | }
--------------------------------------------------------------------------------
/features/cart/src/main/java/com/maruchin/features/cart/CartGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.cart
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.navigation
5 | import androidx.navigation.navDeepLink
6 | import com.maruchin.ui.ROOT_DEEPLINK
7 | import com.maruchin.ui.screenFadeIn
8 | import com.maruchin.ui.screenFadeOut
9 |
10 | const val CART_GRAPH_ROUTE = "cart-graph"
11 | private const val CART_DEEPLINK = "$ROOT_DEEPLINK/cart"
12 |
13 | fun NavGraphBuilder.cartGraph(
14 | onNextClick: () -> Unit,
15 | onProductClick: (product: String) -> Unit,
16 | ) {
17 | navigation(
18 | startDestination = CART_ROUTE,
19 | route = CART_GRAPH_ROUTE,
20 | deepLinks = listOf(
21 | navDeepLink { uriPattern = CART_DEEPLINK }
22 | ),
23 | enterTransition = { screenFadeIn() },
24 | exitTransition = { screenFadeOut() },
25 | popEnterTransition = { screenFadeIn() },
26 | popExitTransition = { screenFadeOut() },
27 | ) {
28 | cartScreen(
29 | onNextClick = onNextClick,
30 | onProductClick = onProductClick,
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/features/cart/src/main/java/com/maruchin/features/cart/CartNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.cart
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val CART_ROUTE = "cart"
10 |
11 | internal fun NavGraphBuilder.cartScreen(
12 | onNextClick: () -> Unit,
13 | onProductClick: (productId: String) -> Unit,
14 | ) {
15 | composable(route = CART_ROUTE) {
16 | val viewModel: CartViewModel = hiltViewModel()
17 | val cart by viewModel.cart.collectAsState()
18 |
19 | CartScreen(
20 | cart = cart,
21 | onNextClick = {
22 | viewModel.createNewOrder()
23 | onNextClick()
24 | },
25 | onProductClick = onProductClick,
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/features/cart/src/main/java/com/maruchin/features/cart/CartViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.cart
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.cart.CartRepository
6 | import com.maruchin.data.order.OrderRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.first
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class CartViewModel @Inject constructor(
16 | private val cartRepository: CartRepository,
17 | private val orderRepository: OrderRepository,
18 | ) : ViewModel() {
19 |
20 | val cart = cartRepository.get()
21 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
22 |
23 | fun createNewOrder() = viewModelScope.launch {
24 | val cart = cartRepository.get().first()
25 | orderRepository.createNew(cart.products)
26 | }
27 | }
--------------------------------------------------------------------------------
/features/cart/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cart
4 | Total to be paid
5 | Add promo code
6 | Next
7 |
--------------------------------------------------------------------------------
/features/category-browser/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.categorybrowser"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:categories"))
12 | }
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/CategoryBrowserGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.navigation
6 | import androidx.navigation.navDeepLink
7 | import com.maruchin.features.categorybrowser.categorylist.CATEGORY_LIST_ROUTE
8 | import com.maruchin.features.categorybrowser.categorylist.categoryListScreen
9 | import com.maruchin.features.categorybrowser.subcategorylist.navigateToSubcategoryList
10 | import com.maruchin.features.categorybrowser.subcategorylist.subcategoryListScreen
11 | import com.maruchin.ui.ROOT_DEEPLINK
12 | import com.maruchin.ui.screenFadeIn
13 | import com.maruchin.ui.screenFadeOut
14 |
15 | const val CATEGORY_BROWSER_GRAPH_ROUTE = "category-browser-graph"
16 | private const val CATEGORY_BROWSER_DEEPLINK = "$ROOT_DEEPLINK/category-browser"
17 |
18 | fun NavGraphBuilder.categoryBrowserGraph(
19 | navController: NavController,
20 | onFinalCategoryClick: (categoryId: String) -> Unit,
21 | ) {
22 |
23 | fun onCategoryClick(categoryId: String, isFinal: Boolean) {
24 | if (isFinal) {
25 | onFinalCategoryClick(categoryId)
26 | } else {
27 | navController.navigateToSubcategoryList(categoryId)
28 | }
29 | }
30 |
31 | navigation(
32 | startDestination = CATEGORY_LIST_ROUTE,
33 | route = CATEGORY_BROWSER_GRAPH_ROUTE,
34 | deepLinks = listOf(
35 | navDeepLink { uriPattern = CATEGORY_BROWSER_DEEPLINK }
36 | ),
37 | enterTransition = { screenFadeIn() },
38 | exitTransition = { screenFadeOut() },
39 | popEnterTransition = { screenFadeIn() },
40 | popExitTransition = { screenFadeOut() },
41 | ) {
42 | categoryListScreen(
43 | onCategoryClick = ::onCategoryClick,
44 | )
45 | subcategoryListScreen(
46 | onBackClick = {
47 | navController.popBackStack()
48 | },
49 | onCategoryClick = ::onCategoryClick,
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/categorylist/CategoryListNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser.categorylist
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val CATEGORY_LIST_ROUTE = "category-list"
10 |
11 | internal fun NavGraphBuilder.categoryListScreen(
12 | onCategoryClick: (categoryId: String, isFinal: Boolean) -> Unit,
13 | ) {
14 | composable(route = CATEGORY_LIST_ROUTE) {
15 | val viewModel: CategoryListViewModel = hiltViewModel()
16 | val categories by viewModel.categories.collectAsState()
17 |
18 | CategoryListScreen(
19 | categories = categories,
20 | onCategoryClick = onCategoryClick,
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/categorylist/CategoryListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser.categorylist
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.CenterAlignedTopAppBar
5 | import androidx.compose.material3.ExperimentalMaterial3Api
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Scaffold
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import com.maruchin.data.categories.Category
14 | import com.maruchin.data.categories.sampleCategories
15 | import com.maruchin.features.categorybrowser.R
16 |
17 | @Composable
18 | internal fun CategoryListScreen(
19 | categories: List,
20 | onCategoryClick: (categoryId: String, isFinal: Boolean) -> Unit,
21 | ) {
22 | Scaffold(
23 | topBar = {
24 | TopBar()
25 | }
26 | ) { padding ->
27 | CategoryList(
28 | categories = categories,
29 | modifier = Modifier.padding(padding),
30 | onCategoryClick = onCategoryClick,
31 | )
32 | }
33 | }
34 |
35 | @OptIn(ExperimentalMaterial3Api::class)
36 | @Composable
37 | private fun TopBar() {
38 | CenterAlignedTopAppBar(
39 | title = {
40 | Text(text = stringResource(R.string.categories))
41 | },
42 | )
43 | }
44 |
45 | @Preview
46 | @Composable
47 | private fun CategoriesScreenPreview() {
48 | MaterialTheme {
49 | CategoryListScreen(
50 | categories = sampleCategories,
51 | onCategoryClick = { _, _ -> },
52 | )
53 | }
54 | }
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/categorylist/CategoryListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser.categorylist
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.categories.CategoriesRepository
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | internal class CategoryListViewModel @Inject constructor(
13 | private val categoriesRepository: CategoriesRepository,
14 | ) : ViewModel() {
15 |
16 | val categories = categoriesRepository.getAll()
17 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
18 | }
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/subcategorylist/SubcategoryListNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser.subcategorylist
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.navigation.NavController
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.composable
10 |
11 | private const val CATEGORY_ID = "categoryId"
12 | internal const val SUBCATEGORY_LIST_ROUTE = "subcategory-list/{$CATEGORY_ID}"
13 |
14 | internal data class SubcategoryListArgs(val categoryId: String) {
15 | constructor(savedStateHandle: SavedStateHandle) : this(
16 | categoryId = checkNotNull(savedStateHandle[CATEGORY_ID])
17 | )
18 | }
19 |
20 | internal fun NavController.navigateToSubcategoryList(categoryId: String) {
21 | navigate(SUBCATEGORY_LIST_ROUTE.replace("{$CATEGORY_ID}", categoryId))
22 | }
23 |
24 | internal fun NavGraphBuilder.subcategoryListScreen(
25 | onBackClick: () -> Unit,
26 | onCategoryClick: (categoryId: String, isFinal: Boolean) -> Unit
27 | ) {
28 | composable(SUBCATEGORY_LIST_ROUTE) {
29 | val viewModel: SubcategoryListViewModel = hiltViewModel()
30 | val category by viewModel.category.collectAsState()
31 |
32 | SubcategoryListScreen(
33 | category = category,
34 | onBackClick = onBackClick,
35 | onCategoryClick = onCategoryClick,
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/features/category-browser/src/main/java/com/maruchin/features/categorybrowser/subcategorylist/SubcategoryListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.categorybrowser.subcategorylist
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.categories.CategoriesRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.stateIn
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | internal class SubcategoryListViewModel @Inject constructor(
14 | savedStateHandle: SavedStateHandle,
15 | private val categoriesRepository: CategoriesRepository,
16 | ) : ViewModel() {
17 |
18 | private val args = SubcategoryListArgs(savedStateHandle)
19 |
20 | val category = categoriesRepository.getById(args.categoryId)
21 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
22 | }
--------------------------------------------------------------------------------
/features/category-browser/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Categories
4 |
--------------------------------------------------------------------------------
/features/favorites/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.favorites"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:products"))
12 | }
--------------------------------------------------------------------------------
/features/favorites/src/main/java/com/maruchin/features/favorites/FavoritesGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.favorites
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.navigation
5 | import androidx.navigation.navDeepLink
6 | import com.maruchin.ui.ROOT_DEEPLINK
7 | import com.maruchin.ui.screenFadeIn
8 | import com.maruchin.ui.screenFadeOut
9 |
10 | const val FAVORITES_GRAPH_ROUTE = "favorites-graph"
11 | private const val FAVORITES_DEEPLINK = "$ROOT_DEEPLINK/favorites"
12 |
13 | fun NavGraphBuilder.favoritesGraph(onProductClick: (productId: String) -> Unit) {
14 | navigation(
15 | startDestination = FAVORITES_ROUTE,
16 | route = FAVORITES_GRAPH_ROUTE,
17 | deepLinks = listOf(
18 | navDeepLink { uriPattern = FAVORITES_DEEPLINK }
19 | ),
20 | enterTransition = { screenFadeIn() },
21 | exitTransition = { screenFadeOut() },
22 | popEnterTransition = { screenFadeIn() },
23 | popExitTransition = { screenFadeOut() },
24 | ) {
25 | favoritesScreen(
26 | onProductClick = onProductClick
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/features/favorites/src/main/java/com/maruchin/features/favorites/FavoritesNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.favorites
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val FAVORITES_ROUTE = "favorites"
10 |
11 | internal fun NavGraphBuilder.favoritesScreen(onProductClick: (productId: String) -> Unit) {
12 | composable(route = FAVORITES_ROUTE) {
13 | val viewModel: FavoritesViewModel = hiltViewModel()
14 | val products by viewModel.products.collectAsState()
15 |
16 | FavoritesScreen(products = products, onProductClick = onProductClick)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/features/favorites/src/main/java/com/maruchin/features/favorites/FavoritesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.favorites
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.products.ProductsRepository
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | internal class FavoritesViewModel @Inject constructor(
13 | private val productsRepository: ProductsRepository,
14 | ) : ViewModel() {
15 |
16 | val products = productsRepository.getFavorites()
17 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
18 | }
--------------------------------------------------------------------------------
/features/favorites/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Favorites
4 |
--------------------------------------------------------------------------------
/features/home/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.home"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:user"))
12 | implementation(project(":data:categories"))
13 | implementation(project(":data:products"))
14 | }
--------------------------------------------------------------------------------
/features/home/src/main/java/com/maruchin/features/home/HomeGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.home
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.navigation
5 | import androidx.navigation.navDeepLink
6 | import com.maruchin.ui.ROOT_DEEPLINK
7 | import com.maruchin.ui.screenFadeIn
8 | import com.maruchin.ui.screenFadeOut
9 |
10 | const val HOME_GRAPH_ROUTE = "home-graph"
11 | private const val HOME_DEEPLINK = "$ROOT_DEEPLINK/home"
12 |
13 | fun NavGraphBuilder.homeGraph(
14 | onCategoryClick: (categoryId: String) -> Unit,
15 | onProductClick: (productId: String) -> Unit,
16 | onLoginClick: () -> Unit,
17 | ) {
18 | navigation(
19 | startDestination = HOME_ROUTE,
20 | route = HOME_GRAPH_ROUTE,
21 | deepLinks = listOf(
22 | navDeepLink { uriPattern = HOME_DEEPLINK }
23 | ),
24 | enterTransition = { screenFadeIn() },
25 | exitTransition = { screenFadeOut() },
26 | popEnterTransition = { screenFadeIn() },
27 | popExitTransition = { screenFadeOut() },
28 | ) {
29 | homeScreen(
30 | onCategoryClick = onCategoryClick,
31 | onProductClick = onProductClick,
32 | onLoginClick = onLoginClick,
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/home/src/main/java/com/maruchin/features/home/HomeNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.home
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val HOME_ROUTE = "home"
10 |
11 | internal fun NavGraphBuilder.homeScreen(
12 | onCategoryClick: (categoryId: String) -> Unit,
13 | onProductClick: (productId: String) -> Unit,
14 | onLoginClick: () -> Unit,
15 | ) {
16 | composable(route = HOME_ROUTE) {
17 | val viewModel: HomeViewModel = hiltViewModel()
18 | val categories by viewModel.categories.collectAsState()
19 | val canLogin by viewModel.canLogin.collectAsState()
20 |
21 | HomeScreen(
22 | categories = categories,
23 | canLogin = canLogin,
24 | onCategoryClick = onCategoryClick,
25 | onProductClick = onProductClick,
26 | onLoginClick = onLoginClick,
27 | )
28 | }
29 | }
--------------------------------------------------------------------------------
/features/home/src/main/java/com/maruchin/features/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.categories.CategoriesRepository
6 | import com.maruchin.data.products.ProductsRepository
7 | import com.maruchin.data.user.User
8 | import com.maruchin.data.user.UserRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.SharingStarted
11 | import kotlinx.coroutines.flow.combine
12 | import kotlinx.coroutines.flow.flatMapLatest
13 | import kotlinx.coroutines.flow.map
14 | import kotlinx.coroutines.flow.stateIn
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | internal class HomeViewModel @Inject constructor(
19 | private val categoriesRepository: CategoriesRepository,
20 | private val productsRepository: ProductsRepository,
21 | private val userRepository: UserRepository,
22 | ) : ViewModel() {
23 |
24 | val categories = categoriesRepository.getRecommended()
25 | .flatMapLatest { categories ->
26 | val products = categories.map { category ->
27 | productsRepository.getRecommendedForCategory(category.id)
28 | .map { category to it }
29 | }
30 | combine(products) { it.toMap() }
31 | }
32 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyMap())
33 |
34 | val canLogin = userRepository.get().map { it is User.LoggedOut }
35 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
36 | }
--------------------------------------------------------------------------------
/features/home/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Home
4 | Login
5 |
--------------------------------------------------------------------------------
/features/login/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.login"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":forms"))
12 | implementation(project(":data:user"))
13 | }
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/LoginGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.navigation
6 | import androidx.navigation.navDeepLink
7 | import com.maruchin.features.login.changepassword.changePasswordScreen
8 | import com.maruchin.features.login.forgotpassword.forgotPasswordScreen
9 | import com.maruchin.features.login.forgotpassword.navigateToForgotPassword
10 | import com.maruchin.features.login.login.LOGIN_ROUTE
11 | import com.maruchin.features.login.login.loginScreen
12 | import com.maruchin.ui.ROOT_DEEPLINK
13 |
14 | internal const val LOGIN_GRAPH_ROUTE = "login_graph"
15 | private const val LOGIN_DEEPLINK = "$ROOT_DEEPLINK/login"
16 |
17 | fun NavController.navigateToLoginGraph() {
18 | navigate(LOGIN_GRAPH_ROUTE)
19 | }
20 |
21 | fun NavGraphBuilder.loginGraph(
22 | navController: NavController,
23 | onRegisterClick: () -> Unit
24 | ) {
25 | navigation(
26 | startDestination = LOGIN_ROUTE,
27 | route = LOGIN_GRAPH_ROUTE,
28 | deepLinks = listOf(
29 | navDeepLink { uriPattern = LOGIN_DEEPLINK }
30 | )
31 | ) {
32 | loginScreen(
33 | onBackClick = {
34 | navController.popBackStack()
35 | },
36 | onRegisterClick = onRegisterClick,
37 | onForgotPasswordClick = {
38 | navController.navigateToForgotPassword()
39 | },
40 | onLoggedIn = {
41 | navController.popBackStack()
42 | }
43 | )
44 | forgotPasswordScreen(
45 | onBackClick = {
46 | navController.popBackStack()
47 | },
48 | )
49 | changePasswordScreen(
50 | onCloseClick = {
51 | navController.popBackStack()
52 | },
53 | onLoggedIn = {
54 | navController.popBackStack(route = LOGIN_GRAPH_ROUTE, inclusive = true)
55 | }
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/changepassword/ChangePasswordNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.changepassword
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.lifecycle.SavedStateHandle
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.composable
10 | import androidx.navigation.navDeepLink
11 | import com.maruchin.ui.ROOT_DEEPLINK
12 |
13 | private const val EMAIL = "email"
14 | private const val TOKEN = "token"
15 | internal const val CHANGE_PASSWORD_ROUTE = "change_password"
16 | internal const val CHANGE_PASSWORD_DEEPLINK = "$ROOT_DEEPLINK/change-password/{$EMAIL}/{$TOKEN}"
17 |
18 | internal data class ChangePasswordArgs(val email: String, val token: String) {
19 | constructor(savedStateHandle: SavedStateHandle) : this(
20 | email = checkNotNull(savedStateHandle[EMAIL]),
21 | token = checkNotNull(savedStateHandle[TOKEN]),
22 | )
23 | }
24 |
25 | internal fun NavGraphBuilder.changePasswordScreen(
26 | onCloseClick: () -> Unit,
27 | onLoggedIn: () -> Unit,
28 | ) {
29 | composable(
30 | route = CHANGE_PASSWORD_ROUTE,
31 | deepLinks = listOf(
32 | navDeepLink { uriPattern = CHANGE_PASSWORD_DEEPLINK }
33 | )
34 | ) {
35 | val viewModel: ChangePasswordViewModel = hiltViewModel()
36 | val isLoggedIn by viewModel.isLoggedIn.collectAsState()
37 |
38 | if (isLoggedIn) {
39 | LaunchedEffect(Unit) {
40 | onLoggedIn()
41 | }
42 | }
43 |
44 | ChangePasswordScreen(
45 | onCloseClick = onCloseClick,
46 | onChangePasswordClick = viewModel::changePassword,
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/changepassword/ChangePasswordViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.changepassword
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.user.User
7 | import com.maruchin.data.user.UserRepository
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.SharingStarted
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class ChangePasswordViewModel @Inject constructor(
17 | savedStateHandle: SavedStateHandle,
18 | private val userRepository: UserRepository,
19 | ) : ViewModel() {
20 |
21 | private val args = ChangePasswordArgs(savedStateHandle)
22 |
23 | val isLoggedIn = userRepository.get()
24 | .map { it is User.LoggedIn }
25 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
26 |
27 | fun changePassword(newPassword: String) = viewModelScope.launch {
28 | userRepository.changePassword(newPassword, args.token)
29 | userRepository.login(args.email, newPassword)
30 | }
31 | }
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/forgotpassword/EmailSentDialog.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.forgotpassword
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Email
6 | import androidx.compose.material3.AlertDialog
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 |
14 | @Composable
15 | internal fun EmailSentDialog(onDismiss: () -> Unit, onOpenEmailBoxClick: () -> Unit) {
16 | AlertDialog(
17 | onDismissRequest = onDismiss,
18 | icon = {
19 | Icon(imageVector = Icons.Default.Email, contentDescription = null)
20 | },
21 | title = {
22 | Text(text = "Check your email")
23 | },
24 | text = {
25 | Text(text = "To change your password, use the link we sent to the email address provided")
26 | },
27 | confirmButton = {
28 | Button(onClick = onOpenEmailBoxClick, modifier = Modifier.fillMaxWidth()) {
29 | Text(text = "Open your email box")
30 | }
31 | }
32 | )
33 | }
34 |
35 | @Preview
36 | @Composable
37 | private fun EmailSentDialogPreview() {
38 | EmailSentDialog(onDismiss = {}, onOpenEmailBoxClick = {})
39 | }
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/forgotpassword/ForgotPasswordNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.forgotpassword
2 |
3 | import androidx.compose.ui.platform.LocalContext
4 | import androidx.hilt.navigation.compose.hiltViewModel
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 | import androidx.navigation.navDeepLink
9 | import com.maruchin.ui.openEmailApp
10 |
11 | internal const val FORGOT_PASSWORD_ROUTE = "forgot_password"
12 | private const val FORGOT_PASSWORD_DEEPLINK = "${com.maruchin.ui.ROOT_DEEPLINK}/forgot-password"
13 |
14 | internal fun NavController.navigateToForgotPassword() {
15 | navigate(FORGOT_PASSWORD_ROUTE)
16 | }
17 |
18 | internal fun NavGraphBuilder.forgotPasswordScreen(onBackClick: () -> Unit) {
19 | composable(
20 | route = FORGOT_PASSWORD_ROUTE,
21 | deepLinks = listOf(
22 | navDeepLink { uriPattern = FORGOT_PASSWORD_DEEPLINK }
23 | )
24 | ) {
25 | val viewModel: ForgotPasswordViewModel = hiltViewModel()
26 | val context = LocalContext.current
27 |
28 | ForgotPasswordScreen(
29 | emailSent = viewModel.emailSent,
30 | onBackClick = onBackClick,
31 | onSendLinkClick = viewModel::sendLink,
32 | onEmailSentInformationShow = viewModel::emailSentInformationShown,
33 | onOpenEmailBoxClick = { context.openEmailApp() },
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/forgotpassword/ForgotPasswordViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.forgotpassword
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class ForgotPasswordViewModel @Inject constructor() : ViewModel() {
15 |
16 | var emailSent: Boolean by mutableStateOf(false)
17 | private set
18 |
19 | fun sendLink(email: String) = viewModelScope.launch {
20 | delay(1_000)
21 | emailSent = true
22 | }
23 |
24 | fun emailSentInformationShown() {
25 | emailSent = false
26 | }
27 | }
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/login/LoginNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.login
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 |
10 | internal const val LOGIN_ROUTE = "login"
11 |
12 | internal fun NavGraphBuilder.loginScreen(
13 | onBackClick: () -> Unit,
14 | onRegisterClick: () -> Unit,
15 | onForgotPasswordClick: () -> Unit,
16 | onLoggedIn: () -> Unit,
17 | ) {
18 | composable(route = LOGIN_ROUTE) {
19 | val viewModel: LoginViewModel = hiltViewModel()
20 | val isLoggedIn by viewModel.isLoggedIn.collectAsState()
21 |
22 | if (isLoggedIn) {
23 | LaunchedEffect(Unit) {
24 | onLoggedIn()
25 | }
26 | }
27 |
28 | LoginScreen(
29 | onBackClick = onBackClick,
30 | onLoginClick = viewModel::login,
31 | onRegisterClick = onRegisterClick,
32 | onForgotPasswordClick = onForgotPasswordClick,
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/login/src/main/java/com/maruchin/features/login/login/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.login.login
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.user.User
6 | import com.maruchin.data.user.UserRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.map
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class LoginViewModel @Inject constructor(
16 | private val userRepository: UserRepository
17 | ) : ViewModel() {
18 |
19 | val isLoggedIn = userRepository.get()
20 | .map { it is User.LoggedIn }
21 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
22 |
23 | fun login(email: String, password: String) = viewModelScope.launch {
24 | userRepository.login(email, password)
25 | }
26 | }
--------------------------------------------------------------------------------
/features/login/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Change password
4 | Change your password
5 | Password change
6 | Send link
7 | Do not you remember the password? Enter your e-mail address to which we will send a link to change your password
8 | Login
9 | Forgot password?
10 | Register
11 |
--------------------------------------------------------------------------------
/features/my-data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.mydata"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":forms"))
12 | implementation(project(":data:user"))
13 | implementation(project(":data:addresses"))
14 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/addaddress/AddAddressNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.addaddress
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.ui.window.DialogProperties
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.dialog
9 |
10 | internal const val ADD_ADDRESS_ROUTE = "add-address"
11 |
12 | internal fun NavController.navigateToAddAddress() {
13 | navigate(ADD_ADDRESS_ROUTE)
14 | }
15 |
16 | internal fun NavGraphBuilder.addAddressScreen(onClose: () -> Unit) {
17 | dialog(
18 | route = ADD_ADDRESS_ROUTE,
19 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
20 | ) {
21 | val viewModel: AddAddressViewModel = hiltViewModel()
22 |
23 | if (viewModel.isSaved) {
24 | LaunchedEffect(Unit) {
25 | onClose()
26 | }
27 | }
28 |
29 | AddAddressScreen(onCloseClick = onClose, onSaveClick = viewModel::submitAddAddress)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/addaddress/AddAddressViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.addaddress
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.maruchin.data.addresses.Address
9 | import com.maruchin.data.addresses.AddressesRepository
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class AddAddressViewModel @Inject constructor(
16 | private val addressesRepository: AddressesRepository,
17 | ) : ViewModel() {
18 |
19 | var isSaved by mutableStateOf(false)
20 | private set
21 |
22 | fun submitAddAddress(address: Address) = viewModelScope.launch {
23 | addressesRepository.save(address)
24 | isSaved = true
25 | }
26 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/changepassword/ChangePasswordNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.changepassword
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.ui.window.DialogProperties
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.dialog
9 |
10 | internal const val CHANGE_PASSWORD_ROUTE = "change-password"
11 |
12 | internal fun NavController.navigateToChangePassword() {
13 | navigate(CHANGE_PASSWORD_ROUTE)
14 | }
15 |
16 | internal fun NavGraphBuilder.changePasswordScreen(onCloseClick: () -> Unit) {
17 | dialog(
18 | route = CHANGE_PASSWORD_ROUTE,
19 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
20 | ) {
21 | val viewModel: ChangePasswordViewModel = hiltViewModel()
22 |
23 | if (viewModel.isSaved) {
24 | LaunchedEffect(Unit) {
25 | onCloseClick()
26 | }
27 | }
28 |
29 | ChangePasswordScreen(
30 | onCloseClick = onCloseClick,
31 | onSaveClick = viewModel::changePassword,
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/changepassword/ChangePasswordViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.changepassword
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.maruchin.data.user.UserRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class ChangePasswordViewModel @Inject constructor(
15 | private val userRepository: UserRepository
16 | ) : ViewModel() {
17 |
18 | var isSaved: Boolean by mutableStateOf(false)
19 | private set
20 |
21 | fun changePassword(currentPassword: String, newPassword: String) = viewModelScope.launch {
22 | userRepository.changePassword(
23 | currentPassword = currentPassword,
24 | newPassword = newPassword,
25 | )
26 | isSaved = true
27 | }
28 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/deleteaccount/DeleteAccountNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.deleteaccount
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.ui.window.DialogProperties
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.dialog
9 | import androidx.navigation.navDeepLink
10 | import com.maruchin.ui.ROOT_DEEPLINK
11 |
12 | internal const val DELETE_ACCOUNT_ROUTE = "delete-account"
13 | private const val DELETE_ACCOUNT_DEEPLINK = "$ROOT_DEEPLINK/delete-account"
14 |
15 | internal fun NavController.navigateToDeleteAccount() {
16 | navigate(DELETE_ACCOUNT_ROUTE)
17 | }
18 |
19 | internal fun NavGraphBuilder.deleteAccountScreen(
20 | onCloseClick: () -> Unit,
21 | onNavigateToProfile: () -> Unit
22 | ) {
23 | dialog(
24 | route = DELETE_ACCOUNT_ROUTE,
25 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
26 | deepLinks = listOf(
27 | navDeepLink { uriPattern = DELETE_ACCOUNT_DEEPLINK }
28 | )
29 | ) {
30 | val viewModel: DeleteAccountViewModel = hiltViewModel()
31 |
32 | if (viewModel.isDeleted) {
33 | LaunchedEffect(Unit) {
34 | onNavigateToProfile()
35 | }
36 | }
37 |
38 | DeleteAccountScreen(
39 | onCloseClick = onCloseClick,
40 | onStartDeletingClick = viewModel::deleteAccount
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/deleteaccount/DeleteAccountViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.deleteaccount
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.maruchin.data.user.UserRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class DeleteAccountViewModel @Inject constructor(
15 | private val userRepository: UserRepository,
16 | ) : ViewModel() {
17 |
18 | var isDeleted by mutableStateOf(false)
19 | private set
20 |
21 | fun deleteAccount() = viewModelScope.launch {
22 | userRepository.deleteAccount()
23 | isDeleted = true
24 | }
25 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/editaddress/EditAddressNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.editaddress
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.window.DialogProperties
7 | import androidx.hilt.navigation.compose.hiltViewModel
8 | import androidx.lifecycle.SavedStateHandle
9 | import androidx.navigation.NavController
10 | import androidx.navigation.NavGraphBuilder
11 | import androidx.navigation.compose.dialog
12 |
13 | private const val ADDRESS_ID = "addressId"
14 | internal const val EDIT_ADDRESS_ROUTE = "edit-address/{$ADDRESS_ID}"
15 |
16 | internal data class EditAddressArgs(val addressId: String) {
17 | constructor(savedStateHandle: SavedStateHandle) : this(
18 | addressId = checkNotNull(savedStateHandle[ADDRESS_ID])
19 | )
20 | }
21 |
22 | internal fun NavController.navigateToEditAddress(addressId: String) {
23 | navigate(EDIT_ADDRESS_ROUTE.replace("{$ADDRESS_ID}", addressId))
24 | }
25 |
26 | internal fun NavGraphBuilder.editAddressScreen(onCloseClick: () -> Unit) {
27 | dialog(
28 | route = EDIT_ADDRESS_ROUTE,
29 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
30 | ) {
31 | val viewModel: EditAddressViewModel = hiltViewModel()
32 | val address by viewModel.address.collectAsState()
33 |
34 | if (viewModel.isSaved) {
35 | LaunchedEffect(Unit) {
36 | onCloseClick()
37 | }
38 | }
39 |
40 | EditAddressScreen(
41 | address = address,
42 | onCloseClick = onCloseClick,
43 | onSaveClick = viewModel::saveAddress,
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/editaddress/EditAddressViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.editaddress
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.maruchin.data.addresses.Address
10 | import com.maruchin.data.addresses.AddressesRepository
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.stateIn
14 | import kotlinx.coroutines.flow.take
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | internal class EditAddressViewModel @Inject constructor(
20 | savedStateHandle: SavedStateHandle,
21 | private val addressesRepository: AddressesRepository,
22 | ) : ViewModel() {
23 |
24 | private val args = EditAddressArgs(savedStateHandle)
25 |
26 | val address = addressesRepository.getById(args.addressId)
27 | .take(1)
28 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
29 |
30 | var isSaved: Boolean by mutableStateOf(false)
31 | private set
32 |
33 | fun saveAddress(address: Address) = viewModelScope.launch {
34 | val updatedAddress = address.copy(id = args.addressId)
35 | addressesRepository.save(updatedAddress)
36 | isSaved = true
37 | }
38 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/editmydata/EditMyDataNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.editmydata
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.window.DialogProperties
7 | import androidx.hilt.navigation.compose.hiltViewModel
8 | import androidx.navigation.NavController
9 | import androidx.navigation.NavGraphBuilder
10 | import androidx.navigation.compose.dialog
11 |
12 | internal const val EDIT_MY_DATE_ROUTE = "edit-my-data"
13 |
14 | internal fun NavController.navigateToEditMyData() {
15 | navigate(EDIT_MY_DATE_ROUTE)
16 | }
17 |
18 | internal fun NavGraphBuilder.editMyDataScreen(onCloseClick: () -> Unit) {
19 | dialog(
20 | route = EDIT_MY_DATE_ROUTE,
21 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
22 | ) {
23 | val viewModel: EditMyDataViewModel = hiltViewModel()
24 | val personalData by viewModel.personalData.collectAsState()
25 |
26 | if (viewModel.isSaved) {
27 | LaunchedEffect(Unit) {
28 | onCloseClick()
29 | }
30 | }
31 |
32 | EditMyDataScreen(
33 | personalData = personalData,
34 | onCloseClick = onCloseClick,
35 | onSaveClick = viewModel::submitChange,
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/editmydata/EditMyDataViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.editmydata
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.maruchin.data.user.PersonalData
9 | import com.maruchin.data.user.User
10 | import com.maruchin.data.user.UserRepository
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.filterIsInstance
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.flow.stateIn
16 | import kotlinx.coroutines.flow.take
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | internal class EditMyDataViewModel @Inject constructor(
22 | private val userRepository: UserRepository,
23 | ) : ViewModel() {
24 |
25 | val personalData = userRepository.get()
26 | .filterIsInstance()
27 | .map { it.personalData }
28 | .take(1)
29 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
30 |
31 | var isSaved: Boolean by mutableStateOf(false)
32 | private set
33 |
34 | fun submitChange(personalData: PersonalData) = viewModelScope.launch {
35 | userRepository.updatePersonalData(personalData)
36 | isSaved = true
37 | }
38 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/myaddresses/MyAddressesNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.myaddresses
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 |
10 | internal const val MY_ADDRESSES_ROUTE = "my-addresses"
11 |
12 | internal fun NavController.navigateToMyAddresses() {
13 | navigate(MY_ADDRESSES_ROUTE)
14 | }
15 |
16 | internal fun NavGraphBuilder.myAddresses(
17 | onBackClick: () -> Unit,
18 | onAddAddressClick: () -> Unit,
19 | onEditAddressClick: (addressId: String) -> Unit
20 | ) {
21 | composable(MY_ADDRESSES_ROUTE) {
22 | val viewModel: MyAddressesViewModel = hiltViewModel()
23 | val addresses by viewModel.addresses.collectAsState()
24 |
25 | MyAddressesScreen(
26 | addresses = addresses,
27 | onBackClick = onBackClick,
28 | onAddAddressClick = onAddAddressClick,
29 | onEditAddressClick = onEditAddressClick,
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/myaddresses/MyAddressesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.myaddresses
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.addresses.AddressesRepository
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | internal class MyAddressesViewModel @Inject constructor(
13 | private val addressesRepository: AddressesRepository
14 | ) : ViewModel() {
15 |
16 | val addresses = addressesRepository.getAll()
17 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
18 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/mydata/MyDataNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.mydata
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 |
10 | internal const val MY_DATA_ROUTE = "my-data"
11 |
12 | internal fun NavGraphBuilder.myDataScreen(
13 | onBackClick: () -> Unit,
14 | onPersonalDataClick: () -> Unit,
15 | onMyAddressesClick: () -> Unit,
16 | onChangePasswordClick: () -> Unit,
17 | onDeleteAccountClick: () -> Unit,
18 | onLoggedOut: () -> Unit,
19 | ) {
20 | composable(MY_DATA_ROUTE) {
21 | val viewModel: MyDataViewModel = hiltViewModel()
22 | val personalData by viewModel.personalData.collectAsState()
23 | val isLoggedOut by viewModel.isLoggedOut.collectAsState()
24 |
25 | if (isLoggedOut) {
26 | LaunchedEffect(Unit) {
27 | onLoggedOut()
28 | }
29 | }
30 |
31 | MyDataScreen(
32 | personalData = personalData,
33 | onBackClick = onBackClick,
34 | onPersonalDataClick = onPersonalDataClick,
35 | onMyAddressesClick = onMyAddressesClick,
36 | onChangePasswordClick = onChangePasswordClick,
37 | onDeleteAccountClick = onDeleteAccountClick,
38 | onLogoutClick = viewModel::logout,
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/features/my-data/src/main/java/com/maruchin/features/mydata/mydata/MyDataViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.mydata.mydata
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.user.User
6 | import com.maruchin.data.user.UserRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.filterIsInstance
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class MyDataViewModel @Inject constructor(
17 | private val userRepository: UserRepository,
18 | ) : ViewModel() {
19 |
20 | val personalData = userRepository.get()
21 | .filterIsInstance()
22 | .map { it.personalData }
23 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
24 |
25 | val isLoggedOut = userRepository.get()
26 | .map { it is User.LoggedOut }
27 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
28 |
29 | fun logout() = viewModelScope.launch {
30 | userRepository.logout()
31 | }
32 | }
--------------------------------------------------------------------------------
/features/my-data/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Change password
4 | Complete the fields below
5 | Start deleting your account in the app
6 | Delete account
7 | Save
8 | My data
9 | Edit address
10 | My addresses
11 | Add
12 | Logout
13 | Add address
14 |
--------------------------------------------------------------------------------
/features/order/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.order"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":forms"))
12 | implementation(project(":data:order"))
13 | }
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/address/AddressNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.address
2 |
3 | import androidx.hilt.navigation.compose.hiltViewModel
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.composable
7 |
8 | internal const val ADDRESS_ROUTE = "address"
9 |
10 | internal fun NavController.navigateToAddress() {
11 | navigate(ADDRESS_ROUTE)
12 | }
13 |
14 | internal fun NavGraphBuilder.addressScreen(
15 | onBackClick: () -> Unit,
16 | onCancelClick: () -> Unit,
17 | onNextClick: () -> Unit,
18 | ) {
19 | composable(ADDRESS_ROUTE) {
20 | val viewModel: AddressViewModel = hiltViewModel()
21 |
22 | AddressScreen(
23 | onBackClick = onBackClick,
24 | onCancelClick = onCancelClick,
25 | onNextClick = { address ->
26 | viewModel.selectAddress(address)
27 | onNextClick()
28 | }
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/address/AddressViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.address
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.addresses.Address
6 | import com.maruchin.data.order.OrderRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.launch
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | internal class AddressViewModel @Inject constructor(
13 | private val orderRepository: OrderRepository,
14 | ) : ViewModel() {
15 |
16 | fun selectAddress(address: Address) = viewModelScope.launch {
17 | orderRepository.selectAddress(address)
18 | }
19 | }
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/confirmation/ConfirmationNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.confirmation
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 | import com.maruchin.features.order.ORDER_GRAPH_ROUTE
10 |
11 | internal const val CONFIRMATION_ROUTE = "confirmation"
12 |
13 | internal fun NavController.navigateToConfirmation() {
14 | navigate(CONFIRMATION_ROUTE) {
15 | popUpTo(ORDER_GRAPH_ROUTE)
16 | }
17 | }
18 |
19 | internal fun NavGraphBuilder.confirmationScreen(onCloseClick: () -> Unit) {
20 | composable(CONFIRMATION_ROUTE) {
21 | val viewModel: ConfirmationViewModel = hiltViewModel()
22 | val order by viewModel.order.collectAsState()
23 |
24 | ConfirmationScreen(
25 | order = order,
26 | onCloseClick = onCloseClick,
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/confirmation/ConfirmationViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.confirmation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.order.Order
6 | import com.maruchin.data.order.OrderRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.filterIsInstance
10 | import kotlinx.coroutines.flow.stateIn
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class ConfirmationViewModel @Inject constructor(
15 | private val orderRepository: OrderRepository,
16 | ) : ViewModel() {
17 |
18 | val order = orderRepository.get()
19 | .filterIsInstance()
20 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
21 | }
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/delivery/DeliveryNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.delivery
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val DELIVERY_ROUTE = "delivery"
10 |
11 | internal fun NavGraphBuilder.deliveryScreen(
12 | onBackClick: () -> Unit,
13 | onCancelClick: () -> Unit,
14 | onDeliveryClick: () -> Unit
15 | ) {
16 | composable(DELIVERY_ROUTE) {
17 | val viewModel: DeliveryViewModel = hiltViewModel()
18 | val state by viewModel.deliveries.collectAsState()
19 |
20 | DeliveryScreen(
21 | deliveries = state,
22 | onBackClick = onBackClick,
23 | onCancelClick = onCancelClick,
24 | onDeliveryClick = { delivery ->
25 | viewModel.selectDelivery(delivery)
26 | onDeliveryClick()
27 | },
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/delivery/DeliveryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.delivery
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.deliveries.DeliveriesRepository
6 | import com.maruchin.data.deliveries.Delivery
7 | import com.maruchin.data.order.OrderRepository
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.SharingStarted
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class DeliveryViewModel @Inject constructor(
16 | private val deliveriesRepository: DeliveriesRepository,
17 | private val orderRepository: OrderRepository,
18 | ) : ViewModel() {
19 |
20 | val deliveries = deliveriesRepository.getAll()
21 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
22 |
23 | fun selectDelivery(delivery: Delivery) = viewModelScope.launch {
24 | orderRepository.selectDelivery(delivery)
25 | }
26 | }
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/payment/PaymentNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.payment
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavController
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 |
10 | internal const val PAYMENT_ROUTE = "payment"
11 |
12 | internal fun NavController.navigateToPayment() {
13 | navigate(PAYMENT_ROUTE)
14 | }
15 |
16 | internal fun NavGraphBuilder.paymentScreen(
17 | onBackClick: () -> Unit,
18 | onPaymentClick: () -> Unit,
19 | onCancelClick: () -> Unit,
20 | ) {
21 | composable(PAYMENT_ROUTE) {
22 | val viewModel: PaymentViewModel = hiltViewModel()
23 | val state by viewModel.payments.collectAsState()
24 |
25 | PaymentScreen(
26 | payments = state,
27 | onBackClick = onBackClick,
28 | onCancelClick = onCancelClick,
29 | onPaymentClick = { payment ->
30 | viewModel.selectPayment(payment)
31 | onPaymentClick()
32 | }
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/payment/PaymentViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.payment
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.order.OrderRepository
6 | import com.maruchin.data.payments.Payment
7 | import com.maruchin.data.payments.PaymentsRepository
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.SharingStarted
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class PaymentViewModel @Inject constructor(
16 | private val paymentsRepository: PaymentsRepository,
17 | private val orderRepository: OrderRepository,
18 | ) : ViewModel() {
19 |
20 | val payments = paymentsRepository.getAll()
21 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
22 |
23 | fun selectPayment(payment: Payment) = viewModelScope.launch {
24 | orderRepository.selectPayment(payment)
25 | }
26 | }
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/summary/SummaryNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.summary
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavController
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.composable
10 |
11 | internal const val SUMMARY_ROUTE = "summary"
12 |
13 | internal fun NavController.navigateToSummary() {
14 | navigate(SUMMARY_ROUTE)
15 | }
16 |
17 | internal fun NavGraphBuilder.summaryScreen(
18 | onBackClick: () -> Unit,
19 | onProductClick: (productId: String) -> Unit,
20 | onCancelClick: () -> Unit,
21 | onSubmitted: () -> Unit,
22 | ) {
23 | composable(SUMMARY_ROUTE) {
24 | val viewModel: SummaryViewModel = hiltViewModel()
25 | val order by viewModel.order.collectAsState()
26 | val isSubmitted by viewModel.isSubmitted.collectAsState()
27 |
28 | if (isSubmitted) {
29 | LaunchedEffect(Unit) {
30 | onSubmitted()
31 | }
32 | }
33 |
34 | SummaryScreen(
35 | order = order,
36 | onBackClick = onBackClick,
37 | onProductClick = onProductClick,
38 | onSubmitOrderClick = viewModel::submit,
39 | onCancelClick = onCancelClick,
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/features/order/src/main/java/com/maruchin/features/order/summary/SummaryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.order.summary
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.order.Order
6 | import com.maruchin.data.order.OrderRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.filterIsInstance
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class SummaryViewModel @Inject constructor(
17 | private val orderRepository: OrderRepository,
18 | ) : ViewModel() {
19 |
20 | val order = orderRepository.get()
21 | .filterIsInstance()
22 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
23 |
24 | val isSubmitted = orderRepository.get()
25 | .map { it is Order.Submitted }
26 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
27 |
28 | fun submit() = viewModelScope.launch {
29 | orderRepository.submit()
30 | }
31 | }
--------------------------------------------------------------------------------
/features/order/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Order number
4 | We received your order
5 | Cancel
6 | Choose delivery
7 | Submit order
8 | Summary
9 | Products
10 | Delivery
11 | Address
12 | Payment
13 | Next
14 | Your order
15 |
--------------------------------------------------------------------------------
/features/product-browser/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | kotlin("kapt")
4 | }
5 |
6 | android {
7 | namespace = "com.maruchin.features.productbrowser"
8 | }
9 |
10 | dependencies {
11 | implementation(project(":ui"))
12 | implementation(project(":data:products"))
13 | }
--------------------------------------------------------------------------------
/features/product-browser/src/main/java/com/maruchin/features/productbrowser/filters/FiltersNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productbrowser.filters
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.ui.window.DialogProperties
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavController
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.dialog
10 |
11 | private const val FILTERS_ROUTE = "filters"
12 |
13 | internal fun NavGraphBuilder.filtersScreen(onBackClick: () -> Unit) {
14 | dialog(
15 | route = FILTERS_ROUTE,
16 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
17 | ) {
18 | val viewModel: FiltersViewModel = hiltViewModel()
19 | val filters by viewModel.filters.collectAsState()
20 |
21 | FiltersScreen(
22 | filters = filters,
23 | onBackClick = onBackClick,
24 | onSortingChange = viewModel::updateSorting,
25 | onPriceChange = viewModel::updatePrice,
26 | )
27 | }
28 | }
29 |
30 | internal fun NavController.navigateToFilters() {
31 | navigate(FILTERS_ROUTE)
32 | }
33 |
--------------------------------------------------------------------------------
/features/product-browser/src/main/java/com/maruchin/features/productbrowser/filters/FiltersViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productbrowser.filters
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.products.ProductFilters
6 | import com.maruchin.data.products.ProductFiltersRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class FiltersViewModel @Inject constructor(
15 | private val productFiltersRepository: ProductFiltersRepository,
16 | ) : ViewModel() {
17 |
18 | val filters = productFiltersRepository.get()
19 | .stateIn(viewModelScope, SharingStarted.Lazily, ProductFilters())
20 |
21 | fun updateSorting(sorting: ProductFilters.Sorting) = viewModelScope.launch {
22 | productFiltersRepository.updateSorting(sorting)
23 | }
24 |
25 | fun updatePrice(price: ProductFilters.Price) = viewModelScope.launch {
26 | productFiltersRepository.updatePrice(price)
27 | }
28 | }
--------------------------------------------------------------------------------
/features/product-browser/src/main/java/com/maruchin/features/productbrowser/productlist/ProductListNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productbrowser.productlist
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val PRODUCT_LIST_ROUTE = "product-list"
10 |
11 | internal fun NavGraphBuilder.productListScreen(
12 | onBackClick: () -> Unit,
13 | onProductClick: (productId: String) -> Unit,
14 | onFiltersClick: () -> Unit,
15 | ) {
16 | composable(route = PRODUCT_LIST_ROUTE) {
17 | val viewModel: ProductListViewModel = hiltViewModel()
18 | val category by viewModel.category.collectAsState()
19 | val products by viewModel.products.collectAsState()
20 |
21 | ProductListScreen(
22 | category = category,
23 | products = products,
24 | onBackClick = onBackClick,
25 | onProductClick = onProductClick,
26 | onFiltersClick = onFiltersClick,
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/features/product-browser/src/main/java/com/maruchin/features/productbrowser/productlist/ProductListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productbrowser.productlist
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.categories.CategoriesRepository
7 | import com.maruchin.data.products.ProductFiltersRepository
8 | import com.maruchin.data.products.ProductsRepository
9 | import com.maruchin.features.productbrowser.ProductBrowserArgs
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.flow.SharingStarted
12 | import kotlinx.coroutines.flow.flatMapLatest
13 | import kotlinx.coroutines.flow.stateIn
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | internal class ProductListViewModel @Inject constructor(
18 | savedStateHandle: SavedStateHandle,
19 | private val categoriesRepository: CategoriesRepository,
20 | private val productFiltersRepository: ProductFiltersRepository,
21 | private val productsRepository: ProductsRepository,
22 | ) : ViewModel() {
23 |
24 | private val args = ProductBrowserArgs(savedStateHandle)
25 |
26 | val category = categoriesRepository.getById(args.categoryId)
27 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
28 |
29 | val products = productFiltersRepository.get()
30 | .flatMapLatest { productsRepository.getForCategory(args.categoryId, it) }
31 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
32 | }
--------------------------------------------------------------------------------
/features/product-browser/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Filters
4 | Price range
5 |
--------------------------------------------------------------------------------
/features/product-card/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.productcard"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:products"))
12 | implementation(project(":data:cart"))
13 | }
--------------------------------------------------------------------------------
/features/product-card/src/main/java/com/maruchin/features/productcard/ProductCardGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productcard
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.navigation
7 | import androidx.navigation.navDeepLink
8 | import com.maruchin.features.productcard.card.CARD_ROUTE
9 | import com.maruchin.features.productcard.card.cardScreen
10 | import com.maruchin.features.productcard.gallery.galleryScreen
11 | import com.maruchin.features.productcard.gallery.navigateToGallery
12 | import com.maruchin.ui.ROOT_DEEPLINK
13 |
14 | private const val PRODUCT_ID_ARG = "productId"
15 | const val PRODUCT_CARD_GRAPH_ROUTE = "product-card-graph/{$PRODUCT_ID_ARG}"
16 | private const val PRODUCT_CARD_DEEPLINK = "$ROOT_DEEPLINK/product-card/{$PRODUCT_ID_ARG}"
17 |
18 | internal data class ProductCardArgs(val productId: String) {
19 | constructor(savedStateHandle: SavedStateHandle) : this(
20 | productId = checkNotNull(savedStateHandle[PRODUCT_ID_ARG])
21 | )
22 | }
23 |
24 | fun NavGraphBuilder.productCardGraph(navController: NavController) {
25 | navigation(
26 | startDestination = CARD_ROUTE,
27 | route = PRODUCT_CARD_GRAPH_ROUTE,
28 | deepLinks = listOf(
29 | navDeepLink { uriPattern = PRODUCT_CARD_DEEPLINK }
30 | )
31 | ) {
32 | cardScreen(
33 | onBackClick = {
34 | navController.popBackStack()
35 | },
36 | onGalleryClick = { productId ->
37 | navController.navigateToGallery(productId)
38 | }
39 | )
40 | galleryScreen(
41 | onBack = {
42 | navController.popBackStack()
43 | }
44 | )
45 | }
46 | }
47 |
48 | fun NavController.navigateToProductCardGraph(productId: String) {
49 | navigate(PRODUCT_CARD_GRAPH_ROUTE.replace("{$PRODUCT_ID_ARG}", productId))
50 | }
51 |
--------------------------------------------------------------------------------
/features/product-card/src/main/java/com/maruchin/features/productcard/card/CardNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productcard.card
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | internal const val CARD_ROUTE = "card"
10 |
11 | internal fun NavGraphBuilder.cardScreen(
12 | onBackClick: () -> Unit,
13 | onGalleryClick: (productId: String) -> Unit
14 | ) {
15 | composable(CARD_ROUTE) {
16 | val viewModel: CardViewModel = hiltViewModel()
17 | val product by viewModel.product.collectAsState()
18 |
19 | CardScreen(
20 | product = product,
21 | onBackClick = onBackClick,
22 | onGalleryClick = {
23 | product?.id?.let(onGalleryClick)
24 | },
25 | onAddToCartClick = viewModel::addToCart,
26 | onFavoriteClick = viewModel::toggleIsFavorite,
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/features/product-card/src/main/java/com/maruchin/features/productcard/card/CardViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productcard.card
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.cart.CartRepository
7 | import com.maruchin.data.products.ProductsRepository
8 | import com.maruchin.features.productcard.ProductCardArgs
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.SharingStarted
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class CardViewModel @Inject constructor(
17 | savedStateHandle: SavedStateHandle,
18 | private val productsRepository: ProductsRepository,
19 | private val cartRepository: CartRepository,
20 | ) : ViewModel() {
21 |
22 | private val args = ProductCardArgs(savedStateHandle)
23 |
24 | val product = productsRepository.getById(args.productId)
25 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
26 |
27 | fun addToCart() = viewModelScope.launch {
28 | val product = product.value ?: return@launch
29 | cartRepository.addProduct(product)
30 | }
31 |
32 | fun toggleIsFavorite() = viewModelScope.launch {
33 | val product = product.value ?: return@launch
34 | productsRepository.updateIsFavorite(
35 | id = product.id,
36 | isFavorite = !product.isFavorite,
37 | )
38 | }
39 | }
--------------------------------------------------------------------------------
/features/product-card/src/main/java/com/maruchin/features/productcard/gallery/GalleryNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productcard.gallery
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.ui.window.DialogProperties
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.lifecycle.SavedStateHandle
8 | import androidx.navigation.NavController
9 | import androidx.navigation.NavGraphBuilder
10 | import androidx.navigation.compose.dialog
11 |
12 | private const val PRODUCT_ID_ARG = "productId"
13 | private const val GALLERY_ROUTE = "gallery/{$PRODUCT_ID_ARG}"
14 |
15 | internal data class GalleryArgs(val productId: String) {
16 | constructor(savedStateHandle: SavedStateHandle) : this(
17 | productId = checkNotNull(savedStateHandle[PRODUCT_ID_ARG])
18 | )
19 | }
20 |
21 | internal fun NavController.navigateToGallery(productId: String) {
22 | navigate(GALLERY_ROUTE.replace("{$PRODUCT_ID_ARG}", productId))
23 | }
24 |
25 | internal fun NavGraphBuilder.galleryScreen(onBack: () -> Unit) {
26 | dialog(
27 | route = GALLERY_ROUTE,
28 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
29 | ) {
30 | val viewModel: GalleryViewModel = hiltViewModel()
31 | val images by viewModel.images.collectAsState()
32 |
33 | GalleryScreen(images = images, onBackClick = onBack)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/product-card/src/main/java/com/maruchin/features/productcard/gallery/GalleryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.productcard.gallery
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.products.ProductsRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.map
10 | import kotlinx.coroutines.flow.stateIn
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class GalleryViewModel @Inject constructor(
15 | savedStateHandle: SavedStateHandle,
16 | private val productsRepository: ProductsRepository,
17 | ) : ViewModel() {
18 |
19 | private val args = GalleryArgs(savedStateHandle)
20 |
21 | val images = productsRepository.getById(args.productId)
22 | .map { it.images }
23 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
24 | }
--------------------------------------------------------------------------------
/features/product-card/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Add to cart
4 |
--------------------------------------------------------------------------------
/features/profile/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.profile"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":data:user"))
12 | implementation(project(":data:promotions"))
13 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/club/ClubViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.club
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.user.User
6 | import com.maruchin.data.user.UserRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.filterIsInstance
10 | import kotlinx.coroutines.flow.stateIn
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class ClubViewModel @Inject constructor(
15 | private val userRepository: UserRepository,
16 | ) : ViewModel() {
17 |
18 | val user = userRepository.get()
19 | .filterIsInstance()
20 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
21 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/findoutmore/FindOutMoreNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.findoutmore
2 |
3 | import androidx.compose.ui.window.DialogProperties
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.dialog
7 |
8 | internal const val FIND_OUT_MORE_ROUTE = "find-out-more"
9 |
10 | internal fun NavController.navigateToFindOutMore() {
11 | navigate(FIND_OUT_MORE_ROUTE)
12 | }
13 |
14 | internal fun NavGraphBuilder.findOutMoreScreen(onCloseClick: () -> Unit) {
15 | dialog(
16 | route = FIND_OUT_MORE_ROUTE,
17 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
18 | ) {
19 | FindOutMoreScreen(onCloseClick = onCloseClick)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/findoutmore/FindOutMorePagerState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.findoutmore
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.pager.PagerState
5 | import androidx.compose.foundation.pager.rememberPagerState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.Stable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberCoroutineScope
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.launch
12 |
13 | @OptIn(ExperimentalFoundationApi::class)
14 | @Stable
15 | internal class FindOutMorePagerState(
16 | val pagerState: PagerState,
17 | private val scope: CoroutineScope
18 | ) {
19 |
20 | val currentPage: Int
21 | get() = pagerState.currentPage
22 |
23 | fun isSelectedPage(page: Int) = currentPage == page
24 |
25 | fun selectPage(page: Int) = scope.launch {
26 | pagerState.animateScrollToPage(page)
27 | }
28 | }
29 |
30 | @OptIn(ExperimentalFoundationApi::class)
31 | @Composable
32 | internal fun rememberFindOutPagerState(): FindOutMorePagerState {
33 | val pagerState = rememberPagerState { 3 }
34 | val scope = rememberCoroutineScope()
35 | return remember(pagerState, scope) {
36 | FindOutMorePagerState(pagerState, scope)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/myorders/MyOrdersNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.myorders
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.composable
6 |
7 | internal const val MY_ORDERS_ROUTE = "my-orders"
8 |
9 | internal fun NavController.navigateToMyOrders() {
10 | navigate(MY_ORDERS_ROUTE)
11 | }
12 |
13 | internal fun NavGraphBuilder.myOrdersScreen(onBackClick: () -> Unit) {
14 | composable(MY_ORDERS_ROUTE) {
15 | MyOrdersScreen(onBackClick = onBackClick)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/myorders/MyOrdersScreen.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.myorders
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.ArrowBack
7 | import androidx.compose.material.icons.filled.Folder
8 | import androidx.compose.material3.CenterAlignedTopAppBar
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.IconButton
12 | import androidx.compose.material3.Scaffold
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.tooling.preview.Preview
18 | import com.maruchin.ui.ScreenContentPlaceholder
19 | import com.maruchin.features.profile.R
20 |
21 | @Composable
22 | internal fun MyOrdersScreen(onBackClick: () -> Unit) {
23 | Scaffold(
24 | topBar = {
25 | TopBar(onBackClick = onBackClick)
26 | }
27 | ) { padding ->
28 | Placeholder(padding)
29 | }
30 | }
31 |
32 | @Composable
33 | @OptIn(ExperimentalMaterial3Api::class)
34 | private fun TopBar(onBackClick: () -> Unit) {
35 | CenterAlignedTopAppBar(
36 | title = {
37 | Text(text = stringResource(R.string.my_orders))
38 | },
39 | navigationIcon = {
40 | IconButton(onClick = onBackClick) {
41 | Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
42 | }
43 | }
44 | )
45 | }
46 |
47 | @Composable
48 | private fun Placeholder(padding: PaddingValues) {
49 | ScreenContentPlaceholder(
50 | icon = Icons.Default.Folder,
51 | text = stringResource(R.string.there_are_no_orders_in_your_account_yet),
52 | modifier = Modifier.padding(padding)
53 | )
54 | }
55 |
56 | @Preview
57 | @Composable
58 | private fun MyOrdersScreenPreview() {
59 | MyOrdersScreen(onBackClick = {})
60 | }
61 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/profile/ProfileNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.profile
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 | import com.maruchin.ui.screenFadeIn
9 | import com.maruchin.ui.screenFadeOut
10 |
11 | internal const val PROFILE_ROUTE = "profile"
12 |
13 | internal fun NavGraphBuilder.profileScreen(
14 | onPurchaseHistoryClick: () -> Unit,
15 | onFindOutMoreClick: () -> Unit,
16 | onPromotionClick: (promotionId: String) -> Unit,
17 | onMyDataClick: () -> Unit,
18 | onMyOrdersClick: () -> Unit,
19 | onReturnsClick: () -> Unit,
20 | onLoginClick: () -> Unit,
21 | onJoinClubClick: () -> Unit
22 | ) {
23 | composable(
24 | route = PROFILE_ROUTE,
25 | enterTransition = { screenFadeIn() },
26 | exitTransition = { screenFadeOut() },
27 | popEnterTransition = { screenFadeIn() },
28 | popExitTransition = { screenFadeOut() },
29 | ) {
30 | val viewModel: ProfileViewModel = hiltViewModel()
31 | val isLoggedIn by viewModel.isLoggedIn.collectAsState()
32 |
33 | ProfileScreen(
34 | isLoggedIn = isLoggedIn,
35 | onPurchaseHistoryClick = onPurchaseHistoryClick,
36 | onFindOutMoreClick = onFindOutMoreClick,
37 | onPromotionClick = onPromotionClick,
38 | onMyDataClick = onMyDataClick,
39 | onMyOrdersClick = onMyOrdersClick,
40 | onReturnsClick = onReturnsClick,
41 | onLoginClick = onLoginClick,
42 | onJoinClubClick = onJoinClubClick,
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/profile/ProfileTabsState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.profile
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.pager.PagerState
5 | import androidx.compose.foundation.pager.rememberPagerState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.Stable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberCoroutineScope
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.launch
12 |
13 | @OptIn(ExperimentalFoundationApi::class)
14 | @Stable
15 | internal class ProfileTabsState(
16 | val pagerState: PagerState,
17 | private val scope: CoroutineScope,
18 | ) {
19 |
20 | val selectedTab: Int
21 | get() = pagerState.currentPage
22 |
23 | fun isSelected(tab: Int): Boolean {
24 | return pagerState.currentPage == tab
25 | }
26 |
27 | fun select(tab: Int) = scope.launch {
28 | pagerState.animateScrollToPage(tab)
29 | }
30 | }
31 |
32 | @OptIn(ExperimentalFoundationApi::class)
33 | @Composable
34 | internal fun rememberProfileTabsState(numOfPages: Int): ProfileTabsState {
35 | val pagerState = rememberPagerState { numOfPages }
36 | val scope = rememberCoroutineScope()
37 | return remember(pagerState, scope) {
38 | ProfileTabsState(pagerState, scope)
39 | }
40 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/profile/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.profile
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.user.User
6 | import com.maruchin.data.user.UserRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.map
10 | import kotlinx.coroutines.flow.stateIn
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class ProfileViewModel @Inject constructor(
15 | private val userRepository: UserRepository
16 | ) : ViewModel() {
17 |
18 | val isLoggedIn = userRepository.get()
19 | .map { it is User.LoggedIn }
20 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
21 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/promotion/PromotionNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.promotion
2 |
3 | import androidx.compose.runtime.collectAsState
4 | import androidx.compose.runtime.getValue
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.navigation.NavController
8 | import androidx.navigation.NavGraphBuilder
9 | import androidx.navigation.compose.composable
10 | import androidx.navigation.navDeepLink
11 | import com.maruchin.ui.ROOT_DEEPLINK
12 |
13 | private const val PROMOTION_ID = "promotionId"
14 | internal const val PROMOTION_ROUTE = "promotion/{$PROMOTION_ID}"
15 | private const val PROMOTION_DEEPLINK = "$ROOT_DEEPLINK/promotion/{$PROMOTION_ID}"
16 |
17 | internal data class PromotionArgs(val promotionId: String) {
18 | constructor(savedStateHandle: SavedStateHandle) : this(
19 | promotionId = checkNotNull(savedStateHandle[PROMOTION_ID])
20 | )
21 | }
22 |
23 | internal fun NavController.navigateToPromotion(promotionId: String) {
24 | navigate(PROMOTION_ROUTE.replace("{$PROMOTION_ID}", promotionId))
25 | }
26 |
27 | internal fun NavGraphBuilder.promotionScreen(onBackClick: () -> Unit) {
28 | composable(
29 | route = PROMOTION_ROUTE,
30 | deepLinks = listOf(
31 | navDeepLink { uriPattern = PROMOTION_DEEPLINK }
32 | )
33 | ) {
34 | val viewModel: PromotionsViewModel = hiltViewModel()
35 | val promotion by viewModel.promotion.collectAsState()
36 |
37 | PromotionScreen(promotion = promotion, onBackClick = onBackClick)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/promotion/PromotionsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.promotion
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.maruchin.data.promotions.PromotionsRepository
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.stateIn
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | internal class PromotionsViewModel @Inject constructor(
14 | savedStateHandle: SavedStateHandle,
15 | private val promotionsRepository: PromotionsRepository,
16 | ) : ViewModel() {
17 |
18 | private val args = PromotionArgs(savedStateHandle)
19 |
20 | val promotion = promotionsRepository.getById(args.promotionId)
21 | .stateIn(viewModelScope, SharingStarted.Lazily, null)
22 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/promotions/PromotionsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.promotions
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.promotions.PromotionsRepository
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | internal class PromotionsViewModel @Inject constructor(
13 | private val promotionsRepository: PromotionsRepository,
14 | ) : ViewModel() {
15 |
16 | val promotions = promotionsRepository.getAvailable()
17 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
18 | }
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/purchasehistory/PurchaseHistoryNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.purchasehistory
2 |
3 | import androidx.compose.ui.window.DialogProperties
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.dialog
7 |
8 | internal const val PURCHASE_HISTORY_ROUTE = "purchase-history"
9 |
10 | internal fun NavController.navigateToPurchaseHistory() {
11 | navigate(route = PURCHASE_HISTORY_ROUTE)
12 | }
13 |
14 | internal fun NavGraphBuilder.purchaseHistoryScreen(onCloseClick: () -> Unit) {
15 | dialog(
16 | route = PURCHASE_HISTORY_ROUTE,
17 | dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
18 | ) {
19 | PurchaseHistoryScreen(onCloseClick = onCloseClick)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/features/profile/src/main/java/com/maruchin/features/profile/returns/ReturnsNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.profile.returns
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.composable
6 |
7 | internal const val RETURNS_ROUTE = "returns"
8 |
9 | internal fun NavController.navigateToReturns() {
10 | navigate(RETURNS_ROUTE)
11 | }
12 |
13 | internal fun NavGraphBuilder.returnsScreen(onBackClick: () -> Unit, onGoToFormClick: () -> Unit) {
14 | composable(RETURNS_ROUTE) {
15 | ReturnsScreen(onBackClick = onBackClick, onGoToFormClick = onGoToFormClick)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/profile/src/main/res/drawable/club_auth_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/features/profile/src/main/res/drawable/club_auth_cover.jpg
--------------------------------------------------------------------------------
/features/profile/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Find out more
4 | Purchase history
5 | BALANCE
6 | Join Club
7 | Login
8 | Click and learn more
9 | Join the Club and receive a 10% discount
10 | Club
11 | Gold
12 | Silver
13 | Standard
14 | My data
15 | My orders
16 | Returns
17 | There are no orders in your account yet
18 | Promotions
19 | Profile
20 | Copy code
21 | Your code
22 | We haven\'t recorded any transactions yet, but everything is ahead of you.
23 | Go to the form
24 | Send the parcel back by courier - complete the online return form and we will refund the funds to your account
25 | OR
26 | Free and quick returns, also in stationary stores
27 |
--------------------------------------------------------------------------------
/features/registration/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.featuremodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.features.registration"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":ui"))
11 | implementation(project(":forms"))
12 | implementation(project(":data:user"))
13 | }
--------------------------------------------------------------------------------
/features/registration/src/main/java/com/maruchin/features/registration/RegistrationGraph.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.registration
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.navigation
6 | import androidx.navigation.navDeepLink
7 | import com.maruchin.features.registration.birthdate.birthDateScreen
8 | import com.maruchin.features.registration.birthdate.navigateToBirthDate
9 | import com.maruchin.features.registration.registrationform.REGISTRATION_ROUTE
10 | import com.maruchin.features.registration.registrationform.registrationFormScreen
11 | import com.maruchin.ui.ROOT_DEEPLINK
12 |
13 | const val REGISTRATION_GRAPH_ROUTE = "registration-graph"
14 | private const val REGISTRATION_DEEPLINK = "$ROOT_DEEPLINK/registration"
15 |
16 | fun NavController.navigateToRegistrationGraph() {
17 | navigate(REGISTRATION_GRAPH_ROUTE)
18 | }
19 |
20 | fun NavGraphBuilder.registrationGraph(navController: NavController) {
21 | navigation(
22 | startDestination = REGISTRATION_ROUTE,
23 | route = REGISTRATION_GRAPH_ROUTE,
24 | deepLinks = listOf(
25 | navDeepLink { uriPattern = REGISTRATION_DEEPLINK }
26 | )
27 | ) {
28 | registrationFormScreen(
29 | onNavigateBack = {
30 | navController.popBackStack()
31 | },
32 | onNavigateToBirthDate = {
33 | navController.navigateToBirthDate()
34 | }
35 | )
36 | birthDateScreen(
37 | onExitRegistration = {
38 | navController.popBackStack()
39 | }
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/features/registration/src/main/java/com/maruchin/features/registration/birthdate/BirthDateNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.registration.birthdate
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.hilt.navigation.compose.hiltViewModel
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 | import com.maruchin.features.registration.registrationform.REGISTRATION_ROUTE
9 |
10 | internal const val BIRTH_DATE_ROUTE = "birth-date"
11 |
12 | internal fun NavController.navigateToBirthDate() {
13 | navigate(BIRTH_DATE_ROUTE) {
14 | popUpTo(REGISTRATION_ROUTE) {
15 | inclusive = true
16 | }
17 | }
18 | }
19 |
20 | internal fun NavGraphBuilder.birthDateScreen(onExitRegistration: () -> Unit) {
21 | composable(BIRTH_DATE_ROUTE) {
22 | val viewModel: BirthDateViewModel = hiltViewModel()
23 |
24 | if (viewModel.birthDateSaved) {
25 | LaunchedEffect(Unit) {
26 | onExitRegistration()
27 | }
28 | }
29 |
30 | BirthDateScreen(
31 | onCloseClick = onExitRegistration,
32 | onSaveBirthDateClick = viewModel::saveBirthDate
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/registration/src/main/java/com/maruchin/features/registration/birthdate/BirthDateViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.registration.birthdate
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.maruchin.data.user.UserRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.launch
11 | import java.time.LocalDate
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | internal class BirthDateViewModel @Inject constructor(
16 | private val userRepository: UserRepository
17 | ) : ViewModel() {
18 |
19 | var birthDateSaved: Boolean by mutableStateOf(false)
20 | private set
21 |
22 | fun saveBirthDate(birthDate: LocalDate) = viewModelScope.launch {
23 | userRepository.updateBirthDate(birthDate)
24 | birthDateSaved = true
25 | }
26 | }
--------------------------------------------------------------------------------
/features/registration/src/main/java/com/maruchin/features/registration/registrationform/RegistrationFormNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.registration.registrationform
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.compose.composable
9 |
10 | internal const val REGISTRATION_ROUTE = "registration-form"
11 |
12 | internal fun NavGraphBuilder.registrationFormScreen(
13 | onNavigateBack: () -> Unit,
14 | onNavigateToBirthDate: () -> Unit
15 | ) {
16 | composable(REGISTRATION_ROUTE) {
17 | val viewModel: RegistrationViewModel = hiltViewModel()
18 | val isLoggedIn by viewModel.isLoggedIn.collectAsState()
19 |
20 | if (isLoggedIn) {
21 | LaunchedEffect(Unit) {
22 | onNavigateToBirthDate()
23 | }
24 | }
25 |
26 | RegistrationFormScreen(
27 | onBackClick = onNavigateBack,
28 | onRegisterClick = viewModel::submitRegistration,
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/features/registration/src/main/java/com/maruchin/features/registration/registrationform/RegistrationViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.features.registration.registrationform
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.maruchin.data.user.PersonalData
6 | import com.maruchin.data.user.User
7 | import com.maruchin.data.user.UserRepository
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.SharingStarted
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class RegistrationViewModel @Inject constructor(
17 | private val userRepository: UserRepository
18 | ) : ViewModel() {
19 |
20 | val isLoggedIn = userRepository.get()
21 | .map { it is User.LoggedIn }
22 | .stateIn(viewModelScope, SharingStarted.Lazily, false)
23 |
24 | fun submitRegistration(personalData: PersonalData, password: String) = viewModelScope.launch {
25 | userRepository.register(
26 | personalData = personalData,
27 | password = password,
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/features/registration/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Register
4 | Birth date
5 | Save birth date
6 |
--------------------------------------------------------------------------------
/forms/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.uimodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.forms"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":data:products"))
11 | implementation(project(":data:deliveries"))
12 | implementation(project(":data:payments"))
13 | implementation(project(":data:user"))
14 | implementation(project(":data:addresses"))
15 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/addressform/AddressForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.addressform
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.compose.ui.unit.dp
10 | import com.maruchin.forms.R
11 | import com.maruchin.forms.textfield.TextField
12 |
13 | @Composable
14 | fun AddressForm(
15 | modifier: Modifier = Modifier,
16 | state: AddressFormState = rememberAddressFormState()
17 | ) {
18 | Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp)) {
19 | TextField(label = stringResource(R.string.first_name), state = state.firstName)
20 | TextField(label = stringResource(R.string.last_name), state = state.lastName)
21 | TextField(label = stringResource(R.string.street), state = state.street)
22 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
23 | TextField(
24 | label = stringResource(R.string.house),
25 | state = state.house,
26 | modifier = Modifier.weight(1f)
27 | )
28 | TextField(
29 | label = stringResource(R.string.apartment),
30 | state = state.apartment,
31 | modifier = Modifier.weight(1f)
32 | )
33 | }
34 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
35 | TextField(
36 | label = stringResource(R.string.postal_code),
37 | state = state.postalCode,
38 | modifier = Modifier.weight(1f)
39 | )
40 | TextField(
41 | label = stringResource(R.string.city),
42 | state = state.city,
43 | modifier = Modifier.weight(1f)
44 | )
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/changepasswordform/ChangePasswordForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.changepasswordform
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.res.stringResource
8 | import androidx.compose.ui.unit.dp
9 | import com.maruchin.forms.R
10 | import com.maruchin.forms.passwordfield.PasswordField
11 | import com.maruchin.forms.passwordsform.PasswordsForm
12 |
13 | @Composable
14 | fun ChangePasswordForm(
15 | modifier: Modifier = Modifier,
16 | state: ChangePasswordFormState = rememberChangePasswordFormState()
17 | ) {
18 | Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(32.dp)) {
19 | PasswordField(
20 | label = stringResource(R.string.current_password),
21 | state = state.currentPassword
22 | )
23 | PasswordsForm(
24 | state = state.newPassword,
25 | firstLabel = stringResource(R.string.new_password),
26 | secondLabel = stringResource(R.string.repeat_new_password),
27 | )
28 | }
29 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/changepasswordform/ChangePasswordFormState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.changepasswordform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import com.maruchin.forms.passwordfield.PasswordFieldState
9 | import com.maruchin.forms.passwordsform.PasswordsFormState
10 | import com.maruchin.forms.passwordsform.rememberNewPasswordFormState
11 | import com.maruchin.forms.passwordfield.rememberPasswordFieldState
12 |
13 | @Stable
14 | class ChangePasswordFormState(
15 | val currentPassword: PasswordFieldState,
16 | val newPassword: PasswordsFormState,
17 | ) {
18 |
19 | val isValid: Boolean by derivedStateOf {
20 | currentPassword.isValid && newPassword.isValid
21 | }
22 | }
23 |
24 | @Composable
25 | fun rememberChangePasswordFormState(): ChangePasswordFormState {
26 | val currentPassword = rememberPasswordFieldState()
27 | val newPassword = rememberNewPasswordFormState()
28 |
29 | return remember(currentPassword, newPassword) {
30 | ChangePasswordFormState(
31 | currentPassword = currentPassword,
32 | newPassword = newPassword,
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/datefield/DateField.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.datefield
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.CalendarToday
7 | import androidx.compose.material3.DatePicker
8 | import androidx.compose.material3.DatePickerDialog
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.OutlinedTextField
12 | import androidx.compose.material3.Text
13 | import androidx.compose.material3.TextButton
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.focus.onFocusChanged
17 | import androidx.compose.ui.tooling.preview.Preview
18 | import androidx.compose.ui.unit.dp
19 |
20 | @OptIn(ExperimentalMaterial3Api::class)
21 | @Composable
22 | fun DateField(
23 | label: String,
24 | modifier: Modifier = Modifier,
25 | state: DateFieldState = rememberDateFieldState()
26 | ) {
27 | OutlinedTextField(
28 | value = state.selectedDateText,
29 | readOnly = true,
30 | onValueChange = {},
31 | label = {
32 | Text(text = label)
33 | },
34 | leadingIcon = {
35 | Icon(imageVector = Icons.Default.CalendarToday, contentDescription = null)
36 | },
37 | modifier = Modifier
38 | .fillMaxWidth()
39 | .then(modifier)
40 | .onFocusChanged(state::onFocusChange),
41 | )
42 |
43 | if (state.isDialogOpen) {
44 | DatePickerDialog(
45 | onDismissRequest = state::onDismiss,
46 | confirmButton = {}
47 | ) {
48 | DatePicker(state = state.datePickerState)
49 | }
50 | }
51 | }
52 |
53 | @Preview(showBackground = true)
54 | @Composable
55 | private fun DateFieldPreview() {
56 | DateField(label = "Birth date", modifier = Modifier.padding(16.dp))
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/emailfield/EmailField.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.emailfield
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.Email
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.OutlinedTextField
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import com.maruchin.forms.R
18 |
19 | @Composable
20 | fun EmailField(
21 | modifier: Modifier = Modifier,
22 | state: EmailFieldState = rememberEmailFieldState()
23 | ) {
24 | OutlinedTextField(
25 | value = state.value,
26 | onValueChange = state::onValueChanged,
27 | modifier = Modifier
28 | .fillMaxWidth()
29 | .then(modifier),
30 | label = {
31 | Text(text = stringResource(R.string.email))
32 | },
33 | leadingIcon = {
34 | Icon(imageVector = Icons.Default.Email, contentDescription = null)
35 | },
36 | isError = state.error != null,
37 | supportingText = state.error?.let {
38 | {
39 | Text(text = it, style = MaterialTheme.typography.bodySmall, color = Color.Red)
40 | }
41 | }
42 | )
43 | }
44 |
45 | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
46 | @Composable
47 | private fun EmailFieldPreview() {
48 | MaterialTheme {
49 | EmailField(modifier = Modifier.padding(16.dp))
50 | }
51 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/emailfield/EmailFieldState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.emailfield
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.saveable.listSaver
9 | import androidx.compose.runtime.saveable.rememberSaveable
10 | import androidx.compose.runtime.setValue
11 | import com.maruchin.data.user.EmailValidationResult
12 | import com.maruchin.data.user.isEmailValid
13 | import com.maruchin.data.user.validateEmail
14 |
15 | @Stable
16 | class EmailFieldState(initialValue: String) {
17 |
18 | var value: String by mutableStateOf(initialValue)
19 | private set
20 |
21 | var error: String? by mutableStateOf(null)
22 | private set
23 |
24 | val isValid: Boolean by derivedStateOf {
25 | isEmailValid(value)
26 | }
27 |
28 | fun onValueChanged(newValue: String) {
29 | value = newValue
30 | error = when (validateEmail(newValue)) {
31 | EmailValidationResult.VALID -> null
32 | EmailValidationResult.EMPTY -> "Email cannot be empty"
33 | EmailValidationResult.INVALID_FORMAT -> "Invalid email format"
34 | }
35 | }
36 | }
37 |
38 | private val emailFieldSaver = listSaver(
39 | save = {
40 | listOf(it.value)
41 | },
42 | restore = {
43 | EmailFieldState(it[0])
44 |
45 | }
46 | )
47 |
48 | @Composable
49 | fun rememberEmailFieldState(): EmailFieldState {
50 | return rememberSaveable(saver = emailFieldSaver) {
51 | EmailFieldState(initialValue = "")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/loginform/LoginForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.loginform
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import androidx.compose.ui.unit.dp
12 | import com.maruchin.forms.passwordfield.PasswordField
13 | import com.maruchin.forms.emailfield.EmailField
14 |
15 | @Composable
16 | fun LoginForm(
17 | modifier: Modifier = Modifier,
18 | state: LoginFormState = rememberLoginFormState()
19 | ) {
20 | Column(
21 | modifier = Modifier
22 | .fillMaxWidth()
23 | .then(modifier),
24 | verticalArrangement = Arrangement.spacedBy(24.dp)
25 | ) {
26 | EmailField(state = state.email)
27 | PasswordField(state = state.password)
28 | }
29 | }
30 |
31 | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
32 | @Composable
33 | private fun LoginFormPreview() {
34 | MaterialTheme {
35 | LoginForm(modifier = Modifier.padding(16.dp))
36 | }
37 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/loginform/LoginFormState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.loginform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import com.maruchin.forms.passwordfield.PasswordFieldState
9 | import com.maruchin.forms.emailfield.EmailFieldState
10 | import com.maruchin.forms.emailfield.rememberEmailFieldState
11 | import com.maruchin.forms.passwordfield.rememberPasswordFieldState
12 |
13 | @Stable
14 | class LoginFormState(
15 | val email: EmailFieldState,
16 | val password: PasswordFieldState,
17 | ) {
18 |
19 | val isValid: Boolean by derivedStateOf {
20 | email.isValid && password.isValid
21 | }
22 | }
23 |
24 | @Composable
25 | fun rememberLoginFormState(): LoginFormState {
26 | val email = rememberEmailFieldState()
27 | val password = rememberPasswordFieldState()
28 | return remember {
29 | LoginFormState(email, password)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/passwordfield/PasswordFieldState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.passwordfield
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.saveable.listSaver
9 | import androidx.compose.runtime.saveable.rememberSaveable
10 | import androidx.compose.runtime.setValue
11 | import com.maruchin.data.user.PasswordValidationResult
12 | import com.maruchin.data.user.isPasswordValid
13 | import com.maruchin.data.user.validatePassword
14 |
15 | @Stable
16 | class PasswordFieldState(initialValue: String) {
17 |
18 | var value: String by mutableStateOf(initialValue)
19 | private set
20 |
21 | var error: String? by mutableStateOf(null)
22 | private set
23 |
24 | val isValid: Boolean by derivedStateOf {
25 | isPasswordValid(value)
26 | }
27 |
28 | fun onValueChanged(newValue: String) {
29 | value = newValue
30 | error = when (validatePassword(newValue)) {
31 | PasswordValidationResult.VALID -> null
32 | PasswordValidationResult.EMPTY -> "Password cannot be empty"
33 | }
34 | }
35 | }
36 |
37 | private val passwordFieldSaver = listSaver(
38 | save = {
39 | listOf(it.value)
40 | },
41 | restore = {
42 | PasswordFieldState(it[0])
43 | }
44 |
45 | )
46 |
47 | @Composable
48 | fun rememberPasswordFieldState(): PasswordFieldState {
49 | return rememberSaveable(saver = passwordFieldSaver) {
50 | PasswordFieldState(initialValue = "")
51 | }
52 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/passwordsform/PasswordsForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.passwordsform
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.compose.ui.unit.dp
10 | import com.maruchin.forms.R
11 | import com.maruchin.forms.passwordfield.PasswordField
12 |
13 | @Composable
14 | fun PasswordsForm(
15 | modifier: Modifier = Modifier,
16 | state: PasswordsFormState = rememberNewPasswordFormState(),
17 | firstLabel: String = stringResource(R.string.password),
18 | secondLabel: String = stringResource(R.string.repeat_password)
19 | ) {
20 | Column(modifier = modifier) {
21 | PasswordField(state = state.firstPassword, label = firstLabel)
22 | Spacer(modifier = Modifier.height(24.dp))
23 | PasswordField(state = state.secondPassword, label = secondLabel)
24 | }
25 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/passwordsform/PasswordsFormState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.passwordsform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import com.maruchin.forms.passwordfield.PasswordFieldState
9 | import com.maruchin.forms.passwordfield.rememberPasswordFieldState
10 |
11 | @Stable
12 | class PasswordsFormState(
13 | val firstPassword: PasswordFieldState,
14 | val secondPassword: PasswordFieldState,
15 | ) {
16 |
17 | val isValid: Boolean by derivedStateOf {
18 | firstPassword.isValid && secondPassword.isValid && firstPassword.value == secondPassword.value
19 | }
20 | }
21 |
22 | @Composable
23 | fun rememberNewPasswordFormState(): PasswordsFormState {
24 | val firstPassword = rememberPasswordFieldState()
25 | val secondPassword = rememberPasswordFieldState()
26 |
27 | return remember(firstPassword, secondPassword) {
28 | PasswordsFormState(firstPassword, secondPassword)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/personaldataform/PersonalDataForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.personaldataform
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Person
8 | import androidx.compose.material.icons.filled.Phone
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import com.maruchin.forms.R
14 | import com.maruchin.forms.textfield.TextField
15 | import com.maruchin.forms.emailfield.EmailField
16 |
17 | @Composable
18 | fun PersonalDataForm(
19 | modifier: Modifier = Modifier,
20 | state: PersonalDataFormState = rememberPersonalDataFormState()
21 | ) {
22 | Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp)) {
23 | TextField(
24 | icon = Icons.Default.Person,
25 | label = stringResource(R.string.first_name),
26 | state = state.firstName,
27 | modifier = Modifier.fillMaxWidth()
28 | )
29 | TextField(
30 | icon = Icons.Default.Person,
31 | label = stringResource(R.string.last_name),
32 | state = state.lastName,
33 | modifier = Modifier.fillMaxWidth()
34 | )
35 | EmailField(
36 | state = state.email,
37 | modifier = Modifier.fillMaxWidth()
38 | )
39 | TextField(
40 | icon = Icons.Default.Phone,
41 | label = stringResource(R.string.phone_number),
42 | state = state.phoneNumber,
43 | modifier = Modifier.fillMaxWidth()
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/personaldataform/PersonalDataFormState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.personaldataform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import com.maruchin.forms.textfield.TextFieldState
9 | import com.maruchin.forms.emailfield.EmailFieldState
10 | import com.maruchin.forms.emailfield.rememberEmailFieldState
11 | import com.maruchin.forms.textfield.rememberTextFieldState
12 | import com.maruchin.data.user.PersonalData
13 |
14 | @Stable
15 | class PersonalDataFormState(
16 | val firstName: TextFieldState,
17 | val lastName: TextFieldState,
18 | val email: EmailFieldState,
19 | val phoneNumber: TextFieldState,
20 | ) {
21 |
22 | val isValid: Boolean by derivedStateOf {
23 | firstName.isValid && lastName.isValid && email.isValid && phoneNumber.isValid
24 | }
25 |
26 | var personalData: PersonalData
27 | get() = PersonalData(
28 | firstName = firstName.value,
29 | lastName = lastName.value,
30 | email = email.value,
31 | phoneNumber = phoneNumber.value,
32 | )
33 | set(value) {
34 | firstName.value = value.firstName
35 | lastName.value = value.lastName
36 | email.onValueChanged(value.email)
37 | phoneNumber.value = value.phoneNumber
38 | }
39 | }
40 |
41 | @Composable
42 | fun rememberPersonalDataFormState(): PersonalDataFormState {
43 | val firstName = rememberTextFieldState()
44 | val lastName = rememberTextFieldState()
45 | val email = rememberEmailFieldState()
46 | val phoneNumber = rememberTextFieldState()
47 |
48 | return remember(firstName, lastName, email, phoneNumber) {
49 | PersonalDataFormState(firstName, lastName, email, phoneNumber)
50 | }
51 | }
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/registrationform/RegistrationForm.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.registrationform
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.unit.dp
8 | import com.maruchin.forms.passwordsform.PasswordsForm
9 | import com.maruchin.forms.personaldataform.PersonalDataForm
10 |
11 | @Composable
12 | fun RegistrationForm(
13 | modifier: Modifier = Modifier,
14 | state: RegistrationFormState = rememberRegistrationFormState(),
15 | ) {
16 | Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp)) {
17 | PersonalDataForm(state = state.personalData)
18 | PasswordsForm(state = state.passwords)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/registrationform/RegistrationFormState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.registrationform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import com.maruchin.forms.passwordsform.PasswordsFormState
9 | import com.maruchin.forms.passwordsform.rememberNewPasswordFormState
10 | import com.maruchin.forms.personaldataform.PersonalDataFormState
11 | import com.maruchin.forms.personaldataform.rememberPersonalDataFormState
12 |
13 | @Stable
14 | class RegistrationFormState(
15 | val personalData: PersonalDataFormState,
16 | val passwords: PasswordsFormState,
17 | ) {
18 |
19 | val isValid: Boolean by derivedStateOf {
20 | personalData.isValid && passwords.isValid
21 | }
22 | }
23 |
24 | @Composable
25 | fun rememberRegistrationFormState(): RegistrationFormState {
26 | val personalData = rememberPersonalDataFormState()
27 | val newPasswordFormState = rememberNewPasswordFormState()
28 |
29 | return remember(personalData, newPasswordFormState) {
30 | RegistrationFormState(personalData, newPasswordFormState)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/textfield/TextField.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.textfield
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.material3.Icon
5 | import androidx.compose.material3.OutlinedTextField
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.vector.ImageVector
10 |
11 | @Composable
12 | fun TextField(
13 | label: String,
14 | modifier: Modifier = Modifier,
15 | icon: ImageVector? = null,
16 | state: TextFieldState = rememberTextFieldState()
17 | ) {
18 | OutlinedTextField(
19 | value = state.value,
20 | onValueChange = { state.value = it },
21 | modifier = Modifier
22 | .fillMaxWidth()
23 | .then(modifier),
24 | label = {
25 | Text(text = label)
26 | },
27 | leadingIcon = icon?.let {
28 | {
29 | Icon(imageVector = icon, contentDescription = null)
30 | }
31 | },
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/forms/src/main/java/com/maruchin/forms/textfield/TextFieldState.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.forms.textfield
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.saveable.listSaver
9 | import androidx.compose.runtime.saveable.rememberSaveable
10 | import androidx.compose.runtime.setValue
11 |
12 | @Stable
13 | class TextFieldState {
14 |
15 | var value: String by mutableStateOf("")
16 |
17 | val isValid: Boolean by derivedStateOf {
18 | value.isNotBlank()
19 | }
20 | }
21 |
22 | private val textFieldSaver = listSaver(
23 | save = {
24 | listOf(it.value)
25 | },
26 | restore = {
27 | TextFieldState().apply {
28 | value = it[0]
29 | }
30 | }
31 | )
32 |
33 | @Composable
34 | fun rememberTextFieldState(): TextFieldState {
35 | return rememberSaveable(saver = textFieldSaver) {
36 | TextFieldState()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/forms/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | New password
4 | Repeat new password
5 | Current password
6 | First name
7 | Last name
8 | Phone number
9 | Password
10 | Repeat password
11 | City
12 | Postal code
13 | Apartment
14 | House
15 | Street
16 | Email
17 | Birth date
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Maruchin1/android-navigation/3c423bb6a4ffad208ded652972e69f4221bba5fc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Oct 06 14:30:41 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | includeBuild("build-logic")
3 | repositories {
4 | google()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "android-navigation"
18 |
19 | include(":app")
20 |
21 | include(":features:home")
22 | include(":features:category-browser")
23 | include(":features:product-browser")
24 | include(":features:product-card")
25 | include(":features:login")
26 | include(":features:profile")
27 | include(":features:my-data")
28 | include(":features:registration")
29 | include(":features:cart")
30 | include(":features:order")
31 | include(":features:favorites")
32 |
33 | include(":data:categories")
34 | include(":data:products")
35 | include(":data:user")
36 | include(":data:promotions")
37 | include(":data:addresses")
38 | include(":data:cart")
39 | include(":data:deliveries")
40 | include(":data:order")
41 | include(":data:payments")
42 |
43 | include(":forms")
44 | include(":ui")
45 |
--------------------------------------------------------------------------------
/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("buildlogic.uimodule")
3 | }
4 |
5 | android {
6 | namespace = "com.maruchin.ui"
7 | }
8 |
9 | dependencies {
10 | implementation(project(":data:products"))
11 | implementation(project(":data:deliveries"))
12 | implementation(project(":data:payments"))
13 | implementation(project(":data:user"))
14 | implementation(project(":data:addresses"))
15 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/AllProductsButton.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3Api::class)
2 |
3 | package com.maruchin.ui
4 |
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.aspectRatio
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.OutlinedCard
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.alpha
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun AllProductsButton(onClick: () -> Unit) {
24 | OutlinedCard(
25 | modifier = Modifier
26 | .width(150.dp)
27 | .padding(8.dp),
28 | onClick = onClick
29 | ) {
30 | Box(
31 | modifier = Modifier
32 | .fillMaxWidth()
33 | .aspectRatio(1f / 1f),
34 | contentAlignment = Alignment.Center,
35 | ) {
36 | Text(
37 | text = stringResource(R.string.show_all),
38 | style = MaterialTheme.typography.titleMedium,
39 | modifier = Modifier.alpha(0.5f)
40 | )
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/Deeplink.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | const val ROOT_DEEPLINK = "app://com.maruchin.androidnavigation"
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/DeliveryItem.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material3.Divider
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.res.painterResource
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.dp
20 | import com.maruchin.data.deliveries.R
21 |
22 | @Composable
23 | fun DeliveryItem(
24 | @DrawableRes logo: Int,
25 | name: String,
26 | price: Float,
27 | onClick: () -> Unit,
28 | ) {
29 | Row(
30 | modifier = Modifier
31 | .clickable { onClick() }
32 | .padding(16.dp),
33 | horizontalArrangement = Arrangement.spacedBy(16.dp),
34 | verticalAlignment = Alignment.CenterVertically,
35 | ) {
36 | Image(
37 | painter = painterResource(logo),
38 | contentDescription = null,
39 | modifier = Modifier.size(64.dp)
40 | )
41 | Text(text = name, style = MaterialTheme.typography.titleMedium)
42 | Spacer(modifier = Modifier.weight(1f))
43 | Text(
44 | text = "$ $price",
45 | style = MaterialTheme.typography.titleMedium
46 | )
47 | }
48 | Divider(modifier = Modifier.padding(horizontal = 16.dp))
49 | }
50 |
51 | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
52 | @Composable
53 | private fun DeliveryItemPreview() {
54 | DeliveryItem(
55 | logo = R.drawable.dhl_logo,
56 | name = "DHL",
57 | price = 10f,
58 | onClick = {},
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/NavigationTransitions.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import androidx.compose.animation.AnimatedContentTransitionScope
4 | import androidx.compose.animation.EnterTransition
5 | import androidx.compose.animation.ExitTransition
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.navigation.NavBackStackEntry
9 |
10 | fun AnimatedContentTransitionScope.screenSlideIn(): EnterTransition =
11 | slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start)
12 |
13 | fun screenFadeOut(): ExitTransition = fadeOut()
14 |
15 | fun screenFadeIn(): EnterTransition = fadeIn()
16 |
17 | fun AnimatedContentTransitionScope.screenSlideOut(): ExitTransition =
18 | slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End)
19 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/OpenEmailApp.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 |
6 | fun Context.openEmailApp() {
7 | val intent = Intent(Intent.ACTION_MAIN)
8 | intent.addCategory(Intent.CATEGORY_APP_EMAIL)
9 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
10 | startActivity(intent)
11 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/OpenWebsite.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.browser.customtabs.CustomTabsIntent
6 |
7 | fun Context.openWebsite(uri: Uri) {
8 | CustomTabsIntent.Builder()
9 | .build()
10 | .launchUrl(this, uri)
11 | }
12 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/PaymentItem.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.aspectRatio
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.CardDefaults
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.OutlinedCard
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.layout.ContentScale
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.tooling.preview.Preview
20 | import androidx.compose.ui.unit.dp
21 | import com.maruchin.data.payments.R
22 |
23 | @OptIn(ExperimentalMaterial3Api::class)
24 | @Composable
25 | fun PaymentItem(
26 | @DrawableRes logo: Int,
27 | modifier: Modifier = Modifier,
28 | onClick: (() -> Unit)? = null
29 | ) {
30 | OutlinedCard(
31 | modifier = Modifier
32 | .aspectRatio(1f / 1f)
33 | .fillMaxWidth()
34 | .then(modifier),
35 | colors = CardDefaults.outlinedCardColors(containerColor = Color.White),
36 | onClick = { onClick?.invoke() },
37 | ) {
38 | Box(
39 | modifier = Modifier
40 | .fillMaxSize()
41 | .padding(32.dp),
42 | contentAlignment = Alignment.Center
43 | ) {
44 | Image(
45 | painter = painterResource(logo),
46 | contentDescription = null,
47 | modifier = Modifier.fillMaxWidth(),
48 | contentScale = ContentScale.FillWidth,
49 | )
50 | }
51 | }
52 | }
53 |
54 | @Preview
55 | @Composable
56 | private fun PaymentItemPreview() {
57 | PaymentItem(logo = R.drawable.paypal_logo, onClick = {})
58 | }
59 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/ProductGrid.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import androidx.compose.foundation.lazy.grid.GridCells
4 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
5 | import androidx.compose.foundation.lazy.grid.items
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import com.maruchin.data.products.Product
9 |
10 | @Composable
11 | fun ProductGrid(
12 | modifier: Modifier,
13 | products: List,
14 | onShowProduct: (productId: String) -> Unit
15 | ) {
16 | LazyVerticalGrid(
17 | columns = GridCells.Fixed(2),
18 | modifier = modifier,
19 | ) {
20 | items(products) { product ->
21 | ProductItem(
22 | image = product.images.first(),
23 | title = product.name,
24 | price = product.price,
25 | isFavorite = product.isFavorite,
26 | onClick = { onShowProduct(product.id) }
27 | )
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/maruchin/ui/ScreenContentPlaceholder.kt:
--------------------------------------------------------------------------------
1 | package com.maruchin.ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.vector.ImageVector
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 |
20 | @Composable
21 | fun ScreenContentPlaceholder(icon: ImageVector, text: String, modifier: Modifier = Modifier) {
22 | Column(
23 | modifier = Modifier
24 | .fillMaxSize()
25 | .then(modifier),
26 | horizontalAlignment = Alignment.CenterHorizontally,
27 | verticalArrangement = Arrangement.Center
28 | ) {
29 | Icon(
30 | imageVector = icon,
31 | contentDescription = null,
32 | modifier = Modifier.size(48.dp),
33 | tint = Color.LightGray
34 | )
35 | Spacer(modifier = Modifier.height(16.dp))
36 | Text(
37 | text = text,
38 | style = MaterialTheme.typography.bodyLarge,
39 | textAlign = TextAlign.Center,
40 | color = Color.LightGray
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/ui/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Show all
4 | Delete
5 |
--------------------------------------------------------------------------------