├── .gitignore
├── README.md
├── build.gradle.kts
├── composeApp
├── build.gradle.kts
├── schemas
│ └── data.db.AppDatabase
│ │ └── 1.json
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ ├── com
│ │ │ └── monstarlab
│ │ │ │ └── kmp
│ │ │ │ ├── AndroidApp.kt
│ │ │ │ └── MainActivity.kt
│ │ ├── core
│ │ │ └── ContextProvider.kt
│ │ ├── data
│ │ │ └── preference
│ │ │ │ └── Preference.android.kt
│ │ ├── di
│ │ │ └── modules
│ │ │ │ └── PlatformModule.kt
│ │ └── platform
│ │ │ └── Platform.android.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ └── strings.xml
│ ├── commonMain
│ ├── composeResources
│ │ └── drawable
│ │ │ └── compose-multiplatform.xml
│ ├── kotlin
│ │ ├── data
│ │ │ ├── db
│ │ │ │ ├── AppDatabase.kt
│ │ │ │ ├── DataBaseConstants.kt
│ │ │ │ ├── ProductDao.kt
│ │ │ │ ├── ProductDataSource.kt
│ │ │ │ └── ProductDatabaseDataSource.kt
│ │ │ ├── network
│ │ │ │ ├── ApiDefinition.kt
│ │ │ │ ├── ApiService.kt
│ │ │ │ ├── ApiServiceImpl.kt
│ │ │ │ ├── NetworkConstants.kt
│ │ │ │ ├── errorhandling
│ │ │ │ │ └── ApiErrorInterceptor.kt
│ │ │ │ ├── requests
│ │ │ │ │ └── LoginRequest.kt
│ │ │ │ └── responses
│ │ │ │ │ ├── AuthResponse.kt
│ │ │ │ │ └── ProductsResponse.kt
│ │ │ ├── preference
│ │ │ │ ├── ListDataStorePreferences.kt
│ │ │ │ ├── Preference.kt
│ │ │ │ └── SingleDataStorePreferences.kt
│ │ │ └── repository
│ │ │ │ ├── AuthRepositoryImpl.kt
│ │ │ │ ├── FavoriteRepositoryImpl.kt
│ │ │ │ ├── ProductPagingSource.kt
│ │ │ │ └── ProductRepositoryImpl.kt
│ │ ├── di
│ │ │ ├── KoinCommon.kt
│ │ │ └── modules
│ │ │ │ ├── DataBaseModule.kt
│ │ │ │ ├── DispatcherModule.kt
│ │ │ │ ├── NetworkModule.kt
│ │ │ │ ├── PlatformModule.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ ├── UseCaseModule.kt
│ │ │ │ └── ViewModelsModule.kt
│ │ ├── domain
│ │ │ ├── model
│ │ │ │ └── ProductList.kt
│ │ │ ├── repository
│ │ │ │ ├── AuthRepository.kt
│ │ │ │ ├── FavoriteRepository.kt
│ │ │ │ └── ProductRepository.kt
│ │ │ └── usecase
│ │ │ │ ├── GetUserFromPreferenceUseCase.kt
│ │ │ │ ├── LoginUseCase.kt
│ │ │ │ ├── favorite
│ │ │ │ ├── AddToFavoriteUseCase.kt
│ │ │ │ ├── GetFavoritesUseCase.kt
│ │ │ │ └── RemoveFromFavoriteUseCase.kt
│ │ │ │ └── product
│ │ │ │ ├── GetPaginatedProductsUseCase.kt
│ │ │ │ └── GetProductsUseCase.kt
│ │ ├── error
│ │ │ ├── ApiErrorException.kt
│ │ │ └── ApiErrorValidator.kt
│ │ ├── platform
│ │ │ ├── Greeting.kt
│ │ │ └── Platform.kt
│ │ └── presentation
│ │ │ ├── App.kt
│ │ │ ├── components
│ │ │ ├── appbutton
│ │ │ │ ├── AppButton.kt
│ │ │ │ └── AppButtonType.kt
│ │ │ └── apptextfield
│ │ │ │ └── AppTextField.kt
│ │ │ ├── feature
│ │ │ ├── home
│ │ │ │ ├── HomeContract.kt
│ │ │ │ ├── HomeScreen.kt
│ │ │ │ ├── HomeScreenRoute.kt
│ │ │ │ ├── HomeViewModel.kt
│ │ │ │ ├── HomeViewModelProvider.kt
│ │ │ │ └── ProductItemView.kt
│ │ │ ├── login
│ │ │ │ ├── LoginContract.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ ├── LoginScreenRoute.kt
│ │ │ │ ├── LoginViewModel.kt
│ │ │ │ └── LoginViewModelProvider.kt
│ │ │ ├── pagination
│ │ │ │ ├── PaginatedHomeScreen.kt
│ │ │ │ └── PaginatedHomeViewModel.kt
│ │ │ └── splash
│ │ │ │ ├── SplashContract.kt
│ │ │ │ ├── SplashScreen.kt
│ │ │ │ ├── SplashScreenRoute.kt
│ │ │ │ ├── SplashViewModel.kt
│ │ │ │ └── SplashViewModelProvider.kt
│ │ │ ├── model
│ │ │ └── ErrorModel.kt
│ │ │ └── theme
│ │ │ ├── AppTheme.kt
│ │ │ ├── Colors.kt
│ │ │ ├── Shapes.kt
│ │ │ ├── dimensions
│ │ │ ├── Dimensions.kt
│ │ │ └── DimensionsProvider.kt
│ │ │ └── typography
│ │ │ ├── AppTypography.kt
│ │ │ ├── AppTypographyProvider.kt
│ │ │ ├── FontFamily.kt
│ │ │ └── Typography.kt
│ └── sqldelight
│ │ └── com
│ │ └── monstarlab
│ │ └── kmp
│ │ └── Product.sq
│ ├── commonTest
│ └── kotlin
│ │ ├── domain
│ │ └── usecase
│ │ │ ├── GetProductsUseCaseTest.kt
│ │ │ ├── LoginUseCaseTest.kt
│ │ │ └── favorite
│ │ │ ├── AddToFavoriteUseCaseTest.kt
│ │ │ ├── GetFavoritesUseCaseTest.kt
│ │ │ └── RemoveFromFavoriteUseCaseTest.kt
│ │ └── fakes
│ │ ├── FakeDataSource.kt
│ │ ├── FakeFavoriteRepository.kt
│ │ └── FakeProductRepository.kt
│ └── iosMain
│ └── kotlin
│ ├── MainViewController.kt
│ ├── data
│ └── preference
│ │ └── Preference.ios.kt
│ ├── di
│ └── modules
│ │ └── PlatformModule.kt
│ └── platform
│ ├── Koin.kt
│ └── Platform.ios.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── Podfile
├── Podfile.lock
├── iosApp.xcodeproj
│ └── project.pbxproj
└── iosApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── app-icon-1024.png
│ └── Contents.json
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── presentation
│ ├── ContentView.swift
│ ├── feature
│ │ ├── HomeScreen.swift
│ │ ├── LoginScreen.swift
│ │ └── SplashScreen.swift
│ └── iOSApp.swift
│ └── system
│ └── Koin.swift
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | **/build/
3 | !src/**/build/
4 | local.properties
5 | .idea
6 | .fleet
7 | .DS_Store
8 | captures
9 | .externalNativeBuild
10 | .cxx
11 | *.xcodeproj/*
12 | !*.xcodeproj/project.pbxproj
13 | !*.xcodeproj/xcshareddata/
14 | !*.xcodeproj/project.xcworkspace/
15 | !*.xcworkspace/contents.xcworkspacedata
16 | **/xcshareddata/WorkspaceSettings.xcsettings
17 | .fleet/settings.json
18 |
19 | # Built application files
20 | *.apk
21 | *.aar
22 | *.app
23 | *.ipa
24 |
25 | # Files for the ART/Dalvik VM
26 | *.dex
27 |
28 | # Java class files
29 | *.class
30 |
31 | # Generated files
32 | bin/
33 | gen/
34 | out/
35 | build/
36 | /*/build/
37 |
38 | # Gradle files
39 | .gradle/
40 |
41 | # Local configuration files
42 | /local.properties
43 |
44 | # IntelliJ/Android Studio
45 | .idea/
46 | *.iml
47 | *.iws
48 | *.ipr
49 | *.ids
50 | *.idb
51 | *.icls
52 | *.idea_modules
53 | .idea/workspace.xml
54 | .idea/tasks.xml
55 | .idea/gradle.xml
56 | .idea/assetWizardSettings.xml
57 | .idea/dictionaries
58 | .idea/libraries
59 | .idea/caches
60 |
61 | # Keystore files
62 | *.jks
63 | *.keystore
64 |
65 | # OS generated files
66 | ehthumbs.db
67 | Icon?
68 | Thumbs.db
69 |
70 | # Kotlin Native
71 | *.klib
72 | .kotlin_metadata/
73 | .klibs/
74 |
75 | # Xcode
76 | .DS_Store*
77 | /.AppleDouble
78 | *.lproj/
79 | *.pbxuser
80 | !default.pbxuser
81 | *.mode1v3
82 | !default.mode1v3
83 | *.mode2v3
84 | !default.mode2v3
85 | *.perspectivev3
86 | !default.perspectivev3
87 | xcuserdata
88 | *.xccheckout
89 | *.moved-aside
90 | DerivedData
91 | *.hmap
92 | *.xcworkspace
93 | !default.xcworkspace
94 | xcuserstate
95 | *.xcscheme
96 |
97 | # Swift Package Manager
98 | .build/
99 |
100 | # CocoaPods
101 | iosApp/Pods/
102 | iosApp/Podfile.lock
103 | iosApp/Podfile.private
104 | iosApp/Podfile.shared
105 | iosApp/Podfile.pods
106 | iosApp/Podfile.workspace
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kotlin Multiplatform Project Setup
2 |
3 | 
4 |
5 | This Kotlin Multiplatform project targets Android and iOS using Compose Multiplatform.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 💻 **Techstack**
14 | -
[Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)
15 | - [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform) for sharing UIs across multiple platforms with Kotlin
16 | -
[Swift UI](https://developer.apple.com/xcode/swiftui/) (Optional)
17 | - 🗡️ [koin](https://github.com/InsertKoinIO/koin) for Dependency Injection
18 | - ☁️ [ktor](https://github.com/ktorio/ktor) for networking
19 | - 🔗 [Kotlin Serialization](https://kotlinlang.org/docs/serialization.html) as primary JSON (de)serialization tool
20 | - 🔄 [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) for async operations
21 | - 📦 [Room (Kotlin Multiplatform)](https://developer.android.com/kotlin/multiplatform/room) for persistent SQL storage
22 | - 🗂 [DataStore (Kotlin Multiplatform)](https://developer.android.com/kotlin/multiplatform/datastore) for persistent NoSQL storage
23 | - 📱 Shared [ViewModel](https://developer.android.com/reference/androidx/lifecycle/ViewModel) by AndroidX
24 | - 🧭 [Voyager](https://voyager.adriel.cafe/navigation) for navigation
25 | - 📲 [SKIE](https://skie.touchlab.co/) - Swift Kotlin Interface Enhancer by Touchlab
26 | - 🖼 Coil for loading images
27 | - ✨ [Spotless](https://github.com/diffplug/spotless) for code-format control
28 | - 🔍 [Detekt](https://github.com/detekt/detekt) for static code analysis
29 |
30 | 📒 **Todo**
31 | - Jetpack Navigation
32 | - Deeplink Support
33 | - Build Logic
34 |
35 | ## Project Setup
36 |
37 | ### Android:
38 |
39 | 1. Open the project in Android Studio.
40 | 2. Wait for the build to finish.
41 | 3. Select `composeApp` from the `Run/Debug` Configurations.
42 | 4. Apply and run.
43 |
44 | ### iOS:
45 |
46 | 1. Navigate to the `KMP-Template/iosApp` directory.
47 | 2. For first-time setup:
48 | - Open terminal in that directory and run the command `pod install`.
49 | - It’ll generate the `iosApp.xcworkspace` file.
50 | 3. Open the `iosApp.xcworkspace` file in Xcode.
51 | 4. Choose the correct target for your device or simulator in the scheme chooser at the top of the window.
52 | 5. Build and run.
53 |
54 | ### iOS with Swift UI:
55 |
56 | If you want to run the iOS app with swift UI (not compose multiplatform)
57 | 1. Open the `iosApp.xcworkspace` file in Xcode.
58 | 2. Navigate to the `iosApp/presentation/ContentView` file in Xcode.
59 | 3. Inside the ContentView body, uncomment the line `SplashScreen()` and comment out the `ComposeView().ignoresSafeArea(.keyboard)`.
60 | 4. Build and run.
61 |
62 | ## Structure
63 |
64 | - `/composeApp`: Contains code shared across the Compose Multiplatform applications.
65 | - `commonMain`: Holds code that's common for all targets.
66 | - Platform-specific directories (e.g., `androidMain`, `iosMain`): Contain Kotlin code that will
67 | compile only for the designated platform. For iOS-specific Kotlin code, such as integration with
68 | Apple's CoreCrypto, uses `iosMain`.
69 |
70 | - `/iosApp`: Contains the iOS application. This is the necessary entry point for the iOS app,
71 | even when sharing UI with Compose Multiplatform. This is also where to add any SwiftUI code for
72 | your project.
73 |
74 | ## Architecture
75 | Template implements [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) and follows [best practices](https://developer.android.com/topic/architecture) provided by Google with some tweaks here and there
76 |
77 | ### Presentation layer
78 | The Presentation layer or UI Layer is our Jetpack Compose UI screens and components, and ViewModels. The Presentation layer interacts with the Domain Layer where our business logic resides.
79 |
80 | ### Domain layer
81 | The domain layer contains the application's business logic. This layer only works with abstractions and as such it never knows about how different layers look like. It doesn't know about any Databases, APIs, or even any Frameworks.
82 |
83 | ### Data layer
84 | The data layer is where the actual interactions happen between different data sources. This layer “implements” parts of the Domain layer and communicates with the APIs, Databases, and other services and SDKs.
85 |
86 |
87 | ## Tools
88 | These are the tools used in the template, to improve the development, that you should be aware of:
89 |
90 | ### [Spotless](https://github.com/diffplug/spotless)
91 | Spotless is a Gradle plugin used for consistent code formatting and style conventions across the project. You can use it running the following Gradle commands:
92 |
93 | `./gradlew spotlessCheck` - Checks the style formatting and displays any issues
94 |
95 | `./gradlew spotlessApply` - Same as above but automatically tries to fix most of the issues. If for any reason it can't, then a list of problems is displayed.
96 |
97 | ## Additional Resources
98 |
99 | For more information on Kotlin Multiplatform, visit
100 | the [official documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html).
101 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension
2 |
3 | plugins {
4 | // this is necessary to avoid the plugins to be loaded multiple times
5 | // in each subproject's classloader
6 | alias(libs.plugins.androidApplication) apply false
7 | alias(libs.plugins.androidLibrary) apply false
8 | alias(libs.plugins.jetbrainsCompose) apply false
9 | alias(libs.plugins.kotlinMultiplatform) apply false
10 | alias(libs.plugins.ksp) apply false
11 | alias(libs.plugins.detekt) apply false
12 | alias(libs.plugins.spotless)
13 | alias(libs.plugins.kotlin.dokka)
14 | alias(libs.plugins.skie) apply false
15 | }
16 | subprojects {
17 | apply(plugin = rootProject.libs.plugins.kotlin.dokka.get().pluginId)
18 | apply(plugin = rootProject.libs.plugins.detekt.get().pluginId)
19 | apply(plugin = rootProject.libs.plugins.spotless.get().pluginId)
20 | configure {
21 | buildUponDefaultConfig = true
22 | allRules = false
23 | config.setFrom("$rootDir/detekt.yml")
24 | }
25 | }
26 |
27 | spotless {
28 | kotlin {
29 | target("**/*.kt")
30 | ktlint()
31 | .editorConfigOverride(
32 | mapOf(
33 | "indent_size" to 4,
34 | "indent_style" to "space",
35 | "ij_kotlin_imports_layout" to "*,java.**,javax.**,kotlin.**,kotlinx.**,^",
36 | "ij_kotlin_allow_trailing_comma_on_call_site" to "true",
37 | "ij_kotlin_allow_trailing_comma" to "true",
38 | "ktlint_function_naming_ignore_when_annotated_with" to "Composable",
39 | "ktlint_code_style" to "android_studio",
40 | )
41 | )
42 | trimTrailingWhitespace()
43 | endWithNewline()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlinMultiplatform)
3 | alias(libs.plugins.androidApplication)
4 | alias(libs.plugins.jetbrainsCompose)
5 | alias(libs.plugins.kotlinxSerialization)
6 | alias(libs.plugins.ksp)
7 | alias(libs.plugins.skie)
8 | alias(libs.plugins.room)
9 | }
10 |
11 | // Exclude viewmodel dep from other sources
12 | configurations.all {
13 | resolutionStrategy.force(libs.kmp.viewmodel)
14 | }
15 |
16 | kotlin {
17 | androidTarget {
18 | compilations.all {
19 | kotlinOptions {
20 | jvmTarget = "11"
21 | }
22 | }
23 | }
24 |
25 | listOf(
26 | iosX64(),
27 | iosArm64(),
28 | iosSimulatorArm64()
29 | ).forEach { iosTarget ->
30 | iosTarget.binaries.framework {
31 | baseName = "ComposeApp"
32 | isStatic = true
33 | }
34 | }
35 |
36 | sourceSets {
37 | androidMain.dependencies {
38 | implementation(libs.compose.ui.tooling.preview)
39 | implementation(libs.androidx.activity.compose)
40 | implementation(libs.ktor.client.okhttp)
41 | implementation(libs.bundles.paging.android)
42 | api(libs.androidx.startup)
43 | }
44 | iosMain.dependencies {
45 | implementation(libs.ktor.client.darwin)
46 | implementation(libs.paging.runtime.uikit)
47 | }
48 | commonMain.dependencies {
49 | implementation(compose.components.resources)
50 | implementation(compose.runtime)
51 | implementation(compose.foundation)
52 | implementation(compose.material)
53 | implementation(compose.ui)
54 | implementation(compose.materialIconsExtended)
55 | implementation(libs.bundles.voyager)
56 | implementation(libs.koin.core)
57 | implementation(libs.koin.compose)
58 | implementation(libs.bundles.ktor)
59 | implementation(libs.bundles.coil)
60 | implementation(libs.bundles.paging)
61 | implementation(libs.androidx.room.runtime)
62 | implementation(libs.sqlite.bundled)
63 | implementation(libs.androidx.lifecycle.viewmodel)
64 | implementation(libs.androidx.datastore.preferences.core)
65 | // It's a temporary fix for the issue https://github.com/cashapp/sqldelight/issues/4357
66 | // No need to move the dependency to version catalog
67 | implementation("co.touchlab:stately-common:2.0.5")
68 | }
69 | commonTest.dependencies {
70 | implementation(kotlin("test-junit"))
71 | implementation(kotlin("test-common"))
72 | implementation(kotlin("test-annotations-common"))
73 | implementation(libs.koin.test)
74 | implementation(libs.kotlinx.coroutines.test)
75 | implementation(libs.paging.common.test)
76 | }
77 | }
78 |
79 | task("testClasses")
80 | }
81 |
82 | android {
83 | namespace = "com.monstarlab.kmp"
84 | compileSdk = libs.versions.android.compileSdk.get().toInt()
85 |
86 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
87 | sourceSets["main"].res.srcDirs("src/androidMain/res")
88 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
89 |
90 | defaultConfig {
91 | applicationId = "com.monstarlab.kmp"
92 | minSdk = libs.versions.android.minSdk.get().toInt()
93 | targetSdk = libs.versions.android.targetSdk.get().toInt()
94 | versionCode = 1
95 | versionName = "1.0"
96 | }
97 | packaging {
98 | resources {
99 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
100 | }
101 | }
102 | buildTypes {
103 | getByName("release") {
104 | isMinifyEnabled = false
105 | }
106 | }
107 | compileOptions {
108 | sourceCompatibility = JavaVersion.VERSION_11
109 | targetCompatibility = JavaVersion.VERSION_11
110 | }
111 | dependencies {
112 | debugImplementation(libs.compose.ui.tooling)
113 | }
114 | }
115 |
116 | kotlin.sourceSets.all {
117 | languageSettings {
118 | optIn("kotlin.experimental.ExperimentalObjCName")
119 | optIn("kotlinx.cinterop.ExperimentalForeignApi")
120 | }
121 | }
122 |
123 | dependencies {
124 | add("kspAndroid", libs.androidx.room.compiler)
125 | add("kspIosSimulatorArm64", libs.androidx.room.compiler)
126 | add("kspIosX64", libs.androidx.room.compiler)
127 | add("kspIosArm64", libs.androidx.room.compiler)
128 | }
129 |
130 | room {
131 | schemaDirectory("$projectDir/schemas")
132 | }
133 |
--------------------------------------------------------------------------------
/composeApp/schemas/data.db.AppDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "78ad7fd608406aacb12ae68cc4570894",
6 | "entities": [
7 | {
8 | "tableName": "Product",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `brand` TEXT NOT NULL, `category` TEXT NOT NULL, `description` TEXT NOT NULL, `discountPercentage` REAL NOT NULL, `price` INTEGER NOT NULL, `rating` REAL NOT NULL, `stock` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "brand",
19 | "columnName": "brand",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "category",
25 | "columnName": "category",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "description",
31 | "columnName": "description",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "discountPercentage",
37 | "columnName": "discountPercentage",
38 | "affinity": "REAL",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "price",
43 | "columnName": "price",
44 | "affinity": "INTEGER",
45 | "notNull": true
46 | },
47 | {
48 | "fieldPath": "rating",
49 | "columnName": "rating",
50 | "affinity": "REAL",
51 | "notNull": true
52 | },
53 | {
54 | "fieldPath": "stock",
55 | "columnName": "stock",
56 | "affinity": "INTEGER",
57 | "notNull": true
58 | },
59 | {
60 | "fieldPath": "thumbnail",
61 | "columnName": "thumbnail",
62 | "affinity": "TEXT",
63 | "notNull": true
64 | },
65 | {
66 | "fieldPath": "title",
67 | "columnName": "title",
68 | "affinity": "TEXT",
69 | "notNull": true
70 | }
71 | ],
72 | "primaryKey": {
73 | "autoGenerate": false,
74 | "columnNames": [
75 | "id"
76 | ]
77 | }
78 | }
79 | ],
80 | "setupQueries": [
81 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
82 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '78ad7fd608406aacb12ae68cc4570894')"
83 | ]
84 | }
85 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/monstarlab/kmp/AndroidApp.kt:
--------------------------------------------------------------------------------
1 | package com.monstarlab.kmp
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import di.initKoin
6 | import org.koin.dsl.module
7 |
8 | class AndroidApp : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 | initKoin(appModule = module { single { this@AndroidApp } })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/monstarlab/kmp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.monstarlab.kmp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.tooling.preview.Preview
8 | import presentation.App
9 |
10 | class MainActivity : ComponentActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 |
14 | setContent {
15 | App()
16 | }
17 | }
18 | }
19 |
20 | @Preview
21 | @Composable
22 | fun AppAndroidPreview() {
23 | App()
24 | }
25 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/core/ContextProvider.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 |
6 | /**
7 | * Initializes and provides the application context to other components.
8 | */
9 | internal lateinit var applicationContext: Context
10 | private set
11 |
12 | /**
13 | * Initializes the application context and marks completion of initialization.
14 | */
15 | data object ContextProviderInitializer
16 |
17 | /**
18 | * Provides the application context during initialization.
19 | */
20 | class ContextProvider : Initializer {
21 |
22 | /**
23 | * Sets the application context and completes initialization.
24 | */
25 | override fun create(context: Context): ContextProviderInitializer {
26 | applicationContext = context.applicationContext
27 | return ContextProviderInitializer
28 | }
29 |
30 | /**
31 | * Specifies dependencies, which in this case is empty.
32 | */
33 | override fun dependencies(): List>> = emptyList()
34 | }
35 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/data/preference/Preference.android.kt:
--------------------------------------------------------------------------------
1 | package data.preference
2 |
3 |
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import core.applicationContext
7 |
8 | /**
9 | * Platform-specific function for creating a DataStore instance.
10 | * Creates a DataStore instance for Android platform.
11 | *
12 | * @return DataStore instance.
13 | */
14 |
15 | internal actual fun createDataStore(): DataStore = createDataStore(
16 | producePath = { applicationContext.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath },
17 | )
18 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/di/modules/PlatformModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver
6 | import data.db.AppDatabase
7 | import data.db.DataBaseConstants.DATABASE_NAME
8 | import kotlinx.coroutines.Dispatchers
9 | import org.koin.dsl.module
10 | import java.io.File
11 |
12 | actual val platformModule = module {
13 | single { createRoomDatabase(get()) }
14 | }
15 |
16 | fun createRoomDatabase(ctx: Context): AppDatabase {
17 | val dbFile = ctx.getDatabasePath(DATABASE_NAME)
18 | return Room.databaseBuilder(ctx, dbFile.absolutePath)
19 | .setDriver(BundledSQLiteDriver())
20 | .setQueryCoroutineContext(Dispatchers.IO)
21 | .build()
22 | }
23 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/platform/Platform.android.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import android.os.Build
4 |
5 | class AndroidPlatform : Platform {
6 | override val name: String = "Android ${Build.VERSION.SDK_INT}"
7 | }
8 |
9 | actual fun getPlatform(): Platform = AndroidPlatform()
10 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/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 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | kmp-template
3 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package data.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import domain.model.Product
6 |
7 | @Database(entities = [Product::class], version = 1)
8 | abstract class AppDatabase : RoomDatabase() {
9 | abstract fun productDao(): ProductDao
10 | }
11 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/db/DataBaseConstants.kt:
--------------------------------------------------------------------------------
1 | package data.db
2 |
3 | object DataBaseConstants {
4 | const val DATABASE_NAME = "products.db"
5 | }
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/db/ProductDao.kt:
--------------------------------------------------------------------------------
1 | package data.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import androidx.room.Update
7 | import androidx.room.Delete
8 | import domain.model.Product
9 |
10 | @Dao
11 | interface ProductDao {
12 |
13 | @Insert
14 | suspend fun insert(product: Product)
15 |
16 | @Query("SELECT * FROM Product")
17 | suspend fun getAll(): List
18 |
19 | @Query("SELECT * FROM Product WHERE id = :id")
20 | suspend fun getProductById(id: Long): Product?
21 |
22 | @Update
23 | suspend fun update(product: Product)
24 |
25 | @Delete
26 | suspend fun delete(product: Product)
27 | }
28 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/db/ProductDataSource.kt:
--------------------------------------------------------------------------------
1 | package data.db
2 |
3 | import domain.model.Product
4 |
5 | interface ProductDataSource {
6 | suspend fun insertProduct(product: Product)
7 |
8 | suspend fun fetchAllProducts(): List
9 |
10 | suspend fun deleteProduct(product: Product)
11 | }
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/db/ProductDatabaseDataSource.kt:
--------------------------------------------------------------------------------
1 | package data.db
2 |
3 |
4 | import domain.model.Product
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.withContext
7 |
8 | class ProductDatabaseDataSource(
9 | private val dbRef: AppDatabase,
10 | private val ioDispatcher: CoroutineDispatcher,
11 | ) : ProductDataSource {
12 | override suspend fun insertProduct(product: Product) = withContext(ioDispatcher) {
13 | dbRef.productDao().insert(product = product)
14 | }
15 |
16 | override suspend fun fetchAllProducts() = withContext(ioDispatcher) {
17 | dbRef.productDao().getAll()
18 | }
19 |
20 | override suspend fun deleteProduct(product: Product) = withContext(ioDispatcher) {
21 | dbRef.productDao().delete(product)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/ApiDefinition.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 | sealed class ApiDefinition {
4 | sealed class ApiEndpoint(val path: String) {
5 | data object Login : ApiEndpoint(path = "auth/login")
6 | data object Products : ApiEndpoint(path = "products")
7 | }
8 |
9 | object ApiField {
10 | const val PARAM_LIMIT = "limit"
11 | const val PARAM_SKIP = "skip"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/ApiService.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 | import data.network.requests.LoginRequest
4 | import io.ktor.client.statement.HttpResponse
5 |
6 | interface ApiService {
7 | suspend fun getProducts(limit: Int = 10, skip: Int = 0): HttpResponse
8 |
9 | suspend fun authenticate(loginRequest: LoginRequest): HttpResponse
10 | }
11 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/ApiServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 | import data.network.ApiDefinition.ApiEndpoint.Login
4 | import data.network.ApiDefinition.ApiEndpoint.Products
5 | import data.network.ApiDefinition.ApiField.PARAM_LIMIT
6 | import data.network.ApiDefinition.ApiField.PARAM_SKIP
7 | import data.network.requests.LoginRequest
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.request.get
10 | import io.ktor.client.request.parameter
11 | import io.ktor.client.request.post
12 | import io.ktor.client.request.setBody
13 | import io.ktor.client.statement.HttpResponse
14 | import io.ktor.http.ContentType
15 | import io.ktor.http.contentType
16 |
17 | class ApiServiceImpl(private val httpClient: HttpClient) : ApiService {
18 | override suspend fun getProducts(limit: Int, skip: Int): HttpResponse = httpClient.get(
19 | Products.path,
20 | ) {
21 | parameter(PARAM_LIMIT, limit)
22 | parameter(PARAM_SKIP, skip)
23 | }
24 |
25 | override suspend fun authenticate(loginRequest: LoginRequest): HttpResponse {
26 | return httpClient.post(Login.path) {
27 | contentType(ContentType.Application.Json)
28 | setBody(loginRequest)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/NetworkConstants.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 | object NetworkConstants {
4 | const val BASE_URL = "dummyjson.com"
5 | }
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/errorhandling/ApiErrorInterceptor.kt:
--------------------------------------------------------------------------------
1 | package data.network.errorhandling
2 |
3 | import error.ApiErrorException
4 | import io.ktor.client.statement.HttpResponse
5 | import io.ktor.http.HttpStatusCode
6 |
7 | fun validateResponse(response: HttpResponse) {
8 | val statusCode = response.status.value
9 | val displayableMessage = response.status.toDisplayableMessage()
10 | val loggableMessage = "Error code: $statusCode, message: $displayableMessage"
11 | throw ApiErrorException(statusCode, displayableMessage, loggableMessage)
12 | }
13 |
14 | fun HttpStatusCode.toDisplayableMessage(): String {
15 | return when (this) {
16 | HttpStatusCode.Forbidden -> "Access denied"
17 | HttpStatusCode.BadRequest -> "Bad request"
18 | HttpStatusCode.Unauthorized -> "Unauthorized"
19 | HttpStatusCode.NotFound -> "Resource not found"
20 | HttpStatusCode.InternalServerError -> "Internal server error"
21 | HttpStatusCode.ServiceUnavailable -> "Service unavailable"
22 | HttpStatusCode.RequestTimeout -> "The request timed out"
23 | else -> "Unexpected status code: $value"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/requests/LoginRequest.kt:
--------------------------------------------------------------------------------
1 | package data.network.requests
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LoginRequest(val username: String, val password: String)
7 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/responses/AuthResponse.kt:
--------------------------------------------------------------------------------
1 | package data.network.responses
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class AuthResponse(
7 | val id: Int,
8 | val token: String,
9 | )
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/network/responses/ProductsResponse.kt:
--------------------------------------------------------------------------------
1 | package data.network.responses
2 |
3 | import domain.model.Product
4 | import domain.model.ProductList
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | internal data class ProductsResponse(
9 | val limit: Int,
10 | val products: List,
11 | val skip: Int,
12 | val total: Int,
13 | )
14 |
15 | @Serializable
16 | internal data class ProductDTO(
17 | val brand: String,
18 | val category: String,
19 | val description: String,
20 | val discountPercentage: Double,
21 | val id: Int,
22 | val price: Int,
23 | val rating: Double,
24 | val stock: Int,
25 | val thumbnail: String,
26 | val title: String,
27 | )
28 |
29 | internal fun ProductDTO.toDomainModel() = Product(
30 | id, brand, category, description, discountPercentage, price, rating, stock, thumbnail, title,
31 | )
32 |
33 | internal fun ProductsResponse.toDomainModel() = ProductList(
34 | limit,
35 | products.map { it.toDomainModel() },
36 | skip,
37 | total,
38 | )
39 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/preference/ListDataStorePreferences.kt:
--------------------------------------------------------------------------------
1 | package data.preference
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.serialization.KSerializer
10 | import kotlinx.serialization.SerializationException
11 | import kotlinx.serialization.builtins.ListSerializer
12 | import kotlinx.serialization.json.Json
13 |
14 | interface ListDataSource {
15 | suspend fun getAll(): List
16 | suspend fun add(item: T)
17 | suspend fun addAll(items: List)
18 | suspend fun clear()
19 | }
20 |
21 | abstract class ListDataStorePreferences(
22 | private val dataStore: DataStore,
23 | private val serializer: KSerializer,
24 | ) : ListDataSource {
25 |
26 | private val key = stringPreferencesKey(this::class.simpleName ?: "")
27 |
28 | override suspend fun getAll(): List {
29 | return try {
30 | val json = dataStore.data.map { it[key] ?: "" }.first()
31 | Json.decodeFromString(ListSerializer(serializer), json)
32 | } catch (e: SerializationException) {
33 | emptyList()
34 | }
35 | }
36 |
37 | override suspend fun add(item: T) {
38 | val list = getAll().toMutableList()
39 | list.add(item)
40 | addAll(list)
41 | }
42 |
43 | override suspend fun addAll(items: List) {
44 | val json = Json.encodeToString(ListSerializer(serializer), items)
45 | dataStore.edit {
46 | it[key] = json
47 | }
48 | }
49 |
50 | override suspend fun clear() {
51 | dataStore.edit {
52 | it[key] = ""
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/preference/Preference.kt:
--------------------------------------------------------------------------------
1 | package data.preference
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory
5 | import androidx.datastore.preferences.core.Preferences
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.SupervisorJob
9 | import okio.Path.Companion.toPath
10 |
11 | internal const val DATA_STORE_FILE_NAME = "theme.preferences_pb"
12 |
13 | /**
14 | * Creates a DataStore instance for storing and accessing preferences.
15 | * Uses the PreferenceDataStoreFactory to create the DataStore.
16 | *
17 | * @param producePath Function that produces the path where the DataStore file will be stored.
18 | * @return DataStore instance.
19 | */
20 | fun createDataStore(producePath: () -> String): DataStore {
21 | return PreferenceDataStoreFactory.createWithPath(
22 | corruptionHandler = null,
23 | migrations = emptyList(),
24 | // Coroutine scope for IO operations
25 | scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
26 | // Produces the file path for the DataStore
27 | produceFile = { producePath().toPath() },
28 | )
29 | }
30 |
31 | internal expect fun createDataStore(): DataStore
32 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/preference/SingleDataStorePreferences.kt:
--------------------------------------------------------------------------------
1 | package data.preference
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.flow.map
9 | import kotlinx.serialization.KSerializer
10 | import kotlinx.serialization.SerializationException
11 | import kotlinx.serialization.json.Json
12 |
13 | interface SingleDataSource {
14 | suspend fun get(): T?
15 | suspend fun add(item: T)
16 | suspend fun clear()
17 | }
18 |
19 | abstract class SingleDataStorePreferences(
20 | private val dataStore: DataStore,
21 | private val serializer: KSerializer,
22 | ) : SingleDataSource {
23 | private val key = stringPreferencesKey(this::class.simpleName ?: "")
24 |
25 | override suspend fun get(): T? {
26 | return try {
27 | val json = dataStore.data.map { it[key] ?: "" }.first()
28 | val entries = Json.decodeFromString(serializer, json)
29 | entries
30 | } catch (e: SerializationException) {
31 | null
32 | }
33 | }
34 |
35 | override suspend fun add(item: T) {
36 | try {
37 | val json = Json.encodeToString(serializer, item)
38 | dataStore.edit {
39 | it[key] = json
40 | }
41 | } catch (e: SerializationException) {
42 | print("$e")
43 | }
44 | }
45 |
46 | override suspend fun clear() {
47 | dataStore.edit { it[key] = "" }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/repository/AuthRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import data.network.ApiService
4 | import data.network.requests.LoginRequest
5 | import data.network.responses.AuthResponse
6 | import domain.repository.AuthRepository
7 | import io.ktor.client.call.body
8 |
9 | class AuthRepositoryImpl(private val apiService: ApiService) : AuthRepository {
10 | override suspend fun authenticate(loginRequest: LoginRequest): AuthResponse {
11 | return apiService.authenticate(loginRequest).body()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/repository/FavoriteRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import data.db.ProductDataSource
4 | import domain.model.Product
5 | import domain.repository.FavoriteRepository
6 |
7 | class FavoriteRepositoryImpl(private val productDataSource: ProductDataSource) :
8 | FavoriteRepository {
9 | override suspend fun getFavorites(): List = productDataSource.fetchAllProducts()
10 |
11 | override suspend fun addToFavorite(product: Product) = productDataSource.insertProduct(product)
12 |
13 | override suspend fun removeFromFavorite(product: Product) =
14 | productDataSource.deleteProduct(product)
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/repository/ProductPagingSource.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import app.cash.paging.PagingSource
4 | import app.cash.paging.PagingSourceLoadParams
5 | import app.cash.paging.PagingSourceLoadResult
6 | import app.cash.paging.PagingSourceLoadResultError
7 | import app.cash.paging.PagingSourceLoadResultPage
8 | import app.cash.paging.PagingState
9 | import data.network.ApiService
10 | import data.network.responses.ProductsResponse
11 | import data.network.responses.toDomainModel
12 | import domain.model.Product
13 | import io.ktor.client.call.body
14 | import io.ktor.http.isSuccess
15 |
16 | class ProductPagingSource(private val apiService: ApiService) :
17 | PagingSource() {
18 |
19 | override suspend fun load(
20 | params: PagingSourceLoadParams
21 | ): PagingSourceLoadResult {
22 | val page = params.key ?: FIRST_PAGE_INDEX
23 | val httpResponse = apiService.getProducts(skip = page * 10)
24 | return when {
25 | httpResponse.status.isSuccess() -> {
26 | val productList = httpResponse.body().toDomainModel()
27 | PagingSourceLoadResultPage(
28 | data = productList.products,
29 | prevKey = (page - 1).takeIf { it >= FIRST_PAGE_INDEX },
30 | nextKey = if (productList.products.isNotEmpty()) page + 1 else null,
31 | )
32 | }
33 |
34 | else -> {
35 | PagingSourceLoadResultError(Exception("Received a ${httpResponse.status}."))
36 | }
37 | }
38 | }
39 |
40 | override fun getRefreshKey(state: PagingState): Int? = null
41 |
42 | companion object {
43 | const val FIRST_PAGE_INDEX = 0
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/data/repository/ProductRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import data.network.ApiService
4 | import data.network.responses.ProductsResponse
5 | import data.network.responses.toDomainModel
6 | import domain.repository.ProductRepository
7 | import io.ktor.client.call.body
8 |
9 | class ProductRepositoryImpl(private val apiService: ApiService) : ProductRepository {
10 | override suspend fun getProducts() =
11 | apiService.getProducts().body().toDomainModel()
12 | }
13 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/KoinCommon.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import di.modules.dataPersistenceModule
4 | import di.modules.dispatcherModule
5 | import di.modules.networkModule
6 | import di.modules.platformModule
7 | import di.modules.repositoryModule
8 | import di.modules.useCaseModule
9 | import di.modules.viewModelsModule
10 | import org.koin.core.KoinApplication
11 | import org.koin.core.context.startKoin
12 | import org.koin.core.module.Module
13 | import org.koin.dsl.module
14 |
15 | fun initKoin(appModule: Module = module { }): KoinApplication = startKoin {
16 | modules(
17 | appModule,
18 | dispatcherModule,
19 | networkModule,
20 | dataPersistenceModule,
21 | repositoryModule,
22 | useCaseModule,
23 | platformModule,
24 | viewModelsModule,
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/DataBaseModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import data.db.ProductDataSource
4 | import data.db.ProductDatabaseDataSource
5 | import data.preference.createDataStore
6 | import org.koin.core.qualifier.named
7 | import org.koin.dsl.module
8 |
9 | val dataPersistenceModule = module {
10 | single { ProductDatabaseDataSource(get(), get(named(Dispatcher.IO))) }
11 | single { createDataStore() }
12 | }
13 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/DispatcherModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.IO
5 | import org.koin.core.qualifier.named
6 | import org.koin.dsl.module
7 |
8 | val dispatcherModule = module {
9 | single(named(Dispatcher.IO)) { Dispatchers.IO }
10 | single(named(Dispatcher.MAIN)) { Dispatchers.Main }
11 | single(named(Dispatcher.DEFAULT)) { Dispatchers.Default }
12 | single(named(Dispatcher.UNCONFINED)) { Dispatchers.Unconfined }
13 | }
14 |
15 | enum class Dispatcher {
16 | IO,
17 | MAIN,
18 | DEFAULT,
19 | UNCONFINED,
20 | }
21 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import data.network.ApiService
4 | import data.network.ApiServiceImpl
5 | import data.network.NetworkConstants.BASE_URL
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
8 | import io.ktor.client.plugins.defaultRequest
9 | import io.ktor.client.plugins.logging.LogLevel
10 | import io.ktor.client.plugins.logging.Logger
11 | import io.ktor.client.plugins.logging.Logging
12 | import io.ktor.client.plugins.logging.SIMPLE
13 | import io.ktor.client.request.header
14 | import io.ktor.http.ContentType
15 | import io.ktor.http.HttpHeaders
16 | import io.ktor.http.URLProtocol
17 | import io.ktor.serialization.kotlinx.json.json
18 | import kotlinx.serialization.json.Json
19 | import org.koin.dsl.module
20 |
21 | val networkModule = module {
22 | single {
23 | HttpClient {
24 | defaultRequest {
25 | url {
26 | protocol = URLProtocol.HTTPS
27 | host = BASE_URL
28 | }
29 | header(HttpHeaders.ContentType, ContentType.Application.Json)
30 | }
31 | install(Logging) {
32 | logger = Logger.SIMPLE
33 | level = LogLevel.ALL
34 | }
35 | install(ContentNegotiation) {
36 | json(
37 | Json {
38 | isLenient = true
39 | ignoreUnknownKeys = true
40 | },
41 | )
42 | }
43 | }
44 | }
45 | single { ApiServiceImpl(get()) }
46 | }
47 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/PlatformModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect val platformModule: Module
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import data.repository.AuthRepositoryImpl
4 | import data.repository.FavoriteRepositoryImpl
5 | import data.repository.ProductPagingSource
6 | import data.repository.ProductRepositoryImpl
7 | import domain.repository.AuthRepository
8 | import domain.repository.FavoriteRepository
9 | import domain.repository.ProductRepository
10 | import org.koin.dsl.module
11 |
12 | val repositoryModule = module {
13 | factory { ProductPagingSource(get()) }
14 | factory { ProductRepositoryImpl(get()) }
15 | factory { AuthRepositoryImpl(get()) }
16 | factory { FavoriteRepositoryImpl(get()) }
17 | }
18 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import domain.usecase.GetUserFromPreferenceUseCase
4 | import domain.usecase.LoginUseCase
5 | import domain.usecase.favorite.AddToFavoriteUseCase
6 | import domain.usecase.favorite.GetFavoritesUseCase
7 | import domain.usecase.favorite.RemoveFromFavoriteUseCase
8 | import domain.usecase.product.GetPaginatedProductsUseCase
9 | import domain.usecase.product.GetProductsUseCase
10 | import org.koin.dsl.module
11 |
12 | val useCaseModule = module {
13 | factory { GetPaginatedProductsUseCase(get()) }
14 | factory { GetProductsUseCase(get()) }
15 | factory { GetFavoritesUseCase(get()) }
16 | factory { AddToFavoriteUseCase(get()) }
17 | factory { RemoveFromFavoriteUseCase(get()) }
18 | factory { LoginUseCase(get()) }
19 | factory { GetUserFromPreferenceUseCase(get()) }
20 | }
21 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/di/modules/ViewModelsModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import org.koin.dsl.module
4 | import presentation.feature.home.HomeViewModel
5 | import presentation.feature.login.LoginViewModel
6 | import presentation.feature.pagination.PaginatedHomeViewModel
7 | import presentation.feature.splash.SplashViewModel
8 |
9 | val viewModelsModule = module {
10 | factory { SplashViewModel(get()) }
11 | factory { LoginViewModel(get(), get()) }
12 | factory { HomeViewModel(get(), get(), get(), get()) }
13 | factory { PaginatedHomeViewModel(get()) }
14 | }
15 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/model/ProductList.kt:
--------------------------------------------------------------------------------
1 | package domain.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.ForeignKey
5 | import androidx.room.PrimaryKey
6 |
7 | data class ProductList(
8 | val limit: Int,
9 | val products: List,
10 | val skip: Int,
11 | val total: Int,
12 | )
13 |
14 | @Entity(tableName = "Product")
15 | data class Product(
16 | @PrimaryKey val id: Int,
17 | val brand: String,
18 | val category: String,
19 | val description: String,
20 | val discountPercentage: Double,
21 | val price: Int,
22 | val rating: Double,
23 | val stock: Int,
24 | val thumbnail: String,
25 | val title: String,
26 | )
27 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import data.network.requests.LoginRequest
4 | import data.network.responses.AuthResponse
5 |
6 | interface AuthRepository {
7 | suspend fun authenticate(loginRequest: LoginRequest): AuthResponse
8 | }
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/repository/FavoriteRepository.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import domain.model.Product
4 |
5 | interface FavoriteRepository {
6 | suspend fun getFavorites(): List
7 |
8 | suspend fun addToFavorite(product: Product)
9 |
10 | suspend fun removeFromFavorite(product: Product)
11 | }
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/repository/ProductRepository.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import domain.model.ProductList
4 |
5 | interface ProductRepository {
6 | suspend fun getProducts(): ProductList
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/GetUserFromPreferenceUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import data.preference.SingleDataStorePreferences
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | data class User(val name: String, val email: String)
10 | class GetUserFromPreferenceUseCase(dataStore: DataStore) :
11 | SingleDataStorePreferences(dataStore, User.serializer())
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import data.network.requests.LoginRequest
4 | import domain.repository.AuthRepository
5 |
6 | class LoginUseCase(
7 | private val authRepository: AuthRepository,
8 | ) {
9 |
10 | suspend operator fun invoke(loginRequest: LoginRequest) = runCatching {
11 | authRepository.authenticate(loginRequest)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/favorite/AddToFavoriteUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import domain.model.Product
4 | import domain.repository.FavoriteRepository
5 |
6 | class AddToFavoriteUseCase(private val repository: FavoriteRepository) {
7 |
8 | suspend operator fun invoke(product: Product) =
9 | runCatching { repository.addToFavorite(product) }
10 | }
11 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/favorite/GetFavoritesUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import domain.repository.FavoriteRepository
4 |
5 | class GetFavoritesUseCase(private val repository: FavoriteRepository) {
6 |
7 | suspend operator fun invoke() = runCatching { repository.getFavorites() }
8 | }
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/favorite/RemoveFromFavoriteUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import domain.model.Product
4 | import domain.repository.FavoriteRepository
5 |
6 | class RemoveFromFavoriteUseCase(private val repository: FavoriteRepository) {
7 |
8 | suspend operator fun invoke(product: Product) =
9 | runCatching { repository.removeFromFavorite(product) }
10 | }
11 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/product/GetPaginatedProductsUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.product
2 |
3 | import app.cash.paging.Pager
4 | import app.cash.paging.PagingConfig
5 | import data.repository.ProductPagingSource
6 | import domain.model.Product
7 |
8 | class GetPaginatedProductsUseCase(private val pagingSource: ProductPagingSource) {
9 | private val pager: Pager = run {
10 | val pagingConfig = PagingConfig(pageSize = 10, initialLoadSize = 10)
11 | check(pagingConfig.pageSize == pagingConfig.initialLoadSize) {
12 | "An elegant PagingSource implementation requires each page to be of equal size."
13 | }
14 | Pager(pagingConfig) { pagingSource }
15 | }
16 |
17 | operator fun invoke() = pager.flow
18 | }
19 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/domain/usecase/product/GetProductsUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.product
2 |
3 |
4 | import domain.repository.ProductRepository
5 |
6 | class GetProductsUseCase(private val repository: ProductRepository) {
7 |
8 | suspend operator fun invoke() = runCatching { repository.getProducts() }
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/error/ApiErrorException.kt:
--------------------------------------------------------------------------------
1 | package error
2 |
3 | import io.ktor.utils.io.errors.IOException
4 |
5 | class ApiErrorException(
6 | val code: Int,
7 | val displayableMessage: String,
8 | val loggableMessage: String,
9 | ) : IOException(displayableMessage)
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/error/ApiErrorValidator.kt:
--------------------------------------------------------------------------------
1 | package error
2 |
3 | import io.ktor.client.statement.HttpResponse
4 |
5 | interface ApiErrorValidator {
6 | fun validateResponse(response: HttpResponse)
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Greeting.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | class Greeting {
4 | private val platform = getPlatform()
5 |
6 | fun greet(): String {
7 | return "Hello, ${platform.name}!"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/platform/Platform.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | interface Platform {
4 | val name: String
5 | }
6 |
7 | expect fun getPlatform(): Platform
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/App.kt:
--------------------------------------------------------------------------------
1 | package presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import cafe.adriel.voyager.navigator.Navigator
5 | import cafe.adriel.voyager.transitions.SlideTransition
6 | import presentation.feature.splash.SplashScreenRoute
7 | import presentation.theme.AppTheme
8 |
9 | @Composable
10 | fun App() {
11 | AppTheme {
12 | Navigator(SplashScreenRoute) { SlideTransition(it) }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/appbutton/AppButton.kt:
--------------------------------------------------------------------------------
1 | package presentation.components.appbutton
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.foundation.BorderStroke
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.RowScope
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material.Button
9 | import androidx.compose.material.ButtonDefaults
10 | import androidx.compose.material.CircularProgressIndicator
11 | import androidx.compose.material.OutlinedButton
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.TextButton
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.unit.dp
18 | import presentation.theme.Theme
19 |
20 | @Composable
21 | fun AppButton(
22 | text: String,
23 | modifier: Modifier = Modifier,
24 | enabled: Boolean = true,
25 | isLoading: Boolean = false,
26 | type: AppButtonType = AppButtonType.Filled,
27 | onClick: () -> Unit,
28 | ) {
29 | when (type) {
30 | AppButtonType.Filled -> PrimaryButton(
31 | modifier = modifier,
32 | onClick = onClick,
33 | enabled = enabled && !isLoading,
34 | content = { ButtonContent(text = text, isLoading = isLoading) },
35 | )
36 |
37 | AppButtonType.Outlined -> OutlinedButton(
38 | onClick = onClick,
39 | modifier = modifier,
40 | enabled = enabled && !isLoading,
41 | content = { ButtonContent(text = text, isLoading = isLoading) },
42 | colors = ButtonDefaults.outlinedButtonColors(
43 | contentColor = Theme.colors.secondary,
44 | backgroundColor = Color.Transparent,
45 | ),
46 | border = BorderStroke(1.dp, Theme.colors.secondary),
47 | )
48 |
49 | AppButtonType.Text -> {
50 | TextButton(
51 | onClick = onClick,
52 | modifier = modifier,
53 | enabled = enabled && !isLoading,
54 | content = {
55 | ButtonContent(text = text, isLoading = isLoading)
56 | },
57 | )
58 | }
59 | }
60 | }
61 |
62 | @Composable
63 | private fun PrimaryButton(
64 | modifier: Modifier = Modifier,
65 | onClick: () -> Unit = {},
66 | enabled: Boolean = false,
67 | content: @Composable RowScope.() -> Unit,
68 | ) {
69 | val colors = ButtonDefaults.buttonColors()
70 | Button(
71 | onClick = onClick,
72 | modifier = modifier,
73 | enabled = enabled,
74 | content = content,
75 | colors = colors,
76 | contentPadding = PaddingValues(vertical = Theme.dimensions.medium2),
77 | )
78 | }
79 |
80 | @Composable
81 | private fun ButtonContent(text: String, isLoading: Boolean) {
82 | Crossfade(targetState = isLoading) {
83 | if (it) {
84 | CircularProgressIndicator(
85 | color = Theme.colors.primary,
86 | modifier = Modifier.size(24.dp),
87 | strokeWidth = 1.dp,
88 | )
89 | } else {
90 | Text(
91 | text = text,
92 | style = Theme.typography.button,
93 | )
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/appbutton/AppButtonType.kt:
--------------------------------------------------------------------------------
1 | package presentation.components.appbutton
2 |
3 | enum class AppButtonType {
4 | Filled,
5 | Outlined,
6 | Text,
7 | }
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/components/apptextfield/AppTextField.kt:
--------------------------------------------------------------------------------
1 | package presentation.components.apptextfield
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.text.KeyboardActions
8 | import androidx.compose.foundation.text.KeyboardOptions
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.OutlinedTextField
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.TextFieldColors
13 | import androidx.compose.material.TextFieldDefaults
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Shape
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.input.VisualTransformation
20 | import presentation.theme.Theme
21 |
22 | @Composable
23 | fun AppTextField(
24 | value: String,
25 | onValueChange: (String) -> Unit,
26 | modifier: Modifier = Modifier,
27 | enabled: Boolean = true,
28 | readOnly: Boolean = false,
29 | textStyle: TextStyle = TextStyle.Default,
30 | label: @Composable (() -> Unit)? = null,
31 | placeholder: @Composable (() -> Unit)? = null,
32 | leadingIcon: @Composable (() -> Unit)? = null,
33 | trailingIcon: @Composable (() -> Unit)? = null,
34 | errorMessage: String? = null,
35 | visualTransformation: VisualTransformation = VisualTransformation.None,
36 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
37 | keyboardActions: KeyboardActions = KeyboardActions.Default,
38 | singleLine: Boolean = false,
39 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
40 | minLines: Int = 1,
41 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
42 | shape: Shape = MaterialTheme.shapes.small,
43 | colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
44 | ) {
45 | Column {
46 | OutlinedTextField(
47 | value = value,
48 | onValueChange = onValueChange,
49 | modifier = modifier.fillMaxWidth(),
50 | enabled = enabled,
51 | readOnly = readOnly,
52 | textStyle = textStyle,
53 | label = label,
54 | placeholder = placeholder,
55 | leadingIcon = leadingIcon,
56 | trailingIcon = trailingIcon,
57 | isError = errorMessage != null,
58 | visualTransformation = visualTransformation,
59 | keyboardOptions = keyboardOptions,
60 | keyboardActions = keyboardActions,
61 | singleLine = singleLine,
62 | maxLines = maxLines,
63 | minLines = minLines,
64 | interactionSource = interactionSource,
65 | shape = shape,
66 | colors = colors,
67 | )
68 |
69 | AnimatedVisibility(visible = errorMessage != null) {
70 | Text(
71 | text = errorMessage ?: "",
72 | color = Theme.colors.error,
73 | style = MaterialTheme.typography.caption,
74 | )
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/HomeContract.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import domain.model.Product
4 |
5 | data class HomeScreenState(
6 | val productList: List = emptyList(),
7 | val favoriteList: List = emptyList(),
8 | val isLoading: Boolean = false,
9 | ) {
10 | // Needed for SwiftUI
11 | constructor() : this(
12 | productList = emptyList(),
13 | favoriteList = emptyList(),
14 | isLoading = false
15 | )
16 | }
17 |
18 | sealed class HomeScreenIntent {
19 | data object OnLaunch : HomeScreenIntent()
20 |
21 | data class OnFavoriteClick(val product: Product) : HomeScreenIntent()
22 | }
23 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material.CircularProgressIndicator
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Scaffold
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.TopAppBar
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.text.style.TextAlign
19 | import presentation.theme.Theme
20 |
21 | @Composable
22 | fun HomeScreen(state: HomeScreenState, action: (HomeScreenIntent) -> Unit) {
23 | LaunchedEffect(Unit) {
24 | action(HomeScreenIntent.OnLaunch)
25 | }
26 |
27 | Scaffold(
28 | topBar = {
29 | TopAppBar(
30 | title = {
31 | Text(
32 | text = "Home",
33 | fontWeight = FontWeight.Black,
34 | style = MaterialTheme.typography.h5,
35 | textAlign = TextAlign.Center,
36 | modifier = Modifier.fillMaxWidth(),
37 | )
38 | },
39 | backgroundColor = Theme.colors.primary,
40 | contentColor = Theme.colors.onPrimary,
41 | )
42 | },
43 | ) {
44 | Box(
45 | modifier = Modifier.fillMaxSize(),
46 | contentAlignment = Alignment.Center,
47 | ) {
48 | LazyColumn {
49 | items(state.productList) { product ->
50 | ProductItemView(
51 | product = product,
52 | isFavorite = state.favoriteList.contains(product),
53 | onFavoriteClick = {
54 | action(HomeScreenIntent.OnFavoriteClick(product))
55 | },
56 | onClick = {
57 | // Handle OnClick
58 | },
59 | )
60 | }
61 | }
62 |
63 | if (state.isLoading) CircularProgressIndicator()
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/HomeScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import androidx.compose.material.Surface
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import cafe.adriel.voyager.core.screen.Screen
8 | import org.koin.compose.koinInject
9 |
10 | object HomeScreenRoute : Screen {
11 |
12 | @Composable
13 | override fun Content() {
14 | val viewModel = koinInject()
15 | val state by viewModel.state.collectAsState()
16 |
17 | Surface {
18 | HomeScreen(state, viewModel::handleIntent)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import domain.model.Product
6 | import domain.usecase.favorite.AddToFavoriteUseCase
7 | import domain.usecase.favorite.GetFavoritesUseCase
8 | import domain.usecase.favorite.RemoveFromFavoriteUseCase
9 | import domain.usecase.product.GetProductsUseCase
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 |
15 | class HomeViewModel(
16 | private val getProductsUseCase: GetProductsUseCase,
17 | private val getFavoritesUseCase: GetFavoritesUseCase,
18 | private val addToFavoriteUseCase: AddToFavoriteUseCase,
19 | private val removeFromFavoriteUseCase: RemoveFromFavoriteUseCase,
20 | ) : ViewModel() {
21 | private val _state = MutableStateFlow(HomeScreenState())
22 | val state = _state.asStateFlow()
23 |
24 | fun handleIntent(intent: HomeScreenIntent) {
25 | when (intent) {
26 | is HomeScreenIntent.OnLaunch -> {
27 | viewModelScope.launch {
28 | getFavorites()
29 | getProducts()
30 | }
31 | }
32 |
33 | is HomeScreenIntent.OnFavoriteClick -> {
34 | val favoriteList = state.value.favoriteList
35 |
36 | if (favoriteList.contains(intent.product)) {
37 | removeFromFavorite(intent.product)
38 | } else {
39 | addToFavorite(intent.product)
40 | }
41 | }
42 | }
43 | }
44 |
45 | private suspend fun getFavorites() {
46 | _state.update { it.copy(isLoading = true) }
47 | getFavoritesUseCase()
48 | .onSuccess { response ->
49 | _state.update { it.copy(favoriteList = response) }
50 | }
51 | .onFailure {
52 | // handle error
53 | }
54 | _state.update { it.copy(isLoading = false) }
55 | }
56 |
57 | private suspend fun getProducts() {
58 | _state.update { it.copy(isLoading = true) }
59 | getProductsUseCase()
60 | .onSuccess { response ->
61 | _state.update { it.copy(productList = response.products) }
62 | }
63 | .onFailure {
64 | // handle error
65 | }
66 | _state.update { it.copy(isLoading = false) }
67 | }
68 |
69 | private fun addToFavorite(product: Product) {
70 | viewModelScope.launch {
71 | addToFavoriteUseCase(product)
72 | .onSuccess { getFavorites() }
73 | .onFailure {
74 | // handle error
75 | }
76 | }
77 | }
78 |
79 | private fun removeFromFavorite(product: Product) {
80 | viewModelScope.launch {
81 | removeFromFavoriteUseCase(product)
82 | .onSuccess { getFavorites() }
83 | .onFailure {
84 | // handle error
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/HomeViewModelProvider.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import org.koin.core.component.KoinComponent
4 | import org.koin.core.component.inject
5 |
6 | class HomeViewModelProvider : KoinComponent {
7 | private val viewModel by inject()
8 | fun provide() = viewModel
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/home/ProductItemView.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.home
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
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.foundation.layout.wrapContentHeight
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material.Card
12 | import androidx.compose.material.Icon
13 | import androidx.compose.material.IconButton
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Text
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.Star
18 | import androidx.compose.material.icons.outlined.StarOutline
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.Brush
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.layout.ContentScale
25 | import androidx.compose.ui.text.style.TextAlign
26 | import androidx.compose.ui.unit.dp
27 | import coil3.compose.AsyncImage
28 | import domain.model.Product
29 |
30 | @Composable
31 | fun ProductItemView(
32 | product: Product,
33 | isFavorite: Boolean,
34 | onFavoriteClick: () -> Unit,
35 | onClick: () -> Unit,
36 | ) {
37 | Card(
38 | modifier = Modifier
39 | .padding(5.dp)
40 | .fillMaxWidth()
41 | .wrapContentHeight()
42 | .clickable { onClick() },
43 | shape = RoundedCornerShape(8.dp),
44 | elevation = 8.dp,
45 | backgroundColor = MaterialTheme.colors.surface,
46 | ) {
47 | AsyncImage(
48 | model = product.thumbnail,
49 | contentDescription = product.title,
50 | modifier = Modifier.size(220.dp),
51 | contentScale = ContentScale.Crop,
52 | )
53 |
54 | Box(
55 | modifier = Modifier
56 | .size(220.dp)
57 | .background(
58 | brush = Brush.verticalGradient(
59 | colors = listOf(
60 | Color.Transparent,
61 | Color.Transparent,
62 | Color(0xFF1F2023),
63 | ),
64 | ),
65 | ),
66 | ) {
67 | IconButton(
68 | modifier = Modifier.align(Alignment.TopEnd),
69 | onClick = { onFavoriteClick() },
70 | ) {
71 | if (isFavorite) {
72 | Icon(
73 | Icons.Filled.Star,
74 | contentDescription = "Favorite",
75 | tint = Color(0xFFFFD81A),
76 | modifier = Modifier
77 | .size(44.dp)
78 | .padding(8.dp),
79 | )
80 | } else {
81 | Icon(
82 | Icons.Outlined.StarOutline,
83 | contentDescription = "Not Favorite",
84 | tint = Color(0xFFFFD81A),
85 | modifier = Modifier
86 | .size(44.dp)
87 | .padding(8.dp),
88 | )
89 | }
90 | }
91 |
92 | Text(
93 | text = product.title,
94 | textAlign = TextAlign.Center,
95 | color = Color.White,
96 | style = MaterialTheme.typography.body1,
97 | modifier = Modifier
98 | .align(Alignment.BottomCenter)
99 | .padding(8.dp),
100 | )
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/login/LoginContract.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.login
2 |
3 | /**
4 | * UI State that represents LoginScreen
5 | **/
6 | data class LoginScreenState(
7 | var email: String = "kminchelle",
8 | var password: String = "0lelplR",
9 | val isLoading: Boolean = false,
10 | val error: String? = null,
11 | val loginButtonEnabled: Boolean = false,
12 | val isLoggedIn: Boolean = false,
13 | ) {
14 | // Needed for SwiftUI
15 | constructor() : this(
16 | email = "kminchelle",
17 | password = "0lelplR",
18 | isLoading = false,
19 | error = null,
20 | loginButtonEnabled = false,
21 | isLoggedIn = false
22 | )
23 | }
24 |
25 | /**
26 | * Login Actions emitted from the UI Layer
27 | * passed to the viewmodel to handle
28 | **/
29 | sealed class LoginIntent {
30 | data class OnPasswordChange(val pass: String) : LoginIntent()
31 | data class OnEmailChange(val email: String) : LoginIntent()
32 | data object Login : LoginIntent()
33 | }
34 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/login/LoginScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.login
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.imePadding
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Scaffold
13 | import androidx.compose.material.Text
14 | import androidx.compose.material.TopAppBar
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.text.input.PasswordVisualTransformation
20 | import androidx.compose.ui.text.style.TextAlign
21 | import presentation.components.appbutton.AppButton
22 | import presentation.components.apptextfield.AppTextField
23 | import presentation.theme.Theme
24 |
25 | @Composable
26 | fun LoginScreen(
27 | state: LoginScreenState,
28 | action: (LoginIntent) -> Unit,
29 | navigate: () -> Unit
30 | ) {
31 | Scaffold(
32 | topBar = {
33 | TopAppBar(
34 | title = {
35 | Text(
36 | text = "Login",
37 | fontWeight = FontWeight.Black,
38 | style = MaterialTheme.typography.h5,
39 | textAlign = TextAlign.Center,
40 | modifier = Modifier.fillMaxWidth(),
41 | )
42 | },
43 | backgroundColor = Theme.colors.primary,
44 | contentColor = Theme.colors.onPrimary,
45 | )
46 | },
47 | ) {
48 | Column(
49 | modifier = Modifier
50 | .padding(it)
51 | .fillMaxSize()
52 | .padding(Theme.dimensions.big1)
53 | .imePadding(),
54 | horizontalAlignment = Alignment.CenterHorizontally,
55 | verticalArrangement = Arrangement.Center,
56 | ) {
57 | AppTextField(
58 | value = state.email,
59 | onValueChange = { text ->
60 | action(LoginIntent.OnEmailChange(email = text))
61 | },
62 | modifier = Modifier.fillMaxWidth(),
63 | placeholder = { Text(text = "Username") },
64 | )
65 |
66 | Spacer(modifier = Modifier.size(Theme.dimensions.medium3))
67 |
68 | AppTextField(
69 | value = state.password,
70 | onValueChange = { text ->
71 | action(LoginIntent.OnPasswordChange(text))
72 | },
73 | modifier = Modifier.fillMaxWidth(),
74 | visualTransformation = PasswordVisualTransformation(),
75 | placeholder = { Text(text = "Password") },
76 | )
77 |
78 | Spacer(modifier = Modifier.size(Theme.dimensions.medium3))
79 |
80 | AppButton(
81 | text = "Login",
82 | onClick = {
83 | action(LoginIntent.Login)
84 | },
85 | modifier = Modifier.fillMaxWidth(),
86 | isLoading = state.isLoading,
87 | )
88 |
89 | if (state.isLoggedIn) navigate()
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/login/LoginScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.login
2 |
3 | import androidx.compose.material.Surface
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import cafe.adriel.voyager.core.screen.Screen
8 | import cafe.adriel.voyager.navigator.LocalNavigator
9 | import cafe.adriel.voyager.navigator.currentOrThrow
10 | import org.koin.compose.koinInject
11 | import presentation.feature.home.HomeScreenRoute
12 |
13 | object LoginScreenRoute : Screen {
14 | @Composable
15 | override fun Content() {
16 | val viewModel = koinInject()
17 | val navigator = LocalNavigator.currentOrThrow
18 | val state by viewModel.state.collectAsState()
19 |
20 | Surface {
21 | LoginScreen(state, viewModel::handleIntent) {
22 | navigator.replaceAll(HomeScreenRoute)
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/login/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.login
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import data.network.requests.LoginRequest
6 | import domain.usecase.GetUserFromPreferenceUseCase
7 | import domain.usecase.LoginUseCase
8 | import domain.usecase.User
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.flow.update
12 | import kotlinx.coroutines.launch
13 | import presentation.model.toError
14 |
15 | class LoginViewModel(
16 | private val loginUseCase: LoginUseCase,
17 | private val userUseCase: GetUserFromPreferenceUseCase,
18 | ) : ViewModel() {
19 | private val _state = MutableStateFlow(LoginScreenState())
20 | val state = _state.asStateFlow()
21 |
22 | fun handleIntent(intent: LoginIntent) {
23 | when (intent) {
24 | is LoginIntent.Login -> login()
25 | is LoginIntent.OnEmailChange -> onEmailChange(intent.email)
26 | is LoginIntent.OnPasswordChange -> onPasswordChange(intent.pass)
27 | }
28 | }
29 |
30 | private fun onEmailChange(value: String) = _state.update { it.copy(email = value) }
31 |
32 | private fun onPasswordChange(value: String) = _state.update { it.copy(password = value) }
33 |
34 | private fun login() {
35 | viewModelScope.launch {
36 | _state.update { it.copy(isLoading = true) }
37 | val state = _state.value
38 | val result = loginUseCase(LoginRequest(state.email, state.password))
39 |
40 | result.onSuccess {
41 | userUseCase.add(User(state.email.uppercase(), state.email))
42 | }
43 |
44 | _state.update {
45 | it.copy(
46 | error = result.exceptionOrNull()?.toError()?.message,
47 | isLoading = false,
48 | isLoggedIn = result.isSuccess,
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/login/LoginViewModelProvider.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.login
2 |
3 | import org.koin.core.component.KoinComponent
4 | import org.koin.core.component.inject
5 |
6 | class LoginViewModelProvider : KoinComponent {
7 | private val viewModel by inject()
8 | fun provide() = viewModel
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/pagination/PaginatedHomeScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.pagination
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.material.CircularProgressIndicator
8 | import androidx.compose.material.MaterialTheme
9 | import androidx.compose.material.Scaffold
10 | import androidx.compose.material.Text
11 | import androidx.compose.material.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.text.style.TextAlign
18 | import app.cash.paging.LoadStateError
19 | import app.cash.paging.LoadStateLoading
20 | import app.cash.paging.LoadStateNotLoading
21 | import app.cash.paging.compose.collectAsLazyPagingItems
22 | import cafe.adriel.voyager.core.screen.Screen
23 | import org.koin.compose.koinInject
24 | import presentation.feature.home.ProductItemView
25 | import presentation.theme.Theme
26 |
27 | object PaginatedHomeScreen : Screen {
28 |
29 | @Composable
30 | override fun Content() {
31 | val viewModel = koinInject()
32 | val productList = viewModel.productList.collectAsLazyPagingItems()
33 |
34 | Scaffold(
35 | topBar = {
36 | TopAppBar(
37 | title = {
38 | Text(
39 | text = "Products",
40 | fontWeight = FontWeight.Black,
41 | style = MaterialTheme.typography.h5,
42 | textAlign = TextAlign.Center,
43 | modifier = Modifier.fillMaxWidth(),
44 | )
45 | },
46 | backgroundColor = Theme.colors.primary,
47 | contentColor = Theme.colors.onPrimary,
48 | )
49 | },
50 | ) {
51 | Box(
52 | modifier = Modifier.fillMaxSize(),
53 | contentAlignment = Alignment.Center,
54 | ) {
55 | LazyColumn {
56 | when (val loadState = productList.loadState.refresh) {
57 | is LoadStateLoading -> {
58 | item { CircularProgressIndicator() }
59 | }
60 |
61 | is LoadStateNotLoading -> {
62 | items(productList.itemCount) { index ->
63 | val product = productList[index]
64 | product?.let {
65 | ProductItemView(it, false, {}, {})
66 | }
67 | }
68 | }
69 |
70 | is LoadStateError -> {
71 | item {
72 | Text(loadState.error.message!!)
73 | }
74 | }
75 |
76 | else -> error("when should be exhaustive")
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/pagination/PaginatedHomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.pagination
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.paging.cachedIn
6 | import domain.usecase.product.GetPaginatedProductsUseCase
7 |
8 | class PaginatedHomeViewModel(
9 | getPaginatedProductsUseCase: GetPaginatedProductsUseCase,
10 | ) : ViewModel() {
11 | val productList = getPaginatedProductsUseCase().cachedIn(viewModelScope)
12 | }
13 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/splash/SplashContract.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.splash
2 |
3 | import domain.usecase.User
4 | import platform.Greeting
5 |
6 | data class SplashScreenState(
7 | val user: User? = null,
8 | val showContent: Boolean = false,
9 | val greeting: String = Greeting().greet(),
10 | ) {
11 | // Needed for SwiftUI
12 | constructor() : this(
13 | user = null,
14 | showContent = false,
15 | greeting = Greeting().greet()
16 | )
17 | }
18 |
19 | sealed class SplashScreenIntent {
20 | data class OnContentVisibilityChange(val show: Boolean) : SplashScreenIntent()
21 | }
22 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/splash/SplashScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.splash
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import kmp_template.composeapp.generated.resources.Res
15 | import kmp_template.composeapp.generated.resources.compose_multiplatform
16 | import kotlinx.coroutines.delay
17 | import org.jetbrains.compose.resources.ExperimentalResourceApi
18 | import org.jetbrains.compose.resources.painterResource
19 |
20 | @OptIn(ExperimentalResourceApi::class)
21 | @Composable
22 | fun SplashScreen(
23 | state: SplashScreenState,
24 | action: (SplashScreenIntent) -> Unit,
25 | navigate: () -> Unit
26 | ) {
27 | LaunchedEffect(Unit) {
28 | delay(500)
29 | action(SplashScreenIntent.OnContentVisibilityChange(show = true))
30 | delay(1000)
31 | action(SplashScreenIntent.OnContentVisibilityChange(show = false))
32 | delay(500)
33 | navigate()
34 | }
35 |
36 | Column(
37 | Modifier.fillMaxSize(),
38 | horizontalAlignment = Alignment.CenterHorizontally,
39 | verticalArrangement = Arrangement.Center,
40 | ) {
41 | AnimatedVisibility(state.showContent) {
42 | Column(
43 | Modifier.fillMaxWidth(),
44 | horizontalAlignment = Alignment.CenterHorizontally,
45 | ) {
46 | Image(painterResource(Res.drawable.compose_multiplatform), null)
47 | Text("Compose: ${state.greeting}")
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/splash/SplashScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.splash
2 |
3 | import androidx.compose.material.Surface
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import cafe.adriel.voyager.core.screen.Screen
8 | import cafe.adriel.voyager.navigator.LocalNavigator
9 | import cafe.adriel.voyager.navigator.currentOrThrow
10 | import org.koin.compose.koinInject
11 | import presentation.feature.home.HomeScreenRoute
12 | import presentation.feature.login.LoginScreenRoute
13 |
14 | object SplashScreenRoute : Screen {
15 | @Composable
16 | override fun Content() {
17 | val viewModel = koinInject()
18 | val navigator = LocalNavigator.currentOrThrow
19 | val state by viewModel.state.collectAsState()
20 |
21 | Surface {
22 | SplashScreen(state, viewModel::handleIntent) {
23 | state.user?.let {
24 | navigator.replaceAll(HomeScreenRoute)
25 | } ?: navigator.replaceAll(LoginScreenRoute)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.splash
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import domain.usecase.GetUserFromPreferenceUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.update
9 | import kotlinx.coroutines.launch
10 |
11 | class SplashViewModel(
12 | private val userUseCase: GetUserFromPreferenceUseCase
13 | ) : ViewModel() {
14 | private val _state = MutableStateFlow(SplashScreenState())
15 | val state = _state.asStateFlow()
16 |
17 | fun handleIntent(intent: SplashScreenIntent) {
18 | when (intent) {
19 | is SplashScreenIntent.OnContentVisibilityChange -> _state.update { it.copy(showContent = intent.show) }
20 | }
21 | }
22 |
23 | init {
24 | getUser()
25 | }
26 |
27 | private fun getUser() = viewModelScope.launch {
28 | _state.update { it.copy(user = userUseCase.get()) }
29 | println("USER | ${state.value.user}")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/feature/splash/SplashViewModelProvider.kt:
--------------------------------------------------------------------------------
1 | package presentation.feature.splash
2 |
3 | import org.koin.core.component.KoinComponent
4 | import org.koin.core.component.inject
5 |
6 | class SplashViewModelProvider : KoinComponent {
7 | private val viewModel by inject()
8 | fun provide() = viewModel
9 | }
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/model/ErrorModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.model
2 |
3 | import error.ApiErrorException
4 | import io.ktor.client.network.sockets.SocketTimeoutException
5 |
6 | sealed class ErrorModel(open val message: String) {
7 | data class ApiError(val exception: ApiErrorException, override val message: String) :
8 | ErrorModel(message)
9 |
10 | sealed class Connection(override val message: String) : ErrorModel(message) {
11 | data object Timeout : Connection("Connection timed out")
12 | }
13 |
14 | data class Unknown(val throwable: Throwable, override val message: String) : ErrorModel(message)
15 | }
16 |
17 | fun Throwable.toError(): ErrorModel {
18 | return when (this) {
19 | is ApiErrorException -> {
20 | ErrorModel.ApiError(this, "API error: ${this.message}")
21 | }
22 |
23 | is SocketTimeoutException -> ErrorModel.Connection.Timeout
24 | else -> ErrorModel.Unknown(this, "Unknown error: ${this.message}")
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/AppTheme.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.Colors
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.Shapes
7 | import androidx.compose.material.darkColors
8 | import androidx.compose.material.lightColors
9 | import androidx.compose.runtime.Composable
10 | import presentation.theme.dimensions.Dimensions
11 | import presentation.theme.dimensions.LocalDimensions
12 | import presentation.theme.dimensions.ProvideDimensions
13 | import presentation.theme.typography.AppTypography
14 | import presentation.theme.typography.LocalAppTypography
15 | import presentation.theme.typography.ProvideAppTypography
16 | import presentation.theme.typography.Typography
17 |
18 | /**
19 | * Main theme provider
20 | * Use [Theme.*] to access colors, typography and etc
21 | */
22 | @Composable
23 | fun AppTheme(isDarkMode: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
24 | val dimensions = Dimensions.Default
25 | ProvideDimensions(dimensions = dimensions) {
26 | ProvideAppTypography {
27 | MaterialTheme(
28 | typography = Typography,
29 | shapes = Shapes,
30 | content = content,
31 | colors = if (isDarkMode) DarkColors else LightColors,
32 | )
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Shortcut to obtain App Theme values instead of using [MaterialTheme]
39 | * - Provides custom AppTypography instead of Material one
40 | * - Provides access to dimensions, that can vary based on device size
41 | */
42 | object Theme {
43 | val typography: AppTypography @Composable get() = LocalAppTypography.current
44 | val colors: Colors @Composable get() = MaterialTheme.colors
45 | val shapes: Shapes @Composable get() = MaterialTheme.shapes
46 | val dimensions: Dimensions @Composable get() = LocalDimensions.current
47 | }
48 |
49 | private val LightColors = lightColors(
50 | primary = MonstarlabYellow,
51 | onPrimary = Black,
52 | background = LightGrey,
53 | onBackground = DarkGrey,
54 | onSurface = Black,
55 | surface = White,
56 | error = Red,
57 | )
58 |
59 | private val DarkColors = darkColors(
60 | primary = MonstarlabYellow,
61 | onPrimary = Black,
62 | background = DarkGrey,
63 | onBackground = White,
64 | onSurface = White,
65 | surface = Black,
66 | error = Red,
67 | )
68 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/Colors.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val MonstarlabYellow = Color(0xFFF6FF00)
6 | val DarkGrey = Color(0xFF363636)
7 | val Black = Color.Black
8 | val White = Color.White
9 | val Red = Color(0xFFFF2600)
10 | val LightGrey = Color(0xFFE4E4E4)
11 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/Shapes.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(8.dp),
10 | large = RoundedCornerShape(32.dp),
11 | )
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/dimensions/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.dimensions
2 |
3 | import androidx.compose.ui.unit.Dp
4 | import androidx.compose.ui.unit.dp
5 |
6 | /**
7 | * Holder for the app dimensions that can be used to avoid hardcoded margins and paddings
8 | * Provides Default variant and [Small] that can be used for smaller devices
9 | */
10 | class Dimensions(
11 | val medium1: Dp,
12 | val medium2: Dp,
13 | val medium3: Dp,
14 | val big1: Dp,
15 | val big2: Dp,
16 | ) {
17 | companion object {
18 | val Small = Dimensions(
19 | medium1 = 12.dp,
20 | medium2 = 9.dp,
21 | medium3 = 6.dp,
22 | big1 = 18.dp,
23 | big2 = 16.dp,
24 | )
25 |
26 | val Default = Dimensions(
27 | medium1 = 16.dp,
28 | medium2 = 12.dp,
29 | medium3 = 8.dp,
30 | big1 = 24.dp,
31 | big2 = 32.dp,
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/dimensions/DimensionsProvider.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.dimensions
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 |
7 | val LocalDimensions = staticCompositionLocalOf {
8 | Dimensions.Default
9 | }
10 |
11 | @Composable
12 | fun ProvideDimensions(dimensions: Dimensions, content: @Composable () -> Unit) {
13 | CompositionLocalProvider(LocalDimensions provides dimensions) {
14 | content.invoke()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/typography/AppTypography.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.typography
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 |
6 | /**
7 | * Custom holder for the Text styles instead of the one used in Material Design
8 | * Here You can put the styles the app needs and the one specified in you projects Figma
9 | *
10 | * secondary constructor uses Material Theme typography from [Typography]
11 | * to maintain some corelation with Material Specification
12 | */
13 | data class AppTypography(
14 | val headline1: TextStyle,
15 | val headline2: TextStyle,
16 | val body1: TextStyle,
17 | val body2: TextStyle,
18 | val button: TextStyle,
19 | ) {
20 |
21 | constructor(typography: Typography) : this(
22 | headline1 = typography.h1,
23 | headline2 = typography.h2,
24 | body1 = typography.body1,
25 | body2 = typography.body2,
26 | button = typography.button,
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/typography/AppTypographyProvider.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.typography
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 |
7 | val LocalAppTypography = staticCompositionLocalOf {
8 | AppTypography(Typography)
9 | }
10 |
11 | @Composable
12 | fun ProvideAppTypography(
13 | typography: AppTypography = AppTypography(Typography),
14 | content: @Composable () -> Unit = {},
15 | ) {
16 | CompositionLocalProvider(LocalAppTypography provides typography) {
17 | content.invoke()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/typography/FontFamily.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.typography
2 |
3 | import androidx.compose.ui.text.font.FontFamily
4 |
5 | val FontFamily = FontFamily.Default
6 |
7 | /**
8 | val CustomFontFamily = Font(
9 | resId = R.font.my_custom_font,
10 | weight = FontWeight.Bold,
11 | style = FontStyle.Normal
12 | )
13 | */
14 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/presentation/theme/typography/Typography.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme.typography
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontWeight
6 | import androidx.compose.ui.unit.sp
7 |
8 | val Typography = Typography(
9 | defaultFontFamily = FontFamily,
10 | h1 = TextStyle(
11 | fontWeight = FontWeight.Bold,
12 | fontSize = 34.sp,
13 | ),
14 |
15 | h2 = TextStyle(
16 | fontWeight = FontWeight.Bold,
17 | fontSize = 28.sp,
18 | lineHeight = 32.sp,
19 | ),
20 |
21 | h3 = TextStyle(
22 | fontWeight = FontWeight.Bold,
23 | fontSize = 24.sp,
24 | ),
25 |
26 | h5 = TextStyle(
27 | fontWeight = FontWeight.Bold,
28 | fontSize = 20.sp,
29 | ),
30 |
31 | h6 = TextStyle(
32 | fontWeight = FontWeight.Bold,
33 | fontSize = 17.sp,
34 | ),
35 |
36 | body1 = TextStyle(
37 | fontWeight = FontWeight.Normal,
38 | fontSize = 16.sp,
39 | lineHeight = 21.sp,
40 | ),
41 |
42 | body2 = TextStyle(
43 | fontWeight = FontWeight.Normal,
44 | fontSize = 14.sp,
45 | ),
46 |
47 | button = TextStyle(
48 | fontWeight = FontWeight.Normal,
49 | fontSize = 16.sp,
50 | ),
51 | caption = TextStyle(
52 | fontWeight = FontWeight.Normal,
53 | fontSize = 12.sp,
54 | ),
55 | overline = TextStyle(
56 | fontWeight = FontWeight.Normal,
57 | fontSize = 8.sp,
58 | ),
59 | )
60 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/sqldelight/com/monstarlab/kmp/Product.sq:
--------------------------------------------------------------------------------
1 | import kotlin.String;
2 | import kotlin.collections.List;
3 |
4 | CREATE TABLE ProductDB (
5 | id INTEGER NOT NULL PRIMARY KEY,
6 | brand TEXT NOT NULL,
7 | category TEXT NOT NULL,
8 | description TEXT NOT NULL,
9 | discountPercentage REAL NOT NULL,
10 | rating REAL NOT NULL,
11 | price INTEGER NOT NULL,
12 | stock INTEGER NOT NULL,
13 | thumbnail TEXT NOT NULL,
14 | title TEXT NOT NULL,
15 | images TEXT AS List NOT NULL
16 | );
17 |
18 | selectAll:
19 | SELECT * FROM ProductDB;
20 |
21 | insertProduct:
22 | INSERT OR IGNORE INTO ProductDB(
23 | id,
24 | brand,
25 | category,
26 | description,
27 | discountPercentage,
28 | rating,
29 | price,
30 | stock,
31 | thumbnail,
32 | title,
33 | images
34 | )
35 | VALUES ?;
36 |
37 | deleteProduct:
38 | DELETE FROM ProductDB WHERE id = ?;
39 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/domain/usecase/GetProductsUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import domain.model.ProductList
4 | import domain.usecase.product.GetProductsUseCase
5 | import fakes.FakeDataSource
6 | import fakes.FakeProductRepository
7 | import kotlinx.coroutines.test.runTest
8 | import kotlin.test.Test
9 | import kotlin.test.assertEquals
10 |
11 | class GetProductsUseCaseTest {
12 | private lateinit var sut: GetProductsUseCase
13 |
14 | @Test
15 | fun `invoke returns Success`() = runTest {
16 | // Arrange
17 | sut = GetProductsUseCase(FakeProductRepository(true))
18 | val assertedResponse = FakeDataSource.productList
19 |
20 | // Act
21 | lateinit var actualResponse: ProductList
22 | sut().onSuccess { actualResponse = it }
23 |
24 | // Assert
25 | assertEquals(assertedResponse, actualResponse)
26 | }
27 |
28 | @Test
29 | fun `invoke returns Failure`() = runTest {
30 | // Arrange
31 | sut = GetProductsUseCase(FakeProductRepository(false))
32 | val assertedResponse = "No Data"
33 |
34 | // Act
35 | lateinit var actualResponse: String
36 | sut().onFailure { actualResponse = it.message.toString() }
37 |
38 | // Assert
39 | assertEquals(assertedResponse, actualResponse)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/domain/usecase/LoginUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import data.network.requests.LoginRequest
4 | import data.network.responses.AuthResponse
5 | import domain.repository.AuthRepository
6 | import kotlin.test.BeforeTest
7 | import kotlin.test.Test
8 | import kotlin.test.assertEquals
9 | import kotlinx.coroutines.test.runTest
10 |
11 | class LoginUseCaseTest {
12 |
13 | private lateinit var sut: LoginUseCase
14 | private val mockAuthRepository = object : AuthRepository {
15 | override suspend fun authenticate(loginRequest: LoginRequest): AuthResponse {
16 | return AuthResponse(id = 0, token = "token")
17 | }
18 | }
19 |
20 | @BeforeTest
21 | fun setUp() {
22 | sut = LoginUseCase(mockAuthRepository)
23 | }
24 |
25 | @Test
26 | fun loginUseCase_ValidCredentials_ReturnsAuthResponse() = runTest {
27 | // Arrange
28 | val loginRequest = LoginRequest("test", "password")
29 | val expectedResult = AuthResponse(id = 0, token = "token")
30 |
31 | // Act
32 | val result = sut(loginRequest)
33 |
34 | // Assert
35 | assertEquals(expectedResult, result.getOrNull())
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/domain/usecase/favorite/AddToFavoriteUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import fakes.FakeDataSource
4 | import fakes.FakeFavoriteRepository
5 | import kotlinx.coroutines.test.runTest
6 | import kotlin.test.Test
7 | import kotlin.test.assertEquals
8 |
9 | class AddToFavoriteUseCaseTest {
10 | private lateinit var sut: AddToFavoriteUseCase
11 |
12 | @Test
13 | fun `invoke returns Success`() = runTest {
14 | // Arrange
15 | sut = AddToFavoriteUseCase(FakeFavoriteRepository(true))
16 | val assertedResponse = Unit
17 |
18 | // Act
19 | lateinit var actualResponse: Unit
20 | sut(FakeDataSource.product).onSuccess { actualResponse = it }
21 |
22 | // Assert
23 | assertEquals(assertedResponse, actualResponse)
24 | }
25 |
26 | @Test
27 | fun `invoke returns Failure`() = runTest {
28 | // Arrange
29 | sut = AddToFavoriteUseCase(FakeFavoriteRepository(false))
30 | val assertedResponse = "Error"
31 |
32 | // Act
33 | lateinit var actualResponse: String
34 | sut(FakeDataSource.product).onFailure { actualResponse = it.message.toString() }
35 |
36 | // Assert
37 | assertEquals(assertedResponse, actualResponse)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/domain/usecase/favorite/GetFavoritesUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import domain.model.Product
4 | import fakes.FakeDataSource
5 | import fakes.FakeFavoriteRepository
6 | import kotlinx.coroutines.test.runTest
7 | import kotlin.test.Test
8 | import kotlin.test.assertEquals
9 |
10 | class GetFavoritesUseCaseTest {
11 | private lateinit var sut: GetFavoritesUseCase
12 |
13 | @Test
14 | fun `invoke returns Success`() = runTest {
15 | // Arrange
16 | sut = GetFavoritesUseCase(FakeFavoriteRepository(true))
17 | val assertedResponse = listOf(FakeDataSource.product)
18 |
19 | // Act
20 | lateinit var actualResponse: List
21 | sut().onSuccess { actualResponse = it }
22 |
23 | // Assert
24 | assertEquals(assertedResponse, actualResponse)
25 | }
26 |
27 | @Test
28 | fun `invoke returns Failure`() = runTest {
29 | // Arrange
30 | sut = GetFavoritesUseCase(FakeFavoriteRepository(false))
31 | val assertedResponse = "Error"
32 |
33 | // Act
34 | lateinit var actualResponse: String
35 | sut().onFailure { actualResponse = it.message.toString() }
36 |
37 | // Assert
38 | assertEquals(assertedResponse, actualResponse)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/domain/usecase/favorite/RemoveFromFavoriteUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase.favorite
2 |
3 | import fakes.FakeDataSource
4 | import fakes.FakeFavoriteRepository
5 | import kotlinx.coroutines.test.runTest
6 | import kotlin.test.Test
7 | import kotlin.test.assertEquals
8 |
9 | class RemoveFromFavoriteUseCaseTest {
10 | private lateinit var sut: RemoveFromFavoriteUseCase
11 |
12 | @Test
13 | fun `invoke returns Success`() = runTest {
14 | // Arrange
15 | sut = RemoveFromFavoriteUseCase(FakeFavoriteRepository(true))
16 | val assertedResponse = Unit
17 |
18 | // Act
19 | lateinit var actualResponse: Unit
20 | sut(FakeDataSource.product).onSuccess { actualResponse = it }
21 |
22 | // Assert
23 | assertEquals(assertedResponse, actualResponse)
24 | }
25 |
26 | @Test
27 | fun `invoke returns Failure`() = runTest {
28 | // Arrange
29 | sut = RemoveFromFavoriteUseCase(FakeFavoriteRepository(false))
30 | val assertedResponse = "Error"
31 |
32 | // Act
33 | lateinit var actualResponse: String
34 | sut(FakeDataSource.product).onFailure { actualResponse = it.message.toString() }
35 |
36 | // Assert
37 | assertEquals(assertedResponse, actualResponse)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/fakes/FakeDataSource.kt:
--------------------------------------------------------------------------------
1 | package fakes
2 |
3 | import domain.model.Product
4 | import domain.model.ProductList
5 |
6 | internal object FakeDataSource {
7 | val product = Product(
8 | id = 1,
9 | title = "iPhone 9",
10 | description = "An apple mobile which is nothing like apple",
11 | price = 549,
12 | discountPercentage = 12.96,
13 | rating = 4.69,
14 | stock = 94,
15 | brand = "Apple",
16 | category = "smartphones",
17 | thumbnail = "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
18 | )
19 |
20 | val productList = ProductList(products = listOf(product), limit = 1, skip = 0, total = 100)
21 | }
22 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/fakes/FakeFavoriteRepository.kt:
--------------------------------------------------------------------------------
1 | package fakes
2 |
3 | import domain.model.Product
4 | import domain.repository.FavoriteRepository
5 |
6 | internal class FakeFavoriteRepository(private val isSuccess: Boolean) : FavoriteRepository {
7 | override suspend fun getFavorites() =
8 | if (isSuccess) listOf(FakeDataSource.product) else throw Exception("Error")
9 |
10 | override suspend fun addToFavorite(product: Product) =
11 | if (isSuccess) Unit else throw Exception("Error")
12 |
13 | override suspend fun removeFromFavorite(product: Product) =
14 | if (isSuccess) Unit else throw Exception("Error")
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/fakes/FakeProductRepository.kt:
--------------------------------------------------------------------------------
1 | package fakes
2 |
3 | import domain.repository.ProductRepository
4 |
5 | internal class FakeProductRepository(private val isSuccess: Boolean) : ProductRepository {
6 | override suspend fun getProducts() =
7 | if (isSuccess) FakeDataSource.productList else throw Exception("No Data")
8 | }
9 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/MainViewController.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 | import presentation.App
3 |
4 | fun MainViewController() = ComposeUIViewController { App() }
5 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/data/preference/Preference.ios.kt:
--------------------------------------------------------------------------------
1 | package data.preference
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import platform.Foundation.NSDocumentDirectory
6 | import platform.Foundation.NSFileManager
7 | import platform.Foundation.NSURL
8 | import platform.Foundation.NSUserDomainMask
9 |
10 | /**
11 | * Platform-specific function for creating a DataStore instance.
12 | * Creates a DataStore instance for iOS platform.
13 | *
14 | * @return DataStore instance.
15 | */
16 |
17 | internal actual fun createDataStore(): DataStore = createDataStore(
18 | producePath = {
19 | // Retrieves the document directory path for iOS
20 | val docDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
21 | directory = NSDocumentDirectory,
22 | inDomain = NSUserDomainMask,
23 | appropriateForURL = null,
24 | create = false,
25 | error = null,
26 | )
27 |
28 | // Constructs the full file path for the DataStore
29 | requireNotNull(docDirectory).path + "/$DATA_STORE_FILE_NAME"
30 | },
31 | )
32 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/di/modules/PlatformModule.kt:
--------------------------------------------------------------------------------
1 | package di.modules
2 |
3 | import androidx.room.Room
4 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver
5 | import data.db.AppDatabase
6 | import data.db.DataBaseConstants.DATABASE_NAME
7 | import data.db.instantiateImpl
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.IO
10 | import org.koin.dsl.module
11 | import platform.Foundation.NSDocumentDirectory
12 | import platform.Foundation.NSFileManager
13 | import platform.Foundation.NSURL
14 | import platform.Foundation.NSUserDomainMask
15 |
16 | actual val platformModule = module {
17 | single { createRoomDatabase() }
18 | }
19 |
20 | fun createRoomDatabase(): AppDatabase {
21 | val dbFile = "${fileDirectory()}/$DATABASE_NAME"
22 | return Room.databaseBuilder(
23 | name = dbFile,
24 | factory = { AppDatabase::class.instantiateImpl() }
25 | ).setDriver(BundledSQLiteDriver())
26 | .setQueryCoroutineContext(Dispatchers.IO)
27 | .build()
28 | }
29 |
30 | private fun fileDirectory(): String {
31 | val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
32 | directory = NSDocumentDirectory,
33 | inDomain = NSUserDomainMask,
34 | appropriateForURL = null,
35 | create = false,
36 | error = null,
37 | )
38 | return requireNotNull(documentDirectory).path!!
39 | }
40 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Koin.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import di.initKoin
4 | import org.koin.core.Koin
5 | import org.koin.core.KoinApplication
6 | import org.koin.core.parameter.parametersOf
7 | import org.koin.core.qualifier.Qualifier
8 | import kotlinx.cinterop.BetaInteropApi
9 | import kotlinx.cinterop.ObjCClass
10 | import kotlinx.cinterop.getOriginalKotlinClass
11 |
12 | object KoinIOS {
13 | fun initialize(): KoinApplication = initKoin()
14 | }
15 |
16 | @OptIn(BetaInteropApi::class)
17 | fun Koin.get(objCClass: ObjCClass): Any {
18 | val kClazz = getOriginalKotlinClass(objCClass)!!
19 | return get(kClazz, null, null)
20 | }
21 |
22 | @OptIn(BetaInteropApi::class)
23 | fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any {
24 | val kClazz = getOriginalKotlinClass(objCClass)!!
25 | return get(kClazz, qualifier) { parametersOf(parameter) }
26 | }
27 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/platform/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | package platform
2 |
3 | import platform.UIKit.UIDevice
4 |
5 | class IOSPlatform : Platform {
6 | override val name: String = UIDevice.currentDevice.systemName() + " " +
7 | UIDevice.currentDevice.systemVersion
8 | }
9 |
10 | actual fun getPlatform(): Platform = IOSPlatform()
11 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #Gradle
4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
5 |
6 |
7 | #Android
8 | android.nonTransitiveRClass=true
9 | android.useAndroidX=true
10 |
11 | #MPP
12 | kotlin.mpp.androidSourceSetLayoutVersion=2
13 | kotlin.mpp.enableCInteropCommonization=true
14 |
15 | #Development
16 | development=true
17 |
18 | kotlin.native.disableCompilerDaemon = true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.2.2"
3 | android-compileSdk = "34"
4 | android-minSdk = "24"
5 | android-targetSdk = "34"
6 | androidx-activityCompose = "1.9.0"
7 | androidx-appcompat = "1.6.1"
8 | androidx-constraintlayout = "2.1.4"
9 | androidx-core-ktx = "1.13.0"
10 | androidx-espresso-core = "3.5.1"
11 | androidx-material = "1.11.0"
12 | androidx-test-junit = "1.1.5"
13 | coil = "3.0.0-alpha03"
14 | compose = "1.6.6"
15 | compose-plugin = "1.6.1"
16 | datastorePreferencesCore = "1.1.0"
17 | junit = "5.10.2"
18 | kotlin = "1.9.23"
19 | kotlin-dokka = "1.9.0"
20 | kotlinx-coroutines = "1.8.0"
21 | pagingCommon = "3.3.0-alpha02-0.4.0"
22 | pagingAndroid = "3.3.0-alpha02"
23 | voyager = "1.0.0"
24 | koin-compose-kmp = "1.2.0-alpha3"
25 | koin = "3.5.4"
26 | ktor = "2.3.9"
27 | ksp = "1.9.23-1.0.19"
28 | kmmViewmodelCore = "1.0.0-ALPHA-19"
29 | spotless = "6.25.0"
30 | detekt = "1.23.5"
31 | androidx-lifecycle = "2.8.0-alpha04"
32 | androidx-startup = "1.1.1"
33 | skie = "0.6.4"
34 | androidxRoom = "2.7.0-alpha01"
35 | sqlite = "2.5.0-SNAPSHOT"
36 |
37 | [libraries]
38 | androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastorePreferencesCore" }
39 | androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" }
40 | kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
41 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
42 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
43 | junit = { group = "junit", name = "junit", version.ref = "junit" }
44 |
45 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
46 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
47 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
48 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
49 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
50 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
51 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
52 | androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
53 |
54 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
55 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
56 |
57 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
58 | voyager-bottomSheetNavigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyager" }
59 | voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
60 | voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
61 | voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
62 |
63 | koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
64 | koin-compose = { group = "io.insert-koin", name = "koin-compose", version.ref = "koin-compose-kmp" }
65 | koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" }
66 |
67 | ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
68 | ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
69 | ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" }
70 | ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" }
71 | ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
72 | ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
73 | ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
74 |
75 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
76 | coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" }
77 |
78 | paging-common = { module = "app.cash.paging:paging-common", version.ref = "pagingCommon" }
79 | paging-compose-common = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingCommon" }
80 | paging-runtime-uikit = { module = "app.cash.paging:paging-runtime-uikit", version.ref = "pagingCommon" }
81 | paging-compose-androidx = { module = "androidx.paging:paging-compose", version.ref = "pagingAndroid" }
82 | paging-runtime-androidx = { module = "androidx.paging:paging-runtime", version.ref = "pagingAndroid" }
83 | paging-common-test = { module = "app.cash.paging:paging-testing", version.ref = "pagingCommon" }
84 |
85 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
86 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
87 | sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
88 |
89 | kmp-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
90 |
91 | [bundles]
92 | voyager = ["voyager-navigator", "voyager-bottomSheetNavigator", "voyager-tabNavigator", "voyager-transitions", "voyager-koin"]
93 | ktor = ["ktor-client-core", "ktor-client-logging", "ktor-client-auth", "ktor-serialization-kotlinx-json", "ktor-client-content-negotiation"]
94 | coil = ["coil-compose", "coil-network-ktor"]
95 | paging = ["paging-common", "paging-compose-common"]
96 | paging-android = ["paging-compose-androidx", "paging-runtime-androidx"]
97 |
98 | [plugins]
99 | androidApplication = { id = "com.android.application", version.ref = "agp" }
100 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
101 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
102 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
103 | kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
104 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
105 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
106 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
107 | kotlin-dokka = { id = "org.jetbrains.dokka", version.ref = "kotlin-dokka" }
108 | skie = { id = "co.touchlab.skie", version.ref = "skie" }
109 | room = { id = "androidx.room", version.ref = "androidxRoom" }
110 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/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 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=com.monstarlab.kmp.kmp-template
3 | APP_NAME=kmp-template
--------------------------------------------------------------------------------
/iosApp/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'iosApp' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for iosApp
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/iosApp/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODFILE CHECKSUM: c8b607a42d26f21541b1abb39b8ff3ab6670f169
2 |
3 | COCOAPODS: 1.13.0
4 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
14 | C1ACDA472B8DE0CB000C228D /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ACDA462B8DE0CB000C228D /* Koin.swift */; };
15 | C1ACDA4A2B8DE119000C228D /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ACDA492B8DE119000C228D /* SplashScreen.swift */; };
16 | C1ACDA4C2B8DE133000C228D /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ACDA4B2B8DE133000C228D /* HomeScreen.swift */; };
17 | C1C2D0D62BEA31DF00BFB6AD /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C2D0D52BEA31DE00BFB6AD /* LoginScreen.swift */; };
18 | E402536C8CAC213632F9EA4E /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBC182C52559F275B41F2FDC /* Pods_iosApp.framework */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXFileReference section */
22 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
25 | 7555FF7B242A565900829871 /* kmp-template.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "kmp-template.app"; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
28 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
29 | BBC182C52559F275B41F2FDC /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
30 | C0D9217A7F97513F9651E216 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
31 | C1ACDA462B8DE0CB000C228D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; };
32 | C1ACDA492B8DE119000C228D /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; };
33 | C1ACDA4B2B8DE133000C228D /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; };
34 | C1C2D0D52BEA31DE00BFB6AD /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; };
35 | C1F49C30569FCD4DF157F9E1 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
36 | /* End PBXFileReference section */
37 |
38 | /* Begin PBXFrameworksBuildPhase section */
39 | B92378962B6B1156000C7307 /* Frameworks */ = {
40 | isa = PBXFrameworksBuildPhase;
41 | buildActionMask = 2147483647;
42 | files = (
43 | E402536C8CAC213632F9EA4E /* Pods_iosApp.framework in Frameworks */,
44 | );
45 | runOnlyForDeploymentPostprocessing = 0;
46 | };
47 | /* End PBXFrameworksBuildPhase section */
48 |
49 | /* Begin PBXGroup section */
50 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
54 | );
55 | path = "Preview Content";
56 | sourceTree = "";
57 | };
58 | 2BEDAC8B03DCC2908730247E /* Pods */ = {
59 | isa = PBXGroup;
60 | children = (
61 | C0D9217A7F97513F9651E216 /* Pods-iosApp.debug.xcconfig */,
62 | C1F49C30569FCD4DF157F9E1 /* Pods-iosApp.release.xcconfig */,
63 | );
64 | path = Pods;
65 | sourceTree = "";
66 | };
67 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
68 | isa = PBXGroup;
69 | children = (
70 | BBC182C52559F275B41F2FDC /* Pods_iosApp.framework */,
71 | );
72 | name = Frameworks;
73 | sourceTree = "";
74 | };
75 | 7555FF72242A565900829871 = {
76 | isa = PBXGroup;
77 | children = (
78 | AB1DB47929225F7C00F7AF9C /* Configuration */,
79 | 7555FF7D242A565900829871 /* iosApp */,
80 | 7555FF7C242A565900829871 /* Products */,
81 | 42799AB246E5F90AF97AA0EF /* Frameworks */,
82 | 2BEDAC8B03DCC2908730247E /* Pods */,
83 | );
84 | sourceTree = "";
85 | };
86 | 7555FF7C242A565900829871 /* Products */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 7555FF7B242A565900829871 /* kmp-template.app */,
90 | );
91 | name = Products;
92 | sourceTree = "";
93 | };
94 | 7555FF7D242A565900829871 /* iosApp */ = {
95 | isa = PBXGroup;
96 | children = (
97 | C1ACDA432B8DE087000C228D /* system */,
98 | C1ACDA422B8DE078000C228D /* presentation */,
99 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
100 | 7555FF8C242A565B00829871 /* Info.plist */,
101 | 058557D7273AAEEB004C7B11 /* Preview Content */,
102 | );
103 | path = iosApp;
104 | sourceTree = "";
105 | };
106 | AB1DB47929225F7C00F7AF9C /* Configuration */ = {
107 | isa = PBXGroup;
108 | children = (
109 | AB3632DC29227652001CCB65 /* Config.xcconfig */,
110 | );
111 | path = Configuration;
112 | sourceTree = "";
113 | };
114 | C1ACDA422B8DE078000C228D /* presentation */ = {
115 | isa = PBXGroup;
116 | children = (
117 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
118 | 7555FF82242A565900829871 /* ContentView.swift */,
119 | C1ACDA482B8DE0FE000C228D /* feature */,
120 | );
121 | path = presentation;
122 | sourceTree = "";
123 | };
124 | C1ACDA432B8DE087000C228D /* system */ = {
125 | isa = PBXGroup;
126 | children = (
127 | C1ACDA462B8DE0CB000C228D /* Koin.swift */,
128 | );
129 | path = system;
130 | sourceTree = "";
131 | };
132 | C1ACDA482B8DE0FE000C228D /* feature */ = {
133 | isa = PBXGroup;
134 | children = (
135 | C1ACDA492B8DE119000C228D /* SplashScreen.swift */,
136 | C1ACDA4B2B8DE133000C228D /* HomeScreen.swift */,
137 | C1C2D0D52BEA31DE00BFB6AD /* LoginScreen.swift */,
138 | );
139 | path = feature;
140 | sourceTree = "";
141 | };
142 | /* End PBXGroup section */
143 |
144 | /* Begin PBXNativeTarget section */
145 | 7555FF7A242A565900829871 /* iosApp */ = {
146 | isa = PBXNativeTarget;
147 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
148 | buildPhases = (
149 | 3B2AA6F83D81A49150BE1815 /* [CP] Check Pods Manifest.lock */,
150 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
151 | 7555FF77242A565900829871 /* Sources */,
152 | B92378962B6B1156000C7307 /* Frameworks */,
153 | 7555FF79242A565900829871 /* Resources */,
154 | );
155 | buildRules = (
156 | );
157 | dependencies = (
158 | );
159 | name = iosApp;
160 | packageProductDependencies = (
161 | );
162 | productName = iosApp;
163 | productReference = 7555FF7B242A565900829871 /* kmp-template.app */;
164 | productType = "com.apple.product-type.application";
165 | };
166 | /* End PBXNativeTarget section */
167 |
168 | /* Begin PBXProject section */
169 | 7555FF73242A565900829871 /* Project object */ = {
170 | isa = PBXProject;
171 | attributes = {
172 | LastSwiftUpdateCheck = 1130;
173 | LastUpgradeCheck = 1130;
174 | ORGANIZATIONNAME = orgName;
175 | TargetAttributes = {
176 | 7555FF7A242A565900829871 = {
177 | CreatedOnToolsVersion = 11.3.1;
178 | };
179 | };
180 | };
181 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
182 | compatibilityVersion = "Xcode 12.0";
183 | developmentRegion = en;
184 | hasScannedForEncodings = 0;
185 | knownRegions = (
186 | en,
187 | Base,
188 | );
189 | mainGroup = 7555FF72242A565900829871;
190 | packageReferences = (
191 | );
192 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
193 | projectDirPath = "";
194 | projectRoot = "";
195 | targets = (
196 | 7555FF7A242A565900829871 /* iosApp */,
197 | );
198 | };
199 | /* End PBXProject section */
200 |
201 | /* Begin PBXResourcesBuildPhase section */
202 | 7555FF79242A565900829871 /* Resources */ = {
203 | isa = PBXResourcesBuildPhase;
204 | buildActionMask = 2147483647;
205 | files = (
206 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
207 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
208 | );
209 | runOnlyForDeploymentPostprocessing = 0;
210 | };
211 | /* End PBXResourcesBuildPhase section */
212 |
213 | /* Begin PBXShellScriptBuildPhase section */
214 | 3B2AA6F83D81A49150BE1815 /* [CP] Check Pods Manifest.lock */ = {
215 | isa = PBXShellScriptBuildPhase;
216 | buildActionMask = 2147483647;
217 | files = (
218 | );
219 | inputFileListPaths = (
220 | );
221 | inputPaths = (
222 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
223 | "${PODS_ROOT}/Manifest.lock",
224 | );
225 | name = "[CP] Check Pods Manifest.lock";
226 | outputFileListPaths = (
227 | );
228 | outputPaths = (
229 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | shellPath = /bin/sh;
233 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
234 | showEnvVarsInLog = 0;
235 | };
236 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
237 | isa = PBXShellScriptBuildPhase;
238 | buildActionMask = 2147483647;
239 | files = (
240 | );
241 | inputFileListPaths = (
242 | );
243 | inputPaths = (
244 | );
245 | name = "Compile Kotlin Framework";
246 | outputFileListPaths = (
247 | );
248 | outputPaths = (
249 | );
250 | runOnlyForDeploymentPostprocessing = 0;
251 | shellPath = /bin/sh;
252 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
253 | };
254 | /* End PBXShellScriptBuildPhase section */
255 |
256 | /* Begin PBXSourcesBuildPhase section */
257 | 7555FF77242A565900829871 /* Sources */ = {
258 | isa = PBXSourcesBuildPhase;
259 | buildActionMask = 2147483647;
260 | files = (
261 | C1ACDA4A2B8DE119000C228D /* SplashScreen.swift in Sources */,
262 | C1ACDA4C2B8DE133000C228D /* HomeScreen.swift in Sources */,
263 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
264 | C1ACDA472B8DE0CB000C228D /* Koin.swift in Sources */,
265 | 7555FF83242A565900829871 /* ContentView.swift in Sources */,
266 | C1C2D0D62BEA31DF00BFB6AD /* LoginScreen.swift in Sources */,
267 | );
268 | runOnlyForDeploymentPostprocessing = 0;
269 | };
270 | /* End PBXSourcesBuildPhase section */
271 |
272 | /* Begin XCBuildConfiguration section */
273 | 7555FFA3242A565B00829871 /* Debug */ = {
274 | isa = XCBuildConfiguration;
275 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
276 | buildSettings = {
277 | ALWAYS_SEARCH_USER_PATHS = NO;
278 | CLANG_ANALYZER_NONNULL = YES;
279 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
280 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
281 | CLANG_CXX_LIBRARY = "libc++";
282 | CLANG_ENABLE_MODULES = YES;
283 | CLANG_ENABLE_OBJC_ARC = YES;
284 | CLANG_ENABLE_OBJC_WEAK = YES;
285 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
286 | CLANG_WARN_BOOL_CONVERSION = YES;
287 | CLANG_WARN_COMMA = YES;
288 | CLANG_WARN_CONSTANT_CONVERSION = YES;
289 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
290 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
291 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
292 | CLANG_WARN_EMPTY_BODY = YES;
293 | CLANG_WARN_ENUM_CONVERSION = YES;
294 | CLANG_WARN_INFINITE_RECURSION = YES;
295 | CLANG_WARN_INT_CONVERSION = YES;
296 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
297 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
298 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
299 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
300 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
301 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
302 | CLANG_WARN_STRICT_PROTOTYPES = YES;
303 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
304 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
305 | CLANG_WARN_UNREACHABLE_CODE = YES;
306 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
307 | COPY_PHASE_STRIP = NO;
308 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
309 | ENABLE_STRICT_OBJC_MSGSEND = YES;
310 | ENABLE_TESTABILITY = YES;
311 | GCC_C_LANGUAGE_STANDARD = gnu11;
312 | GCC_DYNAMIC_NO_PIC = NO;
313 | GCC_NO_COMMON_BLOCKS = YES;
314 | GCC_OPTIMIZATION_LEVEL = 0;
315 | GCC_PREPROCESSOR_DEFINITIONS = (
316 | "DEBUG=1",
317 | "$(inherited)",
318 | );
319 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
320 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
321 | GCC_WARN_UNDECLARED_SELECTOR = YES;
322 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
323 | GCC_WARN_UNUSED_FUNCTION = YES;
324 | GCC_WARN_UNUSED_VARIABLE = YES;
325 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
326 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
327 | MTL_FAST_MATH = YES;
328 | ONLY_ACTIVE_ARCH = YES;
329 | SDKROOT = iphoneos;
330 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
331 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
332 | };
333 | name = Debug;
334 | };
335 | 7555FFA4242A565B00829871 /* Release */ = {
336 | isa = XCBuildConfiguration;
337 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
338 | buildSettings = {
339 | ALWAYS_SEARCH_USER_PATHS = NO;
340 | CLANG_ANALYZER_NONNULL = YES;
341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
343 | CLANG_CXX_LIBRARY = "libc++";
344 | CLANG_ENABLE_MODULES = YES;
345 | CLANG_ENABLE_OBJC_ARC = YES;
346 | CLANG_ENABLE_OBJC_WEAK = YES;
347 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
348 | CLANG_WARN_BOOL_CONVERSION = YES;
349 | CLANG_WARN_COMMA = YES;
350 | CLANG_WARN_CONSTANT_CONVERSION = YES;
351 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
352 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
353 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
354 | CLANG_WARN_EMPTY_BODY = YES;
355 | CLANG_WARN_ENUM_CONVERSION = YES;
356 | CLANG_WARN_INFINITE_RECURSION = YES;
357 | CLANG_WARN_INT_CONVERSION = YES;
358 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
359 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
360 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
362 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
364 | CLANG_WARN_STRICT_PROTOTYPES = YES;
365 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
366 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
367 | CLANG_WARN_UNREACHABLE_CODE = YES;
368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
369 | COPY_PHASE_STRIP = NO;
370 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
371 | ENABLE_NS_ASSERTIONS = NO;
372 | ENABLE_STRICT_OBJC_MSGSEND = YES;
373 | GCC_C_LANGUAGE_STANDARD = gnu11;
374 | GCC_NO_COMMON_BLOCKS = YES;
375 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
376 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
377 | GCC_WARN_UNDECLARED_SELECTOR = YES;
378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
379 | GCC_WARN_UNUSED_FUNCTION = YES;
380 | GCC_WARN_UNUSED_VARIABLE = YES;
381 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
382 | MTL_ENABLE_DEBUG_INFO = NO;
383 | MTL_FAST_MATH = YES;
384 | SDKROOT = iphoneos;
385 | SWIFT_COMPILATION_MODE = wholemodule;
386 | SWIFT_OPTIMIZATION_LEVEL = "-O";
387 | VALIDATE_PRODUCT = YES;
388 | };
389 | name = Release;
390 | };
391 | 7555FFA6242A565B00829871 /* Debug */ = {
392 | isa = XCBuildConfiguration;
393 | baseConfigurationReference = C0D9217A7F97513F9651E216 /* Pods-iosApp.debug.xcconfig */;
394 | buildSettings = {
395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
396 | CODE_SIGN_IDENTITY = "Apple Development";
397 | CODE_SIGN_STYLE = Automatic;
398 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
399 | DEVELOPMENT_TEAM = G227YKW72F;
400 | ENABLE_PREVIEWS = YES;
401 | FRAMEWORK_SEARCH_PATHS = (
402 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
403 | );
404 | INFOPLIST_FILE = iosApp/Info.plist;
405 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
406 | LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
407 | OTHER_LDFLAGS = (
408 | "$(inherited)",
409 | "-framework",
410 | composeApp,
411 | "-lsqlite3",
412 | );
413 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
414 | PRODUCT_NAME = "${APP_NAME}";
415 | PROVISIONING_PROFILE_SPECIFIER = "";
416 | SWIFT_VERSION = 5.0;
417 | TARGETED_DEVICE_FAMILY = "1,2";
418 | };
419 | name = Debug;
420 | };
421 | 7555FFA7242A565B00829871 /* Release */ = {
422 | isa = XCBuildConfiguration;
423 | baseConfigurationReference = C1F49C30569FCD4DF157F9E1 /* Pods-iosApp.release.xcconfig */;
424 | buildSettings = {
425 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
426 | CODE_SIGN_IDENTITY = "Apple Development";
427 | CODE_SIGN_STYLE = Automatic;
428 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
429 | DEVELOPMENT_TEAM = G227YKW72F;
430 | ENABLE_PREVIEWS = YES;
431 | FRAMEWORK_SEARCH_PATHS = (
432 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
433 | );
434 | INFOPLIST_FILE = iosApp/Info.plist;
435 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
436 | LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
437 | OTHER_LDFLAGS = (
438 | "$(inherited)",
439 | "-framework",
440 | composeApp,
441 | "-lsqlite3",
442 | );
443 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
444 | PRODUCT_NAME = "${APP_NAME}";
445 | PROVISIONING_PROFILE_SPECIFIER = "";
446 | SWIFT_VERSION = 5.0;
447 | TARGETED_DEVICE_FAMILY = "1,2";
448 | };
449 | name = Release;
450 | };
451 | /* End XCBuildConfiguration section */
452 |
453 | /* Begin XCConfigurationList section */
454 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
455 | isa = XCConfigurationList;
456 | buildConfigurations = (
457 | 7555FFA3242A565B00829871 /* Debug */,
458 | 7555FFA4242A565B00829871 /* Release */,
459 | );
460 | defaultConfigurationIsVisible = 0;
461 | defaultConfigurationName = Release;
462 | };
463 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
464 | isa = XCConfigurationList;
465 | buildConfigurations = (
466 | 7555FFA6242A565B00829871 /* Debug */,
467 | 7555FFA7242A565B00829871 /* Release */,
468 | );
469 | defaultConfigurationIsVisible = 0;
470 | defaultConfigurationName = Release;
471 | };
472 | /* End XCConfigurationList section */
473 | };
474 | rootObject = 7555FF73242A565900829871 /* Project object */;
475 | }
476 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-opensource/KMP-Template/b699e77be3ae49e37faf7b09848849939bc58d5f/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/presentation/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | /**
14 | Entry point for the iOS App.
15 |
16 | Uncomment the line
17 | ```
18 | SplashScreen()
19 | ```
20 | and comment out the line
21 | ```
22 | ComposeView().ignoresSafeArea(.keyboard)
23 | ```
24 | to run the app in Swift UI.
25 | */
26 | struct ContentView: View {
27 | var body: some View {
28 | // SplashScreen()
29 | ComposeView().ignoresSafeArea(.keyboard) // Compose has own keyboard handler
30 | }
31 | }
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/iosApp/iosApp/presentation/feature/HomeScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeScreen.swift
3 | // iosApp
4 | //
5 | // Created by Golam Shakib Khan on 27/2/24.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct HomeScreen: View {
13 | @State var viewModel = HomeViewModelProvider().provide()
14 | @State var screenState: HomeScreenState = HomeScreenState()
15 |
16 | var body: some View {
17 | NavigationView {
18 | ZStack {
19 | ScrollView(showsIndicators: false) {
20 | VStack(alignment: .leading) {
21 | ForEach(screenState.productList, id: \.id) { product in
22 | ProductItemView(product: product)
23 | }
24 | }
25 | }
26 |
27 | if screenState.isLoading {
28 | ProgressView()
29 | }
30 | }
31 | .navigationBarTitleDisplayMode(.inline)
32 | .navigationTitle("Products")
33 | .onAppear {
34 | viewModel.handleIntent(intent: HomeScreenIntent.OnLaunch())
35 | }
36 | .task {
37 | for await state in viewModel.state {
38 | screenState = state
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
45 | struct ProductItemView: View {
46 | let product: Product
47 |
48 | var body: some View {
49 | ZStack {
50 | AsyncImage(url: URL(string: product.thumbnail)) { image in
51 | image.image?
52 | .resizable()
53 | .scaledToFill()
54 | }
55 | .cornerRadius(8.0)
56 |
57 | VStack {
58 | Spacer()
59 |
60 | Text(product.title)
61 | .foregroundColor(.white)
62 | .frame(maxWidth: .infinity)
63 | .padding(8.0)
64 | }
65 | .background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.gray.opacity(0.2)]), startPoint: .bottom, endPoint: .center))
66 | .cornerRadius(8.0)
67 | }
68 | .padding(.horizontal, 8)
69 | .padding(.vertical, 2)
70 | }
71 | }
72 |
73 | #Preview {
74 | HomeScreen()
75 | }
76 |
--------------------------------------------------------------------------------
/iosApp/iosApp/presentation/feature/LoginScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginScreen.swift
3 | // iosApp
4 | //
5 | // Created by Golam Shakib Khan on 7/5/24.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct LoginScreen: View {
13 | @State var viewModel = LoginViewModelProvider().provide()
14 | @State var screenState: LoginScreenState = LoginScreenState()
15 |
16 | var body: some View {
17 | ZStack {
18 | if screenState.isLoggedIn {
19 | HomeScreen()
20 | } else {
21 | VStack {
22 | TextField("Email", text: $screenState.email)
23 | .padding()
24 | .overlay(
25 | RoundedRectangle(cornerRadius: 4)
26 | .stroke(.gray, lineWidth: 1)
27 | )
28 | .padding(.horizontal)
29 |
30 | SecureField("Password", text: $screenState.password)
31 | .padding()
32 | .overlay(
33 | RoundedRectangle(cornerRadius: 4)
34 | .stroke(.gray, lineWidth: 1)
35 | )
36 | .padding(.horizontal)
37 |
38 | Button(action: {
39 | viewModel.handleIntent(intent: LoginIntent.Login())
40 | }, label: {
41 | if screenState.isLoading {
42 | ProgressView()
43 | } else {
44 | Text("Login")
45 | .font(.headline)
46 | .foregroundColor(.black)
47 | }
48 | })
49 | .frame(maxWidth: .infinity, maxHeight: 48)
50 | .background(.yellow)
51 | .cornerRadius(8)
52 | .padding(.horizontal)
53 | .padding(.vertical)
54 | }
55 | }
56 | }
57 | .task {
58 | for await state in viewModel.state {
59 | screenState = state
60 | }
61 | }
62 | }
63 | }
64 |
65 | #Preview {
66 | LoginScreen()
67 | }
68 |
--------------------------------------------------------------------------------
/iosApp/iosApp/presentation/feature/SplashScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SplashScreen.swift
3 | // iosApp
4 | //
5 | // Created by Golam Shakib Khan on 27/2/24.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ComposeApp
11 |
12 | struct SplashScreen: View {
13 | @State var viewModel = SplashViewModelProvider().provide()
14 | @State var screenState: SplashScreenState = SplashScreenState()
15 | @State var navigate = false
16 |
17 | var body: some View {
18 | ZStack {
19 | if navigate && screenState.user != nil {
20 | HomeScreen()
21 | } else if navigate && screenState.user == nil {
22 | LoginScreen()
23 | } else {
24 | VStack {
25 | Image(systemName: "swift")
26 | .resizable()
27 | .aspectRatio(contentMode: .fill)
28 | .frame(width: 200, height: 200)
29 | .foregroundColor(.blue)
30 |
31 | Text(screenState.greeting)
32 | .padding(.top, 24)
33 | }
34 | .transition(AnyTransition.opacity.combined(with: .move(edge: .top)))
35 | .animation(.easeInOut(duration: 0.35), value: screenState.showContent)
36 | }
37 | }
38 | .onAppear {
39 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
40 | withAnimation {
41 | viewModel.handleIntent(intent: SplashScreenIntent.OnContentVisibilityChange(show: true))
42 | }
43 | }
44 |
45 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
46 | withAnimation {
47 | viewModel.handleIntent(intent: SplashScreenIntent.OnContentVisibilityChange(show: false))
48 | }
49 | }
50 |
51 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
52 | withAnimation {
53 | navigate.toggle()
54 | }
55 | }
56 | }
57 | .task {
58 | for await state in viewModel.state {
59 | screenState = state
60 | }
61 | }
62 | }
63 | }
64 |
65 | #Preview {
66 | SplashScreen()
67 | }
68 |
--------------------------------------------------------------------------------
/iosApp/iosApp/presentation/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | init() { Koin.start() }
6 |
7 | var body: some Scene {
8 | WindowGroup {
9 | ContentView()
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/iosApp/iosApp/system/Koin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Koin.swift
3 | // iosApp
4 | //
5 | // Created by Golam Shakib Khan on 27/2/24.
6 | // Copyright © 2024 orgName. All rights reserved.
7 | //
8 |
9 | import ComposeApp
10 |
11 | final class Koin {
12 | private var core: Koin_coreKoin?
13 |
14 | static let instance = Koin()
15 |
16 | static func start() {
17 | if instance.core == nil {
18 | let app = KoinIOS.shared.initialize()
19 | instance.core = app.koin
20 | }
21 | if instance.core == nil {
22 | fatalError("Can't initialize Koin.")
23 | }
24 | }
25 |
26 | private init() {
27 |
28 | }
29 |
30 | func get() -> T {
31 | guard let core = core else {
32 | fatalError("You should call `start()` before using \(#function)")
33 | }
34 |
35 | guard let result = core.get(objCClass: T.self) as? T else {
36 | fatalError("Koin can't provide an instance of type: \(T.self)")
37 | }
38 |
39 | return result
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "KMP-Template"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 | repositories {
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | google()
8 | gradlePluginPortal()
9 | mavenCentral()
10 | }
11 | }
12 |
13 | dependencyResolutionManagement {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
18 | }
19 | }
20 |
21 | include(":composeApp")
--------------------------------------------------------------------------------