Skip to content

Commit

Permalink
[Add] type-safe navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
jeprubio committed Sep 17, 2024
1 parent 584ac44 commit f399493
Show file tree
Hide file tree
Showing 30 changed files with 222 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FakeCharactersNetwork @Inject constructor(): CharactersNetwork {
),
characters = (offset .. offset + CHARACTERS_SEARCH_LIMIT).map {
HeroDto(
id = it,
id = it.toLong(),
name = "character $it",
description = "description $it",
thumbnail = null,
Expand All @@ -32,6 +32,13 @@ class FakeCharactersNetwork @Inject constructor(): CharactersNetwork {
return Result.success(heroesResult)
}

override suspend fun getHeroDetails(heroId: Long): Result<HeroDto?> {
val hero = searchHeroes(0, 0, "").getOrNull()?.characters?.find { it.id == heroId }
return hero?.let {
Result.success(it)
} ?: Result.failure(Exception("Hero not found"))
}

override suspend fun getComicThumbnail(comicId: Int): Result<String> =
Result.success("thumbnail")
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.rumosoft.characters.presentation.navigation.NavCharItem
import com.rumosoft.comics.presentation.navigation.NavComicItem
import com.rumosoft.components.presentation.component.isWindowCompact
import com.rumosoft.components.presentation.deeplinks.CharactersScreen
import com.rumosoft.components.presentation.deeplinks.ComicsScreen
import com.rumosoft.components.presentation.deeplinks.Screen
import com.rumosoft.components.presentation.theme.MarvelComposeTheme
import com.rumosoft.marvelcompose.R
import com.rumosoft.marvelcompose.presentation.navigation.BottomNavigationBar
Expand Down Expand Up @@ -70,21 +71,19 @@ fun MarvelApp() {
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
bottomBar = {
if (shouldShowBottomBar(navController)) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
BottomNavigationBar(
navigationItems = navigationItems,
currentRoute = navBackStackEntry?.destination?.route,
currentScreen = getCurrentScreen(navController),
onTabClick = { onTabClick(it, navController) },
)
}
},
) { innerPadding ->
if (shouldShowNavigationRail()) {
Row {
val navBackStackEntry by navController.currentBackStackEntryAsState()
NavigationRailBar(
navigationItems = navigationItems,
currentRoute = navBackStackEntry?.destination?.route,
currentScreen = getCurrentScreen(navController),
onTabClick = { onTabClick(it, navController) },
)
NavigationHost(navController, modifier = Modifier.padding(innerPadding))
Expand All @@ -98,10 +97,23 @@ fun MarvelApp() {
@Composable
private fun shouldShowBottomBar(
navController: NavHostController
) = isWindowCompact() && currentRoute(navController) in listOf(
NavCharItem.Characters.destination,
NavComicItem.Comics.route,
)
): Boolean {
println("isWindowCompact: ${isWindowCompact()}")
println("currentRoute: ${currentRoute(navController)}")
return isWindowCompact() && (
currentRoute(navController)?.contains("CharactersScreen") == true ||
currentRoute(navController)?.contains("ComicsScreen") == true)
}

@Composable
private fun getCurrentScreen(navController: NavHostController): Screen {
val route = navController.currentBackStackEntryAsState().value?.destination?.route
return when {
route?.contains("CharactersScreen") == true -> CharactersScreen
route?.contains("ComicsScreen") == true -> ComicsScreen
else -> CharactersScreen
}
}

@Composable
private fun shouldShowNavigationRail() = !isWindowCompact()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.rumosoft.components.presentation.deeplinks.CharactersScreen
import com.rumosoft.components.presentation.deeplinks.Screen
import com.rumosoft.components.presentation.theme.MarvelComposeTheme

@Composable
fun BottomNavigationBar(
navigationItems: List<Tabs>,
currentRoute: String?,
currentScreen: Screen,
onTabClick: (Tabs) -> Unit = {},
) {
NavigationBar {
navigationItems.forEach { tab ->
val tabTitle = stringResource(id = tab.title)
val selected = currentRoute == tab.route
val selected = currentScreen == tab.screen
NavigationBarItem(
icon = {
Icon(
Expand Down Expand Up @@ -49,7 +51,7 @@ fun BottomNavigationBar(
fun PreviewBottomNavigationBar() {
MarvelComposeTheme {
BottomNavigationBar(
currentRoute = Tabs.Characters.route,
currentScreen = CharactersScreen,
navigationItems = listOf(
Tabs.Characters,
Tabs.Comics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.rumosoft.characters.presentation.navigation.NavCharItem
import com.rumosoft.characters.presentation.navigation.charactersGraph
import com.rumosoft.comics.presentation.navigation.comicsGraph
import com.rumosoft.components.presentation.deeplinks.CharactersScreen

const val NAVIGATION_HOST_TEST_TAG = "NavigationHost"

@Composable
fun NavigationHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController,
startDestination = NavCharItem.Characters.route,
startDestination = CharactersScreen,
modifier = modifier.testTag(NAVIGATION_HOST_TEST_TAG)
) {
charactersGraph(navController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.rumosoft.components.presentation.deeplinks.CharactersScreen
import com.rumosoft.components.presentation.deeplinks.Screen
import com.rumosoft.components.presentation.theme.MarvelComposeTheme

@Composable
fun NavigationRailBar(
navigationItems: List<Tabs>,
currentRoute: String?,
currentScreen: Screen,
onTabClick: (Tabs) -> Unit = {},
) {
NavigationRail {
Spacer(modifier = Modifier.height(8.dp))
navigationItems.forEach { tab ->
val tabTitle = stringResource(id = tab.title)
val selected = currentRoute == tab.route
val selected = currentScreen == tab.screen
NavigationRailItem(
icon = {
Icon(
Expand All @@ -46,7 +48,7 @@ fun NavigationRailBar(
fun PreviewNavigationRailBar() {
MarvelComposeTheme {
NavigationRailBar(
currentRoute = Tabs.Characters.route,
currentScreen = CharactersScreen,
navigationItems = listOf(
Tabs.Characters,
Tabs.Comics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ package com.rumosoft.marvelcompose.presentation.navigation

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.rumosoft.characters.presentation.navigation.NavCharItem
import com.rumosoft.comics.presentation.navigation.NavComicItem
import com.rumosoft.components.presentation.deeplinks.CharactersScreen
import com.rumosoft.components.presentation.deeplinks.ComicsScreen
import com.rumosoft.components.presentation.deeplinks.Screen
import com.rumosoft.marvelcompose.R

sealed class Tabs(
val route: String,
val screen: Screen,
@StringRes val title: Int,
@DrawableRes val icon: Int,
) {
object Characters : Tabs(
NavCharItem.Characters.destination,
data object Characters : Tabs(
CharactersScreen,
com.rumosoft.characters.R.string.characters,
R.drawable.ic_characters,
)

object Comics : Tabs(
NavComicItem.Comics.destination,
data object Comics : Tabs(
ComicsScreen,
com.rumosoft.comics.R.string.comics,
R.drawable.ic_comics,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,11 @@ fun onTabClick(
tab: Tabs,
navController: NavHostController,
) {
if (tab.route != navController.currentDestination?.route) {
navController.navigate(tab.route) {
navController.currentDestination?.route?.let {
popUpTo(it) {
saveState = true
inclusive = true
}
}

launchSingleTop = true
navController.navigate(tab.screen) {
popUpTo(tab.screen) {
saveState = true
inclusive = true
}
launchSingleTop = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.rumosoft.marvelcompose.presentation.widget

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.glance.Button
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
Expand All @@ -22,8 +22,11 @@ import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.text.Text
import com.rumosoft.characters.presentation.navigation.NavCharItem
import com.rumosoft.comics.presentation.navigation.NavComicItem
import com.rumosoft.components.presentation.deeplinks.CharactersScreen
import com.rumosoft.components.presentation.deeplinks.ComicsScreen
import com.rumosoft.components.presentation.deeplinks.DEEP_LINKS_BASE_PATH
import com.rumosoft.components.presentation.deeplinks.Screen
import com.rumosoft.marvelcompose.presentation.MainActivity

@Composable
internal fun WidgetContent() {
Expand Down Expand Up @@ -57,7 +60,7 @@ class OpenCharactersAction : ActionCallback {
glanceId: GlanceId,
parameters: ActionParameters
) {
openDeepLink(context, NavCharItem.Characters.deepLink)
openDeepLink(context, CharactersScreen)
}
}

Expand All @@ -67,13 +70,17 @@ class OpenComicsAction : ActionCallback {
glanceId: GlanceId,
parameters: ActionParameters
) {
openDeepLink(context, NavComicItem.Comics.deepLink)
openDeepLink(context, ComicsScreen)
}
}

private fun openDeepLink(context: Context, deepLink: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deepLink)).apply {
setPackage(context.packageName)
private fun openDeepLink(context: Context, screen: Screen) {
val intent = Intent(context, MainActivity::class.java).apply {
if (screen is CharactersScreen) {
data = "$DEEP_LINKS_BASE_PATH/characters".toUri()
} else {
data = "$DEEP_LINKS_BASE_PATH/comics".toUri()
}
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
context.startActivity(intent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ object FakeCharacters {
fun getSampleCharacters(numCharacters: Int = 5): List<Character> =
(1..numCharacters).map { characterId ->
Character(
id = characterId.toLong(),
name = "character $characterId",
description = "description $characterId",
thumbnail = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.rumosoft.marvelapi.data.network.apimodels.toThumbnailUrl

fun HeroDto.toHero(): Character {
return Character(
id = id,
name = name.orEmpty(),
description = description.orEmpty(),
thumbnail = thumbnail?.toThumbnailUrl().orEmpty(),
Expand All @@ -28,4 +29,4 @@ fun ComicSummaryDto.toComicSummary(): ComicSummary =
ComicSummary(
title = name.orEmpty(),
url = resourceUri.orEmpty(),
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class SearchRepositoryImpl @Inject constructor(
return networkResult
}

override suspend fun getCharacterDetails(heroId: Long): Result<Character?> {
val heroDetails = network.getHeroDetails(heroId)
if (heroDetails.isSuccess) {
Timber.d("Returned hero details")
}
return network.getHeroDetails(heroId).map { it?.toHero() }
}

override suspend fun getThumbnail(comicId: Int): Result<String> {
return network.getComicThumbnail(comicId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json
@Parcelize
@Serializable
data class Character(
val id: Long,
val name: String,
val description: String,
val thumbnail: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.rumosoft.characters.domain.usecase

import com.rumosoft.characters.domain.model.Character
import com.rumosoft.characters.domain.usecase.interfaces.SearchRepository
import javax.inject.Inject

class GetCharacterDetailsUseCase @Inject constructor(
private val repository: SearchRepository,
) {
suspend operator fun invoke(
characterId: Long,
): Result<Character?> =
repository.getCharacterDetails(characterId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import com.rumosoft.characters.domain.model.Character

interface SearchRepository {
suspend fun performSearch(nameStartsWith: String, page: Int): Result<List<Character>>
suspend fun getCharacterDetails(heroId: Long): Result<Character?>
suspend fun getThumbnail(comicId: Int): Result<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.rumosoft.marvelapi.data.network.apimodels.UrlDto
object SampleData {
val heroesDtoSample = (1..10).map {
HeroDto(
id = it,
id = it.toLong(),
name = "Hero$it",
description = "Description hero $it",
thumbnail = ImageDto(
Expand Down
Loading

0 comments on commit f399493

Please sign in to comment.