Skip to content

Commit

Permalink
Merge pull request #706 from arkivanov/merge-v3.0
Browse files Browse the repository at this point in the history
Merge v3.0
  • Loading branch information
arkivanov authored Apr 29, 2024
2 parents 0917cc1 + fb25c56 commit e8dece3
Show file tree
Hide file tree
Showing 266 changed files with 4,976 additions and 5,532 deletions.
9 changes: 2 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ jobs:
uses: gradle/gradle-build-action@v2
with:
arguments: build -Dsplit_targets
- name: Android instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 21
arch: x86
disable-animations: true
script: ./gradlew :extensions-compose-jetpack:connectedDebugAndroidTest --stacktrace
macos-build:
name: Build on macOS
runs-on: macos-14
Expand All @@ -50,3 +43,5 @@ jobs:
arguments: build -Dsplit_targets
- name: Build iOS sample
run: xcodebuild -project sample/app-ios/app-ios.xcodeproj -scheme app-ios -sdk iphonesimulator -arch x86_64 build
- name: Build iOS Compose sample
run: xcodebuild -project sample/app-ios-compose/app-ios-compose.xcodeproj -scheme app-ios-compose -sdk iphonesimulator -arch x86_64 build
9 changes: 8 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ buildscript {
classpath(deps.android.gradle)
classpath(deps.jetbrains.compose.composeGradlePlug)
classpath(deps.jetbrains.kotlinx.binaryCompatibilityValidator)
classpath(deps.parcelizeDarwin.gradlePlug)
classpath(deps.jetbrains.kotlin.serializationGradlePlug)
}
}
Expand All @@ -34,6 +33,7 @@ setupDefaults(
androidTarget()
jvm()
js { browser() }
wasmJs { browser() }
iosCompat()
watchosCompat()
tvosCompat()
Expand Down Expand Up @@ -75,4 +75,11 @@ allprojects {
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

afterEvaluate {
// Workaround for https://youtrack.jetbrains.com/issue/KT-52776
rootProject.extensions.findByType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>()?.apply {
versions.webpackCli.version = "4.10.0"
}
}
}
231 changes: 74 additions & 157 deletions decompose/api/android/decompose.api

Large diffs are not rendered by default.

181 changes: 62 additions & 119 deletions decompose/api/jvm/decompose.api

Large diffs are not rendered by default.

39 changes: 27 additions & 12 deletions decompose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import com.arkivanov.gradle.setupBinaryCompatibilityValidator
import com.arkivanov.gradle.setupMultiplatform
import com.arkivanov.gradle.setupPublication
import com.arkivanov.gradle.setupSourceSets
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
id("kotlin-multiplatform")
id("com.android.library")
id("kotlinx-serialization")
id("kotlin-parcelize")
id("com.arkivanov.parcelize.darwin")
id("com.arkivanov.gradle.setup")
}

Expand All @@ -26,17 +25,18 @@ android {
kotlin {
setupSourceSets {
val android by bundle()
val nonAndroid by bundle()
val nonNative by bundle()
val darwin by bundle()
val itvos by bundle()
val js by bundle()
val nonJs by bundle()
val wasmJs by bundle()
val nonWeb by bundle()
val web by bundle()

(nonAndroid + darwin + nonNative + nonJs) dependsOn common
(allSet - android) dependsOn nonAndroid
(allSet - nativeSet) dependsOn nonNative
(allSet - js) dependsOn nonJs
(darwin) dependsOn common
nonWeb dependsOn common
(allSet - js - wasmJs) dependsOn nonWeb
web dependsOn common
(js + wasmJs) dependsOn web
(iosSet + tvosSet) dependsOn itvos
(darwinSet - iosSet - tvosSet + itvos) dependsOn darwin

Expand All @@ -53,17 +53,32 @@ kotlin {
api(deps.essenty.instanceKeeper)
api(deps.essenty.backHandler)
api(deps.jetbrains.kotlinx.kotlinxSerializationCore)
implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson)
}

common.test.dependencies {
implementation(deps.jetbrains.kotlinx.kotlinxCoroutinesCore)
implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson)

// Workaround: https://github.com/Kotlin/kotlinx.coroutines/issues/3968
implementation("org.jetbrains.kotlinx:atomicfu:0.23.1")
}

android.main.dependencies {
implementation(deps.androidx.activity.activityKtx)
implementation(deps.androidx.fragment.fragmentKtx)
implementation(deps.androidx.lifecycle.lifecycleCommonJava8)
}

android.test.dependencies {
implementation(deps.robolectric.robolectric)
}

web.main.dependencies {
implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson)
}
}
}

tasks.named<KotlinCompilationTask<*>>("compileKotlinWasmJs").configure {
compilerOptions {
freeCompilerArgs.add("-Xwasm-kclass-fqn")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.arkivanov.decompose

import android.app.Activity
import android.app.TaskStackBuilder
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.savedstate.SavedStateRegistryOwner

/**
* Extracts a deep link URL from this [Activity] [Intent], calls [block]
* function with the extracted deep link URL (if any) and returns the result.
*
* This function must be called from [Activity.onCreate] method.
*
* It is strongly recommended to always use the `standard` (default)
* `launchMode` for the [Activity] when handling deep links. This function takes
* care of restarting the [Activity] task and finishing this [Activity] if needed,
* in which case the returned value is `null`.
*
* Example of creating a root component with deep link support.
*
* ```kotlin
* class MainActivity : AppCompatActivity() {
* override fun onCreate(savedInstanceState: Bundle?) {
* super.onCreate(savedInstanceState)
*
* val root =
* handleDeepLink { uri ->
* val itemId = uri?.extractItemId() // Parse the deep link
* DefaultRootComponent(
* componentContext = defaultComponentContext(discardSavedState = itemId != null),
* itemId = itemId,
* )
* } ?: return
*
* // Display the root component as usual
* }
*
* private fun Uri.extractItemId(): String? =
* TODO("Extract item id from the deep link")
* }
* ```
*/
@ExperimentalDecomposeApi
fun <A, T : Any> A.handleDeepLink(
block: (Uri?) -> T,
): T? where A : Activity, A : SavedStateRegistryOwner {
if (restartIfNeeded()) {
return null
}

val savedState: Bundle? = savedStateRegistry.consumeRestoredStateForKey(key = KEY_SAVED_DEEP_LINK_STATE)
val isDeepLinkHandled = savedState?.getBoolean(KEY_DEEP_LINK_HANDLED) ?: false
val deepLink = intent.data.takeUnless { isDeepLinkHandled }

savedStateRegistry.registerSavedStateProvider(key = KEY_SAVED_DEEP_LINK_STATE) {
bundleOf(KEY_DEEP_LINK_HANDLED to (isDeepLinkHandled || (deepLink != null)))
}

return block(deepLink)
}

// Derived from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt;l=1486;drc=fd7d0dc4a56c2aef65424db7986aa057f9717661
private fun Activity.restartIfNeeded(): Boolean {
if ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK == 0) || (intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK != 0)) {
return false
}

// Someone called us with NEW_TASK, but we don't know what state our whole
// task stack is in, so we need to manually restart the whole stack to
// ensure we're in a predictably good state.

intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
finish()
// Disable second animation in case where the Activity is created twice.
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)

return true
}

private const val KEY_SAVED_DEEP_LINK_STATE = "SAVED_DEEP_LINK_STATE"
private const val KEY_DEEP_LINK_HANDLED = "DEEP_LINK_HANDLED"

Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.instanceKeeper
import com.arkivanov.essenty.lifecycle.asEssentyLifecycle
import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.stateKeeper
import kotlinx.serialization.builtins.serializer
import androidx.lifecycle.Lifecycle as AndroidLifecycle

fun DefaultComponentContext(
Expand All @@ -30,33 +33,67 @@ fun DefaultComponentContext(
/**
* Creates a default implementation of [ComponentContext] and attaches it
* to the receiver (e.g. an [Activity][android.app.Activity]).
*
* @param discardSavedState a flag indicating whether any previously saved state should be discarded or not,
* default value is `false`. Can be useful for handling deep links in `onCreate`, so that the navigation state
* is not restored and initial state from the deep link is applied instead.
* @param isStateSavingAllowed called before saving the state. When `true` then the state will be saved,
* otherwise it won't. Default value is `true`.
*/
fun <T> T.defaultComponentContext(): DefaultComponentContext where
fun <T> T.defaultComponentContext(
discardSavedState: Boolean = false,
isStateSavingAllowed: () -> Boolean = { true },
): DefaultComponentContext where
T : SavedStateRegistryOwner, T : OnBackPressedDispatcherOwner, T : ViewModelStoreOwner, T : LifecycleOwner =
DefaultComponentContext(
lifecycle = (this as LifecycleOwner).lifecycle,
savedStateRegistry = savedStateRegistry,
viewModelStore = viewModelStore,
onBackPressedDispatcher = onBackPressedDispatcher,
defaultComponentContext(
backHandler = BackHandler(onBackPressedDispatcher),
discardSavedState = discardSavedState,
isStateSavingAllowed = isStateSavingAllowed,
)

/**
* Creates a default implementation of [ComponentContext] and attaches it to the [Fragment].
*
* @param onBackPressedDispatcher an optional [OnBackPressedDispatcher] for back button handling,
* or `null` if back button handling is not required. Can be obtained via `requireActivity().onBackPressedDispatcher`.
* @param discardSavedState a flag indicating whether any previously saved state should be discarded or not,
* default value is `false`. Can be useful for handling deep links in `onCreate`, so that the navigation state
* is not restored and initial state from the deep link is applied instead.
* @param isStateSavingAllowed called before saving the state. When `true` then the state will be saved,
* otherwise it won't. Default value is `true`.
*/
fun Fragment.defaultComponentContext(
onBackPressedDispatcher: OnBackPressedDispatcher?,
discardSavedState: Boolean = false,
isStateSavingAllowed: () -> Boolean = { true },
): DefaultComponentContext =
DefaultComponentContext(
lifecycle = lifecycle.asEssentyLifecycle(),
stateKeeper = StateKeeper(savedStateRegistry),
instanceKeeper = InstanceKeeper(viewModelStore),
defaultComponentContext(
backHandler = onBackPressedDispatcher?.let {
BackHandler(
onBackPressedDispatcher = it,
lifecycleOwner = this,
)
},
discardSavedState = discardSavedState,
isStateSavingAllowed = isStateSavingAllowed,
)

private fun <T> T.defaultComponentContext(
backHandler: BackHandler?,
discardSavedState: Boolean,
isStateSavingAllowed: () -> Boolean,
): DefaultComponentContext where
T : SavedStateRegistryOwner, T : ViewModelStoreOwner, T : LifecycleOwner {
val stateKeeper = stateKeeper(discardSavedState = discardSavedState, isSavingAllowed = isStateSavingAllowed)
val marker = stateKeeper.consume(key = KEY_STATE_MARKER, strategy = String.serializer())
stateKeeper.register(key = KEY_STATE_MARKER, strategy = String.serializer()) { "marker" }

return DefaultComponentContext(
lifecycle = lifecycle.asEssentyLifecycle(),
stateKeeper = stateKeeper,
instanceKeeper = instanceKeeper(discardRetainedInstances = marker == null),
backHandler = backHandler,
)
}

private const val KEY_STATE_MARKER = "DefaultComponentContext_state_marker"
Loading

0 comments on commit e8dece3

Please sign in to comment.