From 9ab029981480022afc2c49a33110802d4cbb73c4 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 21 Jun 2022 17:52:13 +0200 Subject: [PATCH 1/7] feat: androidx-compose support for navigation --- build.gradle.kts | 1 + buildSrc/src/main/java/Config.kt | 10 +- .../src/main/AndroidManifest.xml | 2 - .../.gitignore | 0 .../build.gradle.kts | 81 ++++++--- sentry-compose/gradle.properties | 1 + .../proguard-rules.pro | 0 .../src/androidMain/AndroidManifest.xml | 2 + .../compose/SentryNavigationIntegration.kt | 7 +- .../compose/SentryLifecycleObserverTest.kt | 51 ++++++ .../sentry-samples-android/build.gradle.kts | 14 +- .../src/main/AndroidManifest.xml | 4 - .../sentry/samples/android/GitHubService.kt | 8 +- .../sentry/samples/android/MainActivity.java | 6 - .../android/compose/ComposeActivity.kt | 162 ------------------ .../src/main/res/layout/activity_main.xml | 6 - .../src/main/res/values/strings.xml | 1 - settings.gradle.kts | 2 +- 18 files changed, 119 insertions(+), 239 deletions(-) delete mode 100644 sentry-android-compose/src/main/AndroidManifest.xml rename {sentry-android-compose => sentry-compose}/.gitignore (100%) rename {sentry-android-compose => sentry-compose}/build.gradle.kts (53%) create mode 100644 sentry-compose/gradle.properties rename {sentry-android-compose => sentry-compose}/proguard-rules.pro (100%) create mode 100644 sentry-compose/src/androidMain/AndroidManifest.xml rename {sentry-android-compose/src/main/java/io/sentry/android => sentry-compose/src/androidMain/kotlin/io/sentry}/compose/SentryNavigationIntegration.kt (85%) create mode 100644 sentry-compose/src/androidTest/kotlin/io/sentry/compose/SentryLifecycleObserverTest.kt delete mode 100644 sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt diff --git a/build.gradle.kts b/build.gradle.kts index 278b2a94226..401adc72928 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ buildscript { // classpath("io.sentry:sentry-android-gradle-plugin:{version}") classpath(Config.QualityPlugins.binaryCompatibilityValidatorPlugin) + classpath(Config.BuildPlugins.composeGradlePlugin) } } diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a5f6b131992..7f4d8d61755 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -7,6 +7,8 @@ object Config { val springBootVersion = "2.6.8" val kotlinCompatibleLanguageVersion = "1.4" + val composeVersion = "1.1.1" + object BuildPlugins { val androidGradle = "com.android.tools.build:gradle:7.2.1" val kotlinGradlePlugin = "gradle-plugin" @@ -19,6 +21,7 @@ object Config { val grettyVersion = "4.0.0" val gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.18.0" val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:$kotlinVersion" + val composeGradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$composeVersion" } object Android { @@ -105,16 +108,11 @@ object Config { val graphQlJava = "com.graphql-java:graphql-java:17.3" val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" + val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib" val navigationRuntime = "androidx.navigation:navigation-runtime:2.4.2" // compose deps - val composeVersion = "1.1.1" - val composeRuntime = "androidx.compose.runtime:runtime:$composeVersion" val composeNavigation = "androidx.navigation:navigation-compose:2.4.2" - val composeActivity = "androidx.activity:activity-compose:1.4.0" - val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion" - val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion" - val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" } object AnnotationProcessors { diff --git a/sentry-android-compose/src/main/AndroidManifest.xml b/sentry-android-compose/src/main/AndroidManifest.xml deleted file mode 100644 index 4fa96561c64..00000000000 --- a/sentry-android-compose/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/sentry-android-compose/.gitignore b/sentry-compose/.gitignore similarity index 100% rename from sentry-android-compose/.gitignore rename to sentry-compose/.gitignore diff --git a/sentry-android-compose/build.gradle.kts b/sentry-compose/build.gradle.kts similarity index 53% rename from sentry-android-compose/build.gradle.kts rename to sentry-compose/build.gradle.kts index 112a2b7f1b5..512f68477ab 100644 --- a/sentry-android-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -2,11 +2,61 @@ import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension plugins { + kotlin("multiplatform") id("com.android.library") - kotlin("android") + id("org.jetbrains.compose") jacoco id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) + `maven-publish` +} + +kotlin { + explicitApi() + + android { + publishLibraryVariants("release") + } + jvm("desktop") { + compilations.all { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + } + } + + sourceSets.all { + // Allow all experimental APIs, since MPP projects are themselves experimental + languageSettings.apply { + optIn("kotlin.Experimental") + optIn("kotlin.ExperimentalMultiplatform") + } + } + + sourceSets { + val commonMain by getting { + dependencies { + api(compose.runtime) + api(compose.ui) + + implementation(Config.Libs.kotlinStdLib) + } + } + val androidMain by getting { + dependencies { + api(projects.sentry) + api(projects.sentryAndroidNavigation) + + api(Config.Libs.composeNavigation) + implementation(Config.Libs.lifecycleCommonJava8) + } + } + val androidTest by getting { + dependencies { + implementation(Config.TestLibs.kotlinTestJunit) + implementation(Config.TestLibs.mockitoKotlin) + implementation(Config.TestLibs.mockitoInline) + } + } + } } android { @@ -20,12 +70,8 @@ android { buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") } - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Config.Libs.composeVersion + sourceSets["main"].apply { + manifest.srcFile("src/androidMain/AndroidManifest.xml") } buildTypes { @@ -35,10 +81,6 @@ android { } } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - testOptions { animationsDisabled = true unitTests.apply { @@ -68,23 +110,6 @@ tasks.withType { } } -kotlin { - explicitApi() -} - -dependencies { - api(projects.sentry) - api(projects.sentryAndroidNavigation) - - implementation(Config.Libs.composeRuntime) - implementation(Config.Libs.composeNavigation) - - // tests - testImplementation(Config.TestLibs.kotlinTestJunit) - testImplementation(Config.TestLibs.mockitoKotlin) - testImplementation(Config.TestLibs.mockitoInline) -} - tasks.withType { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() diff --git a/sentry-compose/gradle.properties b/sentry-compose/gradle.properties new file mode 100644 index 00000000000..2dcadc5b955 --- /dev/null +++ b/sentry-compose/gradle.properties @@ -0,0 +1 @@ +kotlin.mpp.stability.nowarn=true diff --git a/sentry-android-compose/proguard-rules.pro b/sentry-compose/proguard-rules.pro similarity index 100% rename from sentry-android-compose/proguard-rules.pro rename to sentry-compose/proguard-rules.pro diff --git a/sentry-compose/src/androidMain/AndroidManifest.xml b/sentry-compose/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000000..ae7d1a148fe --- /dev/null +++ b/sentry-compose/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sentry-android-compose/src/main/java/io/sentry/android/compose/SentryNavigationIntegration.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt similarity index 85% rename from sentry-android-compose/src/main/java/io/sentry/android/compose/SentryNavigationIntegration.kt rename to sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt index 0b74f98f695..150a88f0afb 100644 --- a/sentry-android-compose/src/main/java/io/sentry/android/compose/SentryNavigationIntegration.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt @@ -1,4 +1,4 @@ -package io.sentry.android.compose +package io.sentry.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -33,12 +33,13 @@ internal class SentryLifecycleObserver( } } +// As described in https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects#6 @Composable @NonRestartableComposable -fun NavHostController.withSentry(): NavHostController { +public fun NavHostController.withObservableEffect(): NavHostController { val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle, this) { - val observer = SentryLifecycleObserver(this@withSentry) + val observer = SentryLifecycleObserver(this@withObservableEffect) lifecycle.addObserver(observer) diff --git a/sentry-compose/src/androidTest/kotlin/io/sentry/compose/SentryLifecycleObserverTest.kt b/sentry-compose/src/androidTest/kotlin/io/sentry/compose/SentryLifecycleObserverTest.kt new file mode 100644 index 00000000000..6af44680a8c --- /dev/null +++ b/sentry-compose/src/androidTest/kotlin/io/sentry/compose/SentryLifecycleObserverTest.kt @@ -0,0 +1,51 @@ +package io.sentry.compose + +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavController +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import io.sentry.IHub +import io.sentry.android.navigation.SentryNavigationListener +import kotlin.test.Test + +internal class SentryLifecycleObserverTest { + + class Fixture { + val navListener = mock() + val hub = mock() + val navController = mock() + + fun getSut(): SentryLifecycleObserver { + return SentryLifecycleObserver(navController, hub, navListener) + } + } + + private val fixture = Fixture() + + @Test + fun `onResume adds navigation listener`() { + val sut = fixture.getSut() + + sut.onStateChanged(mock(), Lifecycle.Event.ON_RESUME) + + verify(fixture.navController).addOnDestinationChangedListener(fixture.navListener) + } + + @Test + fun `onPause removes navigation listener`() { + val sut = fixture.getSut() + + sut.onStateChanged(mock(), Lifecycle.Event.ON_PAUSE) + + verify(fixture.navController).removeOnDestinationChangedListener(fixture.navListener) + } + + @Test + fun `dispose removes navigation listener`() { + val sut = fixture.getSut() + + sut.dispose() + + verify(fixture.navController).removeOnDestinationChangedListener(fixture.navListener) + } +} diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 9ff7fa2e085..33b50109097 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -8,7 +8,7 @@ android { defaultConfig { applicationId = "io.sentry.samples.android" - minSdk = Config.Android.minSdkVersionCompose + minSdk = Config.Android.minSdkVersionOkHttp targetSdk = Config.Android.targetSdkVersion versionCode = 2 versionName = "1.1.0" @@ -36,11 +36,6 @@ android { // Determines whether to support View Binding. // Note that the viewBinding.enabled property is now deprecated. viewBinding = true - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = Config.Libs.composeVersion } dependenciesInfo { @@ -109,7 +104,6 @@ dependencies { implementation(projects.sentryAndroidOkhttp) implementation(projects.sentryAndroidFragment) implementation(projects.sentryAndroidTimber) - implementation(projects.sentryAndroidCompose) implementation(Config.Libs.fragment) // how to exclude androidx if release health feature is disabled @@ -124,11 +118,5 @@ dependencies { implementation(Config.Libs.retrofit2) implementation(Config.Libs.retrofit2Gson) - implementation(Config.Libs.composeActivity) - implementation(Config.Libs.composeFoundation) - implementation(Config.Libs.composeFoundationLayout) - implementation(Config.Libs.composeNavigation) - implementation(Config.Libs.composeMaterial) - debugImplementation(Config.Libs.leakCanary) } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 3c742226de7..ab6ece24c2b 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -53,10 +53,6 @@ android:name=".PermissionsActivity" android:exported="false" /> - - diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt index 36b6a5a4958..ff725b3a751 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt @@ -3,17 +3,11 @@ package io.sentry.samples.android import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path -import retrofit2.http.Query interface GitHubService { @GET("users/{user}/repos") fun listRepos(@Path("user") user: String): Call> - - @GET("users/{user}/repos") - suspend fun listReposAsync(@Path("user") user: String, @Query("per_page") perPage: Int): List } -class Repo { - val full_name: String = "" -} +class Repo diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index cdd434faa33..5379ec4faaa 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -10,7 +10,6 @@ import io.sentry.UserFeedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; -import io.sentry.samples.android.compose.ComposeActivity; import io.sentry.samples.android.databinding.ActivityMainBinding; import java.io.BufferedWriter; import java.io.File; @@ -184,11 +183,6 @@ protected void onCreate(Bundle savedInstanceState) { startActivity(new Intent(this, PermissionsActivity.class)); }); - binding.openComposeActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, ComposeActivity.class)); - }); - setContentView(binding.getRoot()); } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt deleted file mode 100644 index 4827a250a15..00000000000 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt +++ /dev/null @@ -1,162 +0,0 @@ -package io.sentry.samples.android.compose - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import io.sentry.Sentry -import io.sentry.android.compose.withSentry -import io.sentry.samples.android.GithubAPI -import kotlinx.coroutines.launch - -class ComposeActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - val navController = rememberNavController().withSentry() - SampleNavigation(navController) - } - } - - override fun onResume() { - super.onResume() - Sentry.getSpan()?.finish() - } -} - -@Composable -fun Landing( - navigateGithub: () -> Unit, - navigateGithubWithArgs: () -> Unit -) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Button( - onClick = { navigateGithub() }, - modifier = Modifier.padding(top = 32.dp) - ) { - Text("Navigate to Github Page") - } - Button( - onClick = { navigateGithubWithArgs() }, - modifier = Modifier.padding(top = 32.dp) - ) { - Text("Navigate to Github Page With Args") - } - Button( - onClick = { throw RuntimeException("Crash from Compose") }, - modifier = Modifier.padding(top = 32.dp) - ) { - Text("Crash from Compose") - } - } -} - -@Composable -fun Github( - user: String = "getsentry", - perPage: Int = 30 -) { - var user by remember { mutableStateOf(TextFieldValue(user)) } - var result by remember { mutableStateOf("") } - val scope = rememberCoroutineScope() - - LaunchedEffect(perPage) { - result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name - } - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - TextField( - value = user, - onValueChange = { newText -> - user = newText - } - ) - Text("Random repo $result") - Button( - onClick = { - scope.launch { - result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name - } - }, - modifier = Modifier.padding(top = 32.dp) - ) { - Text("Make Request") - } - } -} - -@Composable -fun SampleNavigation(navController: NavHostController) { - NavHost( - navController = navController, - startDestination = Destination.Landing.route - ) { - composable(Destination.Landing.route) { - Landing( - navigateGithub = { navController.navigate("github") }, - navigateGithubWithArgs = { navController.navigate("github/spotify?per_page=1") } - ) - } - composable(Destination.Github.route) { - Github() - } - composable( - Destination.GithubWithArgs.route, - arguments = listOf( - navArgument(Destination.USER_ARG) { type = NavType.StringType }, - navArgument(Destination.PER_PAGE_ARG) { type = NavType.IntType; defaultValue = 10 } - ) - ) { - Github( - it.arguments?.getString(Destination.USER_ARG) ?: "getsentry", - it.arguments?.getInt(Destination.PER_PAGE_ARG) ?: 10 - ) - } - } -} - -sealed class Destination( - val route: String -) { - object Landing : Destination("landing") - object Github : Destination("github") - object GithubWithArgs : Destination("github/{$USER_ARG}?$PER_PAGE_ARG={$PER_PAGE_ARG}") - - companion object { - const val USER_ARG = "user" - const val PER_PAGE_ARG = "per_page" - } -} diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml index ee75e72299e..3f0549fdf8a 100644 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml @@ -111,12 +111,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/open_permissions_activity"/> - -