Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Architectural Revamp #99

Open
wants to merge 49 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
33b4c81
Initial draft of v3
matejsemancik Oct 24, 2024
e7fa891
Create universal component factory
matejsemancik Oct 24, 2024
178f7c6
Create a simple navigation
matejsemancik Oct 24, 2024
d6d926b
Rename KoinComponentContext -> ArkitektComponentContext
matejsemancik Oct 28, 2024
2076e52
Rename ScreenComponentFactory.kt -> AppComponentFactory
matejsemancik Oct 28, 2024
63c6e74
Remove outdated documentation
matejsemancik Oct 28, 2024
0e7eba6
Add KDoc TODOs
matejsemancik Oct 28, 2024
46a6be9
Add ThirdScreen to the mix
matejsemancik Oct 28, 2024
45e58c9
Code cleanup
matejsemancik Nov 2, 2024
5827375
Add picker with navigation results example
matejsemancik Nov 3, 2024
64183aa
Add bottom navigation example
matejsemancik Nov 2, 2024
df6221a
Refactor: Extract viewState initialization from onStart
matejsemancik Nov 3, 2024
c425fe3
Remove UiEvent and ViewState type restrictions from BaseComponent
matejsemancik Nov 3, 2024
6d8edd3
Remove unused UI Composables
matejsemancik Nov 3, 2024
acd5ea9
Add deep link support to v3
matejsemancik Nov 5, 2024
3bdf885
Remove dead :shared:feature subproject
matejsemancik Nov 5, 2024
9058b5e
Remove LifecycleOwnerExt.kt
matejsemancik Nov 5, 2024
3fc4f54
Remove ViewState, SharedViewModel, UiEvent generics and Destination/N…
matejsemancik Nov 5, 2024
d55dabc
Update SyncDataUseCase error message and delay
matejsemancik Nov 7, 2024
9d244e4
Update README.md
matejsemancik Nov 7, 2024
df75de4
Rename RootNavHost -> SignedInNavHost
matejsemancik Nov 7, 2024
75e46e2
Add fun Flow<VS>.asStateFlow() extension
matejsemancik Nov 25, 2024
3661235
Create RootNavHost with login screen and top-level slot
matejsemancik Nov 25, 2024
6bc4c45
Handle deep links from RootNavHost
matejsemancik Nov 25, 2024
c86918b
Make iOS project compilable
matejsemancik Nov 25, 2024
a61f182
Implement iOS picker
matejsemancik Nov 25, 2024
b32280a
Add root view IDs on iOS
matejsemancik Nov 25, 2024
7b79344
Add ProfileScreen with logout button
matejsemancik Nov 26, 2024
ad0d663
Localize application
matejsemancik Nov 26, 2024
0098e47
Remove updateState function in favor of direct access to `componentSt…
matejsemancik Nov 26, 2024
92668fa
Add FlowCollector receiver to asStateFlow and whenStarted extensions
matejsemancik Nov 26, 2024
7c34b2e
Bump dependencies
matejsemancik Nov 26, 2024
9f32aa8
Generate unique view IDs using kotlin.uuid
matejsemancik Nov 26, 2024
dc0b4b0
Refine TODOs
matejsemancik Nov 26, 2024
3000018
Remove TODO
matejsemancik Nov 26, 2024
c50bebb
Replace deprecated typealias
matejsemancik Nov 27, 2024
cfea288
Rename `feature_v3` package to original `feature`
matejsemancik Nov 27, 2024
64fed84
Rename `:shared:feature_v3` subproject to original `:shared:feature`
matejsemancik Nov 27, 2024
8f5c380
Reformat code using `ktlintFormat`
matejsemancik Nov 27, 2024
e8e0d6b
Remove KoinComponent decoration from ArkitektComponentContext
matejsemancik Nov 27, 2024
6f584e1
Resolve KDoc TODOs - document architectural components
matejsemancik Nov 27, 2024
5175f03
Remove whenStarted extension in favor of built-in lifecycle extensions
matejsemancik Dec 1, 2024
1e619da
Update README.md
matejsemancik Dec 1, 2024
335de5d
Rename SignedInNavHostDefaults -> RootNavHostDefaults
matejsemancik Dec 5, 2024
b145ebe
Revert "Rename SignedInNavHostDefaults -> RootNavHostDefaults"
matejsemancik Dec 5, 2024
9244cc7
Rename RootNavHostDefaults to SignedInNavHostDefaults
matejsemancik Dec 5, 2024
b2123eb
Add missing @InjectedParam annotations
matejsemancik Dec 5, 2024
ee98b45
Refactor asStateFlow() extension
matejsemancik Dec 6, 2024
730ce55
Merge pull request #100 from futuredapp/feature/v3-decompose-stateflow
Syntey Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/android_enterprise_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ jobs:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.androidFiles == 'true' }}
env:
# TODO Verify app distribution groups
# TODO PROJECT-SETUP Verify app distribution groups
APP_DISTRIBUTION_GROUPS: futured-qa, devs
APP_DISTRIBUTION_ARTIFACT_TYPE: APK
FIREBASE_CREDENTIALS_FILE: firebase_credentials.json
# TODO Platform-specific slack channel name for notifications, eg. "gmlh-android"
# TODO PROJECT-SETUP Platform-specific slack channel name for notifications, eg. "gmlh-android"
SLACK_CHANNEL: project-slack-channel-name
# TODO verify product flavor configuration
# TODO PROJECT-SETUP verify product flavor configuration
# Specifies API environment for KMP build.
# One of [dev|prod] as per configuration of Buildkonfig plugin in :shared:network:* Gradle module.
KMP_FLAVOR: dev
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/android_google_play_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
runs-on: [ ubuntu-latest ]
env:
EXCLUDE_AAB_FILTER: .*intermediate
# TODO Platform-specific slack channel name for notifications, eg. "gmlh-android"
# TODO PROJECT-SETUP Platform-specific slack channel name for notifications, eg. "gmlh-android"
SLACK_CHANNEL: project-slack-channel-name
# TODO verify product flavor configuration
# TODO PROJECT-SETUP verify product flavor configuration
# Specifies API environment for KMP build.
# One of [dev|prod] as per configuration of Buildkonfig plugin in :shared:network:* Gradle module.
KMP_FLAVOR: prod
Expand All @@ -38,9 +38,9 @@ jobs:
- name: Generate app bundle
shell: bash
env:
# TODO Set up `ANDROID_RELEASE_KEYSTORE_PASSWORD` secret for this GitHub repository
# TODO Set up `ANDROID_RELEASE_KEY_PASSWORD` secret for this GitHub repository
# TODO Set up `ANDROID_RELEASE_KEY_ALIAS` secret for this GitHub repository
# TODO PROJECT-SETUP Set up `ANDROID_RELEASE_KEYSTORE_PASSWORD` secret for this GitHub repository
# TODO PROJECT-SETUP Set up `ANDROID_RELEASE_KEY_PASSWORD` secret for this GitHub repository
# TODO PROJECT-SETUP Set up `ANDROID_RELEASE_KEY_ALIAS` secret for this GitHub repository
RELEASE_KEYSTORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
RELEASE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
Expand All @@ -54,9 +54,9 @@ jobs:
- name: Upload Android Release to Play Store
uses: r0adkll/upload-google-play@v1.1.1
with:
# TODO Set up `GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT` as plaintext JSON for this GitHub repository
# TODO PROJECT-SETUP Set up `GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT` as plaintext JSON for this GitHub repository
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT }}
# TODO This has to be applicationId
# TODO PROJECT-SETUP This has to be applicationId
packageName: app.futured.kmptemplate.android
releaseFiles: ${{ steps.artifacts.outputs.aab_file }}
track: internal
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/android_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.androidFiles == 'true' }}
env:
# TODO Platform-specific slack channel name for notifications, eg. "gmlh-android"
# TODO PROJECT-SETUP Platform-specific slack channel name for notifications, eg. "gmlh-android"
SLACK_CHANNEL: project-slack-channel-name
steps:
- name: Checkout
Expand Down
54 changes: 34 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ To give you a short overview of our stack, we use:

- Native UI on both platforms. Jetpack Compose on Android and SwiftUI on iOS. The rest of the application is shared in KMP.
- [Decompose](https://github.com/arkivanov/Decompose) for sharing presentation logic and navigation state.
- The app follows the MVVM design pattern, ViewModels are built around the Decompose InstanceKeeper feature.
- The presentation layer follows the MVI-like design pattern.
- [Koin](https://insert-koin.io/) for dependency injection.
- [SKIE](https://skie.touchlab.co/) for better Kotlin->Swift interop (exhaustive enums, sealed classes, Coroutines support).
- [moko-resources](https://github.com/icerockdev/moko-resources) for sharing string, color and image resources.
- [apollo-kotlin](https://github.com/apollographql/apollo-kotlin) client for apps that call GraphQL APIs.
- [ktorfit](https://github.com/Foso/Ktorfit) client for apps that call plain HTTP APIs.
- [moko-resources](https://github.com/icerockdev/moko-resources) for sharing string (and other types of) resources.
- [apollo-kotlin](https://github.com/apollographql/apollo-kotlin) network client for apps that call GraphQL APIs.
- [ktorfit](https://github.com/Foso/Ktorfit) network client for apps that call plain HTTP APIs.
- [Jetpack DataStore](https://developer.android.com/jetpack/androidx/releases/datastore) as a simple preferences storage (we have JSON-based and primitive implementations).
- [iOS-templates](https://github.com/futuredapp/iOS-templates) as template which generates a new iOS scene using MVVM-C architecture.

The template is a sample app with several screens to let you kick off the project with everything set up including navigation and some API calls.
The template is a sample app with several screens to let you kick off the project with everything set up, incl. navigation and some API calls.

-------8<------- CUT HERE AFTER CLONING -------8<-------

Expand Down Expand Up @@ -101,17 +101,33 @@ This project complies with ~~Standard (F0), High (F1), Highest (F2)~~ security s
## Navigation Structure

The app utilizes [Decompose](https://arkivanov.github.io/Decompose/) to share presentation logic and navigation state in KMP.
The following meta-description provides an overview of the Decompose navigation tree:
The following meta-description provides an overview of Decompose navigation tree:

```kotlin
Navigation("RootNavigation") {
Navigation("RootNavHost") {
Slot {
Screen("LoginScreen")
Navigation("HomeNavigation") {
Navigation("SignedInNavHost") {
// Bottom navigation stack
Stack {
Screen("FirstScreen")
Screen("SecondScreen")
Screen("ThirdScreen")
// Home tab
Navigation("HomeNavHost") {
Stack {
Screen("FirstScreen")
Screen("SecondScreen") {
Slot {
Screen("Picker")
}
}
Screen("ThirdScreen")
}
}
// Profile tab
Navigation("ProfileNavHost") {
Stack {
Screen("ProfileScreen")
}
}
}
}
}
Expand Down Expand Up @@ -195,13 +211,11 @@ ${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/uplo

## Deep Linking

Deep links are provided by each platform to common code and processed using `DeepLinkResolver` and `DeepLinkNavigator` classes.
The (example) app currently supports the following scheme: `kmptemplate` and the following links:
Deep links are provided by each platform to common code and parsed using `DeepLinkResolver` class.
The (sample) app currently supports the following url scheme: `kmptemplate` and the following links:

- `kmptemplate://login` -- Navigates to login screen
- `kmptemplate://a` -- Navigates to bottom navigation tab A
- `kmptemplate://b` -- Navigates to bottom navigation tab B
- `kmptemplate://c` -- Navigates to bottom navigation tab C
- `kmptemplate://b/third` -- Navigates to third example screen on tab B.
- `kmptemplate://b/secret?arg={OptionalArgument}` -- Navigates to secret screen reachable only by deep
link with optional argument `arg` on tab B
- `kmptemplate://home` -- Opens Home tab with default stack.
- `kmptemplate://profile` -- Opens Profile tab with default stack.
- `kmptemplate://home/second` -- Opens SecondScreen in Home tab.
- `kmptemplate://home/third?arg={argument}` -- Opens ThirdScreen in Home tab with provided argument. The `argument` is mandatory.
- `kmptemplate://home/third/{argument}` -- Opens ThirdScreen in Home tab with provided argument. The `argument` is mandatory.
2 changes: 1 addition & 1 deletion androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {

alias(libs.plugins.compose.compiler)
alias(libs.plugins.androidx.baselineprofile)
// TODO enable after providing google-services.json
// TODO PROJECT-SETUP enable after providing google-services.json
// alias(libs.plugins.google.services)
alias(libs.plugins.firebase.distribution)
}
Expand Down
6 changes: 3 additions & 3 deletions androidApp/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@
kotlinx.serialization.KSerializer serializer(...);
}

# TODO update package name
# TODO PROJECT-SETUP update package name
-keep,includedescriptorclasses class app.futured.kmptemplate.**$$serializer { *; }
# TODO update package name
# TODO PROJECT-SETUP update package name
-keepclassmembers class app.futured.kmptemplate.** {
*** Companion;
}
# TODO update package name
# TODO PROJECT-SETUP update package name
-keepclasseswithmembers class app.futured.kmptemplate.** {
kotlinx.serialization.KSerializer serializer(...);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.futured.kmptemplate.android.ui.navigation.RootNavGraph
import app.futured.kmptemplate.feature.DefaultAppComponentContext
import app.futured.kmptemplate.feature.navigation.root.RootNavigation
import app.futured.kmptemplate.feature.navigation.root.RootNavigationFactory
import com.arkivanov.decompose.defaultComponentContext
import app.futured.kmptemplate.android.ui.navigation.RootNavHostUi
import app.futured.kmptemplate.feature.navigation.root.RootNavHost
import app.futured.kmptemplate.feature.navigation.root.RootNavHostFactory
import app.futured.kmptemplate.feature.ui.base.DefaultAppComponentContext
import com.arkivanov.decompose.retainedComponent

class MainActivity : ComponentActivity() {

private lateinit var rootNavigation: RootNavigation
private lateinit var rootNavHost: RootNavHost

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rootNavigation = RootNavigationFactory.create(DefaultAppComponentContext(defaultComponentContext()))
rootNavigation.openDeepLinkIfNeeded(intent)
rootNavHost = retainedComponent { retainedContext ->
RootNavHostFactory.create(DefaultAppComponentContext(retainedContext))
}
rootNavHost.handleIntent(intent)

enableEdgeToEdge()
setContent {
Expand All @@ -33,24 +35,24 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
RootNavGraph(rootNavigation = rootNavigation)
RootNavHostUi(navHost = rootNavHost)
}
}
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
rootNavigation.openDeepLinkIfNeeded(intent)
rootNavHost.handleIntent(intent)
}

private fun RootNavigation.openDeepLinkIfNeeded(intent: Intent?) {
private fun RootNavHost.handleIntent(intent: Intent?) {
if (intent == null) {
return
}

val uri = intent.dataString ?: return
actions.openDeepLink(uri)
actions.onDeepLink(uri)
}
matejsemancik marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import app.futured.kmptemplate.platform.binding.PlatformFirebaseCrashlytics
class PlatformFirebaseCrashlyticsImpl : PlatformFirebaseCrashlytics {

override fun logMessage(message: String) {
// TODO Uncomment when Firebase Crashlytics is added as dependency
// TODO PROJECT-SETUP Uncomment when Firebase Crashlytics is added as dependency
// Firebase.crashlytics.log(message)
}

override fun sendNonFatalException(error: Throwable) {
// TODO Uncomment when Firebase Crashlytics is added as dependency
// TODO PROJECT-SETUP Uncomment when Firebase Crashlytics is added as dependency
// Firebase.crashlytics.recordException(error)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package app.futured.kmptemplate.android.ui.navigation

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.futured.kmptemplate.android.ui.screen.FirstScreenUi
import app.futured.kmptemplate.android.ui.screen.SecondScreenUi
import app.futured.kmptemplate.android.ui.screen.ThirdScreenUi
import app.futured.kmptemplate.feature.navigation.home.HomeChild
import app.futured.kmptemplate.feature.navigation.home.HomeConfig
import app.futured.kmptemplate.feature.navigation.home.HomeNavHost
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatable
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.router.stack.ChildStack

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun HomeNavHostUi(
navHost: HomeNavHost,
modifier: Modifier = Modifier,
) {
val stack: ChildStack<HomeConfig, HomeChild> by navHost.stack.collectAsStateWithLifecycle()
val actions = navHost.actions

Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.navigationBars,
content = { paddings ->
Children(
stack = stack,
modifier = Modifier.padding(paddings),
animation = predictiveBackAnimation(
backHandler = navHost.backHandler,
onBack = actions::pop,
selector = { backEvent, _, _ -> androidPredictiveBackAnimatable(backEvent) },
),
) { child ->
when (val childInstance = child.instance) {
is HomeChild.First -> FirstScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
is HomeChild.Second -> SecondScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
is HomeChild.Third -> ThirdScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
}
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package app.futured.kmptemplate.android.ui.navigation

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.futured.kmptemplate.android.ui.screen.ProfileScreenUi
import app.futured.kmptemplate.feature.navigation.profile.ProfileChild
import app.futured.kmptemplate.feature.navigation.profile.ProfileConfig
import app.futured.kmptemplate.feature.navigation.profile.ProfileNavHost
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatable
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.router.stack.ChildStack

@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun ProfileNavHostUi(
navHost: ProfileNavHost,
modifier: Modifier = Modifier,
) {
val stack: ChildStack<ProfileConfig, ProfileChild> by navHost.stack.collectAsStateWithLifecycle()
val actions = navHost.actions

Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.navigationBars,
content = { paddings ->
Children(
stack = stack,
modifier = Modifier.padding(paddings),
animation = predictiveBackAnimation(
backHandler = navHost.backHandler,
onBack = actions::pop,
selector = { backEvent, _, _ -> androidPredictiveBackAnimatable(backEvent) },
),
) { child ->
when (val childInstance = child.instance) {
is ProfileChild.Profile -> ProfileScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
}
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app.futured.kmptemplate.android.ui.navigation

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.futured.kmptemplate.android.ui.screen.LoginScreenUi
import app.futured.kmptemplate.feature.navigation.root.RootChild
import app.futured.kmptemplate.feature.navigation.root.RootConfig
import app.futured.kmptemplate.feature.navigation.root.RootNavHost
import com.arkivanov.decompose.router.slot.ChildSlot

@Composable
fun RootNavHostUi(
navHost: RootNavHost,
modifier: Modifier = Modifier,
) {
val slot: ChildSlot<RootConfig, RootChild> by navHost.slot.collectAsStateWithLifecycle()

Box(modifier.background(MaterialTheme.colorScheme.background)) {
when (val childInstance = slot.child?.instance) {
is RootChild.Login -> LoginScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
is RootChild.SignedIn -> SignedInNavHostUi(navHost = childInstance.navHost, modifier = Modifier.fillMaxSize())
null -> ApplicationLoading(Modifier.fillMaxSize())
}
}
}
matejsemancik marked this conversation as resolved.
Show resolved Hide resolved

@Composable
private fun ApplicationLoading(
modifier: Modifier = Modifier,
) = Box(modifier.background(MaterialTheme.colorScheme.background)) {
CircularProgressIndicator(
Modifier
.size(48.dp)
.align(Alignment.Center),
)
}
Loading
Loading