From 4de193f3a8eaf426fb3975b3030beae97af1580c Mon Sep 17 00:00:00 2001 From: Francis Pepin Date: Fri, 3 Nov 2023 16:55:37 -0400 Subject: [PATCH 1/4] Update Android views --- androidApp/build.gradle.kts | 1 + .../app/resources/AndroidImageProvider.kt | 11 +++++- .../app/ui/common/EmptyContentView.kt | 10 +++-- .../boilerplate/app/ui/common/ErrorView.kt | 18 +++++---- .../app/ui/projects/ProjectsContentView.kt | 37 +++++++++++++------ .../kmp/boilerplate/app/ui/root/RootView.kt | 5 ++- .../kmp/boilerplate/app/ui/theme/Colors.kt | 2 + .../kmp/boilerplate/app/ui/theme/Theme.kt | 14 ++++++- .../main/res/drawable/baseline_image_24.xml | 5 +++ .../drawable/baseline_question_mark_24.xml | 5 +++ .../main/res/drawable/baseline_warning_24.xml | 5 +++ gradle/libs.versions.toml | 1 + 12 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 androidApp/src/main/res/drawable/baseline_image_24.xml create mode 100644 androidApp/src/main/res/drawable/baseline_question_mark_24.xml create mode 100644 androidApp/src/main/res/drawable/baseline_warning_24.xml diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a45089d..65ced66 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.accompanist.systemuicontroller) + implementation(libs.accompanist.placeholder.material) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.material) 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 index c96e1e0..75f6101 100644 --- 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 @@ -1,9 +1,18 @@ package com.mirego.kmp.boilerplate.app.resources import android.content.Context +import com.mirego.kmp.boilerplate.R +import com.mirego.kmp.boilerplate.viewmodel.common.SharedImageResource 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 + override fun resourceIdForResource(resource: VMDImageResource, context: Context) = when(resource) { + is SharedImageResource -> when(resource) { + SharedImageResource.emptyPageIcon -> R.drawable.baseline_question_mark_24 + SharedImageResource.errorPageIcon -> R.drawable.baseline_warning_24 + SharedImageResource.imagePlaceholder -> R.drawable.baseline_image_24 + } + else -> null + } } diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/EmptyContentView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/EmptyContentView.kt index ad39d6a..1cc7b8a 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/EmptyContentView.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/EmptyContentView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.graphics.ColorFilter @@ -25,10 +26,11 @@ import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.VMDImage import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.VMDText @Composable -fun EmptyContentView(emptyViewModel: EmptyViewModel) { +fun EmptyContentView(emptyViewModel: EmptyViewModel, modifier: Modifier = Modifier) { val viewModel: EmptyViewModel by emptyViewModel.observeAsState() Column( - modifier = Modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally ) { VMDImage( modifier = Modifier.size(55.dp), @@ -39,13 +41,15 @@ fun EmptyContentView(emptyViewModel: EmptyViewModel) { VMDText( modifier = Modifier.padding(top = padding * 2), viewModel = viewModel.title, + color = Color.White, style = style(TextSize.LARGE_TITLE, TextWeight.REGULAR), maxLines = 1 ) VMDText( modifier = Modifier.padding(top = padding), - viewModel = viewModel.title, + viewModel = viewModel.message, + color = Color.White, style = style(TextSize.BODY, TextWeight.REGULAR), textAlign = TextAlign.Center ) diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/ErrorView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/ErrorView.kt index d19ae49..63cb5af 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/ErrorView.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/ErrorView.kt @@ -1,7 +1,9 @@ package com.mirego.kmp.boilerplate.app.ui.common import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -34,12 +36,14 @@ import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.VMDText @Composable fun ErrorView(errorViewModel: ErrorViewModel) { + val iconPadding = 4.dp val viewModel: ErrorViewModel by errorViewModel.observeAsState() Column( modifier = Modifier .statusBarsPadding() - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { VMDImage( modifier = Modifier.size(55.dp), @@ -48,7 +52,7 @@ fun ErrorView(errorViewModel: ErrorViewModel) { ) VMDText( - modifier = Modifier.padding(top = padding * 2), + modifier = Modifier.padding(top = padding * 2 - iconPadding), viewModel = viewModel.title, style = style(TextSize.LARGE_TITLE, TextWeight.REGULAR), color = Color.White, @@ -66,12 +70,12 @@ fun ErrorView(errorViewModel: ErrorViewModel) { VMDButton( modifier = Modifier .fillMaxWidth() - .padding(horizontal = padding) .widthIn(max = 320.dp) - .padding(vertical = 12.dp) - .clip(RoundedCornerShape(16.dp)) + .padding(top = padding * 2) + .padding(horizontal = padding * 2) + .clip(RoundedCornerShape(percent = 50)) .background(Color.Red) - .padding(top = padding * 2), + .padding(vertical = 12.dp), viewModel = viewModel.retryButton, ) { content -> Text( diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt index 5425595..373b907 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt @@ -1,6 +1,7 @@ package com.mirego.kmp.boilerplate.app.ui.projects import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -9,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -17,6 +17,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign @@ -28,6 +30,7 @@ import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider import com.mirego.kmp.boilerplate.app.ui.theme.AccentOrange import com.mirego.kmp.boilerplate.app.ui.theme.TextSize import com.mirego.kmp.boilerplate.app.ui.theme.TextWeight +import com.mirego.kmp.boilerplate.app.ui.theme.loading import com.mirego.kmp.boilerplate.app.ui.theme.style import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectItem import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectsContentSection @@ -48,7 +51,10 @@ fun ProjectsContentView(listViewModel: VMDListViewModel) ) { section -> when (section) { is ProjectsContentSection.Header -> HeaderView(header = section) - is ProjectsContentSection.NoProjects -> EmptyContentView(emptyViewModel = section.emptyViewModel) + is ProjectsContentSection.NoProjects -> EmptyContentView( + emptyViewModel = section.emptyViewModel, + modifier = Modifier.padding(top = 100.dp) + ) is ProjectsContentSection.ProjectsList -> ProjectsListView(viewModel = section.viewModel) } } @@ -80,9 +86,8 @@ private fun HeaderView(header: ProjectsContentSection.Header) { @Composable private fun ProjectsListView(viewModel: VMDListViewModel) { Column( - modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(padding) + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(padding * 2) ) { viewModel.elements.forEach { item -> ItemView(item) @@ -96,7 +101,14 @@ private fun ItemView(item: ProjectItem) { verticalArrangement = Arrangement.spacedBy(padding) ) { VMDImage( - modifier = Modifier.clip(RoundedCornerShape(16.dp)), + modifier = Modifier + .fillMaxWidth() + .shadow( + spotColor = Color.White.copy(alpha = 0.2f), + elevation = 6.dp, + shape = RoundedCornerShape(16.dp), + ) + .loading(item.isLoading), viewModel = item.image, contentScale = ContentScale.FillWidth ) @@ -104,9 +116,9 @@ private fun ItemView(item: ProjectItem) { Column( modifier = Modifier .padding(horizontal = padding) - .padding(top = padding) ) { Text( + modifier = Modifier.loading(item.isLoading), text = item.title, style = style(TextSize.SUB_HEADLINE, TextWeight.REGULAR), color = Color.White, @@ -114,17 +126,20 @@ private fun ItemView(item: ProjectItem) { ) Text( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier + .loading(item.isLoading), text = item.subtitle, - style = style(TextSize.SUB_HEADLINE, TextWeight.REGULAR), + style = style(TextSize.TITLE1, TextWeight.REGULAR), color = Color.White, maxLines = 2 ) Text( - modifier = Modifier.padding(top = padding), + modifier = Modifier + .loading(item.isLoading) + .padding(top = 12.dp), text = item.description, - style = style(TextSize.SUB_HEADLINE, TextWeight.REGULAR), + style = style(TextSize.CAPTION1, TextWeight.REGULAR), color = Color.AccentOrange, maxLines = 2 ) 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 index 1b0e173..c7906ce 100644 --- 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 @@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider import com.mirego.kmp.boilerplate.app.ui.projects.ProjectsView +import com.mirego.kmp.boilerplate.app.ui.theme.PrimaryBlack import com.mirego.kmp.boilerplate.viewmodel.root.RootViewModel import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsState @@ -18,8 +19,8 @@ import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsStat fun RootView(rootViewModel: RootViewModel) { val viewModel: RootViewModel by rootViewModel.observeAsState() val systemUiController = rememberSystemUiController() - systemUiController.setNavigationBarColor(color = Color.Transparent, darkIcons = false) - systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false) + systemUiController.setNavigationBarColor(color = Color.PrimaryBlack, darkIcons = false) + systemUiController.setStatusBarColor(color = Color.Transparent, darkIcons = false) Box( modifier = Modifier diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Colors.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Colors.kt index 51581b2..3543523 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Colors.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Colors.kt @@ -4,3 +4,5 @@ import androidx.compose.ui.graphics.Color val Color.Companion.PrimaryBlack: Color get() = Color(0xFF211E25) val Color.Companion.AccentOrange: Color get() = Color(0xFFFF3829) +val Color.Companion.ShimmerBackground: Color get() = Color(0xFF4C474f) +val Color.Companion.ShimmerHighlight: Color get() = Color(0xFF938C96) diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt index ed895a5..4e25370 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt @@ -3,7 +3,11 @@ package com.mirego.kmp.boilerplate.app.ui.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import com.google.accompanist.placeholder.PlaceholderHighlight +import com.google.accompanist.placeholder.placeholder +import com.google.accompanist.placeholder.shimmer @Composable fun Theme( @@ -17,4 +21,12 @@ fun Theme( colorScheme = colors, content = content ) -} \ No newline at end of file +} + +fun Modifier.loading(isLoading: Boolean) = this.then( + placeholder( + visible = isLoading, + highlight = PlaceholderHighlight.shimmer(highlightColor = Color.ShimmerHighlight), + color = Color.ShimmerBackground + ) +) diff --git a/androidApp/src/main/res/drawable/baseline_image_24.xml b/androidApp/src/main/res/drawable/baseline_image_24.xml new file mode 100644 index 0000000..d35859d --- /dev/null +++ b/androidApp/src/main/res/drawable/baseline_image_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/androidApp/src/main/res/drawable/baseline_question_mark_24.xml b/androidApp/src/main/res/drawable/baseline_question_mark_24.xml new file mode 100644 index 0000000..fbfe98d --- /dev/null +++ b/androidApp/src/main/res/drawable/baseline_question_mark_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/androidApp/src/main/res/drawable/baseline_warning_24.xml b/androidApp/src/main/res/drawable/baseline_warning_24.xml new file mode 100644 index 0000000..3c9a4b3 --- /dev/null +++ b/androidApp/src/main/res/drawable/baseline_warning_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a95b901..9e2499c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ skie = "0.4.20" trikot = "5.2.0" [libraries] +accompanist-placeholder-material = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist"} accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist"} android-splash = { module = "androidx.core:core-splashscreen", version.ref = "android-splash" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" } From 937b74418ec21d6b50a5aa8e9e42b59ce583837d Mon Sep 17 00:00:00 2001 From: Olivier Pineau Date: Fri, 3 Nov 2023 17:30:46 -0400 Subject: [PATCH 2/4] Analytics stuff --- androidApp/build.gradle.kts | 5 +- .../AndroidSharedAnalyticsService.kt | 30 ++++++ .../app/bootstrap/AndroidBootstrap.kt | 8 ++ gradle/libs.versions.toml | 13 ++- ios/Podfile | 6 +- ios/Podfile.lock | 98 ++++++++++++++++++- ios/iosApp.xcodeproj/project.pbxproj | 4 + ios/iosApp/Domain/AnalyticsServiceImpl.swift | 20 ++++ ios/iosApp/Domain/BootstrapImpl.swift | 9 ++ shared/build.gradle.kts | 2 + .../analytics/EmptySharedAnalyticsService.kt | 12 +++ .../analytics/SharedAnalyticsConfiguration.kt | 5 + .../analytics/SharedAnalyticsService.kt | 9 ++ .../usecase/ProjectsUseCaseTestImpl.kt | 4 +- 14 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 androidApp/src/main/java/com/mirego/kmp/boilerplate/app/analytics/AndroidSharedAnalyticsService.kt create mode 100644 ios/iosApp/Domain/AnalyticsServiceImpl.swift create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/EmptySharedAnalyticsService.kt create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsConfiguration.kt create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsService.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a45089d..219dfb2 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.crashlyticsPlugin) } kotlin { @@ -77,9 +78,11 @@ dependencies { implementation(project(":shared")) implementation(libs.android.splash) + implementation(libs.android.firebase.analytics) + implementation(libs.android.firebase.crashlytics) + implementation(platform(libs.android.firebase.bom)) implementation(libs.androidx.appcompat) implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) implementation(libs.accompanist.systemuicontroller) implementation(libs.androidx.compose.ui) diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/analytics/AndroidSharedAnalyticsService.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/analytics/AndroidSharedAnalyticsService.kt new file mode 100644 index 0000000..3406dc4 --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/analytics/AndroidSharedAnalyticsService.kt @@ -0,0 +1,30 @@ +package com.mirego.kmp.boilerplate.app.analytics + +import android.content.Context +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics +import com.mirego.kmp.boilerplate.analytics.SharedAnalyticsService +import com.mirego.trikot.analytics.AnalyticsEvent +import com.mirego.trikot.analytics.AnalyticsPropertiesType + +class AndroidSharedAnalyticsService( + context: Context, + private var analyticsEnabled: Boolean = true +) : SharedAnalyticsService { + private var firebaseAnalytics = FirebaseAnalytics.getInstance(context) + + override var isEnabled: Boolean + get() = analyticsEnabled + set(value) { + analyticsEnabled = value + firebaseAnalytics.setAnalyticsCollectionEnabled(value) + } + + override fun trackEvent(event: AnalyticsEvent, properties: AnalyticsPropertiesType) { + val bundle = Bundle() + properties.forEach { + bundle.putString(it.key, it.value.toString()) + } + firebaseAnalytics.logEvent(event.name, bundle) + } +} 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 index 59ce0dc..6ad1e91 100644 --- 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 @@ -2,6 +2,8 @@ package com.mirego.kmp.boilerplate.app.bootstrap import android.content.Context import com.mirego.kmp.boilerplate.BuildConfig +import com.mirego.kmp.boilerplate.analytics.SharedAnalyticsConfiguration +import com.mirego.kmp.boilerplate.app.analytics.AndroidSharedAnalyticsService import com.mirego.kmp.boilerplate.app.resources.AndroidImageProvider import com.mirego.kmp.boilerplate.bootstrap.AppEnvironment import com.mirego.kmp.boilerplate.bootstrap.Bootstrap @@ -31,5 +33,11 @@ class AndroidBootstrap(context: Context) : Bootstrap { imageProvider = AndroidImageProvider(), textStyleProvider = DefaultTextStyleProvider() ) + + val analyticsEnabled = !BuildConfig.DEBUG + SharedAnalyticsConfiguration.analyticsManager = AndroidSharedAnalyticsService( + context = context, + analyticsEnabled = analyticsEnabled + ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a95b901..058fe9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,13 @@ accompanist = "0.32.0" androidComposeCompiler = "1.5.3" androidGradlePlugin = "8.1.2" android-splash = "1.0.1" +android-playServices = "20.7.0" +play-services-measurement-api = "21.5.0" androidxActivityCompose = "1.8.0" androidxAppcompat = "1.6.1" androidxComposeBom = "2023.10.01" apollo = "3.7.4" +crashlytics-plugin = "2.9.9" koin = "3.5.0" koin-android = "3.5.0" koin-androidx-compose = "3.5.0" @@ -18,13 +21,14 @@ kotlinxCoroutines = "1.7.3" kotlinxSerialization = "1.6.0" kword-plugin = "4.0.0" ktlint = "11.6.1" +firebase = "32.5.0" mockk = "1.12.5" okio = "3.6.0" skie = "0.4.20" trikot = "5.2.0" [libraries] -accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist"} +accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } 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" } @@ -32,6 +36,10 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-material = { group = "androidx.compose.material", name = "material" } +#android-play-services-measurement-api = { group = "com.google.android.gms", name = "play-services-measurement-api", version.ref = "play-services-measurement-api" } +android-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" } +android-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +android-firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } apollo-runtime = { group = "com.apollographql.apollo3", name = "apollo-runtime" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin_ksp" } @@ -51,6 +59,8 @@ skie = { module = "co.touchlab.skie:configuration-annotations", version.ref = "s mockk-common = { module = "io.mockk:mockk-common", version.ref = "mockk" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +trikot-analytics = { module = "com.mirego.trikot:analytics", version.ref = "trikot" } +trikot-analytics-firebase = { module = "com.mirego.trikot.analytics:firebase-ktx", version.ref = "trikot" } trikot-vmd = { module = "com.mirego.trikot:viewmodels-declarative-flow", version.ref = "trikot" } trikot-kword = { module = "com.mirego.trikot:kword", version.ref = "trikot" } trikot-datasources = { module = "com.mirego.trikot:datasources-flow", version.ref = "trikot" } @@ -68,6 +78,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref kotlin-native-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } kspPlugin = { id = "com.google.devtools.ksp", version.ref = "ksp" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +crashlyticsPlugin = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics-plugin" } mirego-kwordPlugin = { id = "mirego.kword", version.ref = "kword-plugin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } skie-plugin = { id = "co.touchlab.skie", version.ref = "skie" } diff --git a/ios/Podfile b/ios/Podfile index 1bfb53e..75daacd 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -6,7 +6,11 @@ inhibit_all_warnings! target 'iosApp' do use_frameworks! - platform :ios, $deploymentTarget + platform :ios, $deploymentTarget + # Thirdo-party + pod 'FirebaseCore' + pod 'FirebaseAnalytics' + # Multiplatform pod 'Shared', :path => '../shared' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c28419c..4e8e454 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,80 @@ PODS: + - FirebaseAnalytics (10.16.0): + - FirebaseAnalytics/AdIdSupport (= 10.16.0) + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (10.16.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleAppMeasurement (= 10.16.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCore (10.16.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Logger (~> 7.8) + - FirebaseCoreInternal (10.16.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseInstallations (10.16.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - GoogleAppMeasurement (10.16.0): + - GoogleAppMeasurement/AdIdSupport (= 10.16.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.16.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.16.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.16.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.11.5): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Logger - Kingfisher (7.8.1) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - PromisesObjC (2.3.1) - Shared (0.1) - - SwiftGen (6.6.2) - SwiftLint (0.53.0) - SwiftUIIntrospect (1.1.0) - Trikot/kword (5.2.0): @@ -15,16 +88,24 @@ PODS: - Trikot/viewmodels.declarative.flow DEPENDENCIES: + - FirebaseAnalytics + - FirebaseCore - Shared (from `../shared`) - - SwiftGen - SwiftLint - "Trikot/kword (from `git@github.com:mirego/trikot.git`, tag `5.2.0`)" - "Trikot/viewmodels.declarative.SwiftUI.flow (from `git@github.com:mirego/trikot.git`, tag `5.2.0`)" SPEC REPOS: trunk: + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - GoogleAppMeasurement + - GoogleUtilities - Kingfisher - - SwiftGen + - nanopb + - PromisesObjC - SwiftLint - SwiftUIIntrospect @@ -41,13 +122,20 @@ CHECKOUT OPTIONS: :tag: 5.2.0 SPEC CHECKSUMS: + FirebaseAnalytics: 7b41efc4eba5ff841cc94d5994b5f339361258f4 + FirebaseCore: 65a801af84cca84361ef9eac3fd868656968a53b + FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a + FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee + GoogleAppMeasurement: 079d7632810e9d9704a99932547ba1554acc4342 + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 Shared: 185e58fd174d1d4b955d7581bc1bacb9810272fa - SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 SwiftUIIntrospect: 53b6a16734c822804ff82c35d9073d8d8edfb085 Trikot: ca201c8ae67ff21f35a60a0bda6c600fbe51b282 -PODFILE CHECKSUM: a6683730b9e619cd4c94d6f20ba4e3f5943142c0 +PODFILE CHECKSUM: 8c6a75418b236f7d0081b7844d58c4e6b129cedb COCOAPODS: 1.13.0 diff --git a/ios/iosApp.xcodeproj/project.pbxproj b/ios/iosApp.xcodeproj/project.pbxproj index 1a07749..3504fbf 100644 --- a/ios/iosApp.xcodeproj/project.pbxproj +++ b/ios/iosApp.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 895BED172AF416F5005B1212 /* VMDColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BED162AF416F5005B1212 /* VMDColor+Extensions.swift */; }; 895BED192AF42C2B005B1212 /* ProjectDetailsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BED182AF42C2B005B1212 /* ProjectDetailsContentView.swift */; }; 895BED1B2AF4469C005B1212 /* NavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BED1A2AF4469C005B1212 /* NavigationView.swift */; }; + 895BED1D2AF58517005B1212 /* AnalyticsServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895BED1C2AF58517005B1212 /* AnalyticsServiceImpl.swift */; }; 9B8ACFDB4E332DFCA8B97CBB /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E4E1328B104D05A50A097EE /* Pods_iosApp.framework */; }; /* End PBXBuildFile section */ @@ -69,6 +70,7 @@ 895BED162AF416F5005B1212 /* VMDColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VMDColor+Extensions.swift"; sourceTree = ""; }; 895BED182AF42C2B005B1212 /* ProjectDetailsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectDetailsContentView.swift; sourceTree = ""; }; 895BED1A2AF4469C005B1212 /* NavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationView.swift; sourceTree = ""; }; + 895BED1C2AF58517005B1212 /* AnalyticsServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsServiceImpl.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 */ @@ -134,6 +136,7 @@ 89011D4C2AE9A4FC0073544B /* AppEnvironment+iOS.swift */, 89011D4A2AE9A4CD0073544B /* BootstrapImpl.swift */, 89011D502AE9A7750073544B /* ImageProvider.swift */, + 895BED1C2AF58517005B1212 /* AnalyticsServiceImpl.swift */, ); path = Domain; sourceTree = ""; @@ -413,6 +416,7 @@ 895BED192AF42C2B005B1212 /* ProjectDetailsContentView.swift in Sources */, 895BED112AF3FD32005B1212 /* NavigationModifier.swift in Sources */, 89011D512AE9A7750073544B /* ImageProvider.swift in Sources */, + 895BED1D2AF58517005B1212 /* AnalyticsServiceImpl.swift in Sources */, 89011D4B2AE9A4CD0073544B /* BootstrapImpl.swift in Sources */, 895BED032AEFF281005B1212 /* TextStyle.swift in Sources */, 895BECFB2AEAB3DC005B1212 /* RootView.swift in Sources */, diff --git a/ios/iosApp/Domain/AnalyticsServiceImpl.swift b/ios/iosApp/Domain/AnalyticsServiceImpl.swift new file mode 100644 index 0000000..d4e8abd --- /dev/null +++ b/ios/iosApp/Domain/AnalyticsServiceImpl.swift @@ -0,0 +1,20 @@ +import FirebaseAnalytics +import Foundation +import Shared + +public class AnalyticsServiceImpl: SharedAnalyticsService { + + public init(enableAnalytics: Bool = true) { + isEnabled = enableAnalytics + } + + public var isEnabled: Bool { + didSet { + Analytics.setAnalyticsCollectionEnabled(isEnabled) + } + } + + public func trackEvent(event: AnalyticsEvent, properties: [String: Any]) { + Analytics.logEvent(event.name, parameters: properties) + } +} diff --git a/ios/iosApp/Domain/BootstrapImpl.swift b/ios/iosApp/Domain/BootstrapImpl.swift index df75fa2..c26d47f 100644 --- a/ios/iosApp/Domain/BootstrapImpl.swift +++ b/ios/iosApp/Domain/BootstrapImpl.swift @@ -8,5 +8,14 @@ final class BootstrapImpl: Bootstrap { init() { appInformation = AppInformationImpl(environmentKey: environment.key) + + let firebaseAnalyticsService = AnalyticsServiceImpl() + #if DEBUG + firebaseAnalyticsService.isEnabled = false + #else + firebaseAnalyticsService.isEnabled = true + #endif + + SharedAnalyticsConfiguration().analyticsManager = firebaseAnalyticsService } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e992969..fcdfbd0 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -19,6 +19,7 @@ val TRIKOT_FRAMEWORK_NAME = "Shared" fun org.jetbrains.kotlin.gradle.plugin.mpp.Framework.configureFramework() { baseName = TRIKOT_FRAMEWORK_NAME isStatic = false + export(libs.trikot.analytics) export(libs.trikot.vmd) export(libs.trikot.kword) export(libs.trikot.datasources) @@ -102,6 +103,7 @@ kotlin { api(libs.koin.core) implementation(libs.okio) implementation(libs.skie) + api(libs.trikot.analytics) api(libs.trikot.vmd.annotations) api(libs.trikot.datasources) api(libs.trikot.kword) diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/EmptySharedAnalyticsService.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/EmptySharedAnalyticsService.kt new file mode 100644 index 0000000..1f89368 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/EmptySharedAnalyticsService.kt @@ -0,0 +1,12 @@ +package com.mirego.kmp.boilerplate.analytics + +import com.mirego.trikot.analytics.AnalyticsEvent +import com.mirego.trikot.analytics.AnalyticsPropertiesType + +class EmptySharedAnalyticsService : SharedAnalyticsService { + override var isEnabled: Boolean = false + + override fun trackEvent(event: AnalyticsEvent, properties: AnalyticsPropertiesType) { + // No-Op + } +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsConfiguration.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsConfiguration.kt new file mode 100644 index 0000000..d236cbb --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsConfiguration.kt @@ -0,0 +1,5 @@ +package com.mirego.kmp.boilerplate.analytics + +object SharedAnalyticsConfiguration { + var analyticsManager: SharedAnalyticsService = EmptySharedAnalyticsService() +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsService.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsService.kt new file mode 100644 index 0000000..cd5d468 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/SharedAnalyticsService.kt @@ -0,0 +1,9 @@ +package com.mirego.kmp.boilerplate.analytics + +import com.mirego.trikot.analytics.AnalyticsEvent +import com.mirego.trikot.analytics.AnalyticsPropertiesType + +interface SharedAnalyticsService { + var isEnabled: Boolean + fun trackEvent(event: AnalyticsEvent, properties: AnalyticsPropertiesType) +} diff --git a/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/usecase/ProjectsUseCaseTestImpl.kt b/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/usecase/ProjectsUseCaseTestImpl.kt index cee7c4c..68ccec3 100644 --- a/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/usecase/ProjectsUseCaseTestImpl.kt +++ b/shared/src/commonTest/kotlin/com/mirego/kmp/boilerplate/usecase/ProjectsUseCaseTestImpl.kt @@ -74,6 +74,8 @@ class ProjectsUseCaseTestImpl : BaseTest() { listImageUrl = "https://miregologo.com", client = ProjectsQuery.Data.PagePage.ProjectsListBlock.Projects.Entry.Client( name = "Mirego" - ) + ), + "000000", + "FFFFFF" ) } From e0d42b0e6ff9eff0a86876d8affe828a8545ceea Mon Sep 17 00:00:00 2001 From: Olivier Pineau Date: Sat, 4 Nov 2023 13:32:06 -0400 Subject: [PATCH 3/4] Analytics tracking --- .../ProjectDetails/ProjectDetailsView.swift | 1 + .../kmp/boilerplate/analytics/Analytics.kt | 39 +++++++++++++++++++ .../ProjectDetailsViewModelImpl.kt | 6 ++- .../projects/ProjectsViewModelImpl.kt | 7 +++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/Analytics.kt diff --git a/ios/iosApp/UI/ProjectDetails/ProjectDetailsView.swift b/ios/iosApp/UI/ProjectDetails/ProjectDetailsView.swift index 62c0d0b..9828092 100644 --- a/ios/iosApp/UI/ProjectDetails/ProjectDetailsView.swift +++ b/ios/iosApp/UI/ProjectDetails/ProjectDetailsView.swift @@ -36,6 +36,7 @@ struct ProjectDetailsView: View { } } } + .handleNavigation(viewModel, route: viewModel.navigationRoute) } @ViewBuilder private var contentView: some View { diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/Analytics.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/Analytics.kt new file mode 100644 index 0000000..c81082b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/analytics/Analytics.kt @@ -0,0 +1,39 @@ +@file:Suppress("EnumEntryName") + +package com.mirego.kmp.boilerplate.analytics + +import com.mirego.trikot.analytics.AnalyticsConfiguration +import com.mirego.trikot.analytics.AnalyticsEvent + +object Analytics { + private const val PARAM_SCREEN_TITLE = "screen_title" + private const val PARAM_PROJECT_ID = "project_id" + + fun trackScreenView(screen: ScreenName) { + AnalyticsConfiguration.analyticsManager.trackEvent( + event = AnalyticsEvents.screen_view, + properties = mapOf( + PARAM_SCREEN_TITLE to screen.name + ) + ) + } + + fun trackViewProject(projectId: String) { + AnalyticsConfiguration.analyticsManager.trackEvent( + event = AnalyticsEvents.view_project, + properties = mapOf( + PARAM_PROJECT_ID to projectId + ) + ) + } +} + +enum class AnalyticsEvents : AnalyticsEvent { + screen_view, + view_project +} + +enum class ScreenName { + projects, + project_details +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projectdetails/ProjectDetailsViewModelImpl.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projectdetails/ProjectDetailsViewModelImpl.kt index 465c386..58c0800 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projectdetails/ProjectDetailsViewModelImpl.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projectdetails/ProjectDetailsViewModelImpl.kt @@ -1,5 +1,7 @@ package com.mirego.kmp.boilerplate.viewmodel.projectdetails +import com.mirego.kmp.boilerplate.analytics.Analytics +import com.mirego.kmp.boilerplate.analytics.ScreenName import com.mirego.kmp.boilerplate.localization.KWordTranslation import com.mirego.kmp.boilerplate.usecase.preview.ProjectDetailsUseCasePreview import com.mirego.kmp.boilerplate.usecase.projectdetails.ProjectDetailsUseCase @@ -29,7 +31,9 @@ class ProjectDetailsViewModelImpl( closeAction: () -> Unit, coroutineScope: CoroutineScope ) : ProjectDetailsViewModel, BaseProjectDetailsViewModelImpl( - onTrackScreenView = {}, + onTrackScreenView = { + Analytics.trackScreenView(ScreenName.project_details) + }, viewModelFactory = viewModelFactory, coroutineScope = coroutineScope ) { diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projects/ProjectsViewModelImpl.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projects/ProjectsViewModelImpl.kt index aad482b..757a444 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projects/ProjectsViewModelImpl.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodel/projects/ProjectsViewModelImpl.kt @@ -1,5 +1,7 @@ package com.mirego.kmp.boilerplate.viewmodel.projects +import com.mirego.kmp.boilerplate.analytics.Analytics +import com.mirego.kmp.boilerplate.analytics.ScreenName import com.mirego.kmp.boilerplate.extension.prioritiseData import com.mirego.kmp.boilerplate.localization.KWordTranslation import com.mirego.kmp.boilerplate.usecase.preview.ProjectsUseCasePreview @@ -30,7 +32,9 @@ class ProjectsViewModelImpl( viewModelFactory: ViewModelFactory, coroutineScope: CoroutineScope ) : ProjectsViewModel, BaseProjectsViewModelImpl( - onTrackScreenView = {}, + onTrackScreenView = { + Analytics.trackScreenView(ScreenName.projects) + }, viewModelFactory = viewModelFactory, coroutineScope = coroutineScope ) { @@ -95,6 +99,7 @@ class ProjectsViewModelImpl( placeholderImageResource = SharedImageResource.imagePlaceholder ), tapAction = { + Analytics.trackViewProject(projectId = id) navigateToProjectDetails( ProjectDetailsNavigationData( id = id, From f1f798c316a8351f8a1548ba9d767982cfba66bc Mon Sep 17 00:00:00 2001 From: Francis Pepin Date: Mon, 6 Nov 2023 11:38:49 -0500 Subject: [PATCH 4/4] Create better placeholder and loading state on Android --- .../kmp/boilerplate/app/ui/common/Utils.kt | 17 ++++++++ .../app/ui/projects/ProjectsContentView.kt | 42 +++++++++++++++---- .../app/ui/projects/ProjectsView.kt | 27 +++++++++++- .../kmp/boilerplate/app/ui/theme/Theme.kt | 8 ---- ios/Podfile.lock | 6 +-- 5 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/Utils.kt diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/Utils.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/Utils.kt new file mode 100644 index 0000000..9dfa37e --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/common/Utils.kt @@ -0,0 +1,17 @@ +package com.mirego.kmp.boilerplate.app.ui.common + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.google.accompanist.placeholder.PlaceholderHighlight +import com.google.accompanist.placeholder.placeholder +import com.google.accompanist.placeholder.shimmer +import com.mirego.kmp.boilerplate.app.ui.theme.ShimmerBackground +import com.mirego.kmp.boilerplate.app.ui.theme.ShimmerHighlight + +fun Modifier.loading(isLoading: Boolean) = this.then( + placeholder( + visible = isLoading, + highlight = PlaceholderHighlight.shimmer(highlightColor = Color.ShimmerHighlight), + color = Color.ShimmerBackground + ) +) \ No newline at end of file diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt index 373b907..2950421 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsContentView.kt @@ -1,14 +1,17 @@ package com.mirego.kmp.boilerplate.app.ui.projects +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text @@ -17,27 +20,29 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.mirego.kmp.boilerplate.app.ui.common.Const.padding import com.mirego.kmp.boilerplate.app.ui.common.EmptyContentView +import com.mirego.kmp.boilerplate.app.ui.common.loading import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider import com.mirego.kmp.boilerplate.app.ui.theme.AccentOrange import com.mirego.kmp.boilerplate.app.ui.theme.TextSize import com.mirego.kmp.boilerplate.app.ui.theme.TextWeight -import com.mirego.kmp.boilerplate.app.ui.theme.loading import com.mirego.kmp.boilerplate.app.ui.theme.style import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectItem import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectsContentSection import com.mirego.trikot.viewmodels.declarative.components.VMDListViewModel import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsState +import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.LocalImage +import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.PlaceholderState import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.VMDImage import com.mirego.trikot.viewmodels.declarative.compose.viewmodel.VMDLazyColumn +import com.mirego.trikot.viewmodels.declarative.properties.VMDImageResource @Composable fun ProjectsContentView(listViewModel: VMDListViewModel) { @@ -103,14 +108,14 @@ private fun ItemView(item: ProjectItem) { VMDImage( modifier = Modifier .fillMaxWidth() - .shadow( - spotColor = Color.White.copy(alpha = 0.2f), - elevation = 6.dp, - shape = RoundedCornerShape(16.dp), - ) + .aspectRatio(1f) + .clip(RoundedCornerShape(16.dp)) .loading(item.isLoading), viewModel = item.image, - contentScale = ContentScale.FillWidth + contentScale = ContentScale.FillWidth, + placeholder = { placeholderImageResource: VMDImageResource, state: PlaceholderState -> + ImagePlaceholder(placeholderImageResource = placeholderImageResource, state = state) + } ) Column( @@ -147,6 +152,25 @@ private fun ItemView(item: ProjectItem) { } } +@Composable +private fun ImagePlaceholder(placeholderImageResource: VMDImageResource, state: PlaceholderState) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.2f)) + .clip(RoundedCornerShape(16.dp)) + .loading(state == PlaceholderState.LOADING), + contentAlignment = Alignment.Center + ) { + LocalImage( + modifier = Modifier.size(64.dp), + imageResource = placeholderImageResource, + contentScale = ContentScale.FillWidth, + colorFilter = ColorFilter.tint(Color.Gray) + ) + } +} + @Preview @Composable fun PreviewProjectsContentView() { diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsView.kt index c43b605..4b0902c 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsView.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/projects/ProjectsView.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.mirego.kmp.boilerplate.app.ui.common.ErrorView import com.mirego.kmp.boilerplate.app.ui.preview.PreviewProvider import com.mirego.kmp.boilerplate.app.ui.theme.PrimaryBlack +import com.mirego.kmp.boilerplate.usecase.preview.PreviewState import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectsRoot import com.mirego.kmp.boilerplate.viewmodel.projects.ProjectsViewModel import com.mirego.trikot.viewmodels.declarative.compose.extensions.observeAsState @@ -44,6 +45,30 @@ private fun ContentView(viewModel: ProjectsViewModel) { @Composable fun PreviewProjectsView() { PreviewProvider { - ProjectsView(projectsViewModel = it.createProjects()) + ProjectsView(projectsViewModel = it.createProjects(previewState = PreviewState.Data.Content)) } } + +@Preview +@Composable +fun PreviewProjectsEmptyView() { + PreviewProvider { + ProjectsView(projectsViewModel = it.createProjects(previewState = PreviewState.Data.Empty)) + } +} + +@Preview +@Composable +fun PreviewProjectsLoadingView() { + PreviewProvider { + ProjectsView(projectsViewModel = it.createProjects(previewState = PreviewState.Loading)) + } +} + +@Preview +@Composable +fun PreviewProjectsErrorView() { + PreviewProvider { + ProjectsView(projectsViewModel = it.createProjects(previewState = PreviewState.Error)) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt index 4e25370..9839074 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/app/ui/theme/Theme.kt @@ -22,11 +22,3 @@ fun Theme( content = content ) } - -fun Modifier.loading(isLoading: Boolean) = this.then( - placeholder( - visible = isLoading, - highlight = PlaceholderHighlight.shimmer(highlightColor = Color.ShimmerHighlight), - color = Color.ShimmerBackground - ) -) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c28419c..8f1a2f9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,6 @@ PODS: - Kingfisher (7.8.1) - Shared (0.1) - - SwiftGen (6.6.2) - SwiftLint (0.53.0) - SwiftUIIntrospect (1.1.0) - Trikot/kword (5.2.0): @@ -16,7 +15,6 @@ PODS: DEPENDENCIES: - Shared (from `../shared`) - - SwiftGen - SwiftLint - "Trikot/kword (from `git@github.com:mirego/trikot.git`, tag `5.2.0`)" - "Trikot/viewmodels.declarative.SwiftUI.flow (from `git@github.com:mirego/trikot.git`, tag `5.2.0`)" @@ -24,7 +22,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - Kingfisher - - SwiftGen - SwiftLint - SwiftUIIntrospect @@ -43,11 +40,10 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 Shared: 185e58fd174d1d4b955d7581bc1bacb9810272fa - SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 SwiftUIIntrospect: 53b6a16734c822804ff82c35d9073d8d8edfb085 Trikot: ca201c8ae67ff21f35a60a0bda6c600fbe51b282 -PODFILE CHECKSUM: a6683730b9e619cd4c94d6f20ba4e3f5943142c0 +PODFILE CHECKSUM: a8e15659553c290a2147c108f4746a4431cf7594 COCOAPODS: 1.13.0