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

Predictive back gesture and navigation update #17

Merged
merged 6 commits into from
May 16, 2024
Merged
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
3 changes: 1 addition & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ dependencies {
implementation(Dependencies.Compose.animation)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.foundation_layout)
implementation(Dependencies.Compose.material)
implementation(Dependencies.Compose.material_icons_extended)
implementation(Dependencies.Compose.material3)
implementation(Dependencies.Compose.runtime_livedata)
implementation(Dependencies.Compose.runtime)
implementation(Dependencies.Compose.ui)
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.App"
android:supportsRtl="true">
android:supportsRtl="true"
tools:targetApi="tiramisu">

<activity
android:name=".AppActivity"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/app/futured/androidprojecttemplate/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.futured.androidprojecttemplate
import android.app.Application
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import app.futured.androidprojecttemplate.ui.AppUI
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -23,6 +24,7 @@ class App : Application() {
class AppActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppUI()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.futured.androidprojecttemplate.navigation

import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NamedNavArgument
Expand All @@ -11,19 +12,35 @@ import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.navArgument
import app.futured.androidprojecttemplate.ui.screens.detail.DetailScreen
import app.futured.androidprojecttemplate.ui.screens.home.HomeScreen

typealias DestinationArgumentKey = String
typealias DestinationArgumentValue = String

internal val screens = listOf(
Destination.Home,
Destination.Detail,
)

internal val dialogs = listOf<Destination>()

sealed class Destination(
val route: String,
val arguments: List<NamedNavArgument> = emptyList(),
val deepLinks: List<NavDeepLink> = emptyList(),
val destinationScreen: @Composable (router: NavRouter) -> Unit,
) {
data object Home : Destination(route = "home")
data object Home : Destination(
route = "home",
destinationScreen = { HomeScreen(navigation = it) },
)

data object Detail : Destination(
route = "detail/{title}?subtitle={subtitle}?value={value}",
arguments = listOf(
destinationScreen = { DetailScreen(navigation = it) },
arguments =
listOf(
navArgument("title") {
type = NavType.StringType
},
Expand All @@ -47,7 +64,7 @@ sealed class Destination(
/**
* Registers provided [destination] as a composable in [NavGraphBuilder].
*/
fun NavGraphBuilder.composable(
fun NavGraphBuilder.composableScreen(
destination: Destination,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) = composable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.futured.androidprojecttemplate.navigation

interface NavRouter {
fun popBackStack()
fun navigateBack(popUpToDestination: Destination, inclusive: Boolean = false)

fun navigateToDetail(title: String, subtitle: String? = null, value: String? = null)

fun <T> navigateBackWithResult(key: String, value: T)
fun <T> setCurrentResult(key: String, value: T)
fun <T> subscribeForResult(key: String, callback: (T) -> Unit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package app.futured.androidprojecttemplate.navigation

import androidx.navigation.NavController
import app.futured.androidprojecttemplate.tools.extensions.subscribeForResult
import timber.log.Timber

/**
* Class that triggers navigation actions on provided [navController].
*/
class NavRouterImpl(private val navController: NavController) : NavRouter {
override fun popBackStack() {
navController.navigateUp()
}

override fun navigateBack(popUpToDestination: Destination, inclusive: Boolean) {
navController.popBackStack(route = popUpToDestination.route, inclusive = inclusive)
}

override fun navigateToDetail(title: String, subtitle: String?, value: String?) =
Destination.Detail.buildRoute(title, subtitle, value).execute()

override fun <T> navigateBackWithResult(key: String, value: T) {
navController.previousBackStackEntry?.savedStateHandle?.also {
it[key] = value
navController.popBackStack()
}
}

override fun <T> setCurrentResult(key: String, value: T) {
navController.currentBackStackEntry?.savedStateHandle?.also {
it[key] = value
}
}

override fun <T> subscribeForResult(key: String, callback: (T) -> Unit) {
navController.currentBackStackEntry?.savedStateHandle?.subscribeForResult<T>(key) { callback(it) }
}

private fun String.execute(
popUpToDestinationRoute: String? = null,
isInclusive: Boolean = true,
) {
Timber.d("## Navigate to $this, popupTo $popUpToDestinationRoute, inclusive $isInclusive")
if (popUpToDestinationRoute != null) {
navController.navigate(this) {
popUpTo(popUpToDestinationRoute) {
inclusive = isInclusive
}
}
} else {
navController.navigate(this)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package app.futured.androidprojecttemplate.tools.extensions

inline fun <T> T?.ifNull(defaultValue: () -> T): T = this ?: defaultValue.invoke()

inline fun <T> withValue(receiver: T, block: T.() -> Unit) {
receiver.block()
}

inline fun <reified T : Any> withNonNullValue(receiver: T?, block: (T) -> Unit) {
if (receiver != null) block(receiver)
}

inline fun <T> safe(block: () -> T): T? {
return try {
block()
} catch (e: Exception) {
e.printStackTrace()
null
}
}

fun <T> T?.orThrow(): T = this ?: error("UnexpectedError") // Dev error

inline fun <reified T> ifElseNull(predicate: Boolean, block: () -> T?) = if (predicate) block.invoke() else null

inline fun <T> T.runWith(block: (T) -> Unit) {
block(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package app.futured.androidprojecttemplate.tools.extensions

import android.util.Base64.NO_PADDING
import android.util.Base64.NO_WRAP
import android.util.Base64.URL_SAFE
import android.util.Base64.decode
import android.util.Base64.encodeToString
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

inline fun <reified T> SavedStateHandle.getSerializedArgument(key: String): T? = get<String>(key)?.deserializeFromNavArgument()

inline fun <reified T> SavedStateHandle.getRequiredSerializedArgument(key: String): T =
getSerializedArgument(key) ?: error("Required parameter is not present")

inline fun <reified T> SavedStateHandle.getArgument(key: String): T? = safe { get<T>(key) }

inline fun <reified T> SavedStateHandle.getRequiredArgument(key: String): T =
getArgument(key) ?: getSerializedArgument(key) ?: error("Required parameter is not present")

inline fun <reified T : Any> T.serializeAsNavArgument(): String? =
encodeToString(Json.encodeToString(this).encodeToByteArray(), NO_PADDING or NO_WRAP or URL_SAFE)

inline fun <reified T> String.deserializeFromNavArgument(): T? =
safe { Json.decodeFromString(decode(this, NO_PADDING or NO_WRAP or URL_SAFE).decodeToString()) }

fun <T> SavedStateHandle.subscribeForResult(key: String, callback: (T) -> Unit) {
val liveData = this.getLiveData<T>(key)
val observer = object : Observer<T> {
override fun onChanged(t: T) {
if (t != null) {
val value = remove<T>(key)
if (value != null) {
callback(t)
}
liveData.removeObserver(this)
subscribeForResult(key, callback)
}
}
}
liveData.observeForever(observer)
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
package app.futured.androidprojecttemplate.ui

import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import app.futured.androidprojecttemplate.navigation.Destination
import app.futured.androidprojecttemplate.navigation.NavigationDestinations
import app.futured.androidprojecttemplate.navigation.NavigationDestinationsImpl
import app.futured.androidprojecttemplate.navigation.composable
import app.futured.androidprojecttemplate.ui.screens.detail.DetailScreen
import app.futured.androidprojecttemplate.ui.screens.home.HomeScreen
import app.futured.androidprojecttemplate.navigation.NavRouter
import app.futured.androidprojecttemplate.navigation.NavRouterImpl
import app.futured.androidprojecttemplate.navigation.composableDialog
import app.futured.androidprojecttemplate.navigation.composableScreen
import app.futured.androidprojecttemplate.navigation.dialogs
import app.futured.androidprojecttemplate.navigation.screens

@Composable
fun NavGraph(
navController: NavHostController = rememberNavController(),
navigation: NavigationDestinations = remember { NavigationDestinationsImpl(navController) },
navigation: NavRouter = remember { NavRouterImpl(navController) },
) {
LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher?.let {
navController.navigateUp()
}

NavHost(
navController = navController,
startDestination = Destination.Home.route,
) {
composable(Destination.Home) {
HomeScreen(navigation)
// Destinations without navbar at the bottom
screens.forEach { destination ->
composableScreen(destination) { destination.destinationScreen(navigation) }
}

composable(Destination.Detail) {
DetailScreen(navigation)
// Dialogs
dialogs.forEach { destination ->
composableDialog(destination) { destination.destinationScreen(navigation) }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package app.futured.androidprojecttemplate.ui.components

import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.futured.androidprojecttemplate.tools.compose.ComponentPreviews
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.futured.androidprojecttemplate.ui.components

import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.futured.androidprojecttemplate.ui.theme.AppTheme
Expand All @@ -12,7 +12,7 @@ import app.futured.androidprojecttemplate.ui.theme.AppTheme
@Composable
fun Showcase(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
AppTheme {
Surface(color = MaterialTheme.colors.background, modifier = modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
content()
}
}
Expand Down
Loading
Loading