How to link reusable ExtendedFloatingActionButton with reusable Scaffold - android

Expected result:
Reusable ExtendedFloatingActionButton should expand or collapse within a reusable Scaffold when I scroll.
Current result:
ExtendedFloatingActionButton does not expand or collapse when I scroll the list in my Scaffold
I followed this tutorial, but it was not created with reusability in mind. The part I'm confused about is the listState varible within the section called "Joining Everything Together" because I'm not sure where within my code I need to declare it as I have done this a few times in different areas.
#Composable
fun MyReusableScaffold(scaffoldTitle: String, scaffoldFab: #Composable () -> Unit,
scaffoldContent: #Composable (contentPadding: PaddingValues) -> Unit) {
Scaffold(
topBar = { LargeTopAppBar( title = { Text(text = scaffoldTitle) } ) },
floatingActionButton = { scaffoldFab },
content = { contentPadding -> scaffoldContent(contentPadding = contentPadding) }
)
}
#Composable
fun MyFAB(listState: LazyListState) {
ExtendedFloatingActionButton(
text = { Text(text = "title") },
icon = { Icon(Icons.Filled.Add, "") },
expanded = listState.isScrollingUp()
)
}
#Composable
fun <T> MyLazyColumn(modifier: Modifier,
...
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState
) {
...
}
}
#Composable
fun MyHomeScreen() {
MyScaffold(
scaffoldTitle = "title",
scaffoldFab = MyExtendedFAB(listState = LazyListState?)
scaffoldContent = { MyHomeScreenContent(contentPadding = it) },
)
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
}

The expansion state of the ExtendedFloatingActionButton is managed by the expanded.
In your example the expanded state is derived on the listState of the LazyColumn. It means that LazyColumn and ExtendedFloatingActionButton have to use both the same listState.
For example you can do something like:
#Composable
fun MyHomeScreen() {
val listState = rememberLazyListState()
val expandedFab by remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0
}
}
MyReusableScaffold(
scaffoldTitle = "title",
scaffoldFab = { MyFAB(expandedFab) },
scaffoldContent = { MyHomeScreenContent(contentPadding = it, listState = listState) },
)
}
fun MyFAB(expandedFab : Boolean) {
ExtendedFloatingActionButton(
//...
expanded = expandedFab
)
}
#Composable
fun MyLazyColumn(modifier: Modifier = Modifier,
listState: LazyListState
) {
LazyColumn(
state = listState
) { /** ... */ }
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
listState: LazyListState,
contentPadding: PaddingValues = PaddingValues()
) {
MyLazyColumn( listState = listState )
}

Related

Trying to navigate between screens with jetpack compose

I'm fairly new to jetpack and android development and I'm trying to navigate between views. Coming from Swift I thought I could pass NavController to update the view.
My RootScreen looks like this
#Composable
fun RootScreen() {
val navigationController = rememberNavController()
Scaffold(
bottomBar = {
BottomBar(navigationController)
}
) {
NavHost(
navController = navigationController,
startDestination = NavigationItem.Home.route
) {
composable(NavigationItem.Home.route) {
HomeScreen(navController = navigationController)
}
composable(NavigationItem.Routes.route) {
RouteList()
}
composable(NavigationItem.Settings.route) {
Text("Settings")
}
}
}
}
From my HomeScreen I'm tapping a view that has a navigationAction where I am updating the route, but the view does not update.
#Composable
fun HomeScreen(navController: NavController) {
...
HomeCard(title = homeScreen[0].title,
image = painterResource(id = homeScreen[0].image),
frameHeight = 250,
frameWidth = 175,
navigationAction = {
navController.navigate(NavigationItem.Routes.route)
}
)
}
Here's where I'm clicking the action
#Composable
fun HomeCard(
title: String,
image: Painter,
frameHeight: Int,
frameWidth: Int,
navigationAction: () -> Unit
) {
Card(
elevation = 13.dp,
modifier = Modifier
.padding(top = 8.dp)
.clip(RoundedCornerShape(13.dp))
.height(frameHeight.dp)
.width(frameWidth.dp)
.clickable { navigationAction }
) {
....
}
Your screen is not getting navigated because the navigationAction never invoked.
#Composable
fun HomeCard(
navigationAction: () -> Unit
) {
Card(
modifier = Modifier
.clickable { navigationAction.invoke() }
) {
....
}

How to make padding reusable for Scaffold content

How can I set the content in my Scaffold to be reusable with content padding. Is it possible to extract contentPadding from a composable when I declare it? i.e. MyScaffoldContent
Cannot find a parameter with this name: contentPadding
#Composable
fun MyReusableScaffold(scaffoldTitle: String, scaffoldContent: #Composable () -> Unit) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text(text = scaffoldTitle) },
scrollBehavior = scrollBehavior
)
},
content = { contentPadding ->
scaffoldContent(contentPadding = contentPadding)
}
)
}
// Example composable
#Composable
fun MyScaffoldContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
Text(
text = "Header $item",
modifier = Modifier.fillMaxWidth()
)
}
// Example update
#Composable
fun MyHomeScreen(navController: NavController) {
MyReusableScaffold(
scaffoldTitle = "Hello Android",
scaffoldContent = MyScreenContent(contentPadding = ?))
)
}
#Composable
fun MyScreenContent(modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()) {
Text(
text = "Header",
modifier = Modifier.fillMaxWidth()
)
}
if you want the MyHomeScreen composable to use the value of the content padding from MyReusableScaffold, you have to define a parameter in the scaffoldContent lambda and pass the contentPadding to it.
#Composable
fun MyReusableScaffold(scaffoldContent: #Composable ( contentPadding: PaddingValues) -> Unit) {
Scaffold(
topBar = {
},
content = { contentPadding ->
scaffoldContent(contentPadding = contentPadding)
}
)
}
#Composable
fun MyScaffoldContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
Text(
text = "Header",
modifier = Modifier.fillMaxWidth()
)
}
#Composable
fun MyHomeScreen(navController: NavController) {
MyReusableScaffold(
scaffoldContent = {
MyScreenContent(contentPadding = it)
}
)
// or
// MyReusableScaffold(
// scaffoldContent = {
// MyScaffoldContent(contentPadding = it)
// }
// )
}

Jetpack Compose - Search bar won't allow me to search and the cursor will not show up

I have a custom search bar will not allow me to place a cursor and type text into it. Before adding the viewmodel and state, I was able to have the TextField work and allow typing text into it.
I've tried using state variables within the composable instead of separating the logic into the view model and unfortunatly recieved the same result. I have a feeling it's something simple that I'm missing but can't quite find it.
Custom Search Bar:
#Composable
fun SearchBar(
modifier: Modifier,
viewModel: ToolSetListViewModel = hiltViewModel()
){
Surface (
modifier = modifier
.fillMaxWidth()
.height(74.dp)
.padding(20.dp, 15.dp, 20.dp, 0.dp),
elevation = 10.dp,
color = MaterialTheme.colors.primary,
shape = RoundedCornerShape(25)
){
TextField(
modifier = modifier
.fillMaxWidth(),
value = viewModel.searchText,
onValueChange = {
viewModel.onEvent(ToolSetListEvent.OnSearchToolSet(it))
},
placeholder = {
Text(
modifier = modifier
.alpha(ContentAlpha.medium),
text = "Search...",
color = White
)
},
textStyle = TextStyle(
fontSize = MaterialTheme.typography.subtitle1.fontSize,
),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = White
)
)
}
Screen:
#Composable
fun ToolSetListScreen(
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: ToolSetListViewModel = hiltViewModel()
) {
val toolSets = viewModel.toolSets.collectAsState(initial = emptyList())
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when(event) {
is UiEvent.Navigate -> onNavigate(event)
}
}
}
Scaffold (
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
) {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 20.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
}
}
ViewModel:
#HiltViewModel
class ToolSetListViewModel #Inject constructor(
private val repository: ToolSetRepository
): ViewModel() {
val toolSets = repository.getAllToolSets()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
var searchText by mutableStateOf("")
private set
fun onEvent(event: ToolSetListEvent) {
when(event) {
is ToolSetListEvent.OnToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET + "?PONumber=${event.toolSet.PONumber}"))
}
is ToolSetListEvent.OnDeleteToolSetClick -> {
viewModelScope.launch {
repository.deleteToolSet(event.toolset)
}
}
is ToolSetListEvent.OnAddToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET))
}
is ToolSetListEvent.OnSearchToolSet -> {
viewModelScope.launch {
if (event.searchText.isNotBlank()) {
searchText = event.searchText
repository.getToolSetByPO(event.searchText)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
MainActivity:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PunchesManagerTheme {
val navController = rememberNavController()
Scaffold (
content = {
Navigation(navController)
},
bottomBar = { BottomNavigationBar(navController = navController) },
)
}
}
}
}
#Composable
fun BottomNavigationBar(navController: NavHostController) {
BottomNavigation {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
NavBarItems.bottomNavItem.forEach { navItem ->
BottomNavigationItem(selected = currentRoute == navItem.route, onClick = {
navController.navigate(navItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(imageVector = navItem.icon,
contentDescription = navItem.name)
},
label = {
Text(text = navItem.name)
},
)
}
}
}
Any suggestions on what the issue may be?
I think I found the fix!
I adjusted the layout of my scaffoled in my ToolSetListScreen:
Scaffold (
content = {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 75.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
)
}
and this seemed to work.

How to make a #Preview in JetpackCompose, when the component depends of some data provide by ViewModel

I was developing an App where I try to implement some new technologies, as Jetpack Compose. And in general, it's a great tool, except the fact that it has hard pre-visualize system (#Preview) thn the regular xml design files.
My problem comes when I try to create a #Preview of the component which represent the different rows, where I load my data recover from network.
In my case I made this:
#Preview(
name ="ListScreenPreview ",
showSystemUi = true,
showBackground = true,
device = Devices.NEXUS_9)
#Composable
fun myPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
PokedexEntry(
model = pokemonMokData,
navController = rememberNavController(),
viewModel = hiltViewModel())
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Cacamon",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
To represent this #Composable:
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
)
{
Column {
Spacer(modifier = Modifier.height(20.dp))
Image(
painter = painterResource(id = R.drawable.ic_international_pok_mon_logo),
contentDescription = "Pokemon",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
)
SearchBar(
hint = "Search...",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
viewModel.searchPokemonList(it)
}
Spacer(modifier = Modifier.height(16.dp))
PokemonList(navController = navController,
viewModel = viewModel)
}
}
}
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = " ",
onSearch: (String) -> Unit = { }
) {
var text by remember {
mutableStateOf("")
}
var isHintDisplayed by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier) {
BasicTextField(value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHintDisplayed = !it.isFocused
}
)
if (isHintDisplayed) {
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
#Composable
fun PokemonList(
navController: NavController,
viewModel: PokemonListViewModel
) {
val pokemonList by remember { viewModel.pokemonList }
val endReached by remember { viewModel.endReached }
val loadError by remember { viewModel.loadError }
val isLoading by remember { viewModel.isLoading }
val isSearching by remember { viewModel.isSearching }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
val itemCount = if (pokemonList.size % 2 == 0) {
pokemonList.size / 2
} else {
pokemonList.size / 2 + 1
}
items(itemCount) {
if (it >= itemCount - 1 && !endReached && !isLoading && !isSearching) {
viewModel.loadPokemonPaginated()
}
PokedexRow(rowIndex = it, models = pokemonList, navController = navController, viewModel = viewModel)
}
}
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxSize()
) {
if (isLoading) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
if (loadError.isNotEmpty()) {
RetrySection(error = loadError) {
viewModel.loadPokemonPaginated()
}
}
}
}
#SuppressLint("LogNotTimber")
#Composable
fun PokedexEntry(
model: PokedexListModel,
navController: NavController,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel
) {
val defaultDominantColor = MaterialTheme.colors.surface
var dominantColor by remember {
mutableStateOf(defaultDominantColor)
}
Box(
contentAlignment = Center,
modifier = modifier
.shadow(5.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.aspectRatio(1f)
.background(
Brush.verticalGradient(
listOf(dominantColor, defaultDominantColor)
)
)
.clickable {
navController.navigate(
"pokemon_detail_screen/${dominantColor.toArgb()}/${model.pokemonName}/${model.number}"
)
}
) {
Column {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(model.imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}.build(),
imageLoader = ImageLoader.Builder(LocalContext.current)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build(),
contentDescription = model.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
loading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
//Set constrains dynamically
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
// shows an error text message when request failed.
failure = {
Text(text = "image request failed.")
}
)
Log.d("pokemonlist", model.imageUrl)
Text(
text = model.pokemonName,
fontFamily = RobotoCondensed,
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
#Composable
fun PokedexRow(
rowIndex: Int,
models: List<PokedexListModel>,
navController: NavController,
viewModel: PokemonListViewModel
) {
Column {
Row {
PokedexEntry(
model = models[rowIndex * 2],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
Spacer(modifier = Modifier.width(16.dp))
if (models.size >= rowIndex * 2 + 2) {
PokedexEntry(
model = models[rowIndex * 2 + 1],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
#Composable
fun RetrySection(
error: String,
onRetry: () -> Unit,
) {
Column() {
Text(error, color = Color.Red, fontSize = 18.sp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onRetry() },
modifier = Modifier.align(CenterHorizontally)
) {
Text(text = "Retry")
}
}
}
I try to annotate with the #Nullable navController and viewmodel of the PokemonListScreen #Composable, but doesn't work either. I'm still seeing an empty screen:
So I try to search into the Jetpack documentation but, it's just defining quite simple Composables.
So if you have some more knowledge about it and can help, thanks in advance !
The main problem is if I wanna Preview that #Composable, although I made #Nullable to the viewmodel parameter, which I guess it's the problem here, AS still demand to initialize. Because I guess the right way to pass argument to a preview is by #PreviewArgument annotation.
[EDIT]
After some digging, I found AS is returning the following error under the Preview Screen:
So, there anyway to avoid viewmodel error??
[SOLUTION]
Finally a apply the following solution which make works, because the cause of the problem is due to Hilt have some inconpatibilities with Jetpack Compose previews:
Create an interface of the your ViewModel which recover all the variables and methods.
Make yourcurrent viemodel class extends of the interface.
Create a 2ยบ class which extends on the interface and pass that to your #Preview
#SuppressLint("UnrememberedMutableState")
#Preview(
name ="ListScreenPreview",
showSystemUi = true,
showBackground = true,
device = Devices.PIXEL)
#Composable
fun MyPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
JetpackComposePokedexTheme {
PokedexRow(
rowIndex = 0,
models = PokemonListScreenProvider().values.toList(),
navController = rememberNavController(),
viewModel = PokemonListViewModelMock(
0, mutableStateOf(""), mutableStateOf(value = false),
mutableStateOf(false), mutableStateOf(listOf(pokemonMokData))
)
)
}
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Machasaurio",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
PokemonListViewModelInterface
interface PokemonListViewModelInterface {
var curPage : Int
var loadError: MutableState<String>
var isLoading: MutableState<Boolean>
var endReached: MutableState<Boolean>
var pokemonList: MutableState<List<PokedexListModel>>
fun searchPokemonList(query: String)
fun loadPokemonPaginated()
fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit)
}
PokemonListViewModelMock
class PokemonListViewModelMock (
override var curPage: Int,
override var loadError: MutableState<String>,
override var isLoading: MutableState<Boolean>,
override var endReached: MutableState<Boolean>,
override var pokemonList: MutableState<List<PokedexListModel>>
): PokemonListViewModelInterface{
override fun searchPokemonList(query: String) {
TODO("Not yet implemented")
}
override fun loadPokemonPaginated() {
TODO("Not yet implemented")
}
override fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
TODO("Not yet implemented")
}
}
The actual Preview is the following, and although the image doesn't display, is shown correctly:
You could create another composable which invokes the viewmodel logic via lambda functions instead of using the viewmodel itself. Extract your uiState to a separate class, so it can be used as a StateFlow in your viewmodel, which in turn can be observed from the composable.
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
/*
rememberStateWithLifecyle is an extension function based on
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
*/
val uiState by rememberStateWithLifecycle(viewModel.uiState)
PokemonListScreen(
uiState = uiState,
onLoadPokemons = viewModel::loadPokemons,
onSearchPokemon = {viewModel.searchPokemon(it)},
onCalculateDominantColor = {viewModel.calcDominantColor(it)},
onNavigate = {route -> navController.navigate(route, null, null)},
)
}
#Composable
private fun PokemonListScreen(
uiState: PokemonUiState,
onLoadPokemons:()->Unit,
onSearchPokemon: (String) -> Unit,
onCalculateDominantColor: (Drawable) -> Color,
onNavigate:(String)->Unit,
) {
}
#HiltViewModel
class PokemonListViewModel #Inject constructor(/*your datasources*/) {
private val loading = MutableStateFlow(false)
private val loadError = MutableStateFlow(false)
private val endReached = MutableStateFlow(false)
private val searching = MutableStateFlow(false)
private val pokemons = MutableStateFlow<Pokemon?>(null)
val uiState: StateFlow<PokemonUiState> = combine(
loading,
loadError,
endReached,
searching,
pokemons
) { loading, error, endReached, searching, pokemons ->
PokemonUiState(
isLoading = loading,
loadError = error,
endReached = endReached,
isSearching = searching,
pokemonList = pokemons,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PokemonUiState.Empty,
)
}
data class PokemonUiState(
val pokemonList: List<Pokemon> = emptyList(),
val endReached: Boolean = false,
val loadError: Boolean = false,
val isLoading: Boolean = false,
val isSearching: Boolean = false,
) {
companion object {
val Empty = PokemonUiState()
}
}
I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.
That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel
interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}
Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.
Hopefully this helps

Jetpack Compose TopAppBar with dynamic actions

#Composable
fun TopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable (() -> Unit)? = null,
actions: #Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
actions: #Composable RowScope.() -> Unit = {}
Usage Scenario:
Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen
Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.
val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }
Want to change the app bar actions content dynamically. Any way to do it?
This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.
Let's assume I have 2 screens: ScreenA and ScreenB
They are handled by MainActivity screen.
This is our MainActivity:
#ExperimentalComposeUiApi
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoolDrinksTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var appBarState by remember {
mutableStateOf(AppBarState())
}
Scaffold(
topBar = {
SmallTopAppBar(
title = {
Text(text = appBarState.title)
},
actions = {
appBarState.actions?.invoke(this)
}
)
}
) { values ->
NavHost(
navController = navController,
startDestination = "screen_a",
modifier = Modifier.padding(
values
)
) {
composable("screen_a") {
ScreenA(
onComposing = {
appBarState = it
},
navController = navController
)
}
composable("screen_b") {
ScreenB(
onComposing = {
appBarState = it
},
navController = navController
)
}
}
}
}
}
}
}
}
As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.
This mutable state is set with a callback function called inside the composition of each screen.
Here you can see the ScreenA
#Composable
fun ScreenA(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen A",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Filter,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen A"
)
Button(onClick = {
navController.navigate("screen_b")
}) {
Text(text = "Navigate to Screen B")
}
}
}
And the ScreenB
#Composable
fun ScreenB(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen B",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen B"
)
Button(onClick = {
navController.popBackStack()
}) {
Text(text = "Navigate back to Screen A")
}
}
}
And finally this is the data class of our state:
data class AppBarState(
val title: String = "",
val actions: (#Composable RowScope.() -> Unit)? = null
)
In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.
First you need to add navigation dependency on you jetpack compose projects.
You can read the doc from this https://developer.android.com/jetpack/compose/navigation
def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
Then define your screen in sealed class:
sealed class Screen(var icon: ImageVector, var route: String) {
object ContentScreen: Screen(Icons.Default.Home, "home")
object ListingScreen: Screen(Icons.Default.List, "list")
}
and this is the navigation function look like
#Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
composable(Screen.ContentScreen.route) {
//your screen content
}
composable(Screen.ListingScreen.route) {
//your listing screen here
}
}
}
Finally in your mainactivity class
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestAppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "main screen") }, actions = {
if (currentRoute == Screen.ContentScreen.route) {
//your share button action here
} else if (currentRoute == Screen.ListingScreen.route) {
//your filter button here
} else {
//other action
}
})
}
) {
Navigation(paddingValues = it, navController = navController)
}
}
}
}
I'm so sorry if the explanation to sort, because the limitation of my English

Categories

Resources