├── .github
└── workflows
│ └── ComposeFeaturedBasedMultiModule.yml
├── .gitignore
├── .idea
├── .gitignore
├── appInsightsSettings.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinScripting.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── example
│ │ └── composefeaturebasedmultimodule
│ │ ├── MainApplication.kt
│ │ ├── SingleActivity.kt
│ │ └── ui
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_background.xml
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── themes.xml
│ ├── values
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── core
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── example
│ └── core
│ ├── components
│ ├── CoilImageComponent.kt
│ ├── ErrorComponent.kt
│ └── LoadingComponent.kt
│ ├── di
│ └── CoroutineModule.kt
│ ├── model
│ └── GenericException.kt
│ ├── navigation
│ └── NavigationService.kt
│ ├── presentation
│ └── StateAndEventViewModel.kt
│ └── utils
│ ├── Constants.kt
│ └── Qualifiers.kt
├── detail
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── example
│ │ └── detail
│ │ ├── data
│ │ ├── api
│ │ │ ├── DetailApi.kt
│ │ │ ├── datasource
│ │ │ │ ├── DetailDataSource.kt
│ │ │ │ └── DetailDataSourceImpl.kt
│ │ │ ├── di
│ │ │ │ └── DataSourceModule.kt
│ │ │ └── model
│ │ │ │ └── ItemDetailResponse.kt
│ │ └── domain_impl
│ │ │ ├── di
│ │ │ └── UseCaseModule.kt
│ │ │ ├── mapper
│ │ │ └── ItemDetailMapper.kt
│ │ │ └── usecase
│ │ │ └── GetItemDetailUseCaseImpl.kt
│ │ ├── domain
│ │ ├── model
│ │ │ └── ItemDetail.kt
│ │ └── usecase
│ │ │ └── GetItemDetailUseCase.kt
│ │ └── presentation
│ │ ├── DetailScreen.kt
│ │ ├── DetailSearchScreen.kt
│ │ ├── DetailViewModel.kt
│ │ ├── components
│ │ └── DetailContent.kt
│ │ ├── state
│ │ └── DetailUIState.kt
│ │ └── uievent
│ │ └── DetailUIEvent.kt
│ └── test
│ └── java
│ └── com
│ └── example
│ └── detail
│ ├── data
│ ├── datasource
│ │ └── DetailDataSourceTest.kt
│ └── domainimpl
│ │ └── GetItemDetailUseCaseTest.kt
│ └── presentation
│ └── DetailViewModelTest.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── home
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── example
│ └── home
│ ├── data
│ ├── api
│ │ ├── HomeApi.kt
│ │ ├── datasource
│ │ │ ├── HomeDataSource.kt
│ │ │ └── HomeDataSourceImpl.kt
│ │ ├── di
│ │ │ └── DataSourceModule.kt
│ │ └── model
│ │ │ └── HomeResponse.kt
│ └── domainimpl
│ │ ├── di
│ │ └── UseCaseModule.kt
│ │ ├── mapper
│ │ └── HomeSectionsMapper.kt
│ │ └── usecase
│ │ └── GetInitialHomeUseCaseImpl.kt
│ ├── domain
│ ├── model
│ │ ├── BannerItem.kt
│ │ ├── CatalogItem.kt
│ │ ├── HomeSections.kt
│ │ └── ProductItem.kt
│ └── usecase
│ │ └── GetInitialHomeUseCase.kt
│ └── presentation
│ ├── HomeScreen.kt
│ ├── HomeViewModel.kt
│ ├── components
│ ├── DetailBottomSheet.kt
│ ├── HomeScreenContent.kt
│ ├── SectionList.kt
│ └── VerticalItemCard.kt
│ ├── sections
│ ├── BannerSection.kt
│ ├── SectionTitle.kt
│ └── SlidableSection.kt
│ ├── state
│ └── HomeUIState.kt
│ └── uievent
│ └── HomeUIEvent.kt
├── lint.xml
├── list
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── example
│ └── list
│ ├── data
│ ├── api
│ │ ├── ListApi.kt
│ │ ├── datasource
│ │ │ ├── ListDataSource.kt
│ │ │ └── ListDataSourceImpl.kt
│ │ ├── di
│ │ │ └── DataSourceModule.kt
│ │ └── model
│ │ │ └── ListResponse.kt
│ └── domain_impl
│ │ ├── di
│ │ └── UseCaseModule.kt
│ │ ├── mapper
│ │ └── ListDataMapper.kt
│ │ └── usecase
│ │ └── GetListUseCaseImpl.kt
│ ├── domain
│ ├── model
│ │ └── ListData.kt
│ └── usecase
│ │ └── GetListUseCase.kt
│ └── presentation
│ ├── ListScreen.kt
│ ├── ListViewModel.kt
│ ├── components
│ └── ListContent.kt
│ ├── event
│ └── ListUIEvent.kt
│ └── state
│ └── ListUIState.kt
├── navigation
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── example
│ └── navigation
│ ├── AppNavigation.kt
│ ├── Destination.kt
│ ├── Navigator.kt
│ ├── di
│ └── NavigationModule.kt
│ ├── graph
│ ├── DetailGraph.kt
│ ├── DetailGraphBuilder.kt
│ ├── DetailMain.kt
│ └── DetailSearch.kt
│ ├── screens
│ ├── Detail.kt
│ ├── Home.kt
│ └── List.kt
│ └── utils
│ ├── ArgsScreen.kt
│ ├── NavigationDestination.kt
│ ├── NavigationGraph.kt
│ ├── NodeScreen.kt
│ └── WithoutArgsScreen.kt
├── network
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── example
│ └── network
│ ├── di
│ └── NetworkModule.kt
│ └── extensions
│ └── ApiHelper.kt
└── settings.gradle.kts
/.github/workflows/ComposeFeaturedBasedMultiModule.yml:
--------------------------------------------------------------------------------
1 | name: ComposeFeatureBasedMultiModule CI Workflow
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout Repository
15 | uses: actions/checkout@v2
16 |
17 | - name: Setup Java JDK
18 | uses: actions/setup-java@v3.13.0
19 | with:
20 | java-version: '18'
21 | distribution: 'adopt'
22 |
23 | - name: Build with Gradle
24 | run: ./gradlew build
25 |
26 | - name: Run Tests
27 | run: ./gradlew test
28 |
29 | - name: Upload a Build Artifact
30 | uses: actions/upload-artifact@v3.1.3
31 | with:
32 | name: ComposeFeatureBasedMultiModule.apk
33 | path: app/build/outputs/apk/debug/app-debug.apk
34 |
--------------------------------------------------------------------------------
/.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 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
45 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinScripting.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Descriptions
2 |
3 | # Why and What is the aim?
4 |
5 | This project aims to demonstrate a feature-based modularization by managing the inter-feature dependencies through a dedicated navigation module and draws inspiration from Hexagonal Architecture and Clean Architecture to isolate the core of the application (domain logic or business logic) from other factors, allowing for a more flexible and decoupled system design.
6 | 
7 |
8 | # Architecture Opinion
9 |
10 | In addition, the project adopts a Hexagonal Architecture ( Use Case -(Adapter) & Use Case(Port) ) with a Clean Architecture ( Data - Domain - Presentation like in an SS at Feature Module - `home` ). By establishing domain implementation (domain-impl) as the adapter that connects to the domain port, the project leverages certain aspects of various architectural patterns without being strictly bound to any single one. This hybrid approach allows the application to benefit from the strengths of different architectures while maintaining the flexibility to adapt to specific project needs.
11 |
12 | With this opinion, the domain layer has a sole dependency on domain-impl. Within the data layer, structures like API and persistence are focused solely on their respective operations. The dependency of the domain on the data layer is mitigated through the use of mappers within the domain-impl. These mappers transform data responses into domain entities, thus decoupling the domain logic from the specifics of the data source implementations. This is a strategic design choice that preserves the purity of the domain layer, allowing it to evolve independently of the data layer changes and maintaining the domain model's integrity.
13 |
14 | 
15 |
16 | **The main rule and our goal is, to isolate the core of the application (domain logic or business logic) from other factors, Hexagonal Architecture and Clean Architecture are just tools for us, we are trying to use the places that are suitable for us here by taking advantage of both.**
17 |
18 | # Module Descriptions
19 |
20 | ## Feature Module - `home`
21 | 
22 |
23 | Each feature module is divided into data, domain, and presentation layers. The data layer is further divided into API, DomainImpl and Persistence. The main reason for this distinction is the principle of single responsibility and the management of the resource from a single point. For example, while the API module is only responsible for communicating with remote services, with domainimpl we prevent the dependency of the domain layer on the API layer.
24 | This structure shows how an Android application can be developed in a sustainable and scalable way, inspired by architectural principles such as Clean Architecture and Hexagonal Architecture ( Like Adapter is domain-impl and port is domain).
25 |
26 |
27 | ### Advantages:
28 | - **Single-Stop Management of Resources:** Managing data and functions from a central point provides consistency and order within the system.
29 | - **Independence Between Layers:** Thanks to the independence between layers, it is possible to develop each module on its own without being affected by changes.
30 | - **Modularity:** Since the system has a modular structure, it is easier to integrate new features or changes.
31 | - **Testability:** The independence of each layer makes testing processes more efficient and focused.
32 |
33 | ### Disadvantages:
34 | - **Configuration Complexity:** The multitude of layers and modules.
35 | - **Dependency Management:** Maintaining the independence of each module can become difficult as the project grows.
36 |
37 | ## Navigation Module - `navigation`
38 | 
39 |
40 | This module orchestrates the screen transitions and manages the navigation routes within the app. The Navigator class is equipped with functions that facilitate navigation to different screens, while AppNavigation is responsible for setting up the navigation routes. Crucially, the Navigation module operates independently of other modules, which plays a key role in decoupling feature modules from one another. This means that individual features do not have direct knowledge of each other, and all inter-feature navigation is coordinated through the Navigation module.
41 |
42 | ### Navigation Flow:
43 | 
44 |
45 | ### Advantages:
46 |
47 | - **Decoupling of Feature Modules:** The Navigation module's independence ensures that feature modules do not depend on each other, allowing for more modular and interchangeable components within the application.
48 | - **Centralized Navigation Handling:** A dedicated class for navigation streamlines all navigation-related logic into a single, manageable location.
49 | - **Separation of Concerns:** AppNavigation focuses exclusively on route configuration, allowing Navigator to handle the execution of navigation commands without interference.
50 | - **Flexibility:** Supports dynamic navigation flows and is readily extensible to incorporate new features or screens, catering to the evolving needs of the application.
51 |
52 | ### Disadvantages:
53 |
54 | - **Documentation:** Without well-documented argument-passing systems, may find it challenging to grasp the navigation logic.
55 |
56 | ### Concerns:
57 | - **Startup Time:** While @Composable screen functions are only invoked when necessary, the impact on the app's startup time can vary depending on project complexity and screen content it needs to be tried with its project.
58 |
59 | ### Screen Adding Mechanism:
60 |
61 | 
62 |
63 |
64 | ## Network Module - `network`
65 |
66 | The Network Module is a critical component of the architecture, encompassing all aspects of networking logic. It's crafted to function independently, sourcing its constants from the core module while remaining detached from other modules.
67 |
68 | ### Advantages :
69 |
70 | - **Isolation:** Isolating network operations allows the rest of the application to be indifferent to the data's origin, whether it's fetched from a remote server or local database.
71 | - **Single Responsibility:** Dedicated to network transactions, the module serves as a centralized point for implementing changes related to network operations.
72 | - **Reusability:** With consistent data contracts, the Network Module can be repurposed across various projects or features within the same project.
73 |
74 | ### Disadvantages :
75 |
76 | - **Modular Overhead:** An extensive number of modules can introduce complexity in the build configuration and may lead to longer build times.
77 | - **Dependency Management:** Ensuring that the Network Module remains fully decoupled requires meticulous management of dependencies, which can be challenging as the project grows.
78 |
79 | ### Dependencies Flow
80 |
81 | 
82 |
83 | ### E2E Unit Test - Detail Module
84 |
85 | 
86 |
87 | ## App Screens:
88 |
89 | 
90 | 
91 | 
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id(libs.plugins.daggerHilt.get().toString())
5 | id(libs.plugins.ksp.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.mainNamespace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | applicationId = libs.plugins.mainNamespace.get().toString()
14 | minSdk = libs.versions.minSdk.get().toInt()
15 | targetSdk =libs.versions.compileSdk.get().toInt()
16 | versionCode = 1
17 | versionName = "1.0"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 |
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_1_8
26 | targetCompatibility = JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = libs.versions.jvmTarget.get()
30 | }
31 | buildFeatures {
32 | compose = true
33 | }
34 | composeOptions {
35 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
36 | }
37 | packaging {
38 | resources {
39 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
40 | }
41 | }
42 | }
43 |
44 | dependencies {
45 | implementation(project(":navigation"))
46 | implementation(project(":core"))
47 | implementation(project(":home"))// just home Compose Screen
48 | implementation(project(":list")) // just list Compose Screen
49 | implementation(project(":detail")) // just list Compose Screen
50 | implementation(project(":network"))
51 |
52 | //region D.I Dependencies
53 | ksp(libs.hilt.compiler)
54 | ksp(libs.hilt.ksp.compiler)
55 | implementation(libs.hilt.core)
56 | //endregion
57 |
58 | //region Compose Dependencies
59 | implementation(libs.compose.activity)
60 | implementation(platform(libs.compose.bom))
61 | implementation(libs.ui)
62 | implementation(libs.compose.ui.graphics)
63 | implementation(libs.ui.tooling.preview)
64 | implementation(libs.compose.ui.material)
65 | androidTestImplementation(platform(libs.compose.bom))
66 | //endregion
67 |
68 | //region Core Dependencies
69 | implementation(libs.appcompat)
70 | implementation(libs.android.core)
71 | //endregion
72 |
73 | implementation(libs.lifecycle.ktx)
74 |
75 | }
--------------------------------------------------------------------------------
/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 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/composefeaturebasedmultimodule/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.composefeaturebasedmultimodule
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MainApplication: Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/example/composefeaturebasedmultimodule/SingleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.composefeaturebasedmultimodule
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import com.example.composefeaturebasedmultimodule.ui.theme.ComposeFeatureBasedMultiModuleTheme
7 | import com.example.detail.presentation.DetailScreen
8 | import com.example.detail.presentation.DetailSearchScreen
9 | import com.example.home.presentation.HomeScreen
10 | import com.example.list.presentation.ListScreen
11 | import com.example.navigation.AppNavigation
12 | import com.example.navigation.Navigator
13 | import com.example.navigation.graph.DetailScreens
14 | import dagger.hilt.android.AndroidEntryPoint
15 | import javax.inject.Inject
16 |
17 | @AndroidEntryPoint
18 | class SingleActivity : ComponentActivity() {
19 |
20 | @Inject
21 | lateinit var navigator: Navigator
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContent {
26 | ComposeFeatureBasedMultiModuleTheme {
27 | AppNavigation(
28 | navigator = navigator,
29 | homeScreen = {
30 | HomeScreen()
31 | },
32 | listScreen = {
33 | ListScreen()
34 | },
35 | detailScreen = {// We can get args with "it" if we need
36 | DetailScreen()
37 | },
38 | detailScreenWithGraph = DetailScreens(
39 | detailMain = { DetailScreen() },
40 | detailSearch = { DetailSearchScreen() }
41 | )
42 | )
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/composefeaturebasedmultimodule/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.example.composefeaturebasedmultimodule.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/example/composefeaturebasedmultimodule/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.example.composefeaturebasedmultimodule.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun ComposeFeatureBasedMultiModuleTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/composefeaturebasedmultimodule/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.example.composefeaturebasedmultimodule.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ComposeFeatureBasedMultiModule
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.2.1" apply false
4 | id("org.jetbrains.kotlin.android") version "1.9.0" apply false
5 | id("com.android.library") version "8.2.1" apply false
6 | id("com.google.dagger.hilt.android") version "2.47" apply false
7 | id("com.google.devtools.ksp") version "1.9.21-1.0.16" apply false
8 | }
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.daggerHilt.get().toString())
5 | id(libs.plugins.ksp.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.coreNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation(libs.retrofit.core)
33 |
34 | //region D.I Dependencies
35 | implementation(libs.hilt.core)
36 | ksp(libs.hilt.compiler)
37 | ksp(libs.hilt.ksp.compiler)
38 | //endregion
39 |
40 | //region Presentation Dependencies
41 | implementation(platform(libs.compose.bom))
42 | implementation(libs.compose.hilt.navigation)
43 | implementation(libs.compose.ui.graphics)
44 | implementation(libs.compose.navigation)
45 | implementation(libs.compose.ui.material)
46 | implementation(libs.compose.activity)
47 | implementation(libs.coil)
48 | //endregion
49 | }
--------------------------------------------------------------------------------
/core/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/core/consumer-rules.pro
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/components/CoilImageComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.layout.ContentScale
9 | import coil.compose.rememberAsyncImagePainter
10 |
11 | @Composable
12 | fun CoilImageComponent(
13 | imageUrl: String,
14 | modifier: Modifier = Modifier,
15 | contentScale: ContentScale = ContentScale.Fit,
16 | onClick: (() -> Unit)? = null,
17 | contentDescription: String,
18 | ) {
19 | val imageModifier = modifier
20 | .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
21 | .fillMaxSize()
22 |
23 | Image(
24 | painter = rememberAsyncImagePainter(imageUrl),
25 | contentDescription = contentDescription,
26 | modifier = imageModifier,
27 | contentScale = contentScale
28 | )
29 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/components/ErrorComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 |
11 | @Composable
12 | fun ErrorComponent(error: Throwable?) {
13 | Box(
14 | contentAlignment = Alignment.Center,
15 | modifier = Modifier.fillMaxSize()
16 | ) {
17 | error?.message?.let { Text(text = it, color = Color.Red) }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/components/LoadingComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun LoadingComponent() {
12 | Box(
13 | contentAlignment = Alignment.Center,
14 | modifier = Modifier.fillMaxSize()
15 | ) {
16 | CircularProgressIndicator()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/di/CoroutineModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.di
2 |
3 | import com.example.core.utils.IODispatcher
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlin.coroutines.CoroutineContext
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object CoroutineModule {
14 |
15 | @Provides
16 | @IODispatcher
17 | fun provideIODispatcher(): CoroutineContext = Dispatchers.IO
18 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/model/GenericException.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.model
2 |
3 | import java.io.IOException
4 |
5 | data class GenericException(
6 | override val message: String?,
7 | val hasUserFriendlyMessage: Boolean
8 | ) : IOException()
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/navigation/NavigationService.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.navigation
2 |
3 | import androidx.navigation.NavOptionsBuilder
4 |
5 | interface NavigationService {
6 | fun navigateTo(destination: String, navOptions: NavOptionsBuilder.() -> Unit = {})
7 | fun goBack()
8 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/presentation/StateAndEventViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.presentation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.flow.MutableSharedFlow
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 | import kotlinx.coroutines.flow.asStateFlow
9 | import kotlinx.coroutines.flow.update
10 | import kotlinx.coroutines.launch
11 |
12 | abstract class StateAndEventViewModel(initialState: UiState) : ViewModel() {
13 |
14 | private val events = MutableSharedFlow(replay = 0)
15 | private val _uiState: MutableStateFlow = MutableStateFlow(initialState)
16 | val uiState: StateFlow = _uiState.asStateFlow()
17 |
18 | init {
19 | viewModelScope.launch {
20 | events.collect { event ->
21 | handleEvent(event)
22 | }
23 | }
24 | }
25 |
26 | protected abstract suspend fun handleEvent(event: Event)
27 |
28 | protected fun updateUiState(update: UiState.() -> UiState) {
29 | _uiState.update { _uiState.value.update() }
30 | }
31 |
32 | fun onEvent(event: Event) {
33 | viewModelScope.launch {
34 | events.emit(event)
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.utils
2 |
3 | object Constants {
4 | const val DEVELOPMENT_MODE = true
5 | const val BASE_URL = "https://raw.githubusercontent.com"
6 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/example/core/utils/Qualifiers.kt:
--------------------------------------------------------------------------------
1 | package com.example.core.utils
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | @Retention(AnnotationRetention.BINARY)
7 | annotation class IODispatcher
8 |
--------------------------------------------------------------------------------
/detail/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/detail/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.daggerHilt.get().toString())
5 | id(libs.plugins.ksp.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.detailNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation(project(":core"))
33 | implementation(project(":network"))
34 |
35 | implementation(libs.hilt.core)
36 | ksp(libs.hilt.compiler)
37 | ksp(libs.hilt.ksp.compiler)
38 | implementation(libs.retrofit.core)
39 |
40 | //region Presentation Dependencies
41 | implementation(platform(libs.compose.bom))
42 | implementation(libs.compose.hilt.navigation)
43 | implementation(libs.compose.ui.graphics)
44 | implementation(libs.compose.navigation)
45 | implementation(libs.pager)
46 | implementation(libs.compose.ui.material)
47 | implementation(libs.compose.activity)
48 | implementation(libs.coil)
49 | //endregion
50 |
51 | //region test
52 | testImplementation(libs.junit)
53 | testImplementation(libs.mockito)
54 | testImplementation(libs.mockito.core)
55 | testImplementation(libs.android.test)
56 | testImplementation(libs.coroutines.test)
57 | //endregion
58 | }
--------------------------------------------------------------------------------
/detail/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/detail/consumer-rules.pro
--------------------------------------------------------------------------------
/detail/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
--------------------------------------------------------------------------------
/detail/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/api/DetailApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.api
2 |
3 | import com.example.detail.data.api.model.ItemDetailResponse
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 |
7 | interface DetailApi {
8 | @GET("/basaransuleyman/suleyman-basaranoglu-json/main/detail-page")
9 | suspend fun getDetail(): Response
10 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/api/datasource/DetailDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.api.datasource
2 |
3 | import com.example.detail.data.api.model.ItemDetailResponse
4 |
5 | interface DetailDataSource {
6 | suspend fun getDetail(): ItemDetailResponse
7 | }
8 |
9 |
10 |
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/api/datasource/DetailDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.api.datasource
2 |
3 | import com.example.network.extensions.handleCall
4 | import com.example.detail.data.api.DetailApi
5 | import com.example.detail.data.api.model.ItemDetailResponse
6 | import javax.inject.Inject
7 |
8 | internal class DetailDataSourceImpl @Inject constructor(
9 | private val api: DetailApi
10 | ) : DetailDataSource {
11 |
12 | override suspend fun getDetail(): ItemDetailResponse {
13 | return handleCall {
14 | api.getDetail()
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/api/di/DataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.api.di
2 |
3 | import com.example.detail.data.api.DetailApi
4 | import com.example.detail.data.api.datasource.DetailDataSource
5 | import com.example.detail.data.api.datasource.DetailDataSourceImpl
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import retrofit2.Retrofit
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | class DataSourceModule {
16 |
17 | @Singleton
18 | @Provides
19 | fun provideHomeApi(retrofit: Retrofit): DetailApi {
20 | return retrofit.create(DetailApi::class.java)
21 | }
22 |
23 | @Singleton
24 | @Provides
25 | internal fun provideInitialResponseDataSource(apiService: DetailApi): DetailDataSource {
26 | return DetailDataSourceImpl(apiService)
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/api/model/ItemDetailResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.api.model
2 |
3 | data class ItemDetailResponse(
4 | val productImage: String,
5 | val productName: String,
6 | val productId: String,
7 | val subText: String,
8 | val review: String? = null,
9 | val questions: String? = null,
10 | val share: String,
11 | val otherProducts: List? = null,
12 | val productOptions: List
13 | )
14 |
15 | data class OtherProductResponse(
16 | val productImage: String? = null,
17 | val productName: String? = null,
18 | val subText: String? = null
19 | )
20 |
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/domain_impl/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.domain_impl.di
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.detail.data.api.datasource.DetailDataSource
5 | import com.example.detail.data.domain_impl.usecase.GetItemDetailUseCaseImpl
6 | import com.example.detail.domain.usecase.GetItemDetailUseCase
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import kotlinx.coroutines.Dispatchers
12 | import javax.inject.Singleton
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | object UseCaseModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideIODispatcher(): CoroutineContext {
22 | return Dispatchers.IO
23 | }
24 |
25 | @Provides
26 | @Singleton
27 | fun provideGetDetailUseCase(
28 | dataSource: DetailDataSource,
29 | @IODispatcher dispatcher: CoroutineContext
30 | ): GetItemDetailUseCase {
31 | return GetItemDetailUseCaseImpl(dataSource, dispatcher)
32 | }
33 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/domain_impl/mapper/ItemDetailMapper.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.domain_impl.mapper
2 |
3 | import com.example.detail.data.api.model.ItemDetailResponse
4 | import com.example.detail.data.api.model.OtherProductResponse
5 | import com.example.detail.domain.model.ItemDetail
6 | import com.example.detail.domain.model.OtherProducts
7 |
8 | fun ItemDetailResponse.mapToItemDetail(): ItemDetail {
9 |
10 | return ItemDetail(
11 | productImage = this.productImage,
12 | productName = this.productName,
13 | productId = this.productId,
14 | subText = this.subText,
15 | review = this.review ?: "",
16 | questions = this.questions ?: "",
17 | share = this.share,
18 | otherProducts = this.otherProducts.toDomainProducts(),
19 | productOptions = this.productOptions
20 | )
21 | }
22 |
23 | private fun List?.toDomainProducts(): List? {
24 | return this?.map { responseProduct ->
25 | OtherProducts(
26 | productImage = responseProduct.productImage ?: "",
27 | productName = responseProduct.productName ?: "",
28 | subText = responseProduct.subText ?: ""
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/data/domain_impl/usecase/GetItemDetailUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.domain_impl.usecase
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.detail.data.api.datasource.DetailDataSource
5 | import com.example.detail.data.domain_impl.mapper.mapToItemDetail
6 | import com.example.detail.domain.model.ItemDetail
7 | import com.example.detail.domain.usecase.GetItemDetailUseCase
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import javax.inject.Inject
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | internal class GetItemDetailUseCaseImpl @Inject constructor(
15 | private val dataSource: DetailDataSource,
16 | @IODispatcher private val dispatcher: CoroutineContext
17 | ) : GetItemDetailUseCase {
18 | override fun getDetail(): Flow =
19 | flow {
20 | val detailData = dataSource.getDetail().mapToItemDetail()
21 | emit(detailData)
22 | }.flowOn(dispatcher)
23 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/domain/model/ItemDetail.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.domain.model
2 |
3 | data class ItemDetail(
4 | val productImage: String,
5 | val productName: String,
6 | val productId: String,
7 | val subText: String,
8 | val review: String? = null,
9 | val questions: String? = null,
10 | val share: String,
11 | val otherProducts: List? = null,
12 | val productOptions: List
13 | )
14 |
15 | data class OtherProducts(
16 | val productImage: String? = null,
17 | val productName: String? = null,
18 | val subText: String? = null
19 | )
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/domain/usecase/GetItemDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.domain.usecase
2 |
3 | import com.example.detail.domain.model.ItemDetail
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface GetItemDetailUseCase {
7 | fun getDetail(): Flow
8 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import androidx.hilt.navigation.compose.hiltViewModel
8 | import com.example.core.components.ErrorComponent
9 | import com.example.core.components.LoadingComponent
10 | import com.example.detail.presentation.components.DetailContent
11 | import com.example.detail.presentation.uievent.DetailUIEvent
12 |
13 | @Composable
14 | fun DetailScreen() {
15 | val viewModel: DetailViewModel = hiltViewModel()
16 | val state by viewModel.uiState.collectAsState()
17 |
18 | LaunchedEffect(true) {
19 | viewModel.onEvent(DetailUIEvent.LoadItemDetail)
20 | }
21 |
22 | when {
23 | state.isLoading -> { LoadingComponent() }
24 | state.error != null -> { ErrorComponent(error = state.error) }
25 | state.itemData != null -> { DetailContent(
26 | state.itemData!!,
27 | onSearchClicked = { viewModel.onEvent(DetailUIEvent.SearchDetailClick) }
28 | ) }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/DetailSearchScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.text.BasicTextField
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 |
15 | /*The only function of this Composable class is to
16 | functionality of the Navigation module using its own nav graph(DetailGraph) within a module.
17 | For more understandable please check DetailScreens with DetailGraph
18 | */
19 | @Composable
20 | fun DetailSearchScreen() {
21 | var searchText by remember { mutableStateOf("") }
22 |
23 | Column(modifier = Modifier.padding(16.dp)) {
24 | BasicTextField(
25 | value = searchText,
26 | onValueChange = { searchText = it },
27 | decorationBox = { innerTextField ->
28 | if (searchText.isEmpty()) {
29 | Text("Search..")
30 | }
31 | innerTextField()
32 | }
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.example.core.navigation.NavigationService
5 | import com.example.core.presentation.StateAndEventViewModel
6 | import com.example.detail.domain.usecase.GetItemDetailUseCase
7 | import com.example.detail.presentation.state.DetailUIState
8 | import com.example.detail.presentation.uievent.DetailUIEvent
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.catch
11 | import kotlinx.coroutines.flow.onStart
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class DetailViewModel @Inject constructor(
17 | private val getItemDetail: GetItemDetailUseCase,
18 | private val navigator: NavigationService,
19 | ) : StateAndEventViewModel(DetailUIState(null)) {
20 |
21 | private fun loadItemDetail() {
22 | viewModelScope.launch {
23 | getItemDetail.getDetail()
24 | .onStart {
25 | updateUiState { copy(isLoading = true) }
26 | }
27 | .catch { exception ->
28 | updateUiState { copy(error = exception) }
29 | }
30 | .collect {
31 | updateUiState { copy(itemData = it, isLoading = false) }
32 | }
33 | }
34 | }
35 |
36 | private fun handleBack() {
37 | navigator.goBack()
38 | }
39 |
40 | private fun handleSearchDetailClick() {
41 | navigator.navigateTo("detail/search")
42 | }
43 |
44 | override suspend fun handleEvent(event: DetailUIEvent) {
45 | when (event) {
46 | is DetailUIEvent.Dismiss -> handleBack()
47 | is DetailUIEvent.LoadItemDetail -> loadItemDetail()
48 | is DetailUIEvent.SearchDetailClick -> handleSearchDetailClick()
49 | }
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/components/DetailContent.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.Button
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import com.example.core.components.CoilImageComponent
15 | import com.example.detail.domain.model.ItemDetail
16 | import com.example.detail.presentation.uievent.DetailUIEvent
17 |
18 | @Composable
19 | fun DetailContent(
20 | itemData: ItemDetail,
21 | onSearchClicked: (DetailUIEvent) -> Unit
22 | ) {
23 | Column(modifier = Modifier.padding(16.dp)) {
24 | CoilImageComponent(
25 | imageUrl = itemData.productImage,
26 | contentDescription = "Detail Image",
27 | modifier = Modifier
28 | .fillMaxWidth()
29 | .height(200.dp)
30 | )
31 | Spacer(modifier = Modifier.height(8.dp))
32 | Text(itemData.productName, style = MaterialTheme.typography.bodyLarge)
33 | Spacer(modifier = Modifier.height(4.dp))
34 | Text(itemData.subText, style = MaterialTheme.typography.bodyMedium)
35 | Button(
36 | onClick = { onSearchClicked(DetailUIEvent.SearchDetailClick) },
37 | modifier = Modifier.fillMaxWidth()
38 | ) { Text("Route with Nav Graph to Search Detail") }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/state/DetailUIState.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation.state
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.example.detail.domain.model.ItemDetail
5 |
6 | @Immutable
7 | data class DetailUIState(
8 | val itemData: ItemDetail?,
9 | val isLoading: Boolean = false,
10 | val error: Throwable? = null
11 | )
--------------------------------------------------------------------------------
/detail/src/main/java/com/example/detail/presentation/uievent/DetailUIEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation.uievent
2 |
3 | sealed class DetailUIEvent {
4 | data object Dismiss : DetailUIEvent()
5 | data object LoadItemDetail : DetailUIEvent()
6 | data object SearchDetailClick: DetailUIEvent()
7 |
8 | }
--------------------------------------------------------------------------------
/detail/src/test/java/com/example/detail/data/datasource/DetailDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.datasource
2 |
3 | import com.example.core.model.GenericException
4 | import com.example.detail.data.api.DetailApi
5 | import com.example.detail.data.api.datasource.DetailDataSourceImpl
6 | import com.example.detail.data.api.model.ItemDetailResponse
7 | import junit.framework.TestCase.assertEquals
8 | import kotlinx.coroutines.runBlocking
9 | import okhttp3.ResponseBody
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.mockito.Mock
13 | import org.mockito.Mockito.`when`
14 | import org.mockito.MockitoAnnotations
15 | import retrofit2.Response
16 |
17 | class DetailDataSourceTest {
18 |
19 | @Mock
20 | private lateinit var mockApi: DetailApi
21 |
22 | private lateinit var dataSource: DetailDataSourceImpl
23 |
24 | @Before
25 | fun setUp() {
26 | MockitoAnnotations.initMocks(this)
27 | dataSource = DetailDataSourceImpl(mockApi)
28 | }
29 |
30 | @Test
31 | fun `getDetail returns valid response when api call is successful`() = runBlocking {
32 | // Given
33 | val expectedResponse = ItemDetailResponse(
34 | productId = "123",
35 | productImage = "image_url",
36 | productName = "Test Product",
37 | productOptions = listOf("Option 1", "Option 2"),
38 | share = "share_text",
39 | subText = "sub_text"
40 | )
41 | `when`(mockApi.getDetail()).thenReturn(Response.success(expectedResponse))
42 |
43 | // Act
44 | val actualResponse = dataSource.getDetail()
45 |
46 | // Assert
47 | assertEquals(expectedResponse, actualResponse)
48 | }
49 |
50 | @Test(expected = GenericException::class)
51 | fun `getDetail throws GenericException when api call is unsuccessful`(): Unit = runBlocking {
52 | // Arrange
53 | val errorResponse = Response.error(404, ResponseBody.create(null, ""))
54 | `when`(mockApi.getDetail()).thenReturn(errorResponse)
55 |
56 | // Act & Assert
57 | dataSource.getDetail()
58 | }
59 | }
--------------------------------------------------------------------------------
/detail/src/test/java/com/example/detail/data/domainimpl/GetItemDetailUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.data.domainimpl
2 |
3 | import com.example.detail.data.api.datasource.DetailDataSource
4 | import com.example.detail.data.api.model.ItemDetailResponse
5 | import com.example.detail.data.api.model.OtherProductResponse
6 | import com.example.detail.data.domain_impl.mapper.mapToItemDetail
7 | import com.example.detail.data.domain_impl.usecase.GetItemDetailUseCaseImpl
8 | import junit.framework.TestCase.assertEquals
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.flow.first
12 | import kotlinx.coroutines.test.runTest
13 | import org.junit.Before
14 | import org.junit.Test
15 | import org.mockito.Mock
16 | import org.mockito.Mockito.mock
17 | import org.mockito.Mockito.verify
18 | import org.mockito.Mockito.`when`
19 | import org.mockito.MockitoAnnotations
20 |
21 | @ExperimentalCoroutinesApi
22 | class GetItemDetailUseCaseTest {
23 |
24 | @Mock
25 | private lateinit var dataSource: DetailDataSource
26 |
27 | private lateinit var getItemDetailUseCaseImpl: GetItemDetailUseCaseImpl
28 |
29 | @Before
30 | fun setUp() {
31 | MockitoAnnotations.initMocks(this)
32 | dataSource = mock()
33 | getItemDetailUseCaseImpl = GetItemDetailUseCaseImpl(dataSource, Dispatchers.Unconfined)
34 | }
35 |
36 | @Test
37 | fun `getDetail returns correct data`() = runTest {
38 | // Arrange
39 | val mockOtherProducts = listOf(
40 | OtherProductResponse(
41 | productImage = "image1.jpg",
42 | productName = "Product 1",
43 | subText = "Subtext 1"
44 | ),
45 | OtherProductResponse(
46 | productImage = "image2.jpg",
47 | productName = "Product 2",
48 | subText = "Subtext 2"
49 | )
50 | )
51 | val mockResponse = ItemDetailResponse(
52 | productId = "123",
53 | productImage = "image.jpg",
54 | productName = "Test Product",
55 | subText = "Test Subtext",
56 | share = "Share Text",
57 | productOptions = listOf("Option1", "Option2"),
58 | otherProducts = mockOtherProducts
59 | )
60 |
61 | `when`(dataSource.getDetail()).thenReturn(mockResponse)
62 |
63 | // Act
64 | val result = getItemDetailUseCaseImpl.getDetail().first()
65 |
66 | // Assert
67 | assertEquals(mockResponse.mapToItemDetail(), result)
68 | verify(dataSource).getDetail()
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/detail/src/test/java/com/example/detail/presentation/DetailViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.detail.presentation
2 |
3 | import com.example.core.navigation.NavigationService
4 | import com.example.detail.domain.model.ItemDetail
5 | import com.example.detail.domain.model.OtherProducts
6 | import com.example.detail.domain.usecase.GetItemDetailUseCase
7 | import com.example.detail.presentation.state.DetailUIState
8 | import com.example.detail.presentation.uievent.DetailUIEvent
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.flow.flow
12 | import kotlinx.coroutines.flow.flowOf
13 | import kotlinx.coroutines.flow.toList
14 | import kotlinx.coroutines.launch
15 | import kotlinx.coroutines.test.TestCoroutineDispatcher
16 | import kotlinx.coroutines.test.advanceUntilIdle
17 | import kotlinx.coroutines.test.resetMain
18 | import kotlinx.coroutines.test.runTest
19 | import kotlinx.coroutines.test.setMain
20 | import org.junit.After
21 | import org.junit.Assert.assertTrue
22 | import org.junit.Before
23 | import org.junit.Test
24 | import org.junit.runner.RunWith
25 | import org.mockito.Mock
26 | import org.mockito.Mockito.verify
27 | import org.mockito.Mockito.`when`
28 | import org.mockito.junit.MockitoJUnitRunner
29 |
30 | @ExperimentalCoroutinesApi
31 | @RunWith(MockitoJUnitRunner::class)
32 | class DetailViewModelTest {
33 |
34 | private val testDispatcher = TestCoroutineDispatcher()
35 |
36 | @Mock
37 | private lateinit var getItemDetail: GetItemDetailUseCase
38 | @Mock
39 | private lateinit var navigator: NavigationService
40 |
41 | private lateinit var viewModel: DetailViewModel
42 |
43 | @Before
44 | fun setUp() {
45 | Dispatchers.setMain(testDispatcher) // Set the main dispatcher to the test dispatcher
46 | viewModel = DetailViewModel(getItemDetail, navigator)
47 | }
48 |
49 | @Test
50 | fun `loadItemDetail updates uiState correctly`() = runTest {
51 | val itemDetail = ItemDetail(
52 | productImage = "image_url",
53 | productName = "Test Product",
54 | productId = "123",
55 | subText = "sub_text",
56 | review = null,
57 | questions = null,
58 | share = "share_text",
59 | otherProducts = listOf(
60 | OtherProducts(
61 | productImage = "other_product_image_url",
62 | productName = "Other Product Name",
63 | subText = "Other Product Sub Text"
64 | )
65 | ),
66 | productOptions = listOf("Option 1", "Option 2")
67 | )
68 |
69 | `when`(getItemDetail.getDetail()).thenReturn(flowOf(itemDetail))
70 |
71 | val stateList = mutableListOf()
72 | val job = launch {
73 | viewModel.uiState.toList(stateList)
74 | }
75 |
76 | viewModel.onEvent(DetailUIEvent.LoadItemDetail)
77 |
78 | advanceUntilIdle()
79 |
80 | // Assert that uiState was updated correctly
81 | assertTrue("Expected state not found in stateList", stateList.any {
82 | it.itemData == itemDetail && !it.isLoading
83 | })
84 |
85 | job.cancel()
86 | }
87 |
88 | @Test
89 | fun `loadItemDetail updates uiState on error`() = runTest {
90 | val exception = RuntimeException("Test Exception")
91 | `when`(getItemDetail.getDetail()).thenReturn(flow { throw exception })
92 |
93 | viewModel.onEvent(DetailUIEvent.LoadItemDetail)
94 |
95 | val currentState = viewModel.uiState.value
96 | assertTrue(currentState.error === exception)
97 | }
98 |
99 |
100 | @Test
101 | fun `handleBack calls navigator goBack`() = runTest {
102 | viewModel.onEvent(DetailUIEvent.Dismiss)
103 | verify(navigator).goBack()
104 | }
105 |
106 |
107 | @After
108 | fun tearDown() {
109 | Dispatchers.resetMain() // Reset the main dispatcher to the original one
110 | testDispatcher.cleanupTestCoroutines()
111 | }
112 | }
--------------------------------------------------------------------------------
/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 | android.defaults.buildfeatures.buildconfig=true
21 | # Enables namespacing of each library's R class so that its R class includes only the
22 | # resources declared in the library itself and none from the library's dependencies,
23 | # thereby reducing the size of the R class for that library
24 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | # @keep
3 | compileSdk = "34"
4 | minSdk = "24"
5 | androidCore = "1.12.0"
6 | appcompat = "1.6.1"
7 | hilt = "2.47"
8 | okhttp = "4.10.0"
9 | retrofit = "2.9.0"
10 | lifecycleRuntimeKtx = "2.6.2"
11 | composeActivity = "1.8.2"
12 | composeHiltNav = "1.1.0"
13 | pager = "0.19.0"
14 | navigation = "2.7.6"
15 | composeMaterial = "1.1.2"
16 | agp = "8.2.1"
17 | kotlin = "1.9.0"
18 | compose-bom = "2023.08.00"
19 | coil = "2.5.0"
20 | jvmTarget = "1.8"
21 | kotlinCompilerVersion = "1.5.1"
22 | mockito = "5.10.0"
23 | android-test = "1.6.0-alpha04"
24 | junit = "4.13.2"
25 | mockito-kotlin = "3.2.0"
26 | coroutinesCore = "1.7.3"
27 |
28 | [libraries]
29 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
30 | android-core = { module = "androidx.core:core-ktx", version.ref = "androidCore" }
31 | hilt-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
32 | hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
33 | okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
34 | retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
35 | retrofit-gson-converter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
36 | lifecycle-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
37 | compose-activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" }
38 | compose-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "composeHiltNav" }
39 | compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
40 | pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "pager" }
41 | compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
42 | coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
43 | compose-ui-material = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial" }
44 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
45 | ui = { group = "androidx.compose.ui", name = "ui" }
46 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
47 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
48 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
49 | ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
50 | hilt-ksp-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
51 | mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
52 | mockito = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" }
53 | android-test = { module = "androidx.test:core", version.ref = "android-test" }
54 | junit = { module = "junit:junit", version.ref = "junit" }
55 | coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesCore" }
56 |
57 | [plugins]
58 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
59 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
60 | listNameSpace = { id = "com.example.list" }
61 | networkNameSpace = { id = "com.example.network" }
62 | detailNameSpace = { id = "com.example.detail" }
63 | coreNameSpace = { id = "com.example.core" }
64 | mainNamespace = { id = "com.example.composefeaturebasedmultimodule" }
65 | navigationNameSpace = { id = "com.example.navigation" }
66 | homeNameSpace = { id = "com.example.home" }
67 | daggerHilt = { id = "com.google.dagger.hilt.android" }
68 | ksp = { id = "com.google.devtools.ksp" }
69 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 13 16:33:00 TRT 2024
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 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/home/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/home/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.daggerHilt.get().toString())
5 | id(libs.plugins.ksp.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.homeNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
28 | }
29 |
30 | buildTypes {
31 | debug {
32 | buildConfigField("boolean", "DEVELOPMENT_MODE", "true")
33 | }
34 | release {
35 | buildConfigField("boolean", "DEVELOPMENT_MODE", "false")
36 | }
37 | }
38 | }
39 |
40 | dependencies {
41 | implementation(project(":core"))
42 | implementation(project(":network"))
43 |
44 | //region Data Dependencies
45 | implementation(libs.okhttp.logging.interceptor)
46 | implementation(libs.hilt.core)
47 | implementation(libs.retrofit.core)
48 | ksp(libs.hilt.compiler)
49 | ksp(libs.hilt.ksp.compiler)
50 | implementation(libs.retrofit.gson.converter)
51 | //endregion
52 |
53 | //region Presentation Dependencies
54 | implementation(platform(libs.compose.bom))
55 | implementation(libs.compose.hilt.navigation)
56 | implementation(libs.compose.ui.graphics)
57 | implementation(libs.pager)
58 | implementation(libs.compose.ui.material)
59 | implementation(libs.compose.activity)
60 | implementation(libs.coil)
61 | //endregion
62 | }
--------------------------------------------------------------------------------
/home/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/home/consumer-rules.pro
--------------------------------------------------------------------------------
/home/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
--------------------------------------------------------------------------------
/home/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/api/HomeApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.api
2 |
3 | import com.example.home.data.api.model.HomeResponse
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 |
7 | interface HomeApi {
8 |
9 | @GET("/basaransuleyman/suleyman-basaranoglu-json/main/home")
10 | suspend fun getHome(): Response
11 |
12 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/api/datasource/HomeDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.api.datasource
2 |
3 | import com.example.home.data.api.model.HomeResponse
4 |
5 | interface HomeDataSource {
6 | suspend fun getHome(): HomeResponse
7 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/api/datasource/HomeDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.api.datasource
2 |
3 | import com.example.network.extensions.handleCall
4 | import com.example.home.data.api.HomeApi
5 | import com.example.home.data.api.model.HomeResponse
6 | import javax.inject.Inject
7 |
8 | internal class HomeDataSourceImpl @Inject constructor(
9 | private val api: HomeApi
10 | ) : HomeDataSource {
11 | override suspend fun getHome(): HomeResponse {
12 | return handleCall {
13 | api.getHome()
14 | }
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/api/di/DataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.api.di
2 |
3 | import com.example.home.data.api.HomeApi
4 | import com.example.home.data.api.datasource.HomeDataSource
5 | import com.example.home.data.api.datasource.HomeDataSourceImpl
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import retrofit2.Retrofit
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | class DataSourceModule {
16 |
17 | @Singleton
18 | @Provides
19 | fun provideHomeApi(retrofit: Retrofit): HomeApi {
20 | return retrofit.create(HomeApi::class.java)
21 | }
22 |
23 | @Singleton
24 | @Provides
25 | internal fun provideInitialResponseDataSource(apiService: HomeApi): HomeDataSource {
26 | return HomeDataSourceImpl(apiService)
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/api/model/HomeResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.api.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class HomeResponse(
6 | @SerializedName("sections")
7 | val sections: List
8 | )
9 |
10 | data class Section(
11 | @SerializedName("sectionData")
12 | val sectionData: List,
13 | val sectionTitle: String? = null,
14 | val type: Int,
15 | val id: Int
16 | )
17 |
18 | data class HomeSection(
19 | val icon: String? = null,
20 | val image: String,
21 | val navigationData: String = "ND_49581L",
22 | val productId: String = "PI_845481EI",
23 | val productImage: String,
24 | val questions: String? = null,
25 | val rating: String? = null,
26 | val review: String? = null,
27 | val share: String? = null,
28 | val subText: String? = null,
29 | val text: String? = null,
30 | val piece: String? = null,
31 | val soldOutText: String? = null
32 | )
33 |
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/domainimpl/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.domainimpl.di
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.home.data.api.datasource.HomeDataSource
5 | import com.example.home.data.domainimpl.usecase.GetInitialHomeUseCaseImpl
6 | import com.example.home.domain.usecase.GetInitialHomeUseCase
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import kotlinx.coroutines.Dispatchers
12 | import javax.inject.Singleton
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | object UseCaseModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideIODispatcher(): CoroutineContext {
22 | return Dispatchers.IO
23 | }
24 |
25 | @Provides
26 | @Singleton
27 | fun provideGetInitialHomeUseCase(
28 | dataSource: HomeDataSource,
29 | @IODispatcher dispatcher: CoroutineContext
30 | ): GetInitialHomeUseCase {
31 | return GetInitialHomeUseCaseImpl(dataSource, dispatcher)
32 | }
33 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/domainimpl/mapper/HomeSectionsMapper.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.domainimpl.mapper
2 |
3 | import com.example.home.data.api.model.HomeResponse
4 | import com.example.home.data.api.model.HomeSection
5 | import com.example.home.domain.model.BannerItem
6 | import com.example.home.domain.model.HomeSectionAdapterItem
7 | import com.example.home.domain.model.HomeSections
8 | import com.example.home.domain.model.ProductItem
9 |
10 | fun HomeResponse.mapToHomeSections(): HomeSections {
11 | val homeSectionsAdapterItems = mutableListOf()
12 |
13 | this.sections.forEach { section ->
14 | val viewType = when (section.type) {
15 | 1 -> HomeSectionAdapterItem.VIEW_TYPE_BANNER
16 | 2 -> HomeSectionAdapterItem.VIEW_TYPE_SLIDABLE_PRODUCTS
17 | 4 -> HomeSectionAdapterItem.VIEW_TYPE_VERTICAL_PRODUCTS
18 | else -> -1
19 | }
20 |
21 | val sectionItem = when (viewType) {
22 | HomeSectionAdapterItem.VIEW_TYPE_BANNER -> HomeSectionAdapterItem.Banner(
23 | viewType = viewType,
24 | bannerItem = section.sectionData.map { banner ->
25 | mapHomeSectionToBannerItem(banner)
26 | },
27 | id = section.id
28 | )
29 |
30 | HomeSectionAdapterItem.VIEW_TYPE_SLIDABLE_PRODUCTS -> HomeSectionAdapterItem.SlidableProducts(
31 | viewType = viewType,
32 | productItem = section.sectionData.map { product ->
33 | mapToProductItem(product)
34 | },
35 | sectionTitle = section.sectionTitle ?: "",
36 | id = section.id
37 | )
38 |
39 | HomeSectionAdapterItem.VIEW_TYPE_VERTICAL_PRODUCTS -> HomeSectionAdapterItem.VerticalProducts(
40 | viewType = viewType,
41 | productItem = section.sectionData.map { product ->
42 | mapToProductItem(product)
43 | },
44 | sectionTitle = section.sectionTitle ?: "",
45 | id = section.type
46 | )
47 |
48 | else -> null
49 | }
50 |
51 | sectionItem?.let { homeSectionsAdapterItems.add(it) }
52 |
53 | }
54 | return HomeSections(sections = homeSectionsAdapterItems)
55 | }
56 |
57 |
58 | private fun mapHomeSectionToBannerItem(homeSection: HomeSection): BannerItem {
59 | return BannerItem(
60 | image = homeSection.image,
61 | navigationData = homeSection.navigationData
62 | )
63 | }
64 |
65 |
66 | private fun mapToProductItem(response: HomeSection): ProductItem {
67 | return ProductItem(
68 | productId = response.productId,
69 | productImage = response.productImage,
70 | text = response.text,
71 | subText = response.subText,
72 | review = response.review,
73 | questions = response.questions,
74 | rating = response.rating,
75 | share = response.share,
76 | piece = response.piece,
77 | soldOutText = response.soldOutText
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/data/domainimpl/usecase/GetInitialHomeUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.data.domainimpl.usecase
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.home.data.api.datasource.HomeDataSource
5 | import com.example.home.data.domainimpl.mapper.mapToHomeSections
6 | import com.example.home.domain.model.HomeSections
7 | import com.example.home.domain.usecase.GetInitialHomeUseCase
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import javax.inject.Inject
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | internal class GetInitialHomeUseCaseImpl @Inject constructor(
15 | private val dataSource: HomeDataSource,
16 | @IODispatcher private val dispatcher: CoroutineContext
17 | ) : GetInitialHomeUseCase {
18 | override fun getInitialHome(): Flow =
19 | flow {
20 | val initialData = dataSource.getHome().mapToHomeSections()
21 | emit(initialData)
22 | }.flowOn(dispatcher)
23 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/domain/model/BannerItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.domain.model
2 |
3 | data class BannerItem(val image: String, val navigationData: String)
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/domain/model/CatalogItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.domain.model
2 |
3 | data class CatalogItem(val icon: String?, val text: String?)
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/domain/model/HomeSections.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.domain.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | data class HomeSections(
7 | var sections: List
8 | )
9 |
10 | sealed class HomeSectionAdapterItem {
11 | abstract val viewType: Int
12 |
13 | @Immutable
14 | data class Banner(
15 | override val viewType: Int = VIEW_TYPE_BANNER,
16 | val bannerItem: List,
17 | val id: Int
18 | ) : HomeSectionAdapterItem()
19 |
20 | @Immutable
21 | data class SlidableProducts(
22 | override val viewType: Int = VIEW_TYPE_SLIDABLE_PRODUCTS,
23 | val productItem: List,
24 | val sectionTitle: String,
25 | val id: Int
26 | ) : HomeSectionAdapterItem()
27 |
28 | @Immutable
29 | data class VerticalProducts(
30 | override val viewType: Int = VIEW_TYPE_VERTICAL_PRODUCTS,
31 | val productItem: List,
32 | val sectionTitle: String,
33 | val id: Int
34 | ) : HomeSectionAdapterItem()
35 |
36 |
37 | companion object {
38 | const val VIEW_TYPE_BANNER = 1
39 | const val VIEW_TYPE_SLIDABLE_PRODUCTS = 2
40 | const val VIEW_TYPE_VERTICAL_PRODUCTS = 4
41 | }
42 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/domain/model/ProductItem.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.domain.model
2 |
3 | data class ProductItem(
4 | val productId: String,
5 | val productImage: String,
6 | val text: String?,
7 | val subText: String?,
8 | val review: String?,
9 | val questions: String?,
10 | val rating: String?,
11 | val share: String?,
12 | val piece: String?,
13 | val soldOutText:String?
14 | )
15 |
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/domain/usecase/GetInitialHomeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.domain.usecase
2 |
3 | import com.example.home.domain.model.HomeSections
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface GetInitialHomeUseCase {
7 | fun getInitialHome(): Flow
8 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.ModalBottomSheet
5 | import androidx.compose.material3.rememberModalBottomSheetState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.collectAsState
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.rememberCoroutineScope
13 | import androidx.compose.runtime.setValue
14 | import androidx.hilt.navigation.compose.hiltViewModel
15 | import com.example.core.components.ErrorComponent
16 | import com.example.core.components.LoadingComponent
17 | import com.example.home.presentation.components.DetailBottomSheet
18 | import com.example.home.presentation.components.HomeScreenContent
19 | import com.example.home.presentation.uievent.HomeUIEvent
20 | import kotlinx.coroutines.launch
21 |
22 | @OptIn(ExperimentalMaterial3Api::class)
23 | @Composable
24 | fun HomeScreen() {
25 | val viewModel: HomeViewModel = hiltViewModel()
26 | val state by viewModel.uiState.collectAsState()
27 | val scope = rememberCoroutineScope()
28 | var showBottomSheet by remember { mutableStateOf(false) }
29 | val sheetState = rememberModalBottomSheetState()
30 |
31 | LaunchedEffect(true) {
32 | viewModel.onEvent(HomeUIEvent.LoadInitialHome)
33 | }
34 |
35 | LaunchedEffect(state.selectedProductItem) {
36 | showBottomSheet = state.selectedProductItem != null
37 | if (showBottomSheet) {
38 | scope.launch { sheetState.show() }
39 | }
40 | }
41 |
42 | if (showBottomSheet) {
43 | ModalBottomSheet(
44 | sheetState = sheetState,
45 | onDismissRequest = {
46 | viewModel.onEvent(HomeUIEvent.Dismiss)
47 | showBottomSheet = false
48 | },
49 | ) {
50 | state.selectedProductItem?.let { productItem ->
51 | DetailBottomSheet(productItem)
52 | }
53 | }
54 | }
55 |
56 | when {
57 | state.isLoading -> LoadingComponent()
58 | state.error != null -> ErrorComponent(state.error)
59 | state.homeData != null -> {
60 | HomeScreenContent(
61 | homeData = state.homeData!!,
62 | onEvent = viewModel::onEvent
63 | )
64 | }
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.example.core.navigation.NavigationService
5 | import com.example.core.presentation.StateAndEventViewModel
6 | import com.example.home.domain.model.ProductItem
7 | import com.example.home.domain.usecase.GetInitialHomeUseCase
8 | import com.example.home.presentation.state.HomeUIState
9 | import com.example.home.presentation.uievent.HomeUIEvent
10 | import dagger.hilt.android.lifecycle.HiltViewModel
11 | import kotlinx.coroutines.flow.catch
12 | import kotlinx.coroutines.flow.onStart
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class HomeViewModel @Inject constructor(
18 | private val getInitialHomeUseCase: GetInitialHomeUseCase,
19 | private val navigator: NavigationService
20 | ) : StateAndEventViewModel(HomeUIState()) {
21 | override suspend fun handleEvent(event: HomeUIEvent) {
22 | when (event) {
23 | HomeUIEvent.LoadInitialHome -> {
24 | getInitialHome()
25 | }
26 |
27 | is HomeUIEvent.OnBannerClicked -> {
28 | onBannerClicked()
29 | }
30 |
31 | is HomeUIEvent.OnProductClicked -> {
32 | onProductClicked()
33 | }
34 |
35 | is HomeUIEvent.OnVerticalProductClicked -> {
36 | onVerticalProductClicked(event.item)
37 | }
38 |
39 | is HomeUIEvent.Dismiss -> {
40 | handleBack()
41 | }
42 | }
43 | }
44 |
45 | private fun getInitialHome() {
46 | viewModelScope.launch {
47 | getInitialHomeUseCase.getInitialHome()
48 | .onStart {
49 | updateUiState { copy(isLoading = true) }
50 | }
51 | .catch { error ->
52 | updateUiState { copy(error = error) }
53 | }
54 | .collect { homeSections ->
55 | updateUiState {
56 | copy(
57 | homeData = homeSections,
58 | isLoading = false,
59 | selectedProductItem = null,
60 | error = null
61 | )
62 | }
63 | }
64 | }
65 | }
66 |
67 | private fun onBannerClicked() {
68 | navigator.navigateTo("list")
69 | }
70 |
71 | private fun onVerticalProductClicked(productItem: ProductItem) {
72 | updateUiState {
73 | copy(selectedProductItem = productItem, isLoading = false)
74 | }
75 | }
76 |
77 | /* Route with arguments
78 | private fun onProductClicked(isSheetOpen: Boolean) {
79 | navigator.navigateTo( "detail/$isSheetOpen") {
80 | launchSingleTop = true
81 | restoreState = true
82 | }
83 | }
84 | */
85 |
86 | // Route with Detail Graph
87 | private fun onProductClicked() {
88 | navigator.navigateTo("detailgraph") {
89 | launchSingleTop = true
90 | restoreState = true
91 | }
92 | }
93 |
94 |
95 | private fun handleBack() {
96 | navigator.goBack()
97 | }
98 |
99 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/components/DetailBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
16 | import androidx.compose.ui.unit.dp
17 | import com.example.core.components.CoilImageComponent
18 | import com.example.home.domain.model.ProductItem
19 |
20 | @Composable
21 | fun DetailBottomSheet(
22 | productItem: ProductItem
23 | ) {
24 | Box(
25 | modifier = Modifier
26 | .fillMaxWidth()
27 | .padding(16.dp),
28 | contentAlignment = Alignment.Center
29 | ) {
30 | Column(
31 | horizontalAlignment = Alignment.CenterHorizontally
32 | ) {
33 | CoilImageComponent(
34 | imageUrl = productItem.productImage,
35 | contentDescription = "Bottom Sheet Image",
36 | modifier = Modifier
37 | .fillMaxWidth()
38 | .height(300.dp)
39 | .clip(RoundedCornerShape(8.dp))
40 | )
41 | Spacer(modifier = Modifier.height(16.dp))
42 | productItem.text?.let {
43 | Text(
44 | text = it,
45 | style = MaterialTheme.typography.headlineLarge,
46 | color = MaterialTheme.colorScheme.secondaryContainer
47 | )
48 | }
49 | Spacer(modifier = Modifier.height(8.dp))
50 | productItem.subText?.let {
51 | Text(
52 | text = it,
53 | style = MaterialTheme.typography.bodyLarge,
54 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
55 | )
56 | }
57 | Spacer(modifier = Modifier.height(30.dp))
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/components/HomeScreenContent.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.example.home.domain.model.HomeSections
5 | import com.example.home.presentation.uievent.HomeUIEvent
6 |
7 | @Composable
8 | fun HomeScreenContent(homeData: HomeSections, onEvent: (HomeUIEvent) -> Unit) {
9 | SectionList(homeData.sections, onEvent)
10 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/components/SectionList.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.components
2 |
3 | import androidx.compose.foundation.lazy.LazyColumn
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import com.example.home.domain.model.HomeSectionAdapterItem
8 | import com.example.home.presentation.sections.BannerSection
9 | import com.example.home.presentation.sections.SectionTitle
10 | import com.example.home.presentation.sections.SlidableSection
11 | import com.example.home.presentation.uievent.HomeUIEvent
12 |
13 | @Composable
14 | fun SectionList(sections: List?, onEvent: (HomeUIEvent) -> Unit) {
15 | sections?.let {
16 | LazyColumn {
17 | items(items = sections, key = { section ->
18 | when (section) {
19 | is HomeSectionAdapterItem.Banner -> "Banner-" + section.bannerItem.joinToString("-") { it.navigationData }
20 | is HomeSectionAdapterItem.SlidableProducts -> "Slidable-${section.id}"
21 | is HomeSectionAdapterItem.VerticalProducts -> "Vertical-${section.sectionTitle}"
22 | }
23 | }) { section ->
24 | when (section) {
25 | is HomeSectionAdapterItem.Banner -> BannerSection(section.bannerItem, onEvent)
26 | is HomeSectionAdapterItem.SlidableProducts -> SlidableSection(section.productItem, section.sectionTitle, onEvent)
27 | is HomeSectionAdapterItem.VerticalProducts -> {
28 | VerticalSection(section, onEvent)
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
36 | @Composable
37 | fun VerticalSection(section: HomeSectionAdapterItem.VerticalProducts, onEvent: (HomeUIEvent) -> Unit) {
38 | SectionTitle(title = section.sectionTitle)
39 | val products = remember { section.productItem }
40 | products.forEach { productItem ->
41 | VerticalItemCard(
42 | item = productItem,
43 | onProductClick = { onEvent(HomeUIEvent.OnVerticalProductClicked(productItem)) }
44 | )
45 | }
46 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/components/VerticalItemCard.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.material3.Card
10 | import androidx.compose.material3.CardDefaults
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 | import com.example.core.components.CoilImageComponent
20 | import com.example.home.domain.model.ProductItem
21 |
22 | @Composable
23 | fun VerticalItemCard(
24 | item: ProductItem,
25 | onProductClick: (ProductItem) -> Unit
26 | ) {
27 | val cardModifier = remember {
28 | Modifier
29 | .padding(8.dp)
30 | .fillMaxWidth()
31 | .clickable(onClick = { onProductClick(item) })
32 | }
33 |
34 | val imageModifier = remember {
35 | Modifier
36 | .size(88.dp)
37 | }
38 |
39 | val textModifier = remember {
40 | Modifier
41 | .padding(vertical = 12.dp)
42 | .fillMaxWidth()
43 | }
44 |
45 | Card(
46 | modifier = cardModifier,
47 | elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
48 | ) {
49 | Row(
50 | verticalAlignment = Alignment.CenterVertically
51 | ) {
52 | CoilImageComponent(
53 | imageUrl = item.productImage,
54 | contentDescription = "Vertical Image",
55 | modifier = imageModifier
56 | )
57 | Column(
58 | modifier = Modifier
59 | .padding(8.dp)
60 | .fillMaxWidth()
61 | ) {
62 | item.text?.let {
63 | Text(
64 | text = it,
65 | style = MaterialTheme.typography.bodyMedium,
66 | modifier = textModifier,
67 | textAlign = TextAlign.Center
68 | )
69 | }
70 | item.subText?.let {
71 | Text(
72 | text = it,
73 | style = MaterialTheme.typography.bodyMedium,
74 | modifier = Modifier.fillMaxWidth(),
75 | textAlign = TextAlign.Center
76 | )
77 | }
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/sections/BannerSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.sections
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.layout.ContentScale
10 | import androidx.compose.ui.unit.dp
11 | import com.example.core.components.CoilImageComponent
12 | import com.example.home.domain.model.BannerItem
13 | import com.example.home.presentation.uievent.HomeUIEvent
14 | import com.google.accompanist.pager.ExperimentalPagerApi
15 | import com.google.accompanist.pager.HorizontalPager
16 | import com.google.accompanist.pager.PagerState
17 | import com.google.accompanist.pager.rememberPagerState
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.isActive
20 |
21 | @OptIn(ExperimentalPagerApi::class)
22 | @Composable
23 | fun BannerSection(bannerItems: List, onBannerClick: (HomeUIEvent) -> Unit) {
24 |
25 | val pagerState = rememberPagerState(initialPage = 0)
26 |
27 | AutoScrollBanner(pagerState, bannerItems.size)
28 |
29 | HorizontalPager(
30 | count = bannerItems.size,
31 | state = pagerState,
32 | modifier = Modifier
33 | .padding(20.dp)
34 | .height(200.dp)
35 | .fillMaxWidth()
36 | ) { page ->
37 | val bannerItem = bannerItems[page]
38 | CoilImageComponent(
39 | imageUrl = bannerItem.image,
40 | contentScale = ContentScale.FillBounds,
41 | onClick = {
42 | bannerItem.navigationData
43 | ?.let { HomeUIEvent.OnBannerClicked }
44 | ?.let { onBannerClick(it) }
45 | },
46 | contentDescription = "Banner Image"
47 | )
48 | }
49 | }
50 |
51 | @OptIn(ExperimentalPagerApi::class)
52 | @Composable
53 | private fun AutoScrollBanner(pagerState: PagerState, itemCount: Int) {
54 | LaunchedEffect(pagerState) {
55 | while (isActive) {
56 | delay(3000)
57 | val nextPage = (pagerState.currentPage + 1) % itemCount
58 | pagerState.animateScrollToPage(nextPage)
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/sections/SectionTitle.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.sections
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 |
13 | @Composable
14 | fun SectionTitle(title: String) {
15 | Text(
16 | text = title,
17 | color = Color.Blue,
18 | fontWeight = FontWeight.Bold,
19 | style = MaterialTheme.typography.bodyLarge,
20 | modifier = Modifier
21 | .padding(12.dp)
22 | .fillMaxWidth()
23 | )
24 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/sections/SlidableSection.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.sections
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.lazy.LazyRow
10 | import androidx.compose.foundation.lazy.items
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.Card
13 | import androidx.compose.material3.CardDefaults
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.dp
23 | import com.example.core.components.CoilImageComponent
24 | import com.example.home.domain.model.ProductItem
25 | import com.example.home.presentation.uievent.HomeUIEvent
26 |
27 | @Composable
28 | fun SlidableSection(
29 | productItems: List,
30 | sectionTitle: String,
31 | onProductClick: (HomeUIEvent) -> Unit
32 | ) {
33 | Column {
34 | SectionTitle(title = sectionTitle)
35 | LazyRow {
36 | items(items = productItems, key = { product ->
37 | product.productId
38 | }) { product ->
39 | HorizontalCard(
40 | product.productImage,
41 | product.text,
42 | product.subText,
43 | onClick = { onProductClick(HomeUIEvent.OnProductClicked) })
44 | }
45 | }
46 | }
47 | }
48 |
49 | @Composable
50 | fun HorizontalCard(
51 | imageUri: String,
52 | title: String?,
53 | subTitle: String?,
54 | onClick: () -> Unit
55 | ) {
56 | val cardModifier = remember {
57 | Modifier
58 | .padding(horizontal = 8.dp, vertical = 16.dp)
59 | .fillMaxWidth()
60 | .height(200.dp)
61 | .clickable(onClick = onClick)
62 | }
63 | Card(
64 | modifier = cardModifier,
65 | elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
66 | shape = RoundedCornerShape(10.dp)
67 | ) {
68 | Column(
69 | horizontalAlignment = Alignment.CenterHorizontally,
70 | modifier = Modifier.padding(8.dp)
71 | ) {
72 | CoilImageComponent(
73 | imageUri,
74 | contentDescription = "Slidable Image",
75 | modifier = Modifier
76 | .size(100.dp)
77 | .padding(8.dp)
78 | .align(Alignment.CenterHorizontally)
79 | )
80 |
81 | title?.let {
82 | Text(
83 | text = it,
84 | fontWeight = FontWeight.Bold,
85 | textAlign = TextAlign.Left,
86 | modifier = Modifier.align(Alignment.Start)
87 | )
88 | }
89 | subTitle?.let {
90 | Text(
91 | text = it,
92 | color = Color.Gray,
93 | textAlign = TextAlign.Left,
94 | modifier = Modifier.align(Alignment.Start)
95 | )
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/state/HomeUIState.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.state
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.example.home.domain.model.HomeSections
5 | import com.example.home.domain.model.ProductItem
6 |
7 | @Immutable
8 | data class HomeUIState( // can be sealed class
9 | val isLoading: Boolean = false,
10 | val homeData: HomeSections? = null,
11 | val error: Throwable? = null,
12 | val selectedProductItem: ProductItem? = null
13 | )
--------------------------------------------------------------------------------
/home/src/main/java/com/example/home/presentation/uievent/HomeUIEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.home.presentation.uievent
2 |
3 | import com.example.home.domain.model.ProductItem
4 |
5 | sealed class HomeUIEvent {
6 | data object OnBannerClicked : HomeUIEvent()
7 | data object LoadInitialHome : HomeUIEvent()
8 | data object OnProductClicked : HomeUIEvent()
9 | data class OnVerticalProductClicked(val item: ProductItem) : HomeUIEvent()
10 | data object Dismiss : HomeUIEvent()
11 | }
--------------------------------------------------------------------------------
/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/list/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/list/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.daggerHilt.get().toString())
5 | id(libs.plugins.ksp.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.listNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
28 | }
29 | buildTypes {
30 | debug {
31 | buildConfigField("boolean", "DEVELOPMENT_MODE", "true")
32 | }
33 | release {
34 | buildConfigField("boolean", "DEVELOPMENT_MODE", "false")
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation(project(":core"))
41 | implementation(project(":network"))
42 |
43 | implementation(libs.retrofit.core)
44 |
45 | //region D.I Dependencies
46 | implementation(libs.hilt.core)
47 | ksp(libs.hilt.compiler)
48 | ksp(libs.hilt.ksp.compiler)
49 | //endregion
50 |
51 | //region Presentation Dependencies
52 | implementation(platform(libs.compose.bom))
53 | implementation(libs.compose.hilt.navigation)
54 | implementation(libs.compose.navigation)
55 | implementation(libs.compose.ui.graphics)
56 | implementation(libs.pager)
57 | implementation(libs.compose.ui.material)
58 | implementation(libs.compose.activity)
59 | implementation(libs.coil)
60 | //endregion
61 |
62 | }
--------------------------------------------------------------------------------
/list/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/list/consumer-rules.pro
--------------------------------------------------------------------------------
/list/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
--------------------------------------------------------------------------------
/list/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/api/ListApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.api
2 |
3 | import com.example.list.data.api.model.ListResponse
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 |
7 | interface ListApi {
8 |
9 | @GET("/basaransuleyman/suleyman-basaranoglu-json/main/list-page-paging-first")
10 | suspend fun getList() : Response
11 |
12 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/api/datasource/ListDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.api.datasource
2 |
3 | import com.example.list.data.api.model.ListResponse
4 | interface ListDataSource {
5 | suspend fun getList(): ListResponse
6 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/api/datasource/ListDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.api.datasource
2 |
3 | import com.example.list.data.api.model.ListResponse
4 | import javax.inject.Inject
5 | import com.example.list.data.api.ListApi
6 | import com.example.network.extensions.handleCall
7 |
8 | internal class ListDataSourceImpl @Inject constructor(
9 | private val api: ListApi
10 | ) : ListDataSource {
11 | override suspend fun getList(): ListResponse {
12 | return handleCall {
13 | api.getList()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/api/di/DataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.api.di
2 |
3 | import com.example.list.data.api.ListApi
4 | import com.example.list.data.api.datasource.ListDataSource
5 | import com.example.list.data.api.datasource.ListDataSourceImpl
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import retrofit2.Retrofit
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | class DataSourceModule {
16 |
17 | @Singleton
18 | @Provides
19 | fun provideListApi(retrofit: Retrofit): ListApi = retrofit.create(ListApi::class.java)
20 |
21 | @Singleton
22 | @Provides
23 | internal fun provideInitialResponseDataSource(apiService: ListApi): ListDataSource {
24 | return ListDataSourceImpl(apiService)
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/api/model/ListResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.api.model
2 |
3 | data class ListResponse(
4 | val listResponse: List,
5 | val productLimit: Int,
6 | val totalCount: Int
7 | )
8 |
9 | data class ListProducts(
10 | val productId: String,
11 | val productImage: String,
12 | val text: String,
13 | val subText: String,
14 | val review: String,
15 | val questions: String,
16 | val rating: String
17 | )
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/domain_impl/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.domain_impl.di
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.list.data.api.datasource.ListDataSource
5 | import com.example.list.data.domain_impl.usecase.GetListUseCaseImpl
6 | import com.example.list.domain.usecase.GetListUseCase
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import kotlinx.coroutines.Dispatchers
12 | import javax.inject.Singleton
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | object UseCaseModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideIODispatcher(): CoroutineContext {
22 | return Dispatchers.IO
23 | }
24 |
25 | @Provides
26 | @Singleton
27 | fun provideGetInitialHomeUseCase(
28 | dataSource: ListDataSource,
29 | @IODispatcher dispatcher: CoroutineContext
30 | ): GetListUseCase {
31 | return GetListUseCaseImpl(dataSource, dispatcher)
32 | }
33 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/domain_impl/mapper/ListDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.domain_impl.mapper
2 |
3 | import com.example.list.data.api.model.ListProducts
4 | import com.example.list.data.api.model.ListResponse
5 | import com.example.list.domain.model.ListData
6 | import com.example.list.domain.model.ListProductsModel
7 |
8 | fun ListResponse.mapToListData(): ListData {
9 | val domainList = this.listResponse.map { it.toDomainProduct() }
10 | return ListData(
11 | productList = domainList,
12 | productLimit = this.productLimit,
13 | totalCount = this.totalCount
14 | )
15 | }
16 |
17 | private fun ListProducts.toDomainProduct(): ListProductsModel {
18 | return ListProductsModel(
19 | productId = this.productId,
20 | productImage = this.productImage,
21 | text = this.text,
22 | subText = this.subText,
23 | review = this.review,
24 | questions = this.questions,
25 | rating = this.rating
26 | )
27 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/data/domain_impl/usecase/GetListUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.data.domain_impl.usecase
2 |
3 | import com.example.core.utils.IODispatcher
4 | import com.example.list.data.api.datasource.ListDataSource
5 | import com.example.list.data.domain_impl.mapper.mapToListData
6 | import com.example.list.domain.model.ListData
7 | import com.example.list.domain.usecase.GetListUseCase
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import javax.inject.Inject
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | internal class GetListUseCaseImpl @Inject constructor(
15 | private val dataSource: ListDataSource,
16 | @IODispatcher private val dispatcher: CoroutineContext
17 | ) : GetListUseCase {
18 |
19 | override fun getList(): Flow =
20 | flow {
21 | val initialData = dataSource.getList().mapToListData()
22 | emit(initialData)
23 | }.flowOn(dispatcher)
24 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/domain/model/ListData.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.domain.model
2 |
3 | data class ListData(
4 | val productList: List?,
5 | val productLimit: Int?,
6 | val totalCount: Int?
7 | )
8 |
9 | data class ListProductsModel(
10 | val productId: String,
11 | val productImage: String,
12 | val text: String,
13 | val subText: String,
14 | val review: String,
15 | val questions: String,
16 | val rating: String
17 | )
18 |
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/domain/usecase/GetListUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.domain.usecase
2 |
3 | import com.example.list.domain.model.ListData
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface GetListUseCase {
7 | fun getList(): Flow
8 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/presentation/ListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.presentation
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.compose.runtime.getValue
8 | import androidx.hilt.navigation.compose.hiltViewModel
9 | import com.example.core.components.ErrorComponent
10 | import com.example.core.components.LoadingComponent
11 | import com.example.list.presentation.components.ListContent
12 | import com.example.list.presentation.event.ListUIEvent
13 |
14 | @Composable
15 | fun ListScreen() {
16 | val viewModel: ListViewModel = hiltViewModel()
17 | val state by viewModel.uiState.collectAsState()
18 |
19 | LaunchedEffect(true) {
20 | viewModel.onEvent(ListUIEvent.GetList)
21 | }
22 |
23 | when {
24 | state.isLoading -> { LoadingComponent() }
25 | state.error != null -> { ErrorComponent(error = state.error) }
26 | state.listData != null -> ListContent(state.listData!!.productList ?: emptyList())
27 | }
28 |
29 | BackHandler(enabled = true) {
30 | viewModel.onEvent(ListUIEvent.Dismiss)
31 | }
32 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/presentation/ListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.presentation
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.example.core.navigation.NavigationService
5 | import com.example.core.presentation.StateAndEventViewModel
6 | import com.example.list.domain.usecase.GetListUseCase
7 | import com.example.list.presentation.event.ListUIEvent
8 | import com.example.list.presentation.state.ListUIState
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.catch
11 | import kotlinx.coroutines.flow.onStart
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class ListViewModel @Inject constructor(
17 | private val getListUseCase: GetListUseCase,
18 | private val navigator: NavigationService
19 | ) : StateAndEventViewModel(ListUIState(null)) {
20 |
21 | private fun getList() {
22 | viewModelScope.launch {
23 | getListUseCase.getList()
24 | .onStart {
25 | updateUiState { copy(isLoading = true) }
26 | }
27 | .catch { error ->
28 | updateUiState { copy(error = error) }
29 | }
30 | .collect { listData ->
31 | updateUiState { copy(listData = listData, isLoading = false) }
32 | }
33 |
34 | }
35 | }
36 |
37 | private fun handleBack() {
38 | navigator.goBack()
39 | }
40 |
41 | override suspend fun handleEvent(event: ListUIEvent) {
42 | when (event) {
43 | is ListUIEvent.Dismiss -> handleBack()
44 | is ListUIEvent.GetList -> getList()
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/presentation/components/ListContent.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.presentation.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.expandVertically
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.animation.shrinkVertically
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.lazy.grid.GridCells
15 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
16 | import androidx.compose.foundation.lazy.grid.items
17 | import androidx.compose.foundation.shape.RoundedCornerShape
18 | import androidx.compose.material3.Card
19 | import androidx.compose.material3.CardDefaults
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Text
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.draw.clip
25 | import androidx.compose.ui.layout.ContentScale
26 | import androidx.compose.ui.text.style.TextOverflow
27 | import androidx.compose.ui.unit.dp
28 | import com.example.core.components.CoilImageComponent
29 | import com.example.list.domain.model.ListProductsModel
30 |
31 | @Composable
32 | fun ListContent(productList: List) {
33 | AnimatedVisibility(
34 | visible = productList.isNotEmpty(),
35 | enter = expandVertically() + fadeIn(),
36 | exit = shrinkVertically() + fadeOut()
37 | ) {
38 | LazyVerticalGrid(
39 | columns = GridCells.Fixed(2),
40 | contentPadding = PaddingValues(all = 8.dp),
41 | modifier = Modifier.padding(8.dp)
42 | ) {
43 | items(items = productList, key = { product ->
44 | product.productId
45 | }) {product ->
46 | ProductCard(product)
47 | }
48 | }
49 | }
50 | }
51 |
52 | @Composable
53 | fun ProductCard(product: ListProductsModel) {
54 | Card(
55 | modifier = Modifier
56 | .padding(4.dp)
57 | .fillMaxWidth()
58 | .height(250.dp),
59 | shape = RoundedCornerShape(10.dp),
60 | elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
61 | ) {
62 | Column(
63 | modifier = Modifier
64 | .padding(16.dp)
65 | .fillMaxWidth()
66 | ) {
67 | CoilImageComponent(
68 | imageUrl = product.productImage,
69 | contentDescription = "Product Image",
70 | modifier = Modifier
71 | .fillMaxWidth()
72 | .height(100.dp)
73 | .clip(RoundedCornerShape(10.dp)),
74 | contentScale = ContentScale.Fit
75 | )
76 | Column(
77 | modifier = Modifier
78 | .padding(8.dp)
79 | .fillMaxWidth()
80 | ) {
81 | Text(
82 | text = product.text,
83 | style = MaterialTheme.typography.titleLarge,
84 | modifier = Modifier.padding(bottom = 4.dp),
85 | maxLines = 2,
86 | overflow = TextOverflow.Ellipsis
87 | )
88 | Text(
89 | text = product.subText,
90 | style = MaterialTheme.typography.titleMedium,
91 | color = MaterialTheme.colorScheme.secondary
92 | )
93 | Spacer(modifier = Modifier.height(8.dp))
94 | }
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/presentation/event/ListUIEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.presentation.event
2 |
3 | sealed interface ListUIEvent {
4 | data object Dismiss : ListUIEvent
5 | data object GetList: ListUIEvent
6 | }
--------------------------------------------------------------------------------
/list/src/main/java/com/example/list/presentation/state/ListUIState.kt:
--------------------------------------------------------------------------------
1 | package com.example.list.presentation.state
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.example.list.domain.model.ListData
5 |
6 | @Immutable
7 | data class ListUIState(
8 | val listData: ListData?,
9 | val isLoading: Boolean = false,
10 | val error: Throwable? = null
11 | )
--------------------------------------------------------------------------------
/navigation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/navigation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.ksp.get().toString())
5 | id(libs.plugins.daggerHilt.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.navigationNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerVersion.get()
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation(project(":core"))
33 | //region D.I
34 | implementation(libs.hilt.core)
35 | ksp(libs.hilt.compiler)
36 | ksp(libs.hilt.ksp.compiler)
37 | //endregion
38 |
39 | implementation(libs.compose.navigation)
40 | }
--------------------------------------------------------------------------------
/navigation/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/navigation/consumer-rules.pro
--------------------------------------------------------------------------------
/navigation/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
--------------------------------------------------------------------------------
/navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/AppNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.navigation.compose.NavHost
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.compose.rememberNavController
8 | import com.example.navigation.graph.DetailScreens
9 | import com.example.navigation.graph.detailGraph
10 | import com.example.navigation.screens.Detail
11 | import kotlinx.coroutines.flow.collectLatest
12 |
13 | @Composable
14 | fun AppNavigation(
15 | navigator: Navigator,
16 | homeScreen: @Composable () -> Unit,
17 | listScreen: @Composable () -> Unit,
18 | detailScreen: @Composable (Boolean) -> Unit,
19 | detailScreenWithGraph: DetailScreens
20 | ) {
21 | val navController = rememberNavController()
22 |
23 | LaunchedEffect(Unit) {
24 | navigator.actions.collectLatest { action ->
25 | when (action) {
26 | Navigator.Action.Back -> navController.popBackStack()
27 | is Navigator.Action.Navigate -> navController.navigate(
28 | route = action.destination,
29 | builder = action.navOptions
30 | )
31 | }
32 | }
33 | }
34 |
35 | NavHost(navController, startDestination = Destination.home.route) {
36 | detailGraph(detailScreenWithGraph)
37 | composable(Destination.home.route) {
38 | homeScreen()
39 | }
40 | composable(Destination.list.route) {
41 | listScreen()
42 | }
43 | composable(Destination.detail.route, Destination.detail.arguments) {
44 | val isOpenSheet = Detail.objectParser(it)
45 | detailScreen(isOpenSheet)
46 | }
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/Destination.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation
2 |
3 | import com.example.navigation.screens.Detail
4 | import com.example.navigation.screens.Home
5 | import com.example.navigation.screens.List
6 |
7 | object Destination {
8 | val home = Home
9 | val list = List
10 | val detail = Detail
11 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/Navigator.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.navigation.NavOptionsBuilder
5 | import com.example.core.navigation.NavigationService
6 | import com.example.navigation.utils.DestinationRoute
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableSharedFlow
9 | import kotlinx.coroutines.flow.asSharedFlow
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class Navigator @Inject constructor() : NavigationService {
15 | private val _actions = MutableSharedFlow(
16 | replay = 0,
17 | extraBufferCapacity = 10
18 | )
19 | val actions: Flow = _actions.asSharedFlow()
20 | override fun navigateTo(
21 | destination: DestinationRoute,
22 | navOptions: NavOptionsBuilder.() -> Unit
23 | ) {
24 | _actions.tryEmit(
25 | Action.Navigate(destination = destination, navOptions = navOptions)
26 | )
27 | }
28 |
29 | override fun goBack() {
30 | _actions.tryEmit(Action.Back)
31 | }
32 |
33 | sealed class Action {
34 | data class Navigate(
35 | val destination: DestinationRoute,
36 | val navOptions: NavOptionsBuilder.() -> Unit
37 | ) : Action()
38 |
39 | data object Back : Action()
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/di/NavigationModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.di
2 |
3 | import com.example.core.navigation.NavigationService
4 | import com.example.navigation.Navigator
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Module
11 | @InstallIn(SingletonComponent::class)
12 | object NavigationModule {
13 | @Provides
14 | fun provideNavigationCommander(navigator: Navigator): NavigationService = navigator
15 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/graph/DetailGraph.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.graph
2 |
3 | import com.example.navigation.screens.detaiwithowngraph.DetailMain
4 | import com.example.navigation.screens.detaiwithowngraph.DetailSearch
5 | import com.example.navigation.utils.NavigationGraph
6 |
7 | object DetailGraph : NavigationGraph {
8 | override val route: String
9 | get() = "detailgraph"
10 | override val startDestination: String
11 | get() = detailMain.destination(Unit)
12 |
13 | val detailMain = DetailMain
14 | val detailSearch = DetailSearch
15 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/graph/DetailGraphBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.graph
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Immutable
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.navigation
8 | @Immutable
9 | data class DetailScreens(
10 | val detailMain: @Composable () -> Unit,
11 | val detailSearch: @Composable () -> Unit
12 | )
13 |
14 | internal fun NavGraphBuilder.detailGraph(
15 | screens: DetailScreens
16 | ) {
17 | navigation(
18 | startDestination = DetailGraph.startDestination,
19 | route = DetailGraph.route
20 | ) {
21 | composable(DetailGraph.detailMain.route) {
22 | screens.detailMain()
23 | }
24 | composable(DetailGraph.detailSearch.route) {
25 | screens.detailSearch()
26 | }
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/graph/DetailMain.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.screens.detaiwithowngraph
2 |
3 | import androidx.navigation.NamedNavArgument
4 | import androidx.navigation.NavBackStackEntry
5 | import com.example.navigation.utils.ArgsScreen
6 | import com.example.navigation.utils.DestinationRoute
7 |
8 | object DetailMain: ArgsScreen {
9 | override fun destination(arg: Unit): DestinationRoute = route
10 |
11 | override val route: String = "detail/main"
12 | override val arguments: List = emptyList()
13 |
14 | override fun objectParser(entry: NavBackStackEntry){}
15 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/graph/DetailSearch.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.screens.detaiwithowngraph
2 |
3 | import androidx.navigation.NamedNavArgument
4 | import androidx.navigation.NavBackStackEntry
5 | import com.example.navigation.utils.ArgsScreen
6 | import com.example.navigation.utils.DestinationRoute
7 |
8 | object DetailSearch :ArgsScreen{
9 | override fun destination(arg: Unit): DestinationRoute= route
10 |
11 | override val route: String = "detail/search"
12 | override val arguments: List = emptyList()
13 |
14 | override fun objectParser(entry: NavBackStackEntry) {}
15 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/screens/Detail.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.screens
2 |
3 | import androidx.navigation.NamedNavArgument
4 | import androidx.navigation.NavBackStackEntry
5 | import androidx.navigation.NavType
6 | import androidx.navigation.navArgument
7 | import com.example.navigation.utils.ArgsScreen
8 | import com.example.navigation.utils.DestinationRoute
9 | import kotlin.collections.List
10 |
11 | object Detail : ArgsScreen {
12 | override val route: String = "detail/{isOpenSheet}"
13 | override val arguments: List =
14 | listOf(navArgument("isOpenSheet") { type = NavType.BoolType })
15 |
16 | override fun objectParser(entry: NavBackStackEntry): Boolean =
17 | entry.arguments?.getBoolean("isOpenSheet") ?: false
18 |
19 | override fun destination(arg: Boolean): DestinationRoute = "detail/$arg"
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/screens/Home.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.screens
2 |
3 | import com.example.navigation.utils.WithoutArgsScreen
4 |
5 | object Home : WithoutArgsScreen() {
6 | override val route = "home"
7 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/screens/List.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.screens
2 |
3 | import com.example.navigation.utils.WithoutArgsScreen
4 |
5 | object List : WithoutArgsScreen() {
6 | override val route = "list"
7 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/utils/ArgsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.utils
2 |
3 | interface ArgsScreen : NodeScreen, NavDestination
4 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/utils/NavigationDestination.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.utils
2 |
3 | import androidx.navigation.NavBackStackEntry
4 |
5 | interface NavDestination {
6 | fun destination(arg: Arg): DestinationRoute
7 | fun objectParser(entry: NavBackStackEntry): Arg
8 | }
9 | typealias DestinationRoute = String
10 |
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/utils/NavigationGraph.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.utils
2 |
3 | interface NavigationGraph {
4 | val route: String
5 | val startDestination: String
6 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/utils/NodeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.utils
2 |
3 | import androidx.navigation.NamedNavArgument
4 |
5 | interface NodeScreen {
6 | val route: String
7 | val arguments: List
8 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/example/navigation/utils/WithoutArgsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.navigation.utils
2 |
3 | import androidx.navigation.NamedNavArgument
4 | import androidx.navigation.NavBackStackEntry
5 |
6 | abstract class WithoutArgsScreen : NodeScreen, NavDestination {
7 | override val arguments: List get() = emptyList()
8 | override fun objectParser(entry: NavBackStackEntry) {}
9 | override fun destination(arg: Unit): DestinationRoute = route
10 | }
--------------------------------------------------------------------------------
/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.kotlinAndroid)
4 | id(libs.plugins.ksp.get().toString())
5 | id(libs.plugins.daggerHilt.get().toString())
6 | }
7 |
8 | android {
9 | namespace = libs.plugins.networkNameSpace.get().toString()
10 | compileSdk = libs.versions.compileSdk.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = libs.versions.jvmTarget.get()
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation(project(":core"))
27 |
28 | //region D.I
29 | implementation(libs.hilt.core)
30 | ksp(libs.hilt.compiler)
31 | ksp(libs.hilt.ksp.compiler)
32 | //endregion
33 |
34 | //region Data Dependencies
35 | implementation(libs.okhttp.logging.interceptor)
36 | implementation(libs.retrofit.core)
37 | implementation(libs.retrofit.gson.converter)
38 | //endregion
39 | }
--------------------------------------------------------------------------------
/network/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basaransuleyman/Compose-FeatureBase-Multi-Module-Clean-Hexagonal-Architecture-Android-Kotlin/d951d3c552e2ba949244c70b9e2f012c4e03c866/network/consumer-rules.pro
--------------------------------------------------------------------------------
/network/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
--------------------------------------------------------------------------------
/network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/example/network/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.network.di
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import com.example.core.utils.Constants
6 | import com.example.core.utils.Constants.BASE_URL
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import okhttp3.OkHttpClient
12 | import okhttp3.logging.HttpLoggingInterceptor
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import java.time.Duration
16 | import javax.inject.Singleton
17 |
18 | @Module
19 | @InstallIn(SingletonComponent::class)
20 | internal object NetworkModule {
21 | @Singleton
22 | @Provides
23 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
24 | .baseUrl(BASE_URL)//TODO : Do not forget move to BuildConfig
25 | .addConverterFactory(GsonConverterFactory.create())
26 | .client(okHttpClient)
27 | .build()
28 |
29 |
30 | @RequiresApi(Build.VERSION_CODES.O)
31 | @Provides
32 | @Singleton
33 | fun provideHttpClient(
34 | loggingInterceptor: HttpLoggingInterceptor,
35 | ): OkHttpClient {
36 | return OkHttpClient.Builder()
37 | .addNetworkInterceptor(loggingInterceptor)
38 | .connectTimeout(Duration.ofSeconds(10))
39 | .readTimeout(Duration.ofSeconds(30))
40 | .build()
41 | }
42 |
43 | @Singleton
44 | @Provides
45 | fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
46 | val interceptor = HttpLoggingInterceptor()
47 | interceptor.level = if (Constants.DEVELOPMENT_MODE) {
48 | HttpLoggingInterceptor.Level.BODY
49 | } else {
50 | HttpLoggingInterceptor.Level.NONE
51 | }
52 | return interceptor
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/network/src/main/java/com/example/network/extensions/ApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.example.network.extensions
2 |
3 | import com.example.core.model.GenericException
4 | import retrofit2.Response
5 |
6 | fun Response.handleResponse(): T {
7 | return try {
8 | this.takeIf { it.isSuccessful }?.body()!!
9 | } catch (e: Exception) {
10 | throw GenericException(
11 | message = e.message,
12 | hasUserFriendlyMessage = false
13 | )
14 | }
15 | }
16 |
17 | suspend fun handleCall(block: suspend () -> Response): T {
18 | return try {
19 | block.invoke().handleResponse()
20 | } catch (e: Exception) {
21 | throw GenericException(
22 | message = e.message,
23 | hasUserFriendlyMessage = false
24 | )
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "ComposeFeatureBasedMultiModule"
17 | include(":app")
18 | include(":navigation")
19 | include(":core")
20 | include(":home")
21 | include(":list")
22 | include(":network")
23 | include(":detail")
24 |
--------------------------------------------------------------------------------