Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: dynamic feature and voyager sample #104

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import java.nio.file.Paths

buildscript {
repositories {
mavenCentral()
Expand All @@ -21,3 +23,65 @@ subprojects {
tasks.register<Delete>("clean") {
delete(rootProject.buildDir)
}

/**
* ===========================================================================================
* BEGIN SECTION USING GRADLE TO BUILD, TRANSFORM AND INSTALL APP BUNDLE WITH DYNAMIC FEATURE
* SUPPORT TO LOCAL TESTING. On your terminal just run:
*
* ./gradlew runDynamicFeatureSample
*
* ===========================================================================================
*/

val appBundleOriginPath = Paths.get("$rootDir","samples", "dynamic-feature", "app", "build", "outputs", "bundle", "debug")
val appBundleDestPath = Paths.get("${rootProject.buildDir}", "app-debug.aab")
val apksPath = Paths.get("${rootProject.buildDir}", "app.apks")
val bundleToolPath = Paths.get("${rootProject.buildDir}", "bundletool.jar")

tasks.register<Copy>("copyAab") {
dependsOn(":samples:dynamic-feature:app:bundleDebug")

from(appBundleOriginPath)
into(rootProject.buildDir)
include("**/*.aab")
}

tasks.register("downloadBundleTool") {
dependsOn("copyAab")

doLast {
val dest = bundleToolPath.toFile()
if (!dest.exists()) {
ant.invokeMethod("get", mapOf(
"src" to "https://github.com/google/bundletool/releases/download/1.13.2/bundletool-all-1.13.2.jar",
"dest" to dest
))
}
}
}

tasks.register<Delete>("cleanApks") {
delete(apksPath)
}

tasks.register<Exec>("generateApks") {
dependsOn("cleanApks", "downloadBundleTool")

executable("java")

args("-jar", "$bundleToolPath")
args("build-apks", "--local-testing")
args("--bundle", "$appBundleDestPath")
args("--output", "$apksPath")
}

tasks.register<Exec>("runDynamicFeatureSample") {
dependsOn("generateApks")

executable("java")

args("-jar", "$bundleToolPath")
args("install-apks")
args("--apks", "$apksPath")
}
7 changes: 6 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
plugin-android = "8.0.0-alpha08"
plugin-android = "8.0.0-alpha11"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Canary IDE forced me.

plugin-ktlint = "11.0.0"
plugin-maven = "0.22.0"
plugin-multiplatform-compose = "1.2.1"
Expand All @@ -15,6 +15,8 @@ lifecycle = "2.5.1"
compose = "1.3.1"
composeActivity = "1.6.1"

playCore = "1.10.3"

junit = "5.8.2"

[libraries]
Expand All @@ -25,6 +27,7 @@ plugin-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version.
plugin-hilt = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
plugin-multiplatform-compose = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "plugin-multiplatform-compose" }

kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
leakCanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanary" }
coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kodein = { module = "org.kodein.di:kodein-di-framework-compose", version.ref = "kodein" }
Expand All @@ -49,5 +52,7 @@ compose-activity = { module = "androidx.activity:activity-compose", version.ref

composeMultiplatform-runtimeSaveable = { module = "org.jetbrains.compose.runtime:runtime-saveable", version.ref = "plugin-multiplatform-compose" }

play-core = { module = "com.google.android.play:core", version.ref = "playCore" }

junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
Empty file modified gradlew
100644 → 100755
Empty file.
4 changes: 4 additions & 0 deletions includes.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
include(
":samples:android",
":samples:dynamic-feature:app",
":samples:dynamic-feature:details",
":samples:dynamic-feature:home",
":samples:dynamic-feature:navigation",
":samples:multiplatform",

":samples:multi-module:app",
Expand Down
1 change: 1 addition & 0 deletions samples/dynamic-feature/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
34 changes: 34 additions & 0 deletions samples/dynamic-feature/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
plugins {
id("com.android.application")
kotlin("android")
}

setupModuleForAndroidxCompose(
composeCompilerVersion = libs.versions.compose.get(),
withKotlinExplicitMode = false,
)

android {
namespace = "cafe.adriel.voyager.dynamic.feature"
defaultConfig {
applicationId = "cafe.adriel.voyager.dynamic.feature"
}
dynamicFeatures += listOf(
":samples:dynamic-feature:home",
":samples:dynamic-feature:details"
)
}

dependencies {
api(projects.samples.dynamicFeature.navigation)
implementation(projects.voyagerNavigator)

implementation(libs.appCompat)
implementation(libs.compose.activity)
implementation(libs.compose.material)
implementation(libs.compose.ui)

implementation(libs.play.core)

implementation("com.google.android.material:material:1.5.0")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to have XML default theme working

}
21 changes: 21 additions & 0 deletions samples/dynamic-feature/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
26 changes: 26 additions & 0 deletions samples/dynamic-feature/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"
android:supportsRtl="true"
android:theme="@style/Theme.VoyagerDynamicFeature"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cafe.adriel.voyager.dynamic.feature

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import cafe.adriel.voyager.core.registry.rememberScreen
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.dynamic.feature.module.HomeDynamicFeatureScreenProvider
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow

internal object InitialScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow

// Simulate navigation using ScreenRegistry
val homeScreen = rememberScreen(provider = HomeDynamicFeatureScreenProvider)

Column {
Text(
text = "Hey, I'm the app entry point. Congratulations!!!!!",
fontSize = 32.sp,
modifier = Modifier
.background(color = Color.Cyan)
)
Text(
text = "Click me to go to Home",
fontSize = 24.sp,
modifier = Modifier
.background(color = Color.Red)
.clickable {
navigator.push(homeScreen)
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cafe.adriel.voyager.dynamic.feature

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import cafe.adriel.voyager.core.registry.ScreenRegistry
import cafe.adriel.voyager.dynamic.feature.custom.dynamicScreen
import cafe.adriel.voyager.dynamic.feature.module.HomeDynamicFeatureScreenProvider
import cafe.adriel.voyager.dynamic.feature.navigation.DynamicFeatureNavigator

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

ScreenRegistry {
register<HomeDynamicFeatureScreenProvider> { screenProvider ->
dynamicScreen(screenProvider)
}
}

setContent {
DynamicFeatureNavigator(
activity = this@MainActivity,
screen = InitialScreen,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package cafe.adriel.voyager.dynamic.feature.custom

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cafe.adriel.voyager.dynamic.feature.navigation.DynamicFeatureInstallState
import cafe.adriel.voyager.dynamic.feature.navigation.DynamicFeatureScreen
import cafe.adriel.voyager.dynamic.feature.navigation.DynamicFeatureScreenProvider
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus

fun dynamicScreen(screenProvider: DynamicFeatureScreenProvider): DynamicFeatureScreen =
DynamicFeatureScreen(
screenProvider = screenProvider,
content = { state, installedModules, requestUserConfirmation, retry ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
ShowProgressToUserContent(
state = state,
installedModules = installedModules,
requestUserConfirmation = requestUserConfirmation,
retry = retry
)
}
}
)

@Composable
internal fun ShowProgressToUserContent(
state: DynamicFeatureInstallState,
installedModules: Set<String>,
requestUserConfirmation: (requestCode: Int) -> Unit,
retry: () -> Unit,
) {
val multiInstall = state.moduleNames.size > 1
val names = state.moduleNames.joinToString(" - ")

when (state.status) {
SplitInstallSessionStatus.DOWNLOADING -> {
LoadingComponent(
bytesDownloaded = state.bytesDownloaded,
totalBytesToDownload = state.totalBytesToDownload,
message = "Downloading $names",
)
}

SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
Text(
text = "We need user confirmation to download",
fontSize = 24.sp,
modifier = Modifier
.background(color = Color.Cyan)
.clickable {
requestUserConfirmation(1234)
}
)
}

SplitInstallSessionStatus.INSTALLED -> {
Text(
text = "Success installation of [$names].\nNavigation will happen automatically :D",
fontSize = 24.sp,
)
}

SplitInstallSessionStatus.INSTALLING -> {
LoadingComponent(
bytesDownloaded = state.bytesDownloaded,
totalBytesToDownload = state.totalBytesToDownload,
message = "Installing $names",
)
}

SplitInstallSessionStatus.FAILED -> {
Text(
text = "Error: ${state.errorCode} for module ${state.moduleNames}",
fontSize = 24.sp,
modifier = Modifier
.background(color = Color.Cyan)
.clickable {
retry.invoke()
}
)
}
}
}

@Composable
internal fun LoadingComponent(
bytesDownloaded: Long,
totalBytesToDownload: Long,
message: String
) {
Column {
LinearProgressIndicator(
progress = bytesDownloaded / totalBytesToDownload.toFloat()
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = message,
fontSize = 24.sp,
)
}
}
Loading