Skip to content

Commit

Permalink
Use Saved instance state as backup to handle stack routing
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Dec 18, 2023
1 parent e25ae82 commit fc6dfe1
Show file tree
Hide file tree
Showing 16 changed files with 623 additions and 43 deletions.
45 changes: 45 additions & 0 deletions buildSrc/src/main/kotlin/AndroidConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("UNUSED_VARIABLE")

import com.android.build.gradle.BaseExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.*

fun Project.configureAndroid() {
kotlin {
android()

sourceSets.apply {
val androidMain by getting {
findByName("commonMain")?.let { dependsOn(it) }
findByName("jvmAndNixMain")?.let { dependsOn(it) }
findByName("jvmMain")?.let { dependsOn(it) }
}

val androidTest by creating {
findByName("commonTest")?.let { dependsOn(it) }
findByName("jvmAndNixTest")?.let { dependsOn(it) }
findByName("jvmTest")?.let { dependsOn(it) }
}
}
}

extensions.findByType<BaseExtension>()?.apply {
compileSdkVersion(34)
sourceSets["main"].java.srcDirs("android/src/kotlin")
sourceSets["main"].manifest.srcFile("android/src/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("android/src/res")

defaultConfig {
minSdk = 23
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
}
4 changes: 3 additions & 1 deletion buildSrc/src/main/kotlin/TargetsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import org.gradle.api.*
import org.gradle.kotlin.dsl.*
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.targets.native.tasks.*
import java.io.*

val Project.files: Array<File> get() = project.projectDir.listFiles() ?: emptyArray()
val Project.hasAndroid: Boolean get() = files.any { it.name == "android" }
val Project.hasCommon: Boolean get() = files.any { it.name == "common" }
val Project.hasJvmAndNix: Boolean get() = hasCommon || files.any { it.name == "jvmAndNix" }
val Project.hasPosix: Boolean get() = hasCommon || files.any { it.name == "posix" }
Expand All @@ -25,6 +25,8 @@ fun Project.configureTargets() {
configureCommon()
if (hasJvm) configureJvm()

if (hasAndroid) configureAndroid()

kotlin {
if (hasJs) {
js(IR) {
Expand Down
18 changes: 18 additions & 0 deletions core-stack/android/src/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="dev.programadorthi.routing.core.StackInitializer"
android:value="androidx.startup" />
</provider>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.programadorthi.routing.core

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class StackApplicationCallParcelable(
val name: String,
val routeMethod: String,
val uri: String,
val parameters: Map<String, List<String>>,
) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package dev.programadorthi.routing.core

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.savedstate.SavedStateRegistry
import androidx.startup.Initializer
import java.lang.ref.WeakReference

internal class StackInitializer :
Initializer<Unit>,
Application.ActivityLifecycleCallbacks,
StackManagerNotifier {

private val lock = Any()
private var currentActivity = WeakReference<ComponentActivity?>(null)

init {
StackManager.stackManagerNotifier = this
}

//region Initializer
override fun create(context: Context) {
val application = context.applicationContext as Application
application.registerActivityLifecycleCallbacks(this)
}

override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
//endregion

//region Application.ActivityLifecycleCallbacks
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
updateCurrentActivity(activity = activity)
}

override fun onActivityStarted(activity: Activity) {
updateCurrentActivity(activity = activity)
}

override fun onActivityResumed(p0: Activity) {}
override fun onActivityPaused(p0: Activity) {}
override fun onActivityStopped(p0: Activity) {}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
override fun onActivityDestroyed(p0: Activity) {}
//endregion

//region StackManagerNotifier
override fun onRegistered(providerId: String, stackManager: StackManager) = synchronized(lock) {
val registry = currentActivity.get()?.savedStateRegistry ?: return
processRegistration(
registry = registry,
providerId = providerId,
stackManager = stackManager,
)
}

override fun onUnRegistered(providerId: String) = synchronized(lock) {
val registry = currentActivity.get()?.savedStateRegistry ?: return
registry.unregisterSavedStateProvider(providerId)
}
//endregion

private fun updateCurrentActivity(activity: Activity) = synchronized(lock) {
if (activity !is ComponentActivity) {
Log.w(
"kotlin-routing",
"Your activity must be an androidx.activity.ComponentActivity. Current is: $activity"
)
return
}
currentActivity = WeakReference(activity)
val registry = activity.savedStateRegistry
if (registry.isRestored) {
processRestoration(registry)
} else {
StackManager.subscriptions().forEach { (providerId, stackManager) ->
processRegistration(
registry = registry,
providerId = providerId,
stackManager = stackManager,
)
}
}
}

private fun processRegistration(
registry: SavedStateRegistry,
providerId: String,
stackManager: StackManager
) {
registry.unregisterSavedStateProvider(providerId)
registry.registerSavedStateProvider(
key = providerId,
provider = StackSavedStateProvider(providerId, stackManager),
)
}

private fun processRestoration(registry: SavedStateRegistry) {
StackManager.subscriptions().forEach { (providerId, stackManager) ->
val previousState = registry.consumeRestoredStateForKey(providerId)
if (previousState?.isEmpty == false) {
val saver = StackSavedStateProvider(providerId, stackManager)
saver.restoreState(previousState)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package dev.programadorthi.routing.core

import android.os.Build
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.savedstate.SavedStateRegistry
import io.ktor.http.parametersOf
import io.ktor.util.toMap

internal class StackSavedStateProvider(
private val providerId: String,
private val stackManager: StackManager
) : SavedStateRegistry.SavedStateProvider {

override fun saveState(): Bundle {
val calls = stackManager.toSave()
val parcelables = calls.map { call ->
StackApplicationCallParcelable(
name = call.name,
routeMethod = call.routeMethod.value,
uri = call.uri,
parameters = call.parameters.toMap(),
)
}.toTypedArray()
return bundleOf(providerId to parcelables)
}

fun restoreState(previousState: Bundle) {
val parcelables = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
previousState
.getParcelableArray(providerId, StackApplicationCallParcelable::class.java)
?.toList() ?: emptyList()
} else {
buildList<StackApplicationCallParcelable> {
previousState.getParcelableArray(providerId)?.forEach { parcelable ->
if (parcelable is StackApplicationCallParcelable) {
add(parcelable)
}
}
}
}
val calls = parcelables.mapNotNull(::mapToCall)
stackManager.toRestore(calls)
}

private fun mapToCall(parcelable: StackApplicationCallParcelable): StackApplicationCall? {
return when (val routhMethod = StackRouteMethod.parse(parcelable.routeMethod)) {
StackRouteMethod.Push -> mapToPush(parcelable)
StackRouteMethod.Replace,
StackRouteMethod.ReplaceAll -> mapToReplace(parcelable, routhMethod)

else -> null
}
}

private fun mapToPush(parcelable: StackApplicationCallParcelable): StackApplicationCall {
return when {
parcelable.name.isBlank() -> StackApplicationCall.Push(
application = stackManager.application,
uri = parcelable.uri,
parameters = parametersOf(parcelable.parameters),
)

else -> StackApplicationCall.PushNamed(
application = stackManager.application,
name = parcelable.name,
parameters = parametersOf(parcelable.parameters),
)
}
}

private fun mapToReplace(
parcelable: StackApplicationCallParcelable,
routeMethod: StackRouteMethod,
): StackApplicationCall {
return when {
parcelable.name.isBlank() -> StackApplicationCall.Replace(
all = routeMethod == StackRouteMethod.ReplaceAll,
application = stackManager.application,
uri = parcelable.uri,
parameters = parametersOf(parcelable.parameters),
)

else -> StackApplicationCall.ReplaceNamed(
all = routeMethod == StackRouteMethod.ReplaceAll,
application = stackManager.application,
name = parcelable.name,
parameters = parametersOf(parcelable.parameters),
)
}
}
}
13 changes: 13 additions & 0 deletions core-stack/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlin-parcelize")
id("org.jlleitschuh.gradle.ktlint")
id("org.jetbrains.kotlinx.kover")
alias(libs.plugins.maven.publish)
Expand All @@ -14,5 +16,16 @@ kotlin {
api(projects.core)
}
}

val androidMain by getting {
dependencies {
implementation(libs.androidx.activity)
implementation(libs.androidx.startup)
}
}
}
}

android {
namespace = "dev.programadorthi.routing.core.stack"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ public sealed class StackApplicationCall : ApplicationCall, CoroutineScope {

public data class Pop(
override val application: Application,
override val name: String,
override val uri: String,
override val parameters: Parameters = Parameters.Empty,
) : StackApplicationCall() {
override val routeMethod: RouteMethod get() = StackRouteMethod.Pop

override val name: String get() = ""
}

public data class Push(
Expand Down
Loading

0 comments on commit fc6dfe1

Please sign in to comment.