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

Implement add to favorite feature.

Implement changing header feature.
Hoist the top bar state.
Add the top bar action flow.
parent e7c1b0ca
Branches master
No related tags found
No related merge requests found
Showing
with 190 additions and 31 deletions
package cz.fel.barysole.ackeetesttask
import android.app.Notification.Action
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
......@@ -10,22 +11,24 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
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.screen.characterlist.CharacterListScreen
import cz.fel.barysole.ackeetesttask.ui.theme.AckeeTestTaskTheme
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() {
......@@ -49,19 +52,34 @@ fun MainScreen() {
val selectedScreen =
appScreenList.find { item -> currentDestination?.hierarchy?.first()?.route == item.route }
val snackbarHostState = remember { SnackbarHostState() }
val actionFlow = MutableSharedFlow<ScreenAction>(extraBufferCapacity = 1)
val onActionClick = { action: ScreenAction ->
actionFlow.tryEmit(action)
}
var topBarTitle: String by rememberSaveable {
mutableStateOf("")
}
var isFavoriteIconActive by rememberSaveable {
mutableStateOf(false)
}
val onTopBarTitleChangeFun = { newBarTitle: String -> topBarTitle = newBarTitle }
val onTopBarFavoriteIconChangeFun = { isFavoriteActive: Boolean -> isFavoriteIconActive = isFavoriteActive}
// Main content
AckeeTestTaskTheme {
Scaffold(
modifier = Modifier.fillMaxSize(1f),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
MyTopAppBar(selectedScreen, {})
MyTopAppBar(selectedScreen, {}, { navController.navigateUp() }, { action -> onActionClick(action) }, barTitle = topBarTitle, isFavoriteIconEnabled = isFavoriteIconActive )
},
content = { innerPadding ->
MyNavHost(
navController,
snackbarHostState,
Modifier.padding(innerPadding)
Modifier.padding(innerPadding),
actionFlow,
onTopBarTitleChange = onTopBarTitleChangeFun,
onTopBarFavoriteIconChange = onTopBarFavoriteIconChangeFun
)
},
bottomBar = {
......
......@@ -9,14 +9,19 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import cz.fel.barysole.ackeetesttask.ui.screen.Screen
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import cz.fel.barysole.ackeetesttask.ui.screen.characterdetail.CharacterDetailScreen
import cz.fel.barysole.ackeetesttask.ui.screen.characterlist.CharacterListScreen
import kotlinx.coroutines.flow.Flow
@Composable
fun MyNavHost(
navController: NavHostController,
snackbarHostState: SnackbarHostState,
modifier: Modifier
modifier: Modifier,
screenActionFlow: Flow<ScreenAction>,
onTopBarTitleChange: (String) -> Unit,
onTopBarFavoriteIconChange: (Boolean) -> Unit
) {
NavHost(
navController,
......@@ -24,6 +29,7 @@ fun MyNavHost(
modifier = modifier
) {
composable(Screen.Characters.route) {
onTopBarTitleChange("")
CharacterListScreen(
snackbarHostState,
onItemSelected = { characterId ->
......@@ -33,12 +39,17 @@ fun MyNavHost(
}
)
}
composable(Screen.Favorite.route) { }
composable(Screen.Favorite.route) { onTopBarTitleChange("") }
composable(
Screen.CharacterDetail.route,
arguments = listOf(navArgument("characterId") { type = NavType.LongType })
) { backStackEntry ->
CharacterDetailScreen(backStackEntry.arguments?.getLong("characterId"))
CharacterDetailScreen(
backStackEntry.arguments?.getLong("characterId"),
screenActionFlow,
onTopBarTitleChange,
onTopBarFavoriteIconChange
)
}
}
}
......
......@@ -12,4 +12,6 @@ interface CharacterRepository {
suspend fun getCharacter(characterId: Long): CharacterInfo?
suspend fun changeCharacterFavoriteStatus(characterId: Long)
}
\ No newline at end of file
......@@ -55,6 +55,15 @@ class CharacterRepositoryImpl @Inject constructor(
return appDatabase.characterDao().getById(characterId)
}
override suspend fun changeCharacterFavoriteStatus(characterId: Long) {
appDatabase.withTransaction {
val itemInDb = appDatabase.characterDao().getById(characterId)
itemInDb?.let {
appDatabase.characterDao().insert(it.copy(isFavorite = !itemInDb.isFavorite))
}
}
}
companion object {
//based on this description - https://rickandmortyapi.com/documentation/#info-and-pagination
const val NETWORK_PAGE_SIZE = 20
......
package cz.fel.barysole.ackeetesttask.ui.screen
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import cz.fel.barysole.ackeetesttask.R
sealed class Screen(val route: String, val nameResId: Int, @DrawableRes val iconResId: Int? = null, val routeWithoutArgument: String? = null) {
object Characters : Screen("characters", R.string.characters, R.drawable.ic_rick_face)
object Favorite : Screen("favorite", R.string.favorite, R.drawable.baseline_star_24)
object CharacterDetail : Screen("characterdetail/{characterId}", R.string.character_detail, routeWithoutArgument = "characterdetail")
sealed class Screen(
val route: String,
val nameResId: Int,
@DrawableRes val iconResId: Int? = null,
val routeWithoutArgument: String? = null,
val isBackButtonShowing: Boolean = false,
val actionList: List<ScreenAction> = emptyList(),
val showTopAppBar: Boolean = true
) {
object Characters : Screen(
"characters",
R.string.characters,
R.drawable.ic_rick_face,
actionList = listOf(ScreenAction.SearchCharacterScreenAction)
)
object Favorite : Screen(
"favorite",
R.string.favorite,
R.drawable.baseline_star_24,
actionList = listOf(ScreenAction.SearchCharacterScreenAction)
)
object CharacterDetail : Screen(
"characterdetail/{characterId}",
R.string.character_detail,
routeWithoutArgument = "characterdetail",
actionList = listOf(ScreenAction.AddToFavoriteScreenAction)
)
}
enum class ScreenAction {
SearchCharacterScreenAction,
AddToFavoriteScreenAction
}
val appScreenList = listOf(
......
......@@ -10,13 +10,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import kotlinx.coroutines.flow.Flow
@Composable
fun CharacterDetailScreen(
characterId: Long?,
screenActionFlow: Flow<ScreenAction>,
onTopBarTitleChange: (String) -> Unit,
onTopBarFavoriteIconChange: (Boolean) -> Unit,
viewModel: CharacterDetailViewModel = hiltViewModel()
) {
viewModel.onRefreshData(characterId)
viewModel.initialize(characterId, onTopBarTitleChange, onTopBarFavoriteIconChange, screenActionFlow)
val characterDetailScreenState by viewModel.uiState.collectAsState()
Surface(
shape = MaterialTheme.shapes.large,
......
......@@ -4,8 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
......@@ -13,15 +14,29 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class CharacterDetailViewModel @Inject constructor(
private val characterRepository: CharacterRepository
) : ViewModel() {
private var isInit: Boolean = false
// The UI state
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
var onTopBarTitleChange: (String) -> Unit = {}
var onTopBarFavoriteIconChange: (Boolean) -> Unit = {}
fun initialize(characterId: Long?, onTopBarTitleChange: (String) -> Unit = {}, onTopBarFavoriteIconChange: (Boolean) -> Unit = {}, screenFlow: Flow<ScreenAction>? = null) {
if (!isInit) {
this.onTopBarTitleChange = onTopBarTitleChange
this.onTopBarFavoriteIconChange = onTopBarFavoriteIconChange
if (screenFlow != null) {
listenScreenFlow(screenFlow)
}
onRefreshData(characterId)
isInit = true
}
}
fun showLoadingError(hasError: Boolean) {
_uiState.update { state -> state.copy(showError = hasError) }
......@@ -31,17 +46,39 @@ class CharacterDetailViewModel @Inject constructor(
showLoadingError(false)
characterId?.let {
viewModelScope.launch {
val characterInfo = characterRepository.getCharacter(
characterId
)
onTopBarTitleChange(characterInfo?.name ?: "")
onTopBarFavoriteIconChange(characterInfo?.isFavorite ?: false)
_uiState.update { state ->
state.copy(
characterInfo = characterRepository.getCharacter(
characterId
)
characterInfo = characterInfo
)
}
}
}
}
fun listenScreenFlow(screenActionFlow: Flow<ScreenAction>) {
viewModelScope.launch {
screenActionFlow.collect { action ->
if (action == ScreenAction.AddToFavoriteScreenAction) {
changeCharacterFavoriteStatus()
}
}
}
}
fun changeCharacterFavoriteStatus() {
viewModelScope.launch {
uiState.value.characterInfo?.let {
characterRepository.changeCharacterFavoriteStatus(it.id)
onRefreshData(it.id)
}
}
}
data class UiState(
val showError: Boolean = false,
val characterInfo: CharacterInfo? = null
......
package cz.fel.barysole.ackeetesttask.ui.uielement.main
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
......@@ -16,10 +17,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.text.style.TextOverflow
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
......@@ -29,7 +33,11 @@ import cz.fel.barysole.ackeetesttask.ui.screen.screenWithTopAppBarList
@Composable
fun MyTopAppBar(
selectedScreen: Screen?,
onItemSelected: (id: Long) -> Unit
onItemSelected: (id: Long) -> Unit,
onBackButtonPressed: () -> Unit,
onActionClick: (ScreenAction) -> Unit,
isFavoriteIconEnabled: Boolean = false,
barTitle: String? = null,
) {
if (screenWithTopAppBarList.contains(selectedScreen)) {
val isSearchBarShowing = rememberSaveable {
......@@ -41,27 +49,62 @@ fun MyTopAppBar(
true -> MySearchBar(isSearchBarShowing, onItemSelected)
false -> TopAppBar(
modifier = Modifier.shadow(16.dp),
navigationIcon = {
if (selectedScreen!!.isBackButtonShowing) {
Icon(
painterResource(R.drawable.baseline_arrow_back_ios_new_24),
modifier = Modifier
.clickable { onBackButtonPressed() }
.padding(horizontal = 8.dp),
contentDescription = "Arrow back icon"
)
}
},
title = {
Text(
//there is no null in the screenWithTopAppBarList
stringResource(selectedScreen!!.nameResId),
text = if (barTitle.isNullOrBlank()) stringResource(selectedScreen!!.nameResId) else barTitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
actions = {
if (selectedScreen == Screen.Characters) {
IconButton(onClick = { isSearchBarShowing.value = true }) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search button"
)
}
}
ActionButtons(
selectedScreen!!.actionList,
onActionClick,
isFavoriteIconEnabled
)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.background)
)
}
}
}
}
@Composable
fun ActionButtons(
actionButtonList: List<ScreenAction>,
onActionClick: (ScreenAction) -> Unit,
isFavoriteIconEnabled: Boolean = false
) {
for (screenAction in actionButtonList) {
if (screenAction == ScreenAction.SearchCharacterScreenAction) {
IconButton(onClick = { onActionClick(ScreenAction.SearchCharacterScreenAction) }) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search button"
)
}
} else if (screenAction == ScreenAction.AddToFavoriteScreenAction) {
IconButton(onClick = { onActionClick(ScreenAction.AddToFavoriteScreenAction) }) {
Icon(
painter = if (isFavoriteIconEnabled) painterResource(R.drawable.baseline_star_24) else painterResource(
R.drawable.baseline_star_outline_24
),
contentDescription = "Favorite button"
)
}
}
}
}
\ No newline at end of file
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>
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