Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • barysole/ackee-test-task
1 result
Show changes
Commits on Source (2)
Showing
with 251 additions and 201 deletions
...@@ -14,9 +14,7 @@ import androidx.compose.runtime.getValue ...@@ -14,9 +14,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
...@@ -39,6 +37,8 @@ class MainActivity : ComponentActivity() { ...@@ -39,6 +37,8 @@ class MainActivity : ComponentActivity() {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
//Main screen contains the TopAppBar and other functions, which are experimental and are likely to change or to be removed in the future.
//But in the small test app it looks nice, i guess.
MainScreen() MainScreen()
} }
} }
...@@ -46,7 +46,7 @@ class MainActivity : ComponentActivity() { ...@@ -46,7 +46,7 @@ class MainActivity : ComponentActivity() {
} }
// Main handler for screens changing. Perform every (!) screen change operation in the app. // Main handler for navigation performing. Perform every (!) navigation operation in the app.
// todo: split into separate navigation methods // todo: split into separate navigation methods
fun navigate( fun navigate(
navController: NavController, navController: NavController,
...@@ -70,49 +70,64 @@ fun navigate( ...@@ -70,49 +70,64 @@ fun navigate(
} else if (nextScreen == Screen.Previous) { } else if (nextScreen == Screen.Previous) {
navController.navigateUp() navController.navigateUp()
} }
} }
} }
@Composable @Composable
fun MainScreen() { fun MainScreen() {
val navController = rememberNavController() // Top bar settings block
val navBackStackEntry by navController.currentBackStackEntryAsState() // Top bar will set the topBarTitle if current screen doesn't have title
val currentDestination = navBackStackEntry?.destination val topBarTitle = rememberSaveable {
val selectedScreen = remember {
// the initial screen
mutableStateOf(appScreenList.find { item -> currentDestination?.hierarchy?.first()?.route == item.route }
?: Screen.Characters)
}
val snackbarHostState = remember { SnackbarHostState() }
val actionFlow = MutableSharedFlow<ScreenAction>(extraBufferCapacity = 1)
val onActionClick = { action: ScreenAction ->
actionFlow.tryEmit(action)
}
var topBarTitle: String by rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
var isFavoriteIconActive by rememberSaveable { // Favorite icon state for the detail screen
val isFavoriteIconActive = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
val onTopBarTitleChangeFun = { newBarTitle: String -> topBarTitle = newBarTitle } val onTopBarTitleChange =
val onTopBarFavoriteIconChangeFun = remember { { newBarTitle: String -> topBarTitle.value = newBarTitle } }
{ isFavoriteActive: Boolean -> isFavoriteIconActive = isFavoriteActive } val onTopBarFavoriteIconStateChange =
remember { { isFavoriteActive: Boolean -> isFavoriteIconActive.value = isFavoriteActive } }
val isSearchBarShowing = rememberSaveable { val isSearchBarShowing = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
// Top bar action flow
val actionFlow = remember { MutableSharedFlow<ScreenAction>(extraBufferCapacity = 1) }
val onActionClick = remember {
{ action: ScreenAction ->
actionFlow.tryEmit(action)
}
}
// Navigation block.
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
// Current screen.
val selectedScreen = remember {
// The initial screen.
mutableStateOf(appScreenList.find { item -> navBackStackEntry?.destination?.hierarchy?.first()?.route == item.route }
?: Screen.Characters)
}
// Changes selectedScreen when the route changes
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
// Close search bar when navigation was performed if (selectedScreen.value == Screen.CharacterDetail) {
isSearchBarShowing.value = false onTopBarTitleChange("")
}
// Take current screen from the the top of the current navigation stack after navigation // Take current screen from the the top of the current navigation stack after navigation
selectedScreen.value = selectedScreen.value =
appScreenList.find { item -> destination.hierarchy.first().route == item.route } appScreenList.find { item -> destination.hierarchy.first().route == item.route }
?: Screen.Characters ?: Screen.Characters
} }
val onScreenSelected = { screen: Screen, args: List<Any>? -> // onScreenSelected perform all navigation in the app
navigate(navController, selectedScreen, screen, args) val onSelectScreen = remember {
{ screen: Screen, args: List<Any>? ->
navigate(navController, selectedScreen, screen, args)
}
} }
// Snackbar
val snackbarHostState = remember { SnackbarHostState() }
// Main content // Main content
AckeeTestTaskTheme { AckeeTestTaskTheme {
Scaffold( Scaffold(
...@@ -120,33 +135,30 @@ fun MainScreen() { ...@@ -120,33 +135,30 @@ fun MainScreen() {
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { topBar = {
MyTopAppBar( MyTopAppBar(
selectedScreen, selectedScreen = selectedScreen.value,
{ action -> onActionClick(action) }, onActionClick = { action -> onActionClick(action) },
barTitle = topBarTitle, barTitle = topBarTitle.value,
isFavoriteIconEnabled = isFavoriteIconActive, isFavoriteIconEnabled = isFavoriteIconActive.value,
isSearchBarShowing = isSearchBarShowing, isSearchBarShowing = isSearchBarShowing,
onScreenSelected = onScreenSelected onSelectScreen = onSelectScreen
) )
}, },
content = { innerPadding -> content = { innerPadding ->
MyNavHost( MyNavHost(
navController, navController = navController,
snackbarHostState, snackbarHostState = snackbarHostState,
Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
actionFlow, screenActionFlow = actionFlow,
onTopBarTitleChange = onTopBarTitleChangeFun, onTopBarTitleChange = onTopBarTitleChange,
onTopBarFavoriteIconChange = onTopBarFavoriteIconChangeFun, onTopBarFavoriteIconChange = onTopBarFavoriteIconStateChange,
onScreenSelected = onScreenSelected onSelectScreen = onSelectScreen
) )
}, },
bottomBar = { bottomBar = {
MyNavigationBar(selectedScreen, onScreenSelected) MyNavigationBar(
selectedScreen = selectedScreen.value,
onSelectScreen = onSelectScreen
)
}) })
} }
}
@Preview
@Composable
fun MainScreenPreview() {
MainScreen()
} }
\ No newline at end of file
...@@ -24,7 +24,7 @@ fun MyNavHost( ...@@ -24,7 +24,7 @@ fun MyNavHost(
screenActionFlow: Flow<ScreenAction>, screenActionFlow: Flow<ScreenAction>,
onTopBarTitleChange: (String) -> Unit, onTopBarTitleChange: (String) -> Unit,
onTopBarFavoriteIconChange: (Boolean) -> Unit, onTopBarFavoriteIconChange: (Boolean) -> Unit,
onScreenSelected: (Screen, List<Any>?) -> Unit onSelectScreen: (Screen, List<Any>?) -> Unit
) { ) {
NavHost( NavHost(
navController, navController,
...@@ -32,28 +32,28 @@ fun MyNavHost( ...@@ -32,28 +32,28 @@ fun MyNavHost(
modifier = modifier modifier = modifier
) { ) {
composable(Screen.Characters.route) { composable(Screen.Characters.route) {
CustomBackHandler(onScreenSelected) CustomBackHandler(onSelectScreen)
CharacterListScreen( CharacterListScreen(
snackbarHostState, snackbarHostState,
onItemSelected = { characterId -> onSelectItem = { characterId ->
onScreenSelected(Screen.CharacterDetail, listOf(characterId)) onSelectScreen(Screen.CharacterDetail, listOf(characterId))
} }
) )
} }
composable(Screen.Favorite.route) { composable(Screen.Favorite.route) {
CustomBackHandler(onScreenSelected) CustomBackHandler(onSelectScreen)
FavoriteCharacterListScreen( FavoriteCharacterListScreen(
onItemSelected = { characterId -> onSelectItem = { characterId ->
onScreenSelected(Screen.CharacterDetail, listOf(characterId)) onSelectScreen(Screen.CharacterDetail, listOf(characterId))
}) })
} }
composable( composable(
Screen.CharacterDetail.route, Screen.CharacterDetail.route,
arguments = listOf(navArgument("characterId") { type = NavType.LongType }) arguments = listOf(navArgument("characterId") { type = NavType.LongType })
) { backStackEntry -> ) { backStackEntry ->
CustomBackHandler(onScreenSelected) CustomBackHandler(onSelectScreen)
CharacterDetailScreen( CharacterDetailScreen(
backStackEntry.arguments?.getLong("characterId"), characterId = backStackEntry.arguments?.getLong("characterId"),
screenActionFlow, screenActionFlow,
onTopBarTitleChange, onTopBarTitleChange,
onTopBarFavoriteIconChange onTopBarFavoriteIconChange
...@@ -63,9 +63,9 @@ fun MyNavHost( ...@@ -63,9 +63,9 @@ fun MyNavHost(
} }
@Composable @Composable
fun CustomBackHandler(onScreenSelected: (Screen, List<Any>?) -> Unit) { fun CustomBackHandler(onSelectScreen: (Screen, List<Any>?) -> Unit) {
BackHandler(true) { BackHandler(true) {
onScreenSelected(Screen.Previous, null) onSelectScreen(Screen.Previous, null)
} }
} }
package cz.fel.barysole.ackeetesttask.db.room.dao package cz.fel.barysole.ackeetesttask.db.room.dao
import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
......
...@@ -5,9 +5,6 @@ import androidx.room.Entity ...@@ -5,9 +5,6 @@ import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
/**
* Immutable model class for the Retrofit and the Room.
*/
@Entity(tableName = "CharacterInfo") @Entity(tableName = "CharacterInfo")
data class CharacterInfo( data class CharacterInfo(
@PrimaryKey @field:SerializedName("id") val id: Long, @PrimaryKey @field:SerializedName("id") val id: Long,
......
package cz.fel.barysole.ackeetesttask.repository
class RepositoryResult {
}
\ No newline at end of file
...@@ -37,7 +37,7 @@ class CharacterRemoteMediator( ...@@ -37,7 +37,7 @@ class CharacterRemoteMediator(
// compute required page index // compute required page index
val requiredPage = when (loadType) { val requiredPage = when (loadType) {
LoadType.REFRESH -> { LoadType.REFRESH -> {
STARTING_PAGE_NUMBER state.anchorPosition ?: STARTING_PAGE_NUMBER
} }
// REFRESH will always load the first page in the list, so prepend is no longer needed. // REFRESH will always load the first page in the list, so prepend is no longer needed.
LoadType.PREPEND -> { LoadType.PREPEND -> {
......
...@@ -19,14 +19,19 @@ class CharacterRepositoryImpl @Inject constructor( ...@@ -19,14 +19,19 @@ class CharacterRepositoryImpl @Inject constructor(
private val appDatabase: AppDatabase private val appDatabase: AppDatabase
) : CharacterRepository { ) : CharacterRepository {
// Find chracters by name query. Works without DB. // Find characters by name query. Works without DB.
override fun getCharactersByNameFlow(characterNameQuery: String): Flow<PagingData<CharacterInfo>> { override fun getCharactersByNameFlow(characterNameQuery: String): Flow<PagingData<CharacterInfo>> {
return Pager( return Pager(
config = PagingConfig( config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE, pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false enablePlaceholders = false
), ),
pagingSourceFactory = { CharactersSearchPagingSource(rickAndMortyApi, characterNameQuery) } pagingSourceFactory = {
CharactersSearchPagingSource(
rickAndMortyApi,
characterNameQuery
)
}
).flow ).flow
} }
...@@ -53,11 +58,11 @@ class CharacterRepositoryImpl @Inject constructor( ...@@ -53,11 +58,11 @@ class CharacterRepositoryImpl @Inject constructor(
} }
override suspend fun getCharacter(characterId: Long): CharacterInfo? { override suspend fun getCharacter(characterId: Long): CharacterInfo? {
val response = rickAndMortyApi.getCharacterById(characterId) val response = rickAndMortyApi.getCharacterById(characterId)
appDatabase.withTransaction { appDatabase.withTransaction {
insertCharacterItems(listOf(response), appDatabase.characterDao()) insertCharacterItems(listOf(response), appDatabase.characterDao())
} }
return appDatabase.characterDao().getById(characterId) return appDatabase.characterDao().getById(characterId)
} }
override suspend fun changeCharacterFavoriteStatus(characterId: Long) { override suspend fun changeCharacterFavoriteStatus(characterId: Long) {
......
package cz.fel.barysole.ackeetesttask.ui.screen package cz.fel.barysole.ackeetesttask.ui.screen
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import cz.fel.barysole.ackeetesttask.R import cz.fel.barysole.ackeetesttask.R
sealed class Screen( sealed class Screen(
val route: String = "", val route: String = "",
val nameResId: Int? = null, @StringRes val nameResId: Int? = null,
@DrawableRes val iconResId: Int? = null, @DrawableRes val iconResId: Int? = null,
val routeWithoutArgument: String? = null, val routeWithoutArgument: String? = null,
val isBackButtonShowing: Boolean = false, val isBackButtonShowing: Boolean = false,
...@@ -46,10 +47,4 @@ val appScreenList = listOf( ...@@ -46,10 +47,4 @@ val appScreenList = listOf(
Screen.Characters, Screen.Characters,
Screen.Favorite, Screen.Favorite,
Screen.CharacterDetail Screen.CharacterDetail
)
val screenWithTopAppBarList = listOf(
Screen.Characters,
Screen.Favorite,
Screen.CharacterDetail
) )
\ No newline at end of file
...@@ -21,7 +21,12 @@ fun CharacterDetailScreen( ...@@ -21,7 +21,12 @@ fun CharacterDetailScreen(
onTopBarFavoriteIconChange: (Boolean) -> Unit, onTopBarFavoriteIconChange: (Boolean) -> Unit,
viewModel: CharacterDetailViewModel = hiltViewModel() viewModel: CharacterDetailViewModel = hiltViewModel()
) { ) {
viewModel.initialize(characterId, onTopBarTitleChange, onTopBarFavoriteIconChange, screenActionFlow) viewModel.initialize(
characterId,
onTopBarTitleChange,
onTopBarFavoriteIconChange,
screenActionFlow
)
val characterDetailScreenState by viewModel.uiState.collectAsState() val characterDetailScreenState by viewModel.uiState.collectAsState()
Surface( Surface(
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
......
...@@ -6,12 +6,14 @@ import cz.fel.barysole.ackeetesttask.model.CharacterInfo ...@@ -6,12 +6,14 @@ import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository
import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction import cz.fel.barysole.ackeetesttask.ui.screen.ScreenAction
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
...@@ -20,13 +22,19 @@ class CharacterDetailViewModel @Inject constructor( ...@@ -20,13 +22,19 @@ class CharacterDetailViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private var isInit: Boolean = false private var isInit: Boolean = false
// The UI state // The UI state
private val _uiState = MutableStateFlow(UiState()) private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow() val uiState: StateFlow<UiState> = _uiState.asStateFlow()
var onTopBarTitleChange: (String) -> Unit = {} var onTopBarTitleChange: (String) -> Unit = {}
var onTopBarFavoriteIconChange: (Boolean) -> Unit = {} var onTopBarFavoriteIconChange: (Boolean) -> Unit = {}
fun initialize(characterId: Long?, onTopBarTitleChange: (String) -> Unit = {}, onTopBarFavoriteIconChange: (Boolean) -> Unit = {}, screenFlow: Flow<ScreenAction>? = null) { fun initialize(
characterId: Long?,
onTopBarTitleChange: (String) -> Unit = {},
onTopBarFavoriteIconChange: (Boolean) -> Unit = {},
screenFlow: Flow<ScreenAction>? = null
) {
if (!isInit) { if (!isInit) {
this.onTopBarTitleChange = onTopBarTitleChange this.onTopBarTitleChange = onTopBarTitleChange
this.onTopBarFavoriteIconChange = onTopBarFavoriteIconChange this.onTopBarFavoriteIconChange = onTopBarFavoriteIconChange
...@@ -46,9 +54,11 @@ class CharacterDetailViewModel @Inject constructor( ...@@ -46,9 +54,11 @@ class CharacterDetailViewModel @Inject constructor(
showLoadingError(false) showLoadingError(false)
characterId?.let { characterId?.let {
viewModelScope.launch { viewModelScope.launch {
val characterInfo = characterRepository.getCharacter( val characterInfo = withContext(Dispatchers.IO) {
characterId characterRepository.getCharacter(
) characterId
)
}
onTopBarTitleChange(characterInfo?.name ?: "") onTopBarTitleChange(characterInfo?.name ?: "")
onTopBarFavoriteIconChange(characterInfo?.isFavorite ?: false) onTopBarFavoriteIconChange(characterInfo?.isFavorite ?: false)
_uiState.update { state -> _uiState.update { state ->
......
...@@ -15,6 +15,7 @@ import androidx.compose.material3.Text ...@@ -15,6 +15,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
...@@ -22,6 +23,7 @@ import androidx.compose.ui.unit.Dp ...@@ -22,6 +23,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.GlideImage
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.model.CharacterInfo import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.model.Location import cz.fel.barysole.ackeetesttask.model.Location
import cz.fel.barysole.ackeetesttask.model.Origin import cz.fel.barysole.ackeetesttask.model.Origin
...@@ -43,7 +45,7 @@ fun CharacterInfo( ...@@ -43,7 +45,7 @@ fun CharacterInfo(
character.image?.let { character.image?.let {
GlideImage( GlideImage(
model = it, model = it,
contentDescription = "Character image", contentDescription = stringResource(R.string.character_image_description),
modifier = Modifier modifier = Modifier
.padding(top = topPaddingForElements, start = startPaddingForElements) .padding(top = topPaddingForElements, start = startPaddingForElements)
.size(128.dp) .size(128.dp)
...@@ -57,7 +59,7 @@ fun CharacterInfo( ...@@ -57,7 +59,7 @@ fun CharacterInfo(
) )
) { ) {
Text( Text(
text = "Name", text = stringResource(R.string.name_field_title),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -80,7 +82,7 @@ fun CharacterInfo( ...@@ -80,7 +82,7 @@ fun CharacterInfo(
Row(modifier = Modifier.padding(start = startPaddingForElements)) { Row(modifier = Modifier.padding(start = startPaddingForElements)) {
Column { Column {
Text( Text(
text = "Status", text = stringResource(R.string.status_field_title),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -97,7 +99,7 @@ fun CharacterInfo( ...@@ -97,7 +99,7 @@ fun CharacterInfo(
) )
Spacer(modifier = Modifier.height(itemSpaceSize)) Spacer(modifier = Modifier.height(itemSpaceSize))
Text( Text(
text = "Type", text = stringResource(R.string.type_field_title),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -114,7 +116,7 @@ fun CharacterInfo( ...@@ -114,7 +116,7 @@ fun CharacterInfo(
) )
Spacer(modifier = Modifier.height(itemSpaceSize)) Spacer(modifier = Modifier.height(itemSpaceSize))
Text( Text(
text = "Gender", text = stringResource(R.string.gender_field_title),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -131,7 +133,7 @@ fun CharacterInfo( ...@@ -131,7 +133,7 @@ fun CharacterInfo(
) )
Spacer(modifier = Modifier.height(itemSpaceSize)) Spacer(modifier = Modifier.height(itemSpaceSize))
Text( Text(
text = "Origin", text = stringResource(R.string.origin_field_title),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -148,7 +150,7 @@ fun CharacterInfo( ...@@ -148,7 +150,7 @@ fun CharacterInfo(
) )
Spacer(modifier = Modifier.height(itemSpaceSize)) Spacer(modifier = Modifier.height(itemSpaceSize))
Text( Text(
text = "Location", text = stringResource(R.string.location_field_title),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
...@@ -181,7 +183,7 @@ fun CharacterInfoPreview() { ...@@ -181,7 +183,7 @@ fun CharacterInfoPreview() {
gender = "Male", gender = "Male",
origin = Origin("Earth"), origin = Origin("Earth"),
location = Location("Earth"), location = Location("Earth"),
image = "https://rickandmortyapi.com/api/character/avatar/2.jpeg" image = null
) )
) )
} }
\ No newline at end of file
...@@ -14,21 +14,23 @@ import androidx.compose.runtime.Composable ...@@ -14,21 +14,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList
@Composable @Composable
fun CharacterListScreen( fun CharacterListScreen(
snackbar: SnackbarHostState, snackbarHostState: SnackbarHostState,
onItemSelected: (id: Long) -> Unit, onSelectItem: (id: Long) -> Unit,
characterListViewModel: CharacterListViewModel = hiltViewModel() characterListViewModel: CharacterListViewModel = hiltViewModel()
) { ) {
Surface( Surface(
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
characterListViewModel.pagingDataFlow.collectAsLazyPagingItems().let { characterListViewModel.charactersPDFlow.collectAsLazyPagingItems().let {
if (it.loadState.mediator?.refresh is LoadState.Error || it.loadState.mediator?.append is LoadState.Error) { if (it.loadState.mediator?.refresh is LoadState.Error || it.loadState.mediator?.append is LoadState.Error) {
characterListViewModel.showLoadingError(true) characterListViewModel.showLoadingError(true)
} else { } else {
...@@ -36,44 +38,35 @@ fun CharacterListScreen( ...@@ -36,44 +38,35 @@ fun CharacterListScreen(
} }
// If the UI state contains an error, show snackbar // If the UI state contains an error, show snackbar
if (characterListViewModel.uiState.value.showError) { if (characterListViewModel.uiState.value.showError) {
LaunchedEffect(snackbar) { val errorMessage = stringResource(R.string.data_cannot_be_loaded_error)
snackbar.showSnackbar( LaunchedEffect(snackbarHostState) {
message = "Data cannot be loaded!" snackbarHostState.showSnackbar(
message = errorMessage
) )
} }
} }
// todo: there is no pullToRefresh in MD3 at this moment :c Add it later. // todo: there is no pullToRefresh in MD3 at this moment. Add it later. :c
if (it.itemCount == 0) { if (it.itemCount == 0 && it.loadState.refresh != LoadState.Loading) {
Column( Column(
modifier = Modifier.fillMaxWidth().fillMaxHeight(), modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Button( Button(
onClick = { characterListViewModel.refreshList() }, onClick = { characterListViewModel.refreshList() },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.primary) colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary
)
) { ) {
Text("Reload") Text(stringResource(R.string.reload_text))
} }
} }
} else { } else {
CharacterList(it, onItemSelected) CharacterList(it, onSelectItem)
} }
} }
} }
} }
\ No newline at end of file
/*
@Preview
@Composable
fun CharacterListPreview() {
CharacterList(
(
CharacterInfo(
"1",
"Rick Sanchez",
"Alive"
), CharacterInfo("2", "Morty", "Alive")
)
)
}*/
...@@ -6,8 +6,8 @@ import androidx.paging.PagingData ...@@ -6,8 +6,8 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import cz.fel.barysole.ackeetesttask.model.CharacterInfo import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository
import cz.fel.barysole.ackeetesttask.ui.uielement.main.UiState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
...@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow ...@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
...@@ -28,14 +29,18 @@ class CharacterListViewModel @Inject constructor( ...@@ -28,14 +29,18 @@ class CharacterListViewModel @Inject constructor(
// The UI state // The UI state
private val _uiState = MutableStateFlow(UiState()) private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow() val uiState: StateFlow<UiState> = _uiState.asStateFlow()
val pagingDataFlow: Flow<PagingData<CharacterInfo>> val charactersPDFlow: Flow<PagingData<CharacterInfo>>
private val getAllCharacters = MutableSharedFlow<Unit>() private val getAllCharacters = MutableSharedFlow<Unit>()
init { init {
pagingDataFlow = getAllCharacters charactersPDFlow = getAllCharacters
.onStart { emit(Unit) } .onStart { emit(Unit) }
.flatMapLatest { getCharactersPagingData() } .flatMapLatest {
withContext(Dispatchers.IO) {
characterRepository.getCharactersFlow()
}
}
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
} }
...@@ -44,13 +49,12 @@ class CharacterListViewModel @Inject constructor( ...@@ -44,13 +49,12 @@ class CharacterListViewModel @Inject constructor(
} }
fun refreshList() { fun refreshList() {
showLoadingError(false) viewModelScope.launch {
viewModelScope.launch { getAllCharacters.emit(Unit) } showLoadingError(false)
getAllCharacters.emit(Unit)
}
} }
private fun getCharactersPagingData(): Flow<PagingData<CharacterInfo>> =
characterRepository.getCharactersFlow()
data class UiState( data class UiState(
val showError: Boolean = false val showError: Boolean = false
) )
......
...@@ -4,26 +4,26 @@ import androidx.compose.foundation.layout.Arrangement ...@@ -4,26 +4,26 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList
@Composable @Composable
fun FavoriteCharacterListScreen( fun FavoriteCharacterListScreen(
onItemSelected: (id: Long) -> Unit, onSelectItem: (id: Long) -> Unit,
favoriteCharacterListViewModel: FavoriteCharacterListViewModel = hiltViewModel() favoriteCharacterListViewModel: FavoriteCharacterListViewModel = hiltViewModel()
) { ) {
favoriteCharacterListViewModel.refreshList() favoriteCharacterListViewModel.refreshList()
val favoriteCharacters = val favoriteCharacters =
favoriteCharacterListViewModel.pagingDataFlow.collectAsLazyPagingItems() favoriteCharacterListViewModel.favoriteCharactersPDFlow.collectAsLazyPagingItems()
Surface( Surface(
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
...@@ -35,10 +35,10 @@ fun FavoriteCharacterListScreen( ...@@ -35,10 +35,10 @@ fun FavoriteCharacterListScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("There is no favorite characters yet.") Text(stringResource(R.string.no_favorite_characters_text))
} }
} else { } else {
CharacterList(favoriteCharacters, onItemSelected) CharacterList(favoriteCharacters, onSelectItem)
} }
} }
} }
\ No newline at end of file
...@@ -4,40 +4,37 @@ import androidx.lifecycle.ViewModel ...@@ -4,40 +4,37 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.room.withTransaction
import cz.fel.barysole.ackeetesttask.db.room.AppDatabase
import cz.fel.barysole.ackeetesttask.model.CharacterInfo import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class FavoriteCharacterListViewModel @Inject constructor( class FavoriteCharacterListViewModel @Inject constructor(
private val characterRepository: CharacterRepository, private val characterRepository: CharacterRepository
private val appDatabase: AppDatabase
) : ViewModel() { ) : ViewModel() {
// The UI state val favoriteCharactersPDFlow: Flow<PagingData<CharacterInfo>>
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
val pagingDataFlow: Flow<PagingData<CharacterInfo>>
private val getFavoriteCharacters = MutableSharedFlow<Unit>() private val getFavoriteCharacters = MutableSharedFlow<Unit>()
init { init {
pagingDataFlow = getFavoriteCharacters favoriteCharactersPDFlow = getFavoriteCharacters
.onStart { emit(Unit) } .onStart { emit(Unit) }
.flatMapLatest { characterRepository.getFavoriteCharactersFlow() } .flatMapLatest {
withContext(Dispatchers.IO) {
characterRepository.getFavoriteCharactersFlow()
}
}
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
} }
...@@ -45,12 +42,4 @@ class FavoriteCharacterListViewModel @Inject constructor( ...@@ -45,12 +42,4 @@ class FavoriteCharacterListViewModel @Inject constructor(
viewModelScope.launch { getFavoriteCharacters.emit(Unit) } viewModelScope.launch { getFavoriteCharacters.emit(Unit) }
} }
fun showLoadingError(hasError: Boolean) {
_uiState.value = UiState(showError = hasError)
}
data class UiState(
val showError: Boolean = false
)
} }
\ No newline at end of file
...@@ -12,7 +12,7 @@ import cz.fel.barysole.ackeetesttask.model.CharacterInfo ...@@ -12,7 +12,7 @@ import cz.fel.barysole.ackeetesttask.model.CharacterInfo
@Composable @Composable
fun CharacterList( fun CharacterList(
characterList: LazyPagingItems<CharacterInfo>, characterList: LazyPagingItems<CharacterInfo>,
onItemSelected: (id: Long) -> Unit onSelectItem: (id: Long) -> Unit
) { ) {
LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) { LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
items( items(
...@@ -20,7 +20,7 @@ fun CharacterList( ...@@ -20,7 +20,7 @@ fun CharacterList(
contentType = { if (characterList[it] == null) 1 else 0 } contentType = { if (characterList[it] == null) 1 else 0 }
) { index -> ) { index ->
characterList[index]?.let { characterList[index]?.let {
CharacterListItem(it, onItemSelected) CharacterListItem(it, onSelectItem)
} }
} }
} }
......
...@@ -19,25 +19,28 @@ import androidx.compose.ui.Modifier ...@@ -19,25 +19,28 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.GlideImage
import cz.fel.barysole.ackeetesttask.R import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.model.CharacterInfo import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.ui.screen.Screen import cz.fel.barysole.ackeetesttask.model.Location
import cz.fel.barysole.ackeetesttask.model.Origin
@OptIn(ExperimentalGlideComposeApi::class) @OptIn(ExperimentalGlideComposeApi::class)
@Composable @Composable
fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Unit) { fun CharacterListItem(character: CharacterInfo, onSelectItem: (id: Long) -> Unit) {
Surface( Surface(
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
shadowElevation = 4.dp, shadowElevation = 4.dp,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp) .padding(horizontal = 8.dp, vertical = 4.dp)
.clickable { onItemSelected(character.id) } .clickable { onSelectItem(character.id) }
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
...@@ -48,7 +51,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un ...@@ -48,7 +51,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un
character.image?.let { character.image?.let {
GlideImage( GlideImage(
model = it, model = it,
contentDescription = "Character image", contentDescription = stringResource(R.string.character_image_description),
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
...@@ -56,7 +59,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un ...@@ -56,7 +59,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un
} }
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column { Column {
Row (verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "${character.name}", text = "${character.name}",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
...@@ -71,7 +74,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un ...@@ -71,7 +74,7 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.size(16.dp), .size(16.dp),
tint = Color.Blue, tint = Color.Blue,
contentDescription = "Arrow back icon" contentDescription = stringResource(R.string.arrow_back_icon_description)
) )
} }
} }
...@@ -88,4 +91,22 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un ...@@ -88,4 +91,22 @@ fun CharacterListItem(character: CharacterInfo, onItemSelected: (id: Long) -> Un
} }
} }
} }
}
@Preview
@Composable
fun CharacterInfoPreview() {
CharacterListItem(
character = CharacterInfo(
id = 2,
name = "Morty Smith",
status = "Alive",
species = "Human",
type = "",
gender = "Male",
origin = Origin("Earth"),
location = Location("Earth"),
image = null
)
) {}
} }
\ No newline at end of file
package cz.fel.barysole.ackeetesttask.ui.uielement.main package cz.fel.barysole.ackeetesttask.ui.uielement.charactersearch
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
...@@ -7,6 +7,7 @@ import androidx.paging.cachedIn ...@@ -7,6 +7,7 @@ import androidx.paging.cachedIn
import cz.fel.barysole.ackeetesttask.model.CharacterInfo import cz.fel.barysole.ackeetesttask.model.CharacterInfo
import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository import cz.fel.barysole.ackeetesttask.repository.characters.CharacterRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
...@@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.filterNot ...@@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
...@@ -37,21 +39,20 @@ constructor( ...@@ -37,21 +39,20 @@ constructor(
init { init {
pagingDataFlow = searchCharacterByName pagingDataFlow = searchCharacterByName
.distinctUntilChanged() .distinctUntilChanged()
.onEach { name -> .onEach { characterName ->
_uiState.value = UiState(query = name) _uiState.value = UiState(query = characterName)
} }
.filterNot { name -> name.isBlank() } .filterNot { characterName -> characterName.isBlank() }
.flatMapLatest { name -> .flatMapLatest { characterName ->
searchCharacter(characterName = name) withContext(Dispatchers.IO) {
characterRepository.getCharactersByNameFlow(characterName)
}
} }
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
} }
private fun searchCharacter(characterName: String): Flow<PagingData<CharacterInfo>> =
characterRepository.getCharactersByNameFlow(characterName)
fun onCharacterSearch(characterName: String) { fun onCharacterSearch(characterName: String) {
viewModelScope.launch { searchCharacterByName.emit(characterName) }; viewModelScope.launch { searchCharacterByName.emit(characterName) }
} }
fun onFieldClean() { fun onFieldClean() {
......
package cz.fel.barysole.ackeetesttask.ui.uielement.main package cz.fel.barysole.ackeetesttask.ui.uielement.charactersearch
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
...@@ -10,18 +9,17 @@ import androidx.compose.material3.Icon ...@@ -10,18 +9,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
...@@ -30,13 +28,12 @@ import androidx.paging.compose.collectAsLazyPagingItems ...@@ -30,13 +28,12 @@ import androidx.paging.compose.collectAsLazyPagingItems
import cz.fel.barysole.ackeetesttask.R import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.screen.Screen import cz.fel.barysole.ackeetesttask.ui.screen.Screen
import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList import cz.fel.barysole.ackeetesttask.ui.uielement.character.CharacterList
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MySearchBar( fun MySearchBar(
isSearchBarShowing: MutableState<Boolean>, isSearchBarShowing: MutableState<Boolean>,
onScreenSelected: (Screen, List<Any>?) -> Unit, onSelectScreen: (Screen, List<Any>?) -> Unit,
characterSearchViewModel: CharacterSearchViewModel = hiltViewModel() characterSearchViewModel: CharacterSearchViewModel = hiltViewModel()
) { ) {
val isSearchBarIsActive = rememberSaveable { val isSearchBarIsActive = rememberSaveable {
...@@ -55,19 +52,19 @@ fun MySearchBar( ...@@ -55,19 +52,19 @@ fun MySearchBar(
active = isSearchBarIsActive.value, active = isSearchBarIsActive.value,
onActiveChange = { isSearchBarIsActive.value = it }, onActiveChange = { isSearchBarIsActive.value = it },
placeholder = { placeholder = {
Text(text = "Start enter a name...") Text(text = stringResource(R.string.search_bar_placeholder))
}, },
leadingIcon = { leadingIcon = {
Icon( Icon(
painterResource(R.drawable.baseline_arrow_back_ios_new_24), painterResource(R.drawable.baseline_arrow_back_ios_new_24),
modifier = Modifier.clickable { isSearchBarShowing.value = false }, modifier = Modifier.clickable { isSearchBarShowing.value = false },
contentDescription = "Arrow back icon" contentDescription = stringResource(R.string.arrow_back_icon_description)
) )
}, },
trailingIcon = { trailingIcon = {
Icon( Icon(
painterResource(R.drawable.baseline_close_24), painterResource(R.drawable.baseline_close_24),
contentDescription = "Close icon", contentDescription = stringResource(R.string.close_icon_description),
modifier = Modifier.clickable { modifier = Modifier.clickable {
if (searchBarState.query.isNotBlank()) { if (searchBarState.query.isNotBlank()) {
characterSearchViewModel.onFieldClean() characterSearchViewModel.onFieldClean()
...@@ -84,12 +81,21 @@ fun MySearchBar( ...@@ -84,12 +81,21 @@ fun MySearchBar(
// do not show elements when the query is empty // do not show elements when the query is empty
if (searchBarState.query.isNotBlank()) { if (searchBarState.query.isNotBlank()) {
if (it.loadState.refresh is LoadState.Error) { if (it.loadState.refresh is LoadState.Error) {
Box (modifier = Modifier.fillMaxWidth(1f).padding(vertical = 64.dp), contentAlignment = Alignment.Center) { Box(
Text("Data cannot be loaded!", style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center) modifier = Modifier
.fillMaxWidth(1f)
.padding(vertical = 64.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.data_cannot_be_loaded_error),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
} }
} else { } else {
CharacterList(it) { characterId -> CharacterList(it) { characterId ->
onScreenSelected( onSelectScreen(
Screen.CharacterDetail, Screen.CharacterDetail,
listOf(characterId) listOf(characterId)
) )
...@@ -98,4 +104,4 @@ fun MySearchBar( ...@@ -98,4 +104,4 @@ fun MySearchBar(
} }
} }
} }
} }
\ No newline at end of file
...@@ -5,29 +5,33 @@ import androidx.compose.material3.Icon ...@@ -5,29 +5,33 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import cz.fel.barysole.ackeetesttask.R import cz.fel.barysole.ackeetesttask.R
import cz.fel.barysole.ackeetesttask.ui.screen.Screen import cz.fel.barysole.ackeetesttask.ui.screen.Screen
@Composable @Composable
fun MyNavigationBar( fun MyNavigationBar(
selectedScreen: MutableState<Screen>, selectedScreen: Screen,
onScreenSelected: (Screen, List<Any>?) -> Unit onSelectScreen: (Screen, List<Any>?) -> Unit
) { ) {
val navBarItems = listOf( val navBarItems = remember {
Screen.Characters, listOf(
Screen.Favorite Screen.Characters,
) Screen.Favorite
if (selectedScreen.value.showBottomBar == true) { )
}
if (selectedScreen.showBottomBar) {
NavigationBar( NavigationBar(
modifier = Modifier.shadow(8.dp), modifier = Modifier.shadow(8.dp),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
...@@ -39,18 +43,29 @@ fun MyNavigationBar( ...@@ -39,18 +43,29 @@ fun MyNavigationBar(
if (screen.iconResId != null) { if (screen.iconResId != null) {
Icon( Icon(
painterResource(screen.iconResId), painterResource(screen.iconResId),
contentDescription = stringResource(screen.nameResId?: R.string.empty_string), contentDescription = stringResource(
screen.nameResId ?: R.string.empty_string
),
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
}, },
label = { Text(stringResource(screen.nameResId?: R.string.empty_string)) }, colors = NavigationBarItemDefaults.colors(selectedIconColor = Color.Blue),
selected = selectedScreen.value == screen, label = { Text(stringResource(screen.nameResId ?: R.string.empty_string)) },
selected = selectedScreen == screen,
onClick = { onClick = {
onScreenSelected(screen, null) onSelectScreen(screen, null)
} }
) )
} }
} }
} }
}
@Preview
@Composable
fun MyNavigationBarPreview() {
MyNavigationBar(
Screen.Characters
) { _, _ -> }
} }
\ No newline at end of file