I want to show a circular progress bar while the shopping list items are being retrieved from the database. I have a Lazycolumn that displays the retrieved shopping list items, but the circular progress bar is never displayed, and the message "You don't have any items in this shopping list." is displayed briefly before the list is shown. This behavior is not desired. In the viewmodel, placing loading.value = false after the database call in the viewModelScope coroutine does not work. How can I fix this?
ShoppingListScreen Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItems = shoppingListScreenViewModel.shoppingListItemsState.value?.collectAsLazyPagingItems()
val showProgressBar = shoppingListScreenViewModel.loading.value
Scaffold(
topBar = {
CustomAppBar(
title = "Shopping List Screen",
titleFontSize = 20.sp,
appBarElevation = 4.dp,
navController = navController
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue(SHOW_ADD_ITEM_DIALOG_STR, true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
backgroundColor = Color.White,
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Box {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
if (allItems?.itemCount == 0) {
item { Text("You don't have any items in this shopping list.") }
}
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
ConditionalCircularProgressBar(isDisplayed = showProgressBar)
}
}
}
ShoppingListScreenViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
val shoppingListItemsState: State<Flow<PagingData<ShoppingListItem>>?> get() = _shoppingListItemsState
val loading = mutableStateOf(false)
init {
loading.value = true
getAllShoppingListItemsFromDb()
}
private fun getAllShoppingListItemsFromDb() {
viewModelScope.launch {
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
loading.value = false
}
}
}
put the CircularProgressIndicator inside a Surface or Box, maybe you are using Scaffold with navigation, in my case I was using padding top 60.dp in my circular progrees then it was kind of hinding.
Related
I'm making a TextField with ExposedDropDownMenu for selecting a Country.
When i select something i use mutable state variable 'isReadOnly' for setting the readOnly parameter of TextField composable.
So when I select a country, i want it to be readOnly, but when I click on dismiss icon, I want it to not be readOnly.
I works very well, except one part, when I click on dismiss icon, I can type to my TextField, but software keyboard is not showing up. Do you know the solution to this problem?
val selectedFromController = selected.value
var selectedItem by remember {
mutableStateOf(
selectedFromController.ifEmpty { "" }
)
}
var isReadOnly by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded.value,
onExpandedChange = { expanded.value = !expanded.value }
) {
TextField(
value = selectedItem,
onValueChange = {
selectedItem = it
expanded.value = true
},
readOnly = isReadOnly,
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
label = { Text(stringResource(id = label)) },
trailingIcon = { IconButton(
onClick = {
selectedItem = ""
selected.value = ""
savedId.value = -1
isReadOnly = false
keyboardController.show()
},
content = {
Icon(Icons.Filled.Clear, null)
}
) },
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
val filteringOptions =
menuItems.filter { it.contains(selectedItem, ignoreCase = true) }
if (filteringOptions.isNotEmpty()) {
ExposedDropdownMenu(
expanded = expanded.value,
onDismissRequest = {
expanded.value = false
}
) {
filteringOptions.take(5).map {
DropdownMenuItem(
text = { Text(text = it) },
onClick = {
selectedItem = it
selected.value = it
expanded.value = false
isReadOnly = true
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
)
}
}
}
}
I tried to bring it up with keyboardController, but it didn't help.
Perhaps this is the normal behaviour, but i wish it was different. I had tried to google the solution, but did not find anything suitable (or merely missed it).
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun Greeting() {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val focusManager = LocalFocusManager.current
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(count = 20) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Some label $index") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (!focusManager.moveFocus(FocusDirection.Down))
focusManager.clearFocus()
}),
singleLine = true
)
}
}
}
}
Compose version 1.0.5
You could try just hiding the keyboard whenever scrolling occurs. This is okay as long as you don't have a large set of items. But since you're using TextFields, it isn't likely that you'll have such a large number. This sample illustrates hiding the keyboard when scrolling occurs:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Greeting(this)
}
}
}
#Composable
fun Greeting(activity: Activity) {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val lazyListState = rememberLazyListState()
val ctx = LocalContext.current
LaunchedEffect(lazyListState.firstVisibleItemIndex) {
val inputMethodManager = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.window?.decorView?.windowToken, 0)
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
count = 20,
key = { index ->
// Return a stable + unique key for the item
index
}
) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth(),
label = { Text(text = "Some label $index") },
singleLine = true
)
}
}
}
}
If I add floating action button to the bottom bar in bottomnavigation as extended, the menu will be covered by location.
If I'm set up in the center, you can see both menus.
But if I leave it as end, you can only see one menu.
When I set it to end, I want to see both menus.
Here is my code
Scaffold(
topBar = {
if (menu.name != null) {
TopAppBarCompose(title = menu.name)
}
},
floatingActionButton = {
FloatingActionButton(
onClick = {
val route = Screen.BarcodeScan.route
onNavigateToBarcodeScreen(route)
},
shape = RoundedCornerShape(50),
backgroundColor = MaterialTheme.colors.primary
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = "BarcodeScan",
modifier = Modifier.padding(start = 30.dp, top = 20.dp, bottom = 20.dp, end = 5.dp)
)
Text(
text = "Scan",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 5.dp, top = 20.dp, bottom = 20.dp, end = 30.dp)
)
}
}
},
isFloatingActionButtonDocked = true,
floatingActionButtonPosition = FabPosition.End,
bottomBar = {
BottomAppBar(
cutoutShape = RoundedCornerShape(50),
content = {
BottomNavigation {
BottomNavigationItem(
selected = selectedItem.value == "send",
onClick = {
content.value = "Send Screen"
selectedItem.value = "send"
},
icon = {
Icon(Icons.Filled.SwapVert, contentDescription = "send")
},
label = { Text(text = "send") },
alwaysShowLabel = false
)
BottomNavigationItem(
selected = selectedItem.value == "input",
onClick = {
content.value = "input Screen"
selectedItem.value = "input"
},
icon = {
Icon(Icons.Filled.Create, contentDescription = "input")
},
label = { Text(text = "Input") },
alwaysShowLabel = false
)
}
}
)
}
)
When I set in center, I can see two menu
But I set extened fab button to end, It's going to be as follows.
I want to set the place of the extended fab button to end and see two menus. Is there a way to change the place?
ps.The image was simply uploaded to help understand.
I think all you need is to just offset the FAB using the offset modifier. Here's an example:
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Songs", "Artists", "Playlists")
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
ExtendedFloatingActionButton(
modifier = Modifier.offset(x = -10.dp, y = -70.dp),
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
text = { Text("ADD TO BASKET") },
onClick = { /*do something*/ }
)
}
FAB inside a scaffold
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Songs", "Artists", "Playlists")
Scaffold(
scaffoldState = scaffoldState,
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
ExtendedFloatingActionButton(
modifier = Modifier.offset(x = -10.dp, y = -70.dp),
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
text = { Text("ADD TO BASKET") },
onClick = { /*do something*/ }
)
},
content = { innerPadding ->
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomStart) {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
}
)
If we create state for list item like val state = remember(it) { mutableStateOf(ItemState()) } then we loose expanded state while scrolling.
If we lift up states generating higher before LazyColumn then expand state is saving properly
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {....
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
What is the best way to save items state?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RememberStateTheme {
val navController = rememberNavController()
NavHost(navController, startDestination = "items") {
composable("items") {
Greeting(
onItemClick = { navController.navigate("details/$it") }
)
}
composable(
"details/{index}",
arguments = listOf(navArgument("index") { type = NavType.IntType })
) { backStackEntry ->
DetailsScreen(backStackEntry.arguments?.getInt("index") ?: -1)
}
}
}
}
}
}
#Composable
fun Greeting(
onItemClick: (Int) -> Unit
) {
val items = remember { (0..100).toList() }
Surface(color = MaterialTheme.colors.background) {
val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
// If we create state here like val state = remember(it) { mutableStateOf(ItemState()) }
// then we loose expanded state while scrolling
// If we lift up states generating higher before LazyColumn then expand state
// is saving properly
//
// But when we expand an item, click the button and then go back to items we loose
// expanded state
val state = states[item]
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
}
}
#Composable
fun Item(
index: Int,
state: ItemState,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
) {
Text(
text = index.toString(),
modifier = Modifier.padding(16.dp)
)
if (state.expanded) {
Button(
onClick = onClick,
modifier = Modifier.padding(8.dp)
) {
Text(text = "Click me")
}
}
}
}
}
class ItemState {
val expanded: Boolean
get() = _expanded.value
private val _expanded = mutableStateOf(false)
fun changeState() {
_expanded.value = !_expanded.value
}
}
#Composable
fun DetailsScreen(
index: Int,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.3f))
) {
Text(
text = index.toString(),
modifier = Modifier.align(Alignment.Center)
)
}
}
Using rememberSaveable solves the problem.
Thanks to https://stackoverflow.com/users/1424349/leland-richardson
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
val state = rememberSaveable(item) { mutableStateOf(ItemState()) }
key(item) {
Item(index = item,
state = state.value,
onClick = { onItemClick(item) },
modifier = Modifier
.fillMaxSize()
.clickable {
state.value.changeState()
}
)
}
Divider()
}
}
But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.
Correct. remember() remembers for the scope of a specific composition. This means it remembers across recompositions, but not when the composition is replaced by a separate composition. In your case, navigation replaces your Greeting() composition with a DetailsScreen() composition.
What is the best way to save items state?
Hoist the state further, to a composition that does not get replaced by navigation. In this case, that would be your root composition, where you have your rememberNavController() call.
Or, have the state be stored in a viewmodel that is scoped to your activity, or at least to that root composition.
If you want to have this state persist beyond the life of your process, I think that the vision is that we should use effects to save the state via a repository to some persistent store (e.g., JSON file) and restore the state from that store. However, I have not experimented with this approach yet.
Dialog moves up when keyboard appears. I want a Composable do the same , but i cant find how to do this.
Ok, I've found the answer.
First, in manifest:
<activity
android:windowSoftInputMode="adjustResize">
After that use "Insets", that are explained in this page:
https://google.github.io/accompanist/insets/
in the following example I use a fab that makes a textfield appear that is positioned above the keyboard
PD: Notice in FloatingActionButton I use Modifier.systemBarsPadding(), it is made so that it is not hidden
class MainActivity : ComponentActivity() {
#ExperimentalAnimatedInsets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MyApplicationTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val focusRequester = FocusRequester()
Greeting(focusRequester)
}
}
}
}
}
#ExperimentalAnimatedInsets
#Composable
fun Greeting(focusRequester: FocusRequester) {
var creatorVisibility by remember{ mutableStateOf(false)}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(floatingActionButton = {
Fab(onClick = { creatorVisibility = true }, creatorVisibility = creatorVisibility)}
) {
ConstraintLayout(constraintSet = constraints, modifier = Modifier.fillMaxSize(1f)) {
TextField(
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier.layoutId("mainTf")
)
if (creatorVisibility){
Box(
modifier = Modifier
.fillMaxSize()
.clickable {
creatorVisibility = false
}
.background(color = Color(0f, 0f, 0f, 0.5f))
,contentAlignment = Alignment.BottomCenter
) {
SideEffect(effect = { focusRequester.requestFocus() })
TextField(colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier
.layoutId("textField")
.imePadding()
.focusRequester(focusRequester = focusRequester)
)
}
}
}
}
}
}
#Composable
fun Fab(onClick : () -> Unit, creatorVisibility:Boolean){
if (!creatorVisibility){
FloatingActionButton(
onClick = { onClick() },
modifier = Modifier.layoutId("fab").systemBarsPadding()
) {
Text(text = "Crear tarea", modifier = Modifier.padding(start = 10.dp, end = 10.dp))
}
}
}
val constraints = ConstraintSet {
val textField = createRefFor("textField")
val mainTf = createRefFor("mainTf")
val fab = createRefFor("fab")
constrain(textField){
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
constrain(mainTf){
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
constrain(fab){
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
}