Skip to content
Snippets Groups Projects
Commit afaf6f65 authored by Baryshnikov Oleg's avatar Baryshnikov Oleg
Browse files

Hoist the navigation logic to the MainScreen.

Update Screen classes parameters.
Add back button interception to the MyNavHost.
parent 84cfe418
Branches master
No related tags found
No related merge requests found
......@@ -105,7 +105,7 @@ dependencies {
implementation "androidx.paging:paging-compose:3.2.0-rc01"
// Glide image loader
implementation "com.github.bumptech.glide:compose:1.0.0-alpha.1"
implementation "com.github.bumptech.glide:compose:1.0.0-alpha.3"
}
kapt {
......
package cz.fel.barysole.ackeetesttask
import android.app.Notification.Action
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
......@@ -10,6 +9,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
......@@ -18,9 +18,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import cz.fel.barysole.ackeetesttask.ui.screen.Screen
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import cz.fel.barysole.ackeetesttask.ui.screen.appScreenList
import cz.fel.barysole.ackeetesttask.ui.theme.AckeeTestTaskTheme
......@@ -28,7 +31,6 @@ import cz.fel.barysole.ackeetesttask.ui.uielement.main.MyNavigationBar
import cz.fel.barysole.ackeetesttask.ui.uielement.main.MyTopAppBar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
......@@ -40,6 +42,44 @@ class MainActivity : ComponentActivity() {
MainScreen()
}
}
}
fun changeScreen(
navController: NavController,
currentScreen: MutableState<Screen>,
nextScreen: Screen, args: List<Any>?,
isSearchBarShowing: MutableState<Boolean>
) {
if (nextScreen != currentScreen.value) {
if (nextScreen == Screen.Characters || nextScreen == Screen.Favorite) {
currentScreen.value = nextScreen;
navController.navigate(nextScreen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
} else if (nextScreen == Screen.CharacterDetail && !args.isNullOrEmpty()) {
currentScreen.value = nextScreen;
navController.navigate(Screen.CharacterDetail.routeWithoutArgument + "/" + args[0].toString()) {
launchSingleTop = true
}
} else if (nextScreen == Screen.Previous) {
navController.navigateUp()
currentScreen.value =
appScreenList.find { item -> navController.currentBackStackEntry?.destination?.hierarchy?.first()?.route == item.route }
?: Screen.Characters;
}
isSearchBarShowing.value = false
}
}
@Composable
......@@ -49,8 +89,11 @@ fun MainScreen() {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
// We can set selectedScreen by using callbacks from navigation composable, but it not guarantee any performance improvements in this small app
val selectedScreen =
appScreenList.find { item -> currentDestination?.hierarchy?.first()?.route == item.route }
val selectedScreen = remember {
// the initial screen
mutableStateOf(appScreenList.find { item -> navController.currentBackStackEntry?.destination?.hierarchy?.first()?.route == item.route }
?: Screen.Characters)
}
val snackbarHostState = remember { SnackbarHostState() }
val actionFlow = MutableSharedFlow<ScreenAction>(extraBufferCapacity = 1)
val onActionClick = { action: ScreenAction ->
......@@ -63,14 +106,28 @@ fun MainScreen() {
mutableStateOf(false)
}
val onTopBarTitleChangeFun = { newBarTitle: String -> topBarTitle = newBarTitle }
val onTopBarFavoriteIconChangeFun = { isFavoriteActive: Boolean -> isFavoriteIconActive = isFavoriteActive}
val onTopBarFavoriteIconChangeFun =
{ isFavoriteActive: Boolean -> isFavoriteIconActive = isFavoriteActive }
val isSearchBarShowing = rememberSaveable {
mutableStateOf(false)
}
val onScreenSelected = { screen: Screen, args: List<Any>? ->
changeScreen(navController, selectedScreen, screen, args, isSearchBarShowing)
}
// Main content
AckeeTestTaskTheme {
Scaffold(
modifier = Modifier.fillMaxSize(1f),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
MyTopAppBar(selectedScreen, {}, { navController.navigateUp() }, { action -> onActionClick(action) }, barTitle = topBarTitle, isFavoriteIconEnabled = isFavoriteIconActive )
MyTopAppBar(
selectedScreen,
{ action -> onActionClick(action) },
barTitle = topBarTitle,
isFavoriteIconEnabled = isFavoriteIconActive,
isSearchBarShowing = isSearchBarShowing,
onScreenSelected = onScreenSelected
)
},
content = { innerPadding ->
MyNavHost(
......@@ -79,11 +136,12 @@ fun MainScreen() {
Modifier.padding(innerPadding),
actionFlow,
onTopBarTitleChange = onTopBarTitleChangeFun,
onTopBarFavoriteIconChange = onTopBarFavoriteIconChangeFun
onTopBarFavoriteIconChange = onTopBarFavoriteIconChangeFun,
onScreenSelected = onScreenSelected
)
},
bottomBar = {
MyNavigationBar(navController, selectedScreen)
MyNavigationBar(selectedScreen, onScreenSelected)
})
}
}
......
package cz.fel.barysole.ackeetesttask
import androidx.activity.compose.BackHandler
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
......@@ -22,7 +23,8 @@ fun MyNavHost(
modifier: Modifier,
screenActionFlow: Flow<ScreenAction>,
onTopBarTitleChange: (String) -> Unit,
onTopBarFavoriteIconChange: (Boolean) -> Unit
onTopBarFavoriteIconChange: (Boolean) -> Unit,
onScreenSelected: (Screen, List<Any>?) -> Unit
) {
NavHost(
navController,
......@@ -30,28 +32,26 @@ fun MyNavHost(
modifier = modifier
) {
composable(Screen.Characters.route) {
onTopBarTitleChange("")
CustomBackHandler(onScreenSelected)
CharacterListScreen(
snackbarHostState,
onItemSelected = { characterId ->
navController.navigate(Screen.CharacterDetail.routeWithoutArgument + "/" + characterId) {
launchSingleTop = true
}
onScreenSelected(Screen.CharacterDetail, listOf(characterId))
}
)
}
composable(Screen.Favorite.route) { onTopBarTitleChange("")
composable(Screen.Favorite.route) {
CustomBackHandler(onScreenSelected)
FavoriteCharacterListScreen(
onItemSelected = { characterId ->
navController.navigate(Screen.CharacterDetail.routeWithoutArgument + "/" + characterId) {
launchSingleTop = true
}
onScreenSelected(Screen.CharacterDetail, listOf(characterId))
})
}
composable(
Screen.CharacterDetail.route,
arguments = listOf(navArgument("characterId") { type = NavType.LongType })
) { backStackEntry ->
CustomBackHandler(onScreenSelected)
CharacterDetailScreen(
backStackEntry.arguments?.getLong("characterId"),
screenActionFlow,
......@@ -62,3 +62,10 @@ fun MyNavHost(
}
}
@Composable
fun CustomBackHandler(onScreenSelected: (Screen, List<Any>?) -> Unit) {
BackHandler(true) {
onScreenSelected(Screen.Previous, null)
}
}
......@@ -4,14 +4,18 @@ import androidx.annotation.DrawableRes
import cz.fel.barysole.ackeetesttask.R
sealed class Screen(
val route: String,
val nameResId: Int,
val route: String = "",
val nameResId: Int? = null,
@DrawableRes val iconResId: Int? = null,
val routeWithoutArgument: String? = null,
val isBackButtonShowing: Boolean = false,
val actionList: List<ScreenAction> = emptyList(),
val showTopAppBar: Boolean = true
val showTopAppBar: Boolean = true,
val showBottomBar: Boolean = true
) {
object Previous : Screen()
object Characters : Screen(
"characters",
R.string.characters,
......@@ -27,9 +31,9 @@ sealed class Screen(
object CharacterDetail : Screen(
"characterdetail/{characterId}",
R.string.character_detail,
routeWithoutArgument = "characterdetail",
actionList = listOf(ScreenAction.AddToFavoriteScreenAction)
actionList = listOf(ScreenAction.AddToFavoriteScreenAction),
showBottomBar = false
)
}
......
......@@ -7,23 +7,27 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.screen.Screen
@Composable
fun MyNavigationBar(navController: NavController, selectedScreen: Screen?) {
fun MyNavigationBar(
selectedScreen: MutableState<Screen>,
onScreenSelected: (Screen, List<Any>?) -> Unit
) {
val navBarItems = listOf(
Screen.Characters,
Screen.Favorite
)
if (selectedScreen == Screen.Characters || selectedScreen == Screen.Favorite) {
if (selectedScreen.value.showBottomBar == true) {
NavigationBar(
modifier = Modifier.shadow(8.dp),
containerColor = MaterialTheme.colorScheme.surface,
......@@ -35,26 +39,15 @@ fun MyNavigationBar(navController: NavController, selectedScreen: Screen?) {
if (screen.iconResId != null) {
Icon(
painterResource(screen.iconResId),
contentDescription = stringResource(screen.nameResId),
contentDescription = stringResource(screen.nameResId?: R.string.empty_string),
modifier = Modifier.size(24.dp)
)
}
},
label = { Text(stringResource(screen.nameResId)) },
selected = selectedScreen == screen,
label = { Text(stringResource(screen.nameResId?: R.string.empty_string)) },
selected = selectedScreen.value == screen,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
onScreenSelected(screen, null)
}
)
}
......
......@@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.screen.Screen
import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList
import kotlinx.coroutines.launch
......@@ -35,7 +36,7 @@ import kotlinx.coroutines.launch
@Composable
fun MySearchBar(
isSearchBarShowing: MutableState<Boolean>,
onItemSelected: (id: Long) -> Unit,
onScreenSelected: (Screen, List<Any>?) -> Unit,
characterSearchViewModel: CharacterSearchViewModel = hiltViewModel()
) {
val isSearchBarIsActive = rememberSaveable {
......@@ -87,7 +88,12 @@ fun MySearchBar(
Text("Data cannot be loaded!", style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
}
} else {
CharacterList(it, onItemSelected)
CharacterList(it) { characterId ->
onScreenSelected(
Screen.CharacterDetail,
listOf(characterId)
)
}
}
}
}
......
......@@ -13,8 +13,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.painterResource
......@@ -24,7 +23,6 @@ import androidx.compose.ui.unit.dp
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.screen.Screen
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import cz.fel.barysole.ackeetesttask.ui.screen.screenWithTopAppBarList
//Main screen contains the TopAppBar and other functions, which are experimental and are likely to change or to be removed in the future.
......@@ -32,29 +30,26 @@ import cz.fel.barysole.ackeetesttask.ui.screen.screenWithTopAppBarList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyTopAppBar(
selectedScreen: Screen?,
onItemSelected: (id: Long) -> Unit,
onBackButtonPressed: () -> Unit,
selectedScreen: MutableState<Screen>,
onActionClick: (ScreenAction) -> Unit,
isFavoriteIconEnabled: Boolean = false,
barTitle: String? = null,
barTitle: String = "",
isSearchBarShowing: MutableState<Boolean>,
onScreenSelected: (Screen, List<Any>?) -> Unit
) {
if (screenWithTopAppBarList.contains(selectedScreen)) {
val isSearchBarShowing = rememberSaveable {
mutableStateOf(false)
}
if (selectedScreen.value.showTopAppBar == true) {
// showing iff isSearchBarShowing is true
Crossfade(targetState = isSearchBarShowing.value) { showSearchBar ->
when (showSearchBar) {
true -> MySearchBar(isSearchBarShowing, onItemSelected)
true -> MySearchBar(isSearchBarShowing, onScreenSelected)
false -> TopAppBar(
modifier = Modifier.shadow(16.dp),
navigationIcon = {
if (selectedScreen!!.isBackButtonShowing) {
if (selectedScreen.value!!.isBackButtonShowing) {
Icon(
painterResource(R.drawable.baseline_arrow_back_ios_new_24),
modifier = Modifier
.clickable { onBackButtonPressed() }
.clickable { onScreenSelected(Screen.Previous, null) }
.padding(horizontal = 8.dp),
contentDescription = "Arrow back icon"
)
......@@ -63,16 +58,19 @@ fun MyTopAppBar(
title = {
Text(
//there is no null in the screenWithTopAppBarList
text = if (barTitle.isNullOrBlank()) stringResource(selectedScreen!!.nameResId) else barTitle,
text = if (selectedScreen.value.nameResId != null) stringResource(
selectedScreen.value.nameResId ?: R.string.empty_string
) else barTitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
actions = {
ActionButtons(
selectedScreen!!.actionList,
selectedScreen.value!!.actionList,
onActionClick,
isFavoriteIconEnabled
isFavoriteIconEnabled,
onSearchBarClick = { isSearchBarShowing.value = true }
)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.background)
......@@ -86,11 +84,15 @@ fun MyTopAppBar(
fun ActionButtons(
actionButtonList: List<ScreenAction>,
onActionClick: (ScreenAction) -> Unit,
isFavoriteIconEnabled: Boolean = false
isFavoriteIconEnabled: Boolean = false,
onSearchBarClick: () -> Unit = {}
) {
for (screenAction in actionButtonList) {
if (screenAction == ScreenAction.SearchCharacterScreenAction) {
IconButton(onClick = { onActionClick(ScreenAction.SearchCharacterScreenAction) }) {
IconButton(onClick = {
onActionClick(ScreenAction.SearchCharacterScreenAction)
onSearchBarClick()
}) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search button"
......
<resources>
<string name="app_name">Ackee test task</string>
<string name="empty_string"></string>
<string name="characters">Characters</string>
<string name="favorite">Favorite</string>
<string name="character_detail">Character Detail</string>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment