diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Greeting.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Greeting.kt index ce721a5..ecd52a7 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Greeting.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Greeting.kt @@ -4,6 +4,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -12,7 +13,7 @@ import kotlinx.coroutines.flow.flowOf fun Greeting(textFlow: Flow) { val text: String by textFlow.collectAsState("initial") - Text(text = text) + Text(text = text, textAlign = TextAlign.Center) } @Preview diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Home.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Home.kt deleted file mode 100644 index bb3eacb..0000000 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Home.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mirego.kmp.boilerplate.android.composables - -import androidx.compose.runtime.Composable -import com.mirego.kmp.boilerplate.Greeting - -@Composable -fun Home() { - Greeting(textFlow = Greeting().greeting()) -} diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Main.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Main.kt index 94898a7..e8777cb 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Main.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/Main.kt @@ -3,14 +3,25 @@ package com.mirego.kmp.boilerplate.android.composables import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import com.mirego.kmp.boilerplate.routing.MainRouter -import com.mirego.kmp.boilerplate.routing.Screen +import com.mirego.kmp.boilerplate.android.composables.example.Example +import com.mirego.kmp.boilerplate.android.composables.home.Home +import com.mirego.kmp.boilerplate.presentation.routing.MainRouter +import com.mirego.kmp.boilerplate.presentation.routing.Router +import com.mirego.kmp.boilerplate.presentation.routing.Screen +import com.mirego.kmp.boilerplate.presentation.viewmodel.MobileViewModelFactory +import com.mirego.kmp.boilerplate.presentation.viewmodel.ViewModelFactory @Composable -fun Main() { - val screen: Screen by MainRouter.screen.collectAsState(initial = Screen.Home) +fun Main( + router: Router = MainRouter, + viewModelFactory: ViewModelFactory = MobileViewModelFactory +) { + val screen: Screen by router.screen.collectAsState(initial = Screen.Home) - when (screen) { - is Screen.Home -> Home() + screen.let { + when (it) { + Screen.Home -> Home(viewModel = viewModelFactory.homeViewModel) + is Screen.Example -> Example(viewModel = viewModelFactory.exampleViewModel(it.origin)) + } } } diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/example/Example.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/example/Example.kt new file mode 100644 index 0000000..935b95a --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/example/Example.kt @@ -0,0 +1,41 @@ +package com.mirego.kmp.boilerplate.android.composables.example + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mirego.kmp.boilerplate.presentation.viewmodel.example.ExampleViewModel + +@Composable +fun Example(viewModel: ExampleViewModel) { + val exampleText: String by viewModel.exampleMessage.collectAsState(initial = "") + val backButtonText: String by viewModel.backButtonText.collectAsState(initial = "") + + Box(modifier = Modifier.fillMaxSize(1.0f), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = exampleText) + + Spacer(modifier = Modifier.height(8.dp)) + + Button(onClick = viewModel::onBackButtonClick) { + Text(text = backButtonText) + } + } + } +} + +@Preview +@Composable +fun PreviewExample() { + Example(viewModel = ExampleViewModel.Preview) +} diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/home/Home.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/home/Home.kt new file mode 100644 index 0000000..6352a27 --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/android/composables/home/Home.kt @@ -0,0 +1,41 @@ +package com.mirego.kmp.boilerplate.android.composables.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mirego.kmp.boilerplate.android.composables.Greeting +import com.mirego.kmp.boilerplate.presentation.viewmodel.home.HomeViewModel + +@Composable +fun Home(viewModel: HomeViewModel) { + Box(modifier = Modifier.fillMaxSize(1.0f), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Greeting(textFlow = viewModel.greetingMessage) + + Spacer(modifier = Modifier.height(8.dp)) + + val buttonText: String by viewModel.buttonText.collectAsState("") + + Button(onClick = viewModel::onButtonClick) { + Text(text = buttonText) + } + } + } +} + +@Preview +@Composable +fun PreviewHome() { + Home(viewModel = HomeViewModel.Preview) +} diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 7a93edc..af200ae 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ BC5CC7B427760F2C00426C97 /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* Home.swift */; }; BC64D820278C9DD700FAF397 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC64D81F278C9DD700FAF397 /* Main.swift */; }; BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC83B465276E4F080053E064 /* FlowUtils.swift */; }; + BCFFCFD9278F551300EAA9BA /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFFCFD8278F550800EAA9BA /* Example.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -27,6 +28,7 @@ 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BC64D81F278C9DD700FAF397 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; }; BC83B465276E4F080053E064 /* FlowUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowUtils.swift; sourceTree = ""; }; + BCFFCFD8278F550800EAA9BA /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.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 */ @@ -84,8 +86,9 @@ BC5CC7B327760ED800426C97 /* Views */ = { isa = PBXGroup; children = ( + BCFFCFD7278F550400EAA9BA /* Example */, + BCFFCFD6278F54FA00EAA9BA /* Home */, BC64D81F278C9DD700FAF397 /* Main.swift */, - 7555FF82242A565900829871 /* Home.swift */, ); path = Views; sourceTree = ""; @@ -98,6 +101,22 @@ path = Utils; sourceTree = ""; }; + BCFFCFD6278F54FA00EAA9BA /* Home */ = { + isa = PBXGroup; + children = ( + 7555FF82242A565900829871 /* Home.swift */, + ); + path = Home; + sourceTree = ""; + }; + BCFFCFD7278F550400EAA9BA /* Example */ = { + isa = PBXGroup; + children = ( + BCFFCFD8278F550800EAA9BA /* Example.swift */, + ); + path = Example; + sourceTree = ""; + }; C8C629BFDC2144230B71E3BC /* Frameworks */ = { isa = PBXGroup; children = ( @@ -215,6 +234,7 @@ BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */, BC64D820278C9DD700FAF397 /* Main.swift in Sources */, BC5CC7B427760F2C00426C97 /* Home.swift in Sources */, + BCFFCFD9278F551300EAA9BA /* Example.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iosApp/iosApp.xcworkspace/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcworkspace/xcshareddata/xcschemes/iosApp.xcscheme new file mode 100644 index 0000000..d8a8311 --- /dev/null +++ b/iosApp/iosApp.xcworkspace/xcshareddata/xcschemes/iosApp.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/iosApp/Views/Example/Example.swift b/iosApp/iosApp/Views/Example/Example.swift new file mode 100644 index 0000000..eb5e434 --- /dev/null +++ b/iosApp/iosApp/Views/Example/Example.swift @@ -0,0 +1,32 @@ +import SwiftUI +import shared + +struct Example: View { + + private let viewModel: ExampleViewModel + + @ObservedObject var message: ObservableFlowWrapper + @ObservedObject var backButtonText: ObservableFlowWrapper + + init(viewModel: ExampleViewModel) { + self.viewModel = viewModel + + message = ObservableFlowWrapper(viewModel.exampleMessage, initial: "") + backButtonText = ObservableFlowWrapper(viewModel.backButtonText, initial: "") + } + + var body: some View { + VStack(alignment: .center, spacing: 16) { + Text("\(message.value)") + + Button("\(backButtonText.value)", action: viewModel.onBackButtonClick) + } + } + +} + +struct Example_Previews: PreviewProvider { + static var previews: some View { + Example(viewModel: ExampleViewModelPreview()) + } +} diff --git a/iosApp/iosApp/Views/Home.swift b/iosApp/iosApp/Views/Home.swift deleted file mode 100644 index 550fd83..0000000 --- a/iosApp/iosApp/Views/Home.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI -import shared - -struct Home: View { - - @ObservedObject var greet = ObservableFlowWrapper(Greeting().greeting(), initial: "initial") - - var body: some View { - Text("\(greet.value)") - } -} - -struct Home_Previews: PreviewProvider { - static var previews: some View { - Home() - } -} diff --git a/iosApp/iosApp/Views/Home/Home.swift b/iosApp/iosApp/Views/Home/Home.swift new file mode 100644 index 0000000..72fb2db --- /dev/null +++ b/iosApp/iosApp/Views/Home/Home.swift @@ -0,0 +1,31 @@ +import SwiftUI +import shared + +struct Home: View { + + private let viewModel: HomeViewModel + + @ObservedObject var message: ObservableFlowWrapper + @ObservedObject var buttonText: ObservableFlowWrapper + + init(viewModel: HomeViewModel) { + self.viewModel = viewModel + + message = ObservableFlowWrapper(viewModel.greetingMessage, initial: "") + buttonText = ObservableFlowWrapper(viewModel.buttonText, initial: "") + } + + var body: some View { + VStack(alignment: .center, spacing: 16) { + Text("\(message.value)") + + Button("\(buttonText.value)", action: viewModel.onButtonClick) + } + } +} + +struct Home_Previews: PreviewProvider { + static var previews: some View { + Home(viewModel: HomeViewModelPreview()) + } +} diff --git a/iosApp/iosApp/Views/Main.swift b/iosApp/iosApp/Views/Main.swift index 450dc8b..70bb6f7 100644 --- a/iosApp/iosApp/Views/Main.swift +++ b/iosApp/iosApp/Views/Main.swift @@ -3,12 +3,25 @@ import shared struct Main: View { + private let router: Router + private let viewModelFactory: ViewModelFactory + + init(router: Router = MainRouter(), + viewModelFactory: ViewModelFactory = MobileViewModelFactory()) { + self.router = router + self.viewModelFactory = viewModelFactory + } + @ObservedObject var screen = ObservableFlowWrapper(MainRouter().screen, initial: Screen.Home()) var body: some View { switch screen.value { - case is Screen.Home: - Home() + case _ as Screen.Home: + Home(viewModel: viewModelFactory.homeViewModel) + case let screen as Screen.Example: + Example(viewModel: viewModelFactory.exampleViewModel(origin: screen.origin)) + default: + fatalError("Unsupported screen \(screen.value)") } } } diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt index 2b88fdf..32c4aec 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt @@ -1,9 +1,8 @@ package com.mirego.kmp.boilerplate -import com.mirego.kmp.boilerplate.utils.CFlow -import com.mirego.kmp.boilerplate.utils.wrap +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf class Greeting { - fun greeting(): CFlow = flowOf("Hello, ${Platform().platform}!").wrap() + fun greeting(): Flow = flowOf("Hello, ${Platform().platform}!") } diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Router.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Router.kt similarity index 61% rename from shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Router.kt rename to shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Router.kt index db30233..db7b165 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Router.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Router.kt @@ -1,7 +1,7 @@ -package com.mirego.kmp.boilerplate.routing +package com.mirego.kmp.boilerplate.presentation.routing -import com.mirego.kmp.boilerplate.utils.CFlow -import com.mirego.kmp.boilerplate.utils.wrap +import com.mirego.kmp.boilerplate.presentation.utils.CFlow +import com.mirego.kmp.boilerplate.presentation.utils.wrap import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -9,6 +9,7 @@ interface Router { val screen: CFlow fun push(screen: Screen) + fun pop() } object MainRouter : Router { @@ -22,4 +23,8 @@ object MainRouter : Router { override fun push(screen: Screen) { _screens.value += screen } + + override fun pop() { + _screens.value = _screens.value.dropLast(1) + } } diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Screen.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Screen.kt new file mode 100644 index 0000000..4e9750c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/routing/Screen.kt @@ -0,0 +1,6 @@ +package com.mirego.kmp.boilerplate.presentation.routing + +sealed class Screen { + object Home : Screen() + data class Example(val origin: String) : Screen() +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/utils/CFlow.kt similarity index 87% rename from shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt rename to shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/utils/CFlow.kt index d3e8e45..480214e 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/utils/CFlow.kt @@ -1,6 +1,6 @@ @file:Suppress("unused") -package com.mirego.kmp.boilerplate.utils +package com.mirego.kmp.boilerplate.presentation.utils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -30,4 +30,4 @@ class CFlow internal constructor( } } -internal fun Flow.wrap(): CFlow = CFlow(this) +fun Flow.wrap(): CFlow = CFlow(this) diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/ViewModelFactory.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000..445d80c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/ViewModelFactory.kt @@ -0,0 +1,20 @@ +package com.mirego.kmp.boilerplate.presentation.viewmodel + +import com.mirego.kmp.boilerplate.presentation.viewmodel.example.ExampleViewModel +import com.mirego.kmp.boilerplate.presentation.viewmodel.example.ExampleViewModelImpl +import com.mirego.kmp.boilerplate.presentation.viewmodel.home.HomeViewModel +import com.mirego.kmp.boilerplate.presentation.viewmodel.home.HomeViewModelImpl + +interface ViewModelFactory { + val homeViewModel: HomeViewModel + fun exampleViewModel(origin: String): ExampleViewModel +} + +object MobileViewModelFactory : ViewModelFactory { + override val homeViewModel: HomeViewModel + get() = HomeViewModelImpl() + + override fun exampleViewModel(origin: String): ExampleViewModelImpl { + return ExampleViewModelImpl(origin) + } +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/example/ExampleViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/example/ExampleViewModel.kt new file mode 100644 index 0000000..5191daa --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/example/ExampleViewModel.kt @@ -0,0 +1,28 @@ +package com.mirego.kmp.boilerplate.presentation.viewmodel.example + +import com.mirego.kmp.boilerplate.presentation.routing.MainRouter +import com.mirego.kmp.boilerplate.presentation.utils.CFlow +import com.mirego.kmp.boilerplate.presentation.utils.wrap +import kotlinx.coroutines.flow.flowOf + +interface ExampleViewModel { + val exampleMessage: CFlow + val backButtonText: CFlow + fun onBackButtonClick() + + object Preview : ExampleViewModel { + override val exampleMessage = flowOf("Example text").wrap() + override val backButtonText = flowOf("Back button text").wrap() + override fun onBackButtonClick() = Unit + } +} + +class ExampleViewModelImpl( + origin: String +) : ExampleViewModel { + override val exampleMessage = flowOf("You've pushed a new Router Screen!").wrap() + + override val backButtonText = flowOf("Go back to \"$origin\" screen").wrap() + + override fun onBackButtonClick() = MainRouter.pop() +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/home/HomeViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/home/HomeViewModel.kt new file mode 100644 index 0000000..f6c5953 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/presentation/viewmodel/home/HomeViewModel.kt @@ -0,0 +1,33 @@ +package com.mirego.kmp.boilerplate.presentation.viewmodel.home + +import com.mirego.kmp.boilerplate.Greeting +import com.mirego.kmp.boilerplate.presentation.routing.MainRouter +import com.mirego.kmp.boilerplate.presentation.routing.Screen +import com.mirego.kmp.boilerplate.presentation.utils.CFlow +import com.mirego.kmp.boilerplate.presentation.utils.wrap +import kotlinx.coroutines.flow.flowOf + +interface HomeViewModel { + val greetingMessage: CFlow + + val buttonText: CFlow + fun onButtonClick() + + object Preview : HomeViewModel { + override val greetingMessage = flowOf("Hello!").wrap() + override val buttonText = flowOf("Click here!").wrap() + override fun onButtonClick() = Unit + } +} + +class HomeViewModelImpl( + greeting: Greeting = Greeting() +) : HomeViewModel { + override val greetingMessage = greeting.greeting().wrap() + + override val buttonText = flowOf("Click me!").wrap() + + override fun onButtonClick() { + MainRouter.push(Screen.Example(origin = "Home")) + } +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Screen.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Screen.kt deleted file mode 100644 index 2730799..0000000 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/routing/Screen.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mirego.kmp.boilerplate.routing - -sealed class Screen { - object Home: Screen() -}