#Composable
fun ShowScreen(viewModel: UseListViewModel){
Column() {
ShowList(viewModel = viewModel)
Buttons(viewModel)
}
}
#Composable
private fun ShowList(viewModel: UseListViewModel) {
LazyColumn() {
items(viewModel.itemListMutableState.value?: emptyList()){
ShowItem(it,viewModel)
}
}
}
#Composable
private fun ShowItem(item: IBaseDataModel, viewModel: UseListViewModel) {
Row (horizontalArrangement = Arrangement.SpaceBetween,verticalAlignment= Alignment.CenterVertically,
modifier = Modifier
.background(Color(AppConstants.itemHolderBackgroundColor(item.state)))
.clickable { viewModel.selectItem(item) }
.fillMaxWidth()
){
Text(text = item.idString)
Text(text = item.date1String)
Checkbox(checked = item.isSelected,onCheckedChange = { viewModel.setIsSelectedItem(item,it)})
Button(onClick = {viewModel.deleteItem(item)}) {
Text(text = "delete")
}
}
}
#Composable
private fun Buttons(viewModel: UseListViewModel){
Row (verticalAlignment = Alignment.Bottom,modifier = Modifier.fillMaxWidth(),horizontalArrangement = Arrangement.SpaceEvenly){
Button(onClick = {viewModel.refreshClick()}) {
Text(text = "refresh")
}
Button(onClick = {viewModel.addClick()}) {
Text(text = "add")
}
val a=if(viewModel.itemListMutableState.value!=null) viewModel.buttonsViewViewModel.buttonNext.visibility else 8
if(a==0){
Button(onClick = {viewModel.nextStateClick()}) {
Text(text = "next")
}
}
}
}
when the list items fill the screen the buttons disapear ..
how to keep the Buttons on screen bottom ?
Many apps need to display collections of items. This document explains how you can efficiently do this in Jetpack Compose.
If you know that your use case does not require any scrolling, you may wish to use a simple Column or Row (depending on the direction), and emit each item’s content by iterating over a list like so:
Use weight() method like this:
#Composable
fun ShowScreen(viewModel: UseListViewModel) {
Column(Modifier.fillMaxSize()) { // <-- new
ShowList(
modifier = Modifier.weight(1f), // <-- new
viewModel = viewModel
)
Buttons(viewModel)
}
}
#Composable
private fun ShowList(
modifier: Modifier, // <-- new
viewModel: UseListViewModel
) {
LazyColumn(modifier) { // <-- new
items(viewModel.itemListMutableState.value ?: emptyList()) {
ShowItem(it, viewModel)
}
}
}
#Composable
private fun ShowItem(item: IBaseDataModel, viewModel: UseListViewModel) {
Row(horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(Color(AppConstants.itemHolderBackgroundColor(item.state)))
.clickable { viewModel.selectItem(item) }
.fillMaxWidth()
) {
Text(text = item.idString)
Text(text = item.date1String)
Checkbox(
checked = item.isSelected,
onCheckedChange = { viewModel.setIsSelectedItem(item, it) })
Button(onClick = { viewModel.deleteItem(item) }) {
Text(text = "delete")
}
}
}
#Composable
private fun Buttons(viewModel: UseListViewModel) {
Row(
// verticalAlignment = Alignment.Bottom, // remove this
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { viewModel.refreshClick() }) {
Text(text = "refresh")
}
Button(onClick = { viewModel.addClick() }) {
Text(text = "add")
}
val a =
if (viewModel.itemListMutableState.value != null) viewModel.buttonsViewViewModel.buttonNext.visibility else 8
if (a == 0) {
Button(onClick = { viewModel.nextStateClick() }) {
Text(text = "next")
}
}
}
}
You can remove Column and use just one LazyColumn for the ShowScreen.
ShowList can now be an extension function for LazyListScope, this will allow calling items of parent LazyColumn directly within the ShowList function.
#Composable
fun ShowScreen(viewModel: UseListViewModel){
LazyColumn {
ShowList(viewModel)
item {
Buttons(viewModel)
}
}
}
private fun LazyListScope.ShowList(viewModel: UseListViewModel) {
items(viewModel.itemListMutableState.value?: emptyList()){
ShowItem(it,viewModel)
}
}
Note that ShowList is not a #Composable function now.
Related
I am facing an issue while showing the bottom sheet dialog in jetpack compose on each row item click. When I click on an item the app crashes.
Here is my LazyColumn code:
//Setup Recyclerview
#OptIn(ExperimentalMaterialApi::class)
#SuppressLint("CoroutineCreationDuringComposition")
#Composable
fun SetRecyclerview(viewModel: UserViewModel, paddingValues: PaddingValues) {
val isLoading = viewModel.isLoading.value
var showSheet by remember { mutableStateOf(false) }
var sheetUser: User? by remember { mutableStateOf(null) }
//Creating a variable for the StateFlow variable
val userList = viewModel.userData.collectAsState().value
if (showSheet) {
ShowBottomSheetDialog(user = sheetUser!!)
sheetScope.launch {
sheetState.show()
}
}
if (isLoading) {
ProgressBarComponent()
} else {
LazyColumn(
modifier = Modifier.padding(0.dp, 5.dp, 0.dp, 5.dp)
) {
userList.forEach { user ->
items(user.results.size) {
EachRow(user = user.results[it], onClick = {
showSheet = true
sheetUser = user.results[it]
})
}
}
}
}
}
Here is my ShowBottomSheetDialog function:
//ShowBottomSheetDialog
#SuppressLint("CoroutineCreationDuringComposition")
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ShowBottomSheetDialog(user: User) {
sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
sheetScope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = { BottomSheetItem(user = user) },
sheetBackgroundColor = Color.White,
sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
) {}
}
Here is BottomSheetItem function:
//Bottom Sheet Item
#Composable
fun BottomSheetItem(user: User) {
Log.e("TAG", "BottomSheetItem: " + user.email)
}
If my keyboard is opened, then on clicking of button if i try to hide my keyboard and change sheetState to show() then my keyboard hides but my sheet is not visible.
Mimicing opening of sheetState on result from api call.
Note - Sheet is shown for first time only
setContent {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
var otpResponse by remember { mutableStateOf<Boolean?>(null) }
if (otpResponse == false) {
LaunchedEffect(key1 = Unit, block = {
delay(180)
otpResponse = true
})
}
if (otpResponse == true) {
LaunchedEffect(key1 = Unit, block = {
sheetState.show()
})
}
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
otpResponse = false
}) {
Text(text = "TEST")
}
}
}
}
}
I have modified your code have look which is working without delay
#OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
#Preview(showBackground = true)
#Composable
fun DefaultPreview(){
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
scope.launch{
sheetState.show()
}
}) {
Text(text = "TEST")
}
}
}
}
}
thanks for my colleague to fix this
Using a when function, I need to navigate to particular graph in my compose navigation tree. The issue is when navigating away from the current screen to the new one, the screen repeatedly flashes and freezes up. Checking the debugger, it shows me that the code just constantly jumps between LaunchedEffect and navController.navigate(ProDashboardScreens.ProDashboardGraph.route).
Can anyone see a problem in my set up?
composable(BasicDashboardScreens.Manager.route) {
val managerViewModel: ManagerViewModel = hiltViewModel()
val managerState by managerViewModel.state.collectAsState()
ManagerScreen(managerState, managerViewModel)
LaunchedEffect(key1 = managerViewModel.navigationEvent) {
managerViewModel.navigationEvent.collect {
when (it) {
AuthenticationScreens.Login -> {
navController.navigate(AuthenticationScreens.Login.route)
}
AuthenticationScreens.Register -> {
navController.navigate(AuthenticationScreens.Register.route)
}
ProDashboardScreens.ProDashboardGraph -> {
navController.navigate(ProDashboardScreens.ProDashboardGraph.route)
}
else -> Unit
}
}
}
}
when {
!state.userAuthenticated -> {
UnauthenticatedScreen(
title = {
Text(
text = stringResource(id = R.string.login_or_register),
style = MaterialTheme.typography.subtitle1.copy(
fontWeight = FontWeight.Bold
),
textAlign = TextAlign.Center
)
},
buttonOne = {
AppButton(
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
buttonText = stringResource(id = R.string.login),
buttonColor = Color.Blue,
enabled = true,
onClick = {
events.loginClicked()
}
)
},
buttonTwo = {
AppButton(
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
buttonText = stringResource(id = R.string.register),
buttonColor = Color.Blue,
enabled = true,
onClick = {
events.registerClicked()
}
)
}
)
}
state.userAuthenticated && !state.userProfile.proVersion -> {
UnauthenticatedScreen(
title = {
Text(
text = stringResource(id = R.string.purchase_pro_enable_access),
style = MaterialTheme.typography.subtitle1.copy(
fontWeight = FontWeight.Bold
),
textAlign = TextAlign.Center
)
},
buttonOne = {
AppButton(
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
buttonText = stringResource(id = R.string.purchase_pro),
buttonColor = Color.Blue,
enabled = true,
onClick = {
events.purchaseProClicked()
}
)
}
)
}
else -> {
events.navigateToProGraph()
}
}
#HiltViewModel
class ManagerViewModel #Inject constructor(
private val userProfileDao: UserProfileDao,
private val authManager: AuthManager
) : ViewModel(), ManagerEvents {
private val _state = MutableStateFlow(ManagerState.defaultState)
val state: StateFlow<ManagerState> = _state
private val _navigationEvent = MutableSharedFlow<NavigationRoute>()
val navigationEvent: SharedFlow<NavigationRoute> = _navigationEvent
init {
viewModelScope.launch {
_state.value = state.value.copy(
userProfile = userProfileDao.getUserProfile().first() ?: UserProfile.defaultState,
userAuthenticated = authManager.isAuthenticated()
)
}
}
override fun loginClicked() {
viewModelScope.launch {
_navigationEvent.emit(AuthenticationScreens.Login)
}
}
override fun registerClicked() {
viewModelScope.launch {
_navigationEvent.emit(AuthenticationScreens.Register)
}
}
override fun navigateToProGraph() {
viewModelScope.launch {
_navigationEvent.emit(ProDashboardScreens.ProDashboardGraph)
}
}
}
I have this list I am getting from Firebase and displaying in a LazyColumn but it only works when I navigate to a different fragment and press back. My lazyColumn is being composed inside de ScreenController for the bottomNavigation.
What do I have to do to display it directly?
below is my Fragment
#AndroidEntryPoint
class OpenTicketFragment : Fragment() {
private val viewModel: OpenTicketsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
val ticketsList = viewModel.tickets.value.data
val currentUserId = viewModel.currentUserId.value
TheProjectTheme {
val navController = rememberNavController()
val title = remember { mutableStateOf("Open Tickets") }
val isDialogOpen = remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
isDialogOpen.value = true
}) {
Icon(Icons.Default.ExitToApp, contentDescription = null)
}
},
title = {
Text(text = title.value)
},
actions = {
IconButton(onClick = {
findNavController().navigate(R.id.action_openTicketFragment_to_profileFragment)
}) {
Icon(Icons.Default.Person, contentDescription = null)
}
}
)
},
bottomBar = {
val items = listOf(
Screen.Open,
Screen.Available,
Screen.Closed
)
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute =
navBackStackEntry?.arguments?.getString(KEY_ROUTE)
items.forEach {
BottomNavigationItem(
icon = { Icon(it.icon, contentDescription = null) },
selected = currentRoute == it.route,
label = { Text(text = it.label) },
onClick = {
navController.popBackStack(
navController.graph.startDestination, false
)
if (currentRoute != it.route) {
navController.navigate(it.route)
}
})
}
}
}
)
{
ScreenController(
navHostController = navController,
title,
ticketsList!!,
findNavController(),
currentUserId
)
AlertDialogComponent(isDialogOpen, findNavController(), viewModel)
}
}
}
}
}
}
#Composable
fun OpenTicketLazyColumn(
ticket: Ticket,
// onClick: () -> Unit,
navController: NavController,
) {
Card(
modifier = Modifier
.padding(8.dp)
.clickable {
val action =
OpenTicketFragmentDirections.actionOpenTicketFragmentToTicketDetailFragment(
ticket.ticketId,
ticket.problem,
ticket.address,
ticket.dateOpened,
ticket.description,
ticket.name,
ticket.status
)
Log.d("SSS8", ticket.ticketId)
navController.navigate(action)
},
elevation = 4.dp
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Ticket Opened Date: ${ticket.dateOpened}",
fontSize = 16.sp,
)
Text(
text = "Service required: ${ticket.problem}",
fontSize = 14.sp,
)
}
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
text = "Status: ${ticket.status}",
fontSize = 16.sp,
)
Text(
text = "Address: ${ticket.address}",
fontSize = 16.sp,
)
}
}
}
}
and my composables
#Composable
fun ScreenController(
navHostController: NavHostController,
topBarTitle: MutableState<String>,
ticketList: List<Ticket>,
navController: NavController,
currentUserId: String
) {
NavHost(
navController = navHostController, startDestination = "open"
) {
composable("open") {
Log.d("SACO", currentUserId)
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == ASSIGNED && item.assignedToId == currentUserId) {
OpenTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Open Tickets"
}
composable("available") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == OPEN) {
ClosedTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Available Tickets"
}
composable("closed") {
LazyColumn(
modifier = Modifier
.padding(8.dp)
) {
items(items = ticketList) { item ->
val status = remember { mutableStateOf(item.status)}
if (status.value == CLOSED && item.assignedToId == currentUserId) {
AvailableTickets(
item,
navController
)
}
}
}
topBarTitle.value = "Closed Tickets"
}
}
}
#Composable
fun OpenTickets(
ticket: Ticket,
navController: NavController
) {
OpenTicketLazyColumn(
ticket,
navController
)
}
the viewModel
#HiltViewModel
class OpenTicketsViewModel #Inject constructor(
private val repository: WorkerRepositoryImpl,
private val firesource: FireBaseSource
) : ViewModel() {
val currentUserId = mutableStateOf("")
val tickets: MutableState<DataOrException<List<Ticket>, Exception>> = mutableStateOf(
DataOrException(
listOf(),
Exception("")
)
)
init {
getAllTickets(listOfServices)
}
private fun getAllTickets() {
viewModelScope.launch {
val ticketsList = repository.getAllTickets().data
tickets.value.data = ticketsList
}
}
the firebaseSource
class FireBaseSource #Inject constructor(
private val firebaseAuth: FirebaseAuth,
private val firestore: FirebaseFirestore
) {
suspend fun getAllTickets(
listOfServices: List<String>
): DataOrException<List<Ticket>, Exception> {
val dataOrException = DataOrException<List<Ticket>, Exception>()
try {
dataOrException.data = firestore.collection(TICKETS)
.whereEqualTo(PROBLEM, ELECTRICIAN)
.get()
.await().map { document ->
document.toObject(Ticket::class.java)
}
} catch (e: FirebaseFirestoreException) {
dataOrException.e = e
}
return dataOrException
}
}
1. List item
I am trying to rewrite my project UI using Jetpack compose. Any idea to add popup menu using jetpack compose in android?
like this one
I tried to implement it using Stack() layout but the results are not up to the mark.
#Composable
fun LiveDataComponentList(productList: List<Product>) {
AdapterList(data = productList) { product ->
Stack() {
Clickable(onClick = { PopupState.toggleOwner(product) }) {
Card(...) {...}
if (PopupState.owner == product) {
Surface(color = Color.Gray,modifier = Modifier.gravity(Alignment.TopEnd) + Modifier.padding(12.dp)) {
Column() {
Text("menu 1")
Text("menu 2")
Text("menu 3")
Text("menu 4")
Text("menu 5")
}
}
}
}
}
}
and PopupState is
#Model
object PopupState
{
var owner:Product?=null
fun toggleOwner(item:Product)
{
if(owner==item)
owner=null
else
owner=item
}
}
result is
screenshot
You can use the DropdownMenu.
Something like:
var expanded by remember { mutableStateOf(false) }
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = { /* Handle refresh! */ }) {
Text("Refresh")
}
DropdownMenuItem(onClick = { /* Handle settings! */ }) {
Text("Settings")
}
Divider()
DropdownMenuItem(onClick = { /* Handle send feedback! */ }) {
Text("Send Feedback")
}
}
About the position, as explained in the documentation:
A DropdownMenu behaves similarly to a Popup, and will use the position of the parent layout to position itself on screen. Commonly a DropdownMenu will be placed in a Box with a sibling that will be used as the 'anchor'.
Example:
Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
){
//
}
}
Since DropDownPopup was removed, I implemented one using DropDownMenu instead like this:
PopupMenu:
#Composable
fun PopupMenu(
menuItems: List<String>,
onClickCallbacks: List<() -> Unit>,
showMenu: Boolean,
onDismiss: () -> Unit,
toggle: #Composable () -> Unit,
) {
DropdownMenu(
toggle = toggle,
expanded = showMenu,
onDismissRequest = { onDismiss() },
) {
menuItems.forEachIndexed { index, item ->
DropdownMenuItem(onClick = {
onDismiss()
onClickCallbacks[index]
}) {
Text(text = item)
}
}
}
}
Toggle (thing to long click on to trigger PopupMenu):
#Preview
#Composable
fun Toggle() {
var showMenu by remember { mutableStateOf(false) }
PopupMenu(
menuItems = listOf("Delete"),
onClickCallbacks = listOf { println("Deleted") },
showMenu = showMenu,
onDismiss = { showMenu = false }) {
Text(
modifier = Modifier.clickable(onClick = {}, onLongClick = {
showMenu = true
}),
text = "Long click here",
)
}
}
After some research I found a solution to this, the key component is DropdownPopup
#Composable
fun LiveDataComponentList(productList: List<Product>) {
AdapterList(data = productList) { product ->
Clickable(onClick = { PopupState.toggleOwner(product) }) {
Card(...) {...}
}
if (PopupState.owner == product) {
DropdownPopup(dropDownAlignment = DropDownAlignment.End)
{
Surface(
shape = RoundedCornerShape(4.dp),
elevation = 16.dp,
color = Color.White,
modifier = Modifier.gravity(Alignment.End)+ Modifier.padding(end = 10.dp)
)
{
Column(modifier = Modifier.padding(10.dp)) {
MenuItem(text ="Edit", onClick = {})
MenuItem(text = "Delete", onClick = {})
MenuItem(text = "Details", onClick = {})
}
}
}
}
}
}
#Composable
fun MenuItem(text: String, onClick: () -> Unit) {
Clickable(onClick = onClick, modifier = Modifier.padding(6.dp)) {
Text(text = text, style = MaterialTheme.typography.subtitle1)
}
}
This solution works fine with compose version dev10
For My use case I created a Icon button which has pop up menu and that can be used where pop menu is needed.
#Composable
fun PopUpMenuButton(
options: List<PopUpMenuItem>,
action: (String) -> Unit,
iconTint: Color = Color.Black,
modifier: Modifier
) {
var expanded by remember { mutableStateOf(false) }
Column {
Box(modifier = Modifier.size(24.dp)) {
IconButton(onClick = {
expanded = !expanded
}) {
Icon(
painter = painterResource(id = R.drawable.ic_dots),
contentDescription = null,
modifier = Modifier.wrapContentSize(),
tint = iconTint
)
}
}
Box(modifier = modifier) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.widthIn(min = 120.dp, max = 240.dp)
.background(MaterialTheme.colors.background)
) {
options.forEachIndexed { _, item ->
DropdownMenuItem(onClick = {
expanded = false
action(item.id)
}) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painterResource(id = item.icon),
contentDescription = null,
tint = iconTint,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = item.label,
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis
)
}
}
if (item.hasBottomDivider) {
Divider()
}
}
}
}
}
}
Then I created a simple data class for defining menu item
data class PopUpMenuItem(
val id: String,
val label: String,
val icon: Int,
val hasBottomDivider: Boolean = false,
)
Then at the calling side I simply use this button like this
PopUpMenuButton(
modifier = Modifier.wrapContentSize(),
options = PopMenuOptionsProvider.sectionCardMenu,
iconTint = MaterialTheme.extendedColor.regularGray,
action = { menuId -> onSectionMenuAction(menuId) }
)
It can be further refactored to make it more extensible, but this worked for me.