Skip to content

Commit

Permalink
Migrate to type-safe Navigation and enable predictive back animations (
Browse files Browse the repository at this point in the history
…#22)

* migrate to type-safe navigation screens and upgrade to latest compose versions

* enable predictive back animations

* shorten lines in NavigationCommand and PokemonType

* change NavigationCommand to a sealed interface

* fix spotless error
  • Loading branch information
cbeyls authored May 18, 2024
1 parent 6dce6d0 commit 10a0464
Show file tree
Hide file tree
Showing 20 changed files with 85 additions and 137 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
id("skydoves.pokedex.android.application.compose")
id("skydoves.pokedex.android.hilt")
id("skydoves.pokedex.spotless")
id("kotlin-parcelize")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.baselineprofile)
}

Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".PokedexApp"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider
import com.skydoves.pokedex.compose.core.navigation.AppComposeNavigator
import com.skydoves.pokedex.compose.core.navigation.LocalComposeNavigator
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
import com.skydoves.pokedex.compose.ui.PokedexMain
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
Expand All @@ -31,7 +32,7 @@ import javax.inject.Inject
class MainActivity : ComponentActivity() {

@Inject
internal lateinit var composeNavigator: AppComposeNavigator
internal lateinit var composeNavigator: AppComposeNavigator<PokedexScreen>

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.skydoves.pokedex.compose.core.navigation.PokedexScreens
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen

@Composable
fun PokedexNavHost(navHostController: NavHostController) {
SharedTransitionLayout {
NavHost(
navController = navHostController,
startDestination = PokedexScreens.Home.route,
startDestination = PokedexScreen.Home,
) {
pokedexNavigation()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,18 @@ package com.skydoves.pokedex.compose.navigation
import androidx.compose.animation.SharedTransitionScope
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.skydoves.pokedex.compose.core.navigation.PokedexScreens
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
import com.skydoves.pokedex.compose.feature.details.PokedexDetails
import com.skydoves.pokedex.compose.feature.home.PokedexHome

context(SharedTransitionScope)
fun NavGraphBuilder.pokedexNavigation() {
composable(route = PokedexScreens.Home.name) {
composable<PokedexScreen.Home> {
PokedexHome(this)
}

composable(
route = PokedexScreens.Details.name,
arguments = PokedexScreens.Details.navArguments,
composable<PokedexScreen.Details>(
typeMap = PokedexScreen.Details.typeMap,
) {
PokedexDetails(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.rememberNavController
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
import com.skydoves.pokedex.compose.core.navigation.AppComposeNavigator
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
import com.skydoves.pokedex.compose.navigation.PokedexNavHost

@Composable
fun PokedexMain(composeNavigator: AppComposeNavigator) {
fun PokedexMain(composeNavigator: AppComposeNavigator<PokedexScreen>) {
PokedexTheme {
val navHostController = rememberNavController()

Expand Down
2 changes: 1 addition & 1 deletion core/database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ plugins {
id("skydoves.pokedex.android.library")
id("skydoves.pokedex.android.hilt")
id("skydoves.pokedex.spotless")
id("com.google.devtools.ksp")
alias(libs.plugins.ksp)
}

android {
Expand Down
6 changes: 3 additions & 3 deletions core/model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

plugins {
id("skydoves.pokedex.android.library")
id("org.jetbrains.kotlin.plugin.serialization")
id("kotlin-parcelize")
id("com.google.devtools.ksp")
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
id("skydoves.pokedex.spotless")
}

Expand Down
5 changes: 2 additions & 3 deletions core/navigation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id("skydoves.pokedex.android.library")
id("skydoves.pokedex.android.library.compose")
id("org.jetbrains.kotlin.plugin.serialization")
alias(libs.plugins.kotlinx.serialization)
id("skydoves.pokedex.android.hilt")
id("skydoves.pokedex.spotless")
}
Expand All @@ -13,10 +13,9 @@ android {
dependencies {
implementation(projects.core.model)

implementation(libs.androidx.core)
implementation(libs.kotlinx.coroutines.android)

implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.animation)
api(libs.androidx.navigation.compose)

// json parsing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf

public val LocalComposeNavigator: ProvidableCompositionLocal<AppComposeNavigator> =
public val LocalComposeNavigator: ProvidableCompositionLocal<AppComposeNavigator<PokedexScreen>> =
compositionLocalOf {
error(
"No AppComposeNavigator provided! " +
Expand All @@ -32,7 +32,7 @@ public val LocalComposeNavigator: ProvidableCompositionLocal<AppComposeNavigator
/**
* Retrieves the current [AppComposeNavigator] at the call site's position in the hierarchy.
*/
public val currentComposeNavigator: AppComposeNavigator
public val currentComposeNavigator: AppComposeNavigator<PokedexScreen>
@Composable
@ReadOnlyComposable
get() = LocalComposeNavigator.current
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@ package com.skydoves.pokedex.compose.core.navigation

import androidx.navigation.NavOptions

sealed class NavigationCommand {
data object NavigateUp : NavigationCommand()
sealed interface NavigationCommand {
data object NavigateUp : NavigationCommand
}

sealed class ComposeNavigationCommand : NavigationCommand() {
data class NavigateToRoute(val route: String, val options: NavOptions? = null) :
ComposeNavigationCommand()
sealed interface ComposeNavigationCommand : NavigationCommand {
data class NavigateToRoute<T : Any>(val route: T, val options: NavOptions? = null) :
ComposeNavigationCommand

data class NavigateUpWithResult<T>(
data class NavigateUpWithResult<R, T : Any>(
val key: String,
val result: T,
val route: String? = null,
) : ComposeNavigationCommand()
val result: R,
val route: T? = null,
) : ComposeNavigationCommand

data class PopUpToRoute(val route: String, val inclusive: Boolean) : ComposeNavigationCommand()
data class PopUpToRoute<T : Any>(val route: T, val inclusive: Boolean) :
ComposeNavigationCommand
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal abstract class NavigationModule {
internal interface NavigationModule {

@Binds
@Singleton
abstract fun provideComposeNavigator(
fun provideComposeNavigator(
pokedexComposeNavigator: PokedexComposeNavigator,
): AppComposeNavigator
): AppComposeNavigator<PokedexScreen>
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ abstract class Navigator {
}
}

abstract class AppComposeNavigator : Navigator() {
abstract fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null)
abstract fun <T> navigateBackWithResult(key: String, result: T, route: String?)
abstract class AppComposeNavigator<T : Any> : Navigator() {
abstract fun navigate(route: T, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null)
abstract fun <R> navigateBackWithResult(key: String, result: R, route: T?)

abstract fun popUpTo(route: String, inclusive: Boolean)
abstract fun navigateAndClearBackStack(route: String)
abstract fun popUpTo(route: T, inclusive: Boolean)
abstract fun navigateAndClearBackStack(route: T)

suspend fun handleNavigationCommands(navController: NavController) {
navigationCommands
Expand All @@ -51,24 +51,24 @@ abstract class AppComposeNavigator : Navigator() {

private fun NavController.handleComposeNavigationCommand(navigationCommand: NavigationCommand) {
when (navigationCommand) {
is ComposeNavigationCommand.NavigateToRoute -> {
is ComposeNavigationCommand.NavigateToRoute<*> -> {
navigate(navigationCommand.route, navigationCommand.options)
}

NavigationCommand.NavigateUp -> navigateUp()
is ComposeNavigationCommand.PopUpToRoute -> popBackStack(
is ComposeNavigationCommand.PopUpToRoute<*> -> popBackStack(
navigationCommand.route,
navigationCommand.inclusive,
)

is ComposeNavigationCommand.NavigateUpWithResult<*> -> {
is ComposeNavigationCommand.NavigateUpWithResult<*, *> -> {
navUpWithResult(navigationCommand)
}
}
}

private fun NavController.navUpWithResult(
navigationCommand: ComposeNavigationCommand.NavigateUpWithResult<*>,
navigationCommand: ComposeNavigationCommand.NavigateUpWithResult<*, *>,
) {
val backStackEntry =
navigationCommand.route?.let { getBackStackEntry(it) }
Expand All @@ -80,8 +80,6 @@ abstract class AppComposeNavigator : Navigator() {

navigationCommand.route?.let {
popBackStack(it, false)
} ?: run {
navigateUp()
}
} ?: navigateUp()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.navOptions
import javax.inject.Inject

class PokedexComposeNavigator @Inject constructor() : AppComposeNavigator() {
class PokedexComposeNavigator @Inject constructor() : AppComposeNavigator<PokedexScreen>() {

override fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)?) {
override fun navigate(route: PokedexScreen, optionsBuilder: (NavOptionsBuilder.() -> Unit)?) {
val options = optionsBuilder?.let { navOptions(it) }
navigationCommands.tryEmit(ComposeNavigationCommand.NavigateToRoute(route, options))
}

override fun navigateAndClearBackStack(route: String) {
override fun navigateAndClearBackStack(route: PokedexScreen) {
navigationCommands.tryEmit(
ComposeNavigationCommand.NavigateToRoute(
route,
Expand All @@ -38,11 +38,11 @@ class PokedexComposeNavigator @Inject constructor() : AppComposeNavigator() {
)
}

override fun popUpTo(route: String, inclusive: Boolean) {
override fun popUpTo(route: PokedexScreen, inclusive: Boolean) {
navigationCommands.tryEmit(ComposeNavigationCommand.PopUpToRoute(route, inclusive))
}

override fun <T> navigateBackWithResult(key: String, result: T, route: String?) {
override fun <R> navigateBackWithResult(key: String, result: R, route: PokedexScreen?) {
navigationCommands.tryEmit(
ComposeNavigationCommand.NavigateUpWithResult(
key = key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

package com.skydoves.pokedex.compose.core.navigation

import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import com.skydoves.pokedex.compose.core.model.Pokemon
import kotlinx.serialization.Serializable
import kotlin.reflect.typeOf

inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelable(key, T::class.java)
else ->
@Suppress("DEPRECATION")
getParcelable(key)
as? T
sealed interface PokedexScreen {
@Serializable
data object Home : PokedexScreen

@Serializable
data class Details(val pokemon: Pokemon) : PokedexScreen {
companion object {
val typeMap = mapOf(typeOf<Pokemon>() to PokemonType)
}
}
}

This file was deleted.

Loading

0 comments on commit 10a0464

Please sign in to comment.