diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
index d3d17b1..ad00056 100644
--- a/androidApp/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.ktlint)
}
kotlin {
@@ -59,11 +60,25 @@ android {
lint {
lintConfig = file("$rootDir/androidApp/android_picasso_lint.xml")
}
+
+ flavorDimensions += "environment"
+
+ productFlavors {
+ create("ci") {
+ dimension = "environment"
+ applicationIdSuffix = ".ci"
+ }
+
+ create("store") {
+ dimension = "environment"
+ }
+ }
}
dependencies {
implementation(project(":shared"))
+ implementation(libs.android.splash)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index 9516b5f..9bd6ca6 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -4,11 +4,12 @@
@@ -16,4 +17,5 @@
+
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt
deleted file mode 100644
index 404d91e..0000000
--- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.tooling.preview.Preview
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-@Composable
-fun Greeting(textFlow: Flow) {
- val text: String by textFlow.collectAsState("initial")
-
- Text(text = text)
-}
-
-@Preview
-@Composable
-fun PreviewGreeting() {
- val textFlow = flowOf("Hello, Android 31")
- Greeting(textFlow)
-}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt
deleted file mode 100644
index bf86377..0000000
--- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-
-
-class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- Greeting(textFlow = Greeting().greeting())
- }
- }
-}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/AndroidApplication.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/AndroidApplication.kt
new file mode 100644
index 0000000..3aa533b
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/AndroidApplication.kt
@@ -0,0 +1,14 @@
+package com.mirego.kmp.boilerplate.app
+
+import android.app.Application
+import com.mirego.kmp.boilerplate.app.bootstrap.AndroidBootstrap
+import com.mirego.kmp.boilerplate.bootstrap.Bootstrapper
+
+class AndroidApplication : Application() {
+ val bootstrapper = Bootstrapper()
+
+ override fun onCreate() {
+ super.onCreate()
+ bootstrapper.initDependencies(AndroidBootstrap(this))
+ }
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/MainActivity.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/MainActivity.kt
new file mode 100644
index 0000000..6f73876
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/MainActivity.kt
@@ -0,0 +1,31 @@
+package com.mirego.kmp.boilerplate.app
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
+import com.mirego.kmp.boilerplate.app.ui.application.ApplicationView
+import com.mirego.kmp.boilerplate.bootstrap.Bootstrapper
+import com.mirego.kmp.boilerplate.trikot.viewmodels.declarative.compose.getInitialViewModel
+import com.mirego.kmp.boilerplate.viewmodel.application.ApplicationViewModel
+
+class MainActivity : AppCompatActivity() {
+ private val bootstrapper: Bootstrapper
+ get() = (applicationContext as AndroidApplication).bootstrapper
+
+ private val viewModel: ApplicationViewModel by lazy {
+ getInitialViewModel {
+ bootstrapper.applicationViewModel()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ installSplashScreen()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ setContent {
+ ApplicationView(applicationViewModel = viewModel)
+ }
+ }
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AndroidBootstrap.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AndroidBootstrap.kt
new file mode 100644
index 0000000..4beae28
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AndroidBootstrap.kt
@@ -0,0 +1,31 @@
+package com.mirego.kmp.boilerplate.app.bootstrap
+
+import android.content.Context
+import com.mirego.kmp.boilerplate.BuildConfig
+import com.mirego.kmp.boilerplate.app.resources.AndroidImageProvider
+import com.mirego.kmp.boilerplate.bootstrap.AppEnvironment
+import com.mirego.kmp.boilerplate.bootstrap.Bootstrap
+import com.mirego.kmp.boilerplate.bootstrap.LocaleUtils
+import com.mirego.trikot.kword.android.AndroidKWord
+import com.mirego.trikot.viewmodels.declarative.configuration.DefaultTextStyleProvider
+import com.mirego.trikot.viewmodels.declarative.configuration.TrikotViewModelDeclarative
+
+class AndroidBootstrap(context: Context) : Bootstrap {
+
+ override val appInformation = AppInformationImpl(context)
+
+ override val environment = when (BuildConfig.FLAVOR.lowercase()) {
+ "ci" -> AppEnvironment.DEV
+ "store" -> AppEnvironment.PRODUCTION
+ else -> AppEnvironment.PRODUCTION
+ }
+
+ init {
+ AndroidKWord.setCurrentLanguageCode(LocaleUtils.supportedLanguageCode())
+
+ TrikotViewModelDeclarative.initialize(
+ imageProvider = AndroidImageProvider(),
+ textStyleProvider = DefaultTextStyleProvider()
+ )
+ }
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AppInformationImpl.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AppInformationImpl.kt
new file mode 100644
index 0000000..e545235
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/bootstrap/AppInformationImpl.kt
@@ -0,0 +1,17 @@
+package com.mirego.kmp.boilerplate.app.bootstrap
+
+import android.content.Context
+import com.mirego.kmp.boilerplate.BuildConfig
+import com.mirego.kmp.boilerplate.bootstrap.AppInformation
+import com.mirego.kmp.boilerplate.model.Language
+import com.mirego.kmp.boilerplate.model.Locale
+
+class AppInformationImpl(context: Context) : AppInformation {
+ override val locale: Locale = Locale(
+ if (java.util.Locale.getDefault().language.lowercase() == "fr") Language.FRENCH else Language.ENGLISH,
+ java.util.Locale.getDefault().country
+ )
+
+ override val versionNumber: String = "${BuildConfig.VERSION_NAME}.${BuildConfig.VERSION_CODE}"
+ override val diskCachePath: String = context.cacheDir.toString()
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/resources/AndroidImageProvider.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/resources/AndroidImageProvider.kt
new file mode 100644
index 0000000..c96e1e0
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/resources/AndroidImageProvider.kt
@@ -0,0 +1,9 @@
+package com.mirego.kmp.boilerplate.app.resources
+
+import android.content.Context
+import com.mirego.trikot.viewmodels.declarative.configuration.VMDImageProvider
+import com.mirego.trikot.viewmodels.declarative.properties.VMDImageResource
+
+class AndroidImageProvider : VMDImageProvider {
+ override fun resourceIdForResource(resource: VMDImageResource, context: Context) = null
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/application/ApplicationView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/application/ApplicationView.kt
new file mode 100644
index 0000000..93aa2f1
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/application/ApplicationView.kt
@@ -0,0 +1,23 @@
+package com.mirego.kmp.boilerplate.app.ui.application
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.tooling.preview.Preview
+import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider
+import com.mirego.kmp.boilerplate.app.ui.root.RootView
+import com.mirego.kmp.boilerplate.viewmodel.application.ApplicationViewModel
+import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsState
+
+@Composable
+fun ApplicationView(applicationViewModel: ApplicationViewModel) {
+ val viewModel: ApplicationViewModel by applicationViewModel.observeAsState()
+ RootView(rootViewModel = viewModel.rootViewModel)
+}
+
+@Preview
+@Composable
+fun PreviewApplicationView() {
+ PreviewProvider {
+ ApplicationView(applicationViewModel = it.createApplication())
+ }
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/preview/PreviewProvider.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/preview/PreviewProvider.kt
new file mode 100644
index 0000000..7a1925c
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/preview/PreviewProvider.kt
@@ -0,0 +1,18 @@
+package com.mirego.kmp.boilerplate.app.ui.preview
+
+import androidx.compose.runtime.Composable
+import com.mirego.kmp.boilerplate.BuildConfig
+import com.mirego.kmp.boilerplate.app.resources.AndroidImageProvider
+import com.mirego.kmp.boilerplate.viewmodel.factory.ViewModelFactoryPreview
+import com.mirego.trikot.kword.android.PreviewI18N
+import com.mirego.trikot.viewmodels.declarative.configuration.TrikotViewModelDeclarative
+
+@Composable
+fun PreviewProvider(content: @Composable (ViewModelFactoryPreview) -> Unit) {
+ TrikotViewModelDeclarative.initialize(AndroidImageProvider())
+ val viewModelFactoryPreview = ViewModelFactoryPreview(
+ i18N = PreviewI18N(BuildConfig.KWORD_TRANSLATION_FILE_PATH)
+ )
+
+ content(viewModelFactoryPreview)
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/root/RootView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/root/RootView.kt
new file mode 100644
index 0000000..540a442
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/root/RootView.kt
@@ -0,0 +1,39 @@
+package com.mirego.kmp.boilerplate.app.ui.root
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider
+import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModel
+import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsState
+
+@Composable
+fun RootView(rootViewModel: RootViewModel) {
+ val viewModel: RootViewModel by rootViewModel.observeAsState()
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ ) {
+ Text(
+ modifier = Modifier
+ .align(Alignment.Center),
+ text = "Hi"
+ )
+ }
+}
+
+@Preview
+@Composable
+fun PreviewRootView() {
+ PreviewProvider {
+ RootView(rootViewModel = it.createRoot())
+ }
+}
diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/trikot/viewmodels/declarative/compose/VMDViewModelStoreOwnerExtensions.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/trikot/viewmodels/declarative/compose/VMDViewModelStoreOwnerExtensions.kt
new file mode 100644
index 0000000..02ec836
--- /dev/null
+++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/trikot/viewmodels/declarative/compose/VMDViewModelStoreOwnerExtensions.kt
@@ -0,0 +1,25 @@
+package com.mirego.kmp.boilerplate.trikot.viewmodels.declarative.compose
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+import com.mirego.trikot.viewmodels.declarative.viewmodel.VMDViewModel
+
+/**
+ * Use this method to get the initial VMDViewModel. This will make sure that it survives Activity recreation by wrapping it in a androidx.lifecycle.ViewModel
+ */
+@Suppress("UNCHECKED_CAST")
+fun ViewModelStoreOwner.getInitialViewModel(factory: () -> VMD): VMD =
+ ViewModelProvider(
+ viewModelStore,
+ ViewModelProviderFactory(factory)
+ )[ViewModelWrapper::class.java].wrappedViewModel as VMD
+
+@Suppress("UNCHECKED_CAST")
+private class ViewModelProviderFactory(private val factory: () -> VMD) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): VM {
+ return ViewModelWrapper(factory()) as VM
+ }
+}
+
+class ViewModelWrapper(val wrappedViewModel: VMD) : ViewModel()
diff --git a/build.gradle.kts b/build.gradle.kts
index 6ca3283..4d1cd13 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,3 +24,12 @@ dependencyCheck {
knownExploitedEnabled = true
}
}
+
+// CheckCommon with Lint Format
+val cclf: Task by tasks.creating {
+ group = "verification"
+ description = "Like checkCommon, but adds automatic lint fixing"
+ dependsOn("shared:ktlintFormat")
+ dependsOn("androidApp:ktlintFormat")
+ dependsOn("shared:testReleaseUnitTest")
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d29dcee..4490371 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,7 @@
[versions]
androidComposeCompiler = "1.5.3"
androidGradlePlugin = "8.1.2"
+android-splash = "1.0.1"
androidxActivityCompose = "1.8.0"
androidxAppcompat = "1.6.1"
androidxComposeBom = "2023.10.01"
@@ -18,6 +19,7 @@ owasp = "8.4.2"
trikot = "5.2.0"
[libraries]
+android-splash = { module = "androidx.core:core-splashscreen", version.ref = "android-splash" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml
index 45613aa..7c22191 100644
--- a/ios/.swiftlint.yml
+++ b/ios/.swiftlint.yml
@@ -1,23 +1,15 @@
-# Unless otherwise noted, rules are disabled because they reduce the readability of the code
-# See https://realm.github.io/SwiftLint/rule-directory.html for more info on each rule
-
-analyzer_rules:
- - explicit_self
- - unused_declaration
- - unused_import
-
-# Rule identifiers to exclude from running
disabled_rules:
- - comment_spacing
- force_cast
- function_body_length
+ - discouraged_optional_collection
+ - type_name
+ - private_over_fileprivate
+ - multiple_closures_with_trailing_closure
+ - inclusive_language
+ - blanket_disable_command
-# All opt-in rules are listed, with the ones we do not want commented out so we can know which rules we don't want and which rules are new when upgrading Swiftlint
opt_in_rules:
- - anyobject_protocol
- array_init
- - attributes
- #- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
@@ -36,55 +28,36 @@ opt_in_rules:
- empty_xctest_method
- enum_case_associated_values_count
- expiring_todo
- #- explicit_acl
- #- explicit_enum_raw_value
- explicit_init
- #- explicit_top_level_acl
- #- explicit_type_interface
- extension_access_modifier
- fallthrough
- fatal_error_message
- file_header
- #- file_name
- file_name_no_space
- #- file_types_order
- first_where
- flatmap_over_map_reduce
- #- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
- #- implicitly_unwrapped_optional
- #- indentation_width
- joined_default_parameter
- last_where
- legacy_multiple
- legacy_random
- - let_var_whitespace
- literal_expression_end_indentation
- #- lower_acl_than_parent
- #- missing_docs
- modifier_order
- #- multiline_arguments
- #- multiline_arguments_brackets
- #- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- nimble_operator
- - no_extension_access_modifier
- #- no_grouping_extension
- nslocalizedstring_key
- nslocalizedstring_require_bundle
- number_separator
- - object_literal
- operator_usage_whitespace
- optional_enum_case_matching
- overridden_super_call
- override_in_extension
- pattern_matching_keywords
- prefer_self_type_over_type_of_self
- - prefer_zero_over_explicit_init
- prefixed_toplevel_constant
- private_action
- private_outlet
@@ -97,45 +70,35 @@ opt_in_rules:
- reduce_into
- redundant_nil_coalescing
- redundant_type_annotation
- #- required_deinit
- #- required_enum_case
- single_test_class
- sorted_first_last
- - sorted_imports
- static_operator
- - strict_fileprivate
- strong_iboutlet
- #- switch_case_on_newline
- toggle_bool
- - trailing_closure
- #- type_contents_order
- unavailable_function
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- #- vertical_parameter_alignment_on_call
- #- vertical_whitespace_between_cases
- #- vertical_whitespace_closing_braces
- #- vertical_whitespace_opening_braces
- xct_specific_matcher
- yoda_condition
-# Paths to exclude
+analyzer_rules:
+ - explicit_self
+ - unused_declaration
+ - unused_import
+
excluded:
- Pods
- Data
- R.generated.swift
- .bundle
- # Add your own exclusion here
conditional_returns_on_newline:
if_only: true
# Use the same deployment target as the project
deployment_target:
- iOS_deployment_target: 9.0
- tvOS_deployment_target: 9.1
- watchOS_deployment_target: 2.0
+ iOS_deployment_target: 15.0
file_length:
ignore_comment_only_lines: true
@@ -161,6 +124,3 @@ cyclomatic_complexity:
discouraged_object_literal:
color_literal: false
-
-attributes:
- always_on_line_above: [ "@objc" ]
diff --git a/ios/iosApp.xcodeproj/project.pbxproj b/ios/iosApp.xcodeproj/project.pbxproj
index 7cc1b97..ce1d6bc 100644
--- a/ios/iosApp.xcodeproj/project.pbxproj
+++ b/ios/iosApp.xcodeproj/project.pbxproj
@@ -10,9 +10,16 @@
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
- 7555FF83242A565900829871 /* GreetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* GreetingView.swift */; };
+ 7555FF83242A565900829871 /* ApplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ApplicationView.swift */; };
+ 89011D492AE9A3690073544B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D482AE9A3690073544B /* AppDelegate.swift */; };
+ 89011D4B2AE9A4CD0073544B /* BootstrapImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D4A2AE9A4CD0073544B /* BootstrapImpl.swift */; };
+ 89011D4D2AE9A4FC0073544B /* AppEnvironment+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D4C2AE9A4FC0073544B /* AppEnvironment+iOS.swift */; };
+ 89011D4F2AE9A7340073544B /* AppInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D4E2AE9A7340073544B /* AppInitializer.swift */; };
+ 89011D512AE9A7750073544B /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D502AE9A7750073544B /* ImageProvider.swift */; };
+ 89011D552AE9A8150073544B /* AppInformationImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D532AE9A8150073544B /* AppInformationImpl.swift */; };
+ 89011D5B2AE9B0310073544B /* PreviewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89011D5A2AE9B0310073544B /* PreviewHelpers.swift */; };
+ 895BECFB2AEAB3DC005B1212 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BECFA2AEAB3DC005B1212 /* RootView.swift */; };
9B8ACFDB4E332DFCA8B97CBB /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E4E1328B104D05A50A097EE /* Pods_iosApp.framework */; };
- BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC83B465276E4F080053E064 /* FlowUtils.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -22,9 +29,16 @@
308A8A1989CCC0B3DD133EE0 /* 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 = ""; };
4E4E1328B104D05A50A097EE /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 7555FF82242A565900829871 /* GreetingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GreetingView.swift; sourceTree = ""; };
+ 7555FF82242A565900829871 /* ApplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- BC83B465276E4F080053E064 /* FlowUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowUtils.swift; sourceTree = ""; };
+ 89011D482AE9A3690073544B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 89011D4A2AE9A4CD0073544B /* BootstrapImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootstrapImpl.swift; sourceTree = ""; };
+ 89011D4C2AE9A4FC0073544B /* AppEnvironment+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppEnvironment+iOS.swift"; sourceTree = ""; };
+ 89011D4E2AE9A7340073544B /* AppInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppInitializer.swift; sourceTree = ""; };
+ 89011D502AE9A7750073544B /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; };
+ 89011D532AE9A8150073544B /* AppInformationImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppInformationImpl.swift; sourceTree = ""; };
+ 89011D5A2AE9B0310073544B /* PreviewHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewHelpers.swift; sourceTree = ""; };
+ 895BECFA2AEAB3DC005B1212 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; };
E232C917135C2C1E3BC8748A /* 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 = ""; };
/* End PBXFileReference section */
@@ -69,22 +83,61 @@
7555FF7D242A565900829871 /* iosApp */ = {
isa = PBXGroup;
children = (
+ 89011D4E2AE9A7340073544B /* AppInitializer.swift */,
+ 89011D482AE9A3690073544B /* AppDelegate.swift */,
+ 2152FB032600AC8F00CF470E /* iOSApp.swift */,
+ 89011D562AE9AFF90073544B /* Domain */,
+ 89011D572AE9AFFF0073544B /* UI */,
7555FF8C242A565B00829871 /* Info.plist */,
058557BA273AAA24004C7B11 /* Assets.xcassets */,
- 2152FB032600AC8F00CF470E /* iOSApp.swift */,
- 7555FF82242A565900829871 /* GreetingView.swift */,
- BC83B464276E4EF80053E064 /* Utils */,
058557D7273AAEEB004C7B11 /* Preview Content */,
);
path = iosApp;
sourceTree = "";
};
- BC83B464276E4EF80053E064 /* Utils */ = {
+ 89011D562AE9AFF90073544B /* Domain */ = {
+ isa = PBXGroup;
+ children = (
+ 89011D532AE9A8150073544B /* AppInformationImpl.swift */,
+ 89011D4C2AE9A4FC0073544B /* AppEnvironment+iOS.swift */,
+ 89011D4A2AE9A4CD0073544B /* BootstrapImpl.swift */,
+ 89011D502AE9A7750073544B /* ImageProvider.swift */,
+ );
+ path = Domain;
+ sourceTree = "";
+ };
+ 89011D572AE9AFFF0073544B /* UI */ = {
+ isa = PBXGroup;
+ children = (
+ 89011D582AE9B00F0073544B /* Application */,
+ 895BECF92AEAB3D1005B1212 /* Root */,
+ 89011D592AE9B0160073544B /* Previews */,
+ );
+ path = UI;
+ sourceTree = "";
+ };
+ 89011D582AE9B00F0073544B /* Application */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF82242A565900829871 /* ApplicationView.swift */,
+ );
+ path = Application;
+ sourceTree = "";
+ };
+ 89011D592AE9B0160073544B /* Previews */ = {
+ isa = PBXGroup;
+ children = (
+ 89011D5A2AE9B0310073544B /* PreviewHelpers.swift */,
+ );
+ path = Previews;
+ sourceTree = "";
+ };
+ 895BECF92AEAB3D1005B1212 /* Root */ = {
isa = PBXGroup;
children = (
- BC83B465276E4F080053E064 /* FlowUtils.swift */,
+ 895BECFA2AEAB3DC005B1212 /* RootView.swift */,
);
- path = Utils;
+ path = Root;
sourceTree = "";
};
C8C629BFDC2144230B71E3BC /* Frameworks */ = {
@@ -256,9 +309,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 89011D492AE9A3690073544B /* AppDelegate.swift in Sources */,
+ 89011D4D2AE9A4FC0073544B /* AppEnvironment+iOS.swift in Sources */,
+ 89011D5B2AE9B0310073544B /* PreviewHelpers.swift in Sources */,
+ 89011D4F2AE9A7340073544B /* AppInitializer.swift in Sources */,
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
- 7555FF83242A565900829871 /* GreetingView.swift in Sources */,
- BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */,
+ 89011D512AE9A7750073544B /* ImageProvider.swift in Sources */,
+ 89011D4B2AE9A4CD0073544B /* BootstrapImpl.swift in Sources */,
+ 895BECFB2AEAB3DC005B1212 /* RootView.swift in Sources */,
+ 7555FF83242A565900829871 /* ApplicationView.swift in Sources */,
+ 89011D552AE9A8150073544B /* AppInformationImpl.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/ios/iosApp/AppDelegate.swift b/ios/iosApp/AppDelegate.swift
new file mode 100644
index 0000000..0430115
--- /dev/null
+++ b/ios/iosApp/AppDelegate.swift
@@ -0,0 +1,21 @@
+import Shared
+import UIKit
+
+class AppDelegate: NSObject, UIApplicationDelegate {
+ let bootstrapper = Bootstrapper()
+
+ lazy var applicationViewModel: ApplicationViewModel = {
+ bootstrapper.applicationViewModel()
+ }()
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ let bootstrap = BootstrapImpl()
+ AppInitializer.initializeComponents(environment: bootstrap.environment)
+ bootstrapper.doInitDependencies(bootstrap: bootstrap)
+
+ return true
+ }
+}
diff --git a/ios/iosApp/AppInitializer.swift b/ios/iosApp/AppInitializer.swift
new file mode 100644
index 0000000..8169c04
--- /dev/null
+++ b/ios/iosApp/AppInitializer.swift
@@ -0,0 +1,23 @@
+import Foundation
+import Kingfisher
+import Shared
+import Trikot
+
+enum AppInitializer {
+ static func initializeComponents(environment: AppEnvironment) {
+ initializeCommon()
+ initializeKingfisher()
+ }
+
+ private static func initializeCommon() {
+ TrikotKword.shared.setCurrentLanguage(LocaleUtils().supportedLanguageCode())
+ TrikotViewModelDeclarative.shared.initialize(
+ imageProvider: ImageProvider(),
+ spanStyleProvider: DefaultSpanStyleProvider()
+ )
+ }
+
+ private static func initializeKingfisher() {
+ ImageCache.default.diskStorage.config.sizeLimit = 500 * 1_024 * 1_024 // 500 MB
+ }
+}
diff --git a/ios/iosApp/Domain/AppEnvironment+iOS.swift b/ios/iosApp/Domain/AppEnvironment+iOS.swift
new file mode 100644
index 0000000..e99657a
--- /dev/null
+++ b/ios/iosApp/Domain/AppEnvironment+iOS.swift
@@ -0,0 +1,43 @@
+import Foundation
+import Shared
+
+extension AppEnvironment {
+ private static let environmentKey = "Environment"
+
+ static var current: AppEnvironment {
+ loadCurrentEnvironment()
+ }
+
+ private static func persistCurrentEnvironment(_ environment: AppEnvironment) {
+ UserDefaults.standard.set(environment.key, forKey: environmentKey)
+ UserDefaults.standard.synchronize()
+ }
+
+ private static var defaultEnvironment: AppEnvironment {
+ #if DEBUG
+ return .dev
+ #else
+ return .production
+ #endif
+ }
+
+ private static func loadCurrentEnvironment() -> AppEnvironment {
+ if let environmentValue = UserDefaults.standard.string(forKey: environmentKey), let environment = environmentFromString(environmentValue) {
+ return environment
+ } else if let environmentValue = Bundle.main.infoDictionary?[environmentKey] as? String, let environment = environmentFromString(environmentValue) {
+ return environment
+ }
+ return defaultEnvironment
+ }
+
+ private static func environmentFromString(_ stringValue: String) -> AppEnvironment? {
+ switch stringValue.lowercased() {
+ case AppEnvironment.dev.key:
+ return .dev
+ case AppEnvironment.production.key:
+ return .production
+ default:
+ return nil
+ }
+ }
+}
diff --git a/ios/iosApp/Domain/AppInformationImpl.swift b/ios/iosApp/Domain/AppInformationImpl.swift
new file mode 100644
index 0000000..a9476c8
--- /dev/null
+++ b/ios/iosApp/Domain/AppInformationImpl.swift
@@ -0,0 +1,21 @@
+import Foundation
+import Shared
+
+class AppInformationImpl: AppInformation {
+ let locale: Shared.Locale
+ let versionNumber: String
+ let diskCachePath: String
+
+ init (environmentKey: String) {
+ let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? ""
+ let build = Bundle.main.infoDictionary?["CFBundleVersion"] ?? ""
+ versionNumber = "\(version).\(build)"
+
+ let cacheUrl = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
+ diskCachePath = cacheUrl + "/\(environmentKey)/"
+
+ let language = LocaleUtils().supportedLanguageCode() == "fr" ? Shared.Language.french : Shared.Language.english
+ let regionCode = Foundation.Locale.current.regionCode
+ locale = Shared.Locale(language: language, regionCode: regionCode)
+ }
+}
diff --git a/ios/iosApp/Domain/BootstrapImpl.swift b/ios/iosApp/Domain/BootstrapImpl.swift
new file mode 100644
index 0000000..df75fa2
--- /dev/null
+++ b/ios/iosApp/Domain/BootstrapImpl.swift
@@ -0,0 +1,12 @@
+import Shared
+import SwiftUI
+import Trikot
+
+final class BootstrapImpl: Bootstrap {
+ let appInformation: AppInformation
+ let environment = AppEnvironment.current
+
+ init() {
+ appInformation = AppInformationImpl(environmentKey: environment.key)
+ }
+}
diff --git a/ios/iosApp/Domain/ImageProvider.swift b/ios/iosApp/Domain/ImageProvider.swift
new file mode 100644
index 0000000..24a738f
--- /dev/null
+++ b/ios/iosApp/Domain/ImageProvider.swift
@@ -0,0 +1,9 @@
+import Shared
+import SwiftUI
+import Trikot
+
+final class ImageProvider: VMDImageProvider {
+ func imageForResource(imageResource: VMDImageResource) -> Image? {
+ nil
+ }
+}
diff --git a/ios/iosApp/GreetingView.swift b/ios/iosApp/GreetingView.swift
deleted file mode 100644
index 36b0f57..0000000
--- a/ios/iosApp/GreetingView.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import Shared
-import SwiftUI
-
-struct GreetingView: View {
-
- @ObservedObject var greet = ObservableFlowWrapper(Greeting().greeting(), initial: "initial")
-
- var body: some View {
- Text("\(greet.value)")
- }
-}
-
-struct GreetingView_Previews: PreviewProvider {
- static var previews: some View {
- GreetingView()
- }
-}
diff --git a/ios/iosApp/UI/Application/ApplicationView.swift b/ios/iosApp/UI/Application/ApplicationView.swift
new file mode 100644
index 0000000..be21bc3
--- /dev/null
+++ b/ios/iosApp/UI/Application/ApplicationView.swift
@@ -0,0 +1,25 @@
+import Shared
+import SwiftUI
+import Trikot
+
+struct ApplicationView: View {
+ @ObservedObject private var observableViewModel: ObservableViewModelAdapter
+
+ init(viewModel: ApplicationViewModel) {
+ observableViewModel = viewModel.asObservable()
+ }
+
+ var viewModel: ApplicationViewModel {
+ observableViewModel.viewModel
+ }
+
+ var body: some View {
+ RootView(viewModel: viewModel.rootViewModel)
+ }
+}
+
+struct ApplicationView_Previews: PreviewProvider {
+ static var previews: some View {
+ ApplicationView(viewModel: factoryPreview().createApplication())
+ }
+}
diff --git a/ios/iosApp/UI/Previews/PreviewHelpers.swift b/ios/iosApp/UI/Previews/PreviewHelpers.swift
new file mode 100644
index 0000000..aeef041
--- /dev/null
+++ b/ios/iosApp/UI/Previews/PreviewHelpers.swift
@@ -0,0 +1,14 @@
+import Shared
+import SwiftUI
+import Trikot
+
+func previewi18N(languageCode: String) -> I18N {
+ let i18N = DefaultI18N()
+ KwordLoader.shared.setCurrentLanguageCode(i18N: i18N, basePaths: ["translation"], code: languageCode)
+ return i18N
+}
+
+func factoryPreview(languageCode: String = "en") -> ViewModelFactoryPreview {
+ let i18N = previewi18N(languageCode: languageCode)
+ return ViewModelFactoryPreview(i18N: i18N)
+}
diff --git a/ios/iosApp/UI/Root/RootView.swift b/ios/iosApp/UI/Root/RootView.swift
new file mode 100644
index 0000000..b350e7c
--- /dev/null
+++ b/ios/iosApp/UI/Root/RootView.swift
@@ -0,0 +1,25 @@
+import Shared
+import SwiftUI
+import Trikot
+
+struct RootView: View {
+ @ObservedObject private var observableViewModel: ObservableViewModelAdapter
+
+ init(viewModel: RootViewModel) {
+ observableViewModel = viewModel.asObservable()
+ }
+
+ var viewModel: RootViewModel {
+ observableViewModel.viewModel
+ }
+
+ var body: some View {
+ Text("Hi")
+ }
+}
+
+struct RootView_Previews: PreviewProvider {
+ static var previews: some View {
+ RootView(viewModel: factoryPreview().createRoot())
+ }
+}
diff --git a/ios/iosApp/Utils/FlowUtils.swift b/ios/iosApp/Utils/FlowUtils.swift
deleted file mode 100644
index e6394e0..0000000
--- a/ios/iosApp/Utils/FlowUtils.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-import Foundation
-import Shared
-
-public class ObservableFlowWrapper: ObservableObject {
-
- @Published public private(set) var value: T
-
- private var watcher: Closeable_?
-
- public init(_ flow: CFlow, initial value: T) {
- self.value = value
-
- watcher = flow.watch { [weak self] t in
- self?.value = t
- }
- }
-
- deinit {
- watcher?.close()
- }
-}
diff --git a/ios/iosApp/iOSApp.swift b/ios/iosApp/iOSApp.swift
index 947b0aa..81035d7 100644
--- a/ios/iosApp/iOSApp.swift
+++ b/ios/iosApp/iOSApp.swift
@@ -2,9 +2,11 @@ import SwiftUI
@main
struct IOSApp: App {
- var body: some Scene {
- WindowGroup {
- GreetingView()
- }
- }
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+
+ var body: some Scene {
+ WindowGroup {
+ ApplicationView(viewModel: appDelegate.applicationViewModel)
+ }
+ }
}
diff --git a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt b/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
deleted file mode 100644
index 83f3306..0000000
--- a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-actual class Platform actual constructor() {
- actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
-}
diff --git a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt b/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
new file mode 100644
index 0000000..def46b5
--- /dev/null
+++ b/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
@@ -0,0 +1,11 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+import java.util.Locale
+
+actual object LocaleUtils {
+ actual fun supportedLanguageCode() = if (Locale.getDefault().language.lowercase() == "fr") {
+ "fr"
+ } else {
+ "en"
+ }
+}
diff --git a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
deleted file mode 100644
index 1bfe07e..0000000
--- a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.mirego.kmp.boilerplate.utils
-
-import kotlinx.coroutines.flow.Flow
-
-actual class CFlow internal constructor(origin: Flow) : Flow by origin
-
-actual fun Flow.wrap(): CFlow = CFlow(this)
diff --git a/shared/src/androidUnitTest/kotlin/com.mirego.kmp.boilerplate/AndroidGreetingTest.kt b/shared/src/androidUnitTest/kotlin/com.mirego.kmp.boilerplate/AndroidGreetingTest.kt
deleted file mode 100644
index 714b84f..0000000
--- a/shared/src/androidUnitTest/kotlin/com.mirego.kmp.boilerplate/AndroidGreetingTest.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import kotlin.test.Test
-import kotlin.test.assertTrue
-import kotlinx.coroutines.flow.single
-import kotlinx.coroutines.test.runTest
-
-class AndroidGreetingTest {
-
- @Test
- fun testExample() = runTest {
- assertTrue(Greeting().greeting().single().contains("Android"), "Check Android is mentioned")
- }
-}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt
deleted file mode 100644
index 2b88fdf..0000000
--- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import com.mirego.kmp.boilerplate.utils.CFlow
-import com.mirego.kmp.boilerplate.utils.wrap
-import kotlinx.coroutines.flow.flowOf
-
-class Greeting {
- fun greeting(): CFlow = flowOf("Hello, ${Platform().platform}!").wrap()
-}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
deleted file mode 100644
index 514e746..0000000
--- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-expect class Platform() {
- val platform: String
-}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/SharedModule.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/SharedModule.kt
new file mode 100644
index 0000000..2b06eb8
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/SharedModule.kt
@@ -0,0 +1,8 @@
+package com.mirego.kmp.boilerplate
+
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module
+@ComponentScan
+class SharedModule
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppEnvironment.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppEnvironment.kt
new file mode 100644
index 0000000..d31e652
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppEnvironment.kt
@@ -0,0 +1,12 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+enum class AppEnvironment(
+ val key: String
+) {
+ DEV(
+ key = "dev"
+ ),
+ PRODUCTION(
+ key = "production"
+ )
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppInformation.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppInformation.kt
new file mode 100644
index 0000000..0115a4c
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/AppInformation.kt
@@ -0,0 +1,9 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+import com.mirego.kmp.boilerplate.model.Locale
+
+interface AppInformation {
+ val locale: Locale
+ val versionNumber: String
+ val diskCachePath: String
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrap.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrap.kt
new file mode 100644
index 0000000..4134f86
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrap.kt
@@ -0,0 +1,6 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+interface Bootstrap {
+ val environment: AppEnvironment
+ val appInformation: AppInformation
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrapper.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrapper.kt
new file mode 100644
index 0000000..e9aecf9
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Bootstrapper.kt
@@ -0,0 +1,29 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+import com.mirego.kmp.boilerplate.viewmodel.application.ApplicationViewModel
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import org.koin.core.context.startKoin
+import org.koin.core.parameter.parametersOf
+
+class Bootstrapper : KoinComponent {
+ private val exceptionHandler: CoroutineExceptionHandler =
+ CoroutineExceptionHandler { _, throwable ->
+ println("Exception: $throwable")
+ }
+
+ private val rootCoroutineScope =
+ CoroutineScope(Dispatchers.Main.immediate + SupervisorJob() + exceptionHandler)
+
+ fun initDependencies(bootstrap: Bootstrap) = startKoin {
+ configureKoin(bootstrap)
+ }
+
+ fun applicationViewModel(): ApplicationViewModel = get {
+ parametersOf(rootCoroutineScope)
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
new file mode 100644
index 0000000..df09234
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
@@ -0,0 +1,5 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+expect object LocaleUtils {
+ fun supportedLanguageCode(): String
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Module.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Module.kt
new file mode 100644
index 0000000..67f59b8
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/Module.kt
@@ -0,0 +1,50 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+import com.mirego.kmp.boilerplate.SharedModule
+import com.mirego.trikot.kword.I18N
+import com.mirego.trikot.kword.KWord
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import org.koin.core.KoinApplication
+import org.koin.core.module.Module
+import org.koin.core.qualifier.StringQualifier
+import org.koin.dsl.module
+import org.koin.ksp.generated.module
+
+fun KoinApplication.configureKoin(bootstrap: Bootstrap) {
+ modules(
+ generalModule(bootstrap),
+ SharedModule().module
+ )
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+fun buildJson() = Json {
+ explicitNulls = false
+ isLenient = true
+ ignoreUnknownKeys = true
+}
+
+fun generalModule(bootstrap: Bootstrap): Module {
+ return module {
+ single { KWord }
+ single { bootstrap.environment }
+ single { bootstrap.appInformation }
+ single { bootstrap.appInformation.locale.language }
+ single(
+ StringQualifier(ModuleQualifier.REGION_CODE)
+ ) { bootstrap.appInformation.locale.regionCode }
+ single(
+ StringQualifier(ModuleQualifier.DISK_CACHE_PATH)
+ ) {
+ bootstrap.appInformation.diskCachePath +
+ "/${bootstrap.appInformation.locale.language.toLangCode()}"
+ }
+ single { buildJson() }
+ }
+}
+
+object ModuleQualifier {
+ const val DISK_CACHE_PATH = "diskCachePath"
+ const val REGION_CODE = "regionCode"
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/model/Locale.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/model/Locale.kt
new file mode 100644
index 0000000..8a16df9
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/model/Locale.kt
@@ -0,0 +1,16 @@
+package com.mirego.kmp.boilerplate.model
+
+data class Locale(
+ val language: Language,
+ val regionCode: String?
+)
+
+enum class Language {
+ ENGLISH,
+ FRENCH;
+
+ fun toLangCode() = when (this) {
+ ENGLISH -> "en"
+ FRENCH -> "fr"
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
deleted file mode 100644
index fe4d8d3..0000000
--- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.mirego.kmp.boilerplate.utils
-
-import kotlinx.coroutines.flow.Flow
-
-expect class CFlow : Flow
-
-expect fun Flow.wrap(): CFlow
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModel.kt
new file mode 100644
index 0000000..a97e442
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModel.kt
@@ -0,0 +1,8 @@
+package com.mirego.kmp.boilerplate.viewmodel.application
+
+import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModel
+import com.mirego.trikot.viewmodels.declarative.viewmodel.VMDViewModel
+
+interface ApplicationViewModel : VMDViewModel {
+ val rootViewModel: RootViewModel
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModelImpl.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModelImpl.kt
new file mode 100644
index 0000000..ac68d8a
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/application/ApplicationViewModelImpl.kt
@@ -0,0 +1,14 @@
+package com.mirego.kmp.boilerplate.viewmodel.application
+
+import com.mirego.kmp.boilerplate.viewmodel.factory.ViewModelFactory
+import com.mirego.trikot.viewmodels.declarative.viewmodel.VMDViewModelImpl
+import kotlinx.coroutines.CoroutineScope
+import org.koin.core.annotation.Factory
+
+@Factory
+class ApplicationViewModelImpl(
+ viewModelFactory: ViewModelFactory,
+ coroutineScope: CoroutineScope
+) : ApplicationViewModel, VMDViewModelImpl(coroutineScope) {
+ override val rootViewModel = viewModelFactory.createRoot(coroutineScope)
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactory.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactory.kt
new file mode 100644
index 0000000..7952f4f
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactory.kt
@@ -0,0 +1,8 @@
+package com.mirego.kmp.boilerplate.viewmodel.factory
+
+import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModel
+import kotlinx.coroutines.CoroutineScope
+
+interface ViewModelFactory {
+ fun createRoot(coroutineScope: CoroutineScope): RootViewModel
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryImpl.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryImpl.kt
new file mode 100644
index 0000000..c5e5111
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryImpl.kt
@@ -0,0 +1,15 @@
+package com.mirego.kmp.boilerplate.viewmodel.factory
+
+import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModel
+import kotlinx.coroutines.CoroutineScope
+import org.koin.core.annotation.Single
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import org.koin.core.parameter.parametersOf
+
+@Single
+class ViewModelFactoryImpl : ViewModelFactory, KoinComponent {
+ override fun createRoot(coroutineScope: CoroutineScope): RootViewModel = get {
+ parametersOf(coroutineScope)
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryPreview.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryPreview.kt
new file mode 100644
index 0000000..579ee05
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/factory/ViewModelFactoryPreview.kt
@@ -0,0 +1,33 @@
+package com.mirego.kmp.boilerplate.viewmodel.factory
+
+import com.mirego.kmp.boilerplate.viewmodel.application.ApplicationViewModelImpl
+import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModelImpl
+import com.mirego.trikot.kword.I18N
+import com.mirego.trikot.viewmodels.declarative.util.CoroutineScopeProvider
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class ViewModelFactoryPreview(
+ private val i18N: I18N
+) : ViewModelFactory {
+
+ fun createCoroutineScope() = CoroutineScopeProvider.provideMainWithSuperviserJob(
+ CoroutineExceptionHandler { _, exception ->
+ println("CoroutineExceptionHandler got $exception")
+ }
+ )
+
+ fun createApplication() = ApplicationViewModelImpl(
+ this,
+ createCoroutineScope()
+ )
+
+ override fun createRoot(coroutineScope: CoroutineScope) = createRoot()
+
+ fun createRoot() = RootViewModelImpl(
+ i18N = i18N,
+ viewModelFactory = this,
+ coroutineScope = createCoroutineScope()
+ )
+}
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModel.kt
new file mode 100644
index 0000000..5f717fb
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModel.kt
@@ -0,0 +1,5 @@
+package com.mirego.kmp.boilerplate.viewmodel.root
+
+import com.mirego.trikot.viewmodels.declarative.viewmodel.VMDViewModel
+
+interface RootViewModel : VMDViewModel
diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModelImpl.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModelImpl.kt
new file mode 100644
index 0000000..69d1197
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/root/RootViewModelImpl.kt
@@ -0,0 +1,14 @@
+package com.mirego.kmp.boilerplate.viewmodel.root
+
+import com.mirego.kmp.boilerplate.viewmodel.factory.ViewModelFactory
+import com.mirego.trikot.kword.I18N
+import com.mirego.trikot.viewmodels.declarative.viewmodel.VMDViewModelImpl
+import kotlinx.coroutines.CoroutineScope
+import org.koin.core.annotation.Factory
+
+@Factory
+class RootViewModelImpl(
+ i18N: I18N,
+ viewModelFactory: ViewModelFactory,
+ coroutineScope: CoroutineScope
+) : RootViewModel, VMDViewModelImpl(coroutineScope)
diff --git a/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/CommonGreetingTest.kt b/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/CommonGreetingTest.kt
deleted file mode 100644
index 77b73f6..0000000
--- a/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/CommonGreetingTest.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import kotlin.test.Test
-import kotlin.test.assertTrue
-import kotlinx.coroutines.flow.single
-import kotlinx.coroutines.test.runTest
-
-class CommonGreetingTest {
-
- @Test
- fun testExample() = runTest {
- assertTrue(Greeting().greeting().single().contains("Hello"), "Check 'Hello' is mentioned")
- }
-}
diff --git a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt b/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
deleted file mode 100644
index f876564..0000000
--- a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/Platform.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import platform.UIKit.UIDevice
-
-actual class Platform actual constructor() {
- actual val platform: String =
- "${UIDevice.currentDevice.systemName()} ${UIDevice.currentDevice.systemVersion}"
-}
diff --git a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt b/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
new file mode 100644
index 0000000..a9fc55e
--- /dev/null
+++ b/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/bootstrap/LocaleUtils.kt
@@ -0,0 +1,15 @@
+package com.mirego.kmp.boilerplate.bootstrap
+
+import platform.Foundation.NSLocale
+import platform.Foundation.preferredLanguages
+
+actual object LocaleUtils {
+ actual fun supportedLanguageCode(): String {
+ val preferredLanguage: String? = NSLocale.preferredLanguages.firstOrNull() as? String
+ return if (preferredLanguage?.contains("fr") == true) {
+ "fr"
+ } else {
+ "en"
+ }
+ }
+}
diff --git a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
deleted file mode 100644
index 30d013c..0000000
--- a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-@file:Suppress("unused")
-
-package com.mirego.kmp.boilerplate.utils
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-
-fun interface Closeable {
- fun close()
-}
-
-actual class CFlow internal constructor(
- private val origin: Flow,
- private val dispatcher: CoroutineDispatcher = Dispatchers.Main
-) : Flow by origin {
-
- fun watch(block: (T) -> Unit): Closeable {
- val job = Job()
- val scope = CoroutineScope(dispatcher + job)
-
- onEach {
- block(it)
- }.launchIn(scope)
-
- return Closeable { job.cancel() }
- }
-}
-
-actual fun Flow.wrap(): CFlow = CFlow(this)
diff --git a/shared/src/iosTest/kotlin/com/mirego/kmp/boilerplate/IosGreetingTest.kt b/shared/src/iosTest/kotlin/com/mirego/kmp/boilerplate/IosGreetingTest.kt
deleted file mode 100644
index 8f79e2d..0000000
--- a/shared/src/iosTest/kotlin/com/mirego/kmp/boilerplate/IosGreetingTest.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.mirego.kmp.boilerplate
-
-import kotlin.test.Test
-import kotlin.test.assertTrue
-import kotlinx.coroutines.flow.single
-import kotlinx.coroutines.test.runTest
-
-class IosGreetingTest {
-
- @Test
- fun testExample() = runTest {
- assertTrue(Greeting().greeting().single().contains("iOS"), "Check iOS is mentioned")
- }
-}