Trying to implement a feature where the user is able to select a way to sort the list of Breweries when a certain item in a dropdown menu is selected (Name, City, Address). I am having trouble figuring out what I am doing wrong. Whenever I select example "Name" in the dropdown menu, the list of breweries does not sort. I am new at this so any advice will really help. Thank you!
Here is the full code for the MainScreen –
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun MainScreen(
navController: NavController,
mainViewModel: MainViewModel,
search: String?
) {
val apiData = brewData(mainViewModel = mainViewModel, search = search)
Scaffold(
content = {
if (apiData.loading == true){
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.grey_blue)),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
} else if (apiData.data != null){
MainContent(bData = apiData.data!!, viewModel = mainViewModel)
}
},
topBar = { BrewTopBar(navController, search) }
)
}
@Composable
fun BrewTopBar(navController: NavController, search: String?) {
TopAppBar(
modifier = Modifier
.height(55.dp)
.fillMaxWidth(),
title = {
Text(
stringResource(id = R.string.main_title),
style = MaterialTheme.typography.h5,
maxLines = 1
)
},
actions = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "$search")
IconButton(onClick = { navController.navigate(Screens.SearchScreen.name) }) {
Icon(
modifier = Modifier.padding(10.dp),
imageVector = Icons.Filled.Search,
contentDescription = stringResource(id = R.string.search)
)
}
}
},
backgroundColor = colorResource(id = R.color.light_purple)
)
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun MainContent(bData: List<BrewData>, viewModel: MainViewModel){
val allBreweries = bData.size
var sortByName by remember { mutableStateOf(false) }
var sortByCity by remember { mutableStateOf(false) }
var sortByAddress by remember { mutableStateOf(false) }
var dataSorted1 = remember { mutableStateOf(bData.sortedBy { it.name }) }
var dataSorted: MutableState<List<BrewData>>
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.grey_blue))
){
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
){
// Amount text label
Text(
text = "Result(s): ${bData.size}",
modifier = Modifier.padding(top = 15.dp, start = 15.dp, bottom = 5.dp)
)
SortingMenu(sortByName, sortByCity, sortByAddress) // needs mutable booleans for sorting
}
// List of Brewery cards
LazyColumn(
Modifier
.fillMaxSize()
.padding(5.dp)
){
items(allBreweries){ index ->
Breweries(
bData = when {
sortByName == true -> remember { mutableStateOf(bData.sortedBy { it.name }) }
sortByCity == true -> remember { mutableStateOf(bData.sortedBy { it.city }) }
sortByAddress == true -> remember { mutableStateOf(bData.sortedBy { it.address_2 }) }
else -> remember { mutableStateOf(bData) }
} as MutableState<List<BrewData>>, // Todo: create a way to select different sorting conditions
position = index,
viewModel = viewModel
)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun Breweries(
bData: MutableState<List<BrewData>>,
position: Int,
viewModel: MainViewModel
){
val cardNumber = position+1
val cityApiData = bData.value[position].city
val phoneNumberApiData = bData.value[position].phone
val countryApiData = bData.value[position].country
val breweryTypeApiData = bData.value[position].brewery_type
val countyApiData = bData.value[position].county_province
val postalCodeApiData = bData.value[position].postal_code
val stateApiData = bData.value[position].state
val streetApiData = bData.value[position].street
val apiLastUpdated = bData.value[position].updated_at
val context = LocalContext.current
val lastUpdated = apiLastUpdated?.let { viewModel.dateTextConverter(it) }
val websiteUrlApiData = bData.value[position].website_url
var expanded by remember { mutableStateOf(false) }
val clickableWebsiteText = buildAnnotatedString {
if (websiteUrlApiData != null) {
append(websiteUrlApiData)
}
}
val clickablePhoneNumberText = buildAnnotatedString {
if (phoneNumberApiData != null){
append(phoneNumberApiData)
}
}
Column(
Modifier.padding(10.dp)
) {
//Brewery Card
Card(
modifier = Modifier
.padding(start = 15.dp, end = 15.dp)
// .fillMaxSize()
.clickable(
enabled = true,
onClickLabel = "Expand to view details",
onClick = { expanded = !expanded }
)
.semantics { contentDescription = "Brewery Card" },
backgroundColor = colorResource(id = R.color.light_blue),
contentColor = Color.Black,
border = BorderStroke(0.5.dp, colorResource(id = R.color.pink)),
elevation = 15.dp
) {
Column(verticalArrangement = Arrangement.Center) {
//Number text for position of card
Text(
text = cardNumber.toString(),
modifier = Modifier.padding(15.dp),
fontSize = 10.sp,
)
// Second Row
BreweryTitle(bData = bData, position = position)
// Third Row
// Brewery Details
CardDetails(
cityApiData = cityApiData,
stateApiData = stateApiData,
streetApiData = streetApiData,
countryApiData = countryApiData,
countyApiData = countyApiData,
postalCodeApiData = postalCodeApiData,
breweryTypeApiData = breweryTypeApiData,
lastUpdated = lastUpdated,
expanded = expanded
)
//Fourth Row
Row(horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
){
Column(
modifier = Modifier.padding(
start = 10.dp, end = 10.dp,
top = 15.dp, bottom = 15.dp
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//Phone Number Link
LinkBuilder(
clickablePhoneNumberText,
phoneNumberApiData,
modifier = Modifier.padding(bottom = 10.dp)
) {
if (phoneNumberApiData != null) {
viewModel.callNumber(phoneNumberApiData, context)
}
}
//Website Link
LinkBuilder(
clickableWebsiteText,
websiteUrlApiData,
modifier = Modifier.padding(bottom = 15.dp),
intentCall = {
if (websiteUrlApiData != null) {
viewModel.openWebsite(websiteUrlApiData, context)
}
}
)
}
}
}
}
}
}
@Composable
fun CardDetails(
cityApiData: String?,
stateApiData: String?,
streetApiData: String?,
countryApiData: String?,
countyApiData: String?,
postalCodeApiData: String?,
breweryTypeApiData: String?,
lastUpdated: String?,
expanded: Boolean
){
// Third Row
//Brewery Details
Column(
modifier = Modifier.padding(
start = 30.dp, end = 10.dp, top = 25.dp, bottom = 15.dp
),
verticalArrangement = Arrangement.Center
) {
if (expanded) {
Text(text = "City: $cityApiData")
Text(text = "State: $stateApiData")
Text(text = "Street: $streetApiData")
Text(text = "Country: $countryApiData")
Text(text = "County: $countyApiData")
Text(text = "Postal Code: $postalCodeApiData")
Text(text = "Type: $breweryTypeApiData")
Text(text = "Last updated: $lastUpdated")
}
}
}
@Composable
fun BreweryTitle(bData: MutableState<List<BrewData>>, position: Int){
// Second Row
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
// Name of Brewery
Text(
text = bData.value[position].name!!,
modifier = Modifier.padding(start = 15.dp, end = 15.dp),
fontWeight = FontWeight.Bold,
maxLines = 3,
textAlign = TextAlign.Center,
softWrap = true,
style = TextStyle(
color = colorResource(id = R.color.purple_500),
fontStyle = FontStyle.Normal,
fontSize = 17.sp,
fontFamily = FontFamily.SansSerif,
letterSpacing = 2.sp,
)
)
}
}
}
@Composable
fun LinkBuilder(
clickableText: AnnotatedString,
dataText: String?,
modifier: Modifier,
intentCall: (String?) -> Unit
){
if (dataText != null){
ClickableText(
text = clickableText,
modifier = modifier,
style = TextStyle(
textDecoration = TextDecoration.Underline,
letterSpacing = 2.sp
),
onClick = {
intentCall(dataText)
}
)
}
else {
Text(
text = "Sorry, Not Available",
color = Color.Gray,
fontSize = 10.sp
)
}
}
//Gets data from view model
@Composable
fun brewData(
mainViewModel: MainViewModel, search: String?
): DataOrException<List<BrewData>, Boolean, Exception> {
return produceState<DataOrException<List<BrewData>, Boolean, Exception>>(
initialValue = DataOrException(loading = true)
) {
value = mainViewModel.getData(search)
}.value
}
@Composable
fun SortingMenu(sortByName: Boolean, sortByCity: Boolean, sortByAddress: Boolean,) {
var expanded by remember { mutableStateOf(false) }
val items = listOf("Name", "City", "Address")
val disabledValue = "B"
var selectedIndex by remember { mutableStateOf(0) }
Box(
modifier = Modifier
.wrapContentSize(Alignment.TopStart)
) {
Text(
text = "Sort by: ${items[selectedIndex]}",
modifier = Modifier
.clickable(onClick = { expanded = true })
.width(120.dp)
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false // todo: sort list data when clicked
when(selectedIndex){
0 -> sortByName == true
1 -> sortByCity == true
2 -> sortByAddress == true
} }
) {
items.forEachIndexed { index, text ->
DropdownMenuItem(onClick = {
selectedIndex = index
expanded = false
}) {
val disabledText = if (text == disabledValue) {
" (Disabled)"
} else {
""
}
Text(text = text + disabledText)
}
}
}
}
}
//not used
fun sorting(menuList: List<String>, dataList: List<BrewData>, index: Int){
when{
menuList[index] == "Name" -> dataList.sortedBy { it.name }
menuList[index] == "City" -> dataList.sortedBy { it.city }
menuList[index] == "Address" -> dataList.sortedBy { it.address_2 }
}
}
3
Answers
You have tons of codes and I only see
lists
, so its hard to guess what’s going on, but based onI would advice creating a very simple working app with 1
data class
1Viewmodel
1Screen
with aLazyColumn
and 1Button
.Consider this:
Your
data class
Your
ViewModel
Your
Composable
ScreenIt works but its not advisable to use
MutableList
inside aState
ormutableState
as any changes you make on its elements won’t triggerre-composition
, your best way to do it is tomanually copy
the entireList
and create a new one which is again not advisable, thats whySnapshotStateList<T>
ormutableStateListOf<T>
isnot only recommended
but the only way to deal withlists
inJetpack Compose
development efficiently.Using
SnapshotStateList
, any updates (in my experience) such asdeleting
an element,modifying
by doing.copy(..)
and re-inserting to the same index will guarantee are-composition
on aComposable
that reads that element, in the case of the "working sample code", if I change the name of a person in theSnapshotStateList
likepeopleList[index] = person.copy(name="Mr Person")
, theitem
composable
that reads that person object willre-compose
. As for yourSorting
problem, I haven’t tried it yet so I’m not sure, but I think the list will sort if you simply perform a sorting operation in theSnapshotStateList
.Take everything I said with the grain of salt, though I’m confident with the usage of the
SnapshotStateList
for the "working sample code", however there are tons of nuances and things happening under the hood that I’m careful not to just simply throw around, but as you saidSo am I. I’m confident this is a good starting point dealing with
collections
/list
in jetpack compose.It’s really difficult to track this much code especially on my 13 inch screen.
You should move your sort logic to ViewModel or useCase and create a sort function you can call on user interaction such as
viewmodel.sort(sortType)
and update value of oneMutableState
with new or use mutableStateListOf and update with sorted list.Sorting is business logic and i would do it in a class that doesn’t contain any Android related dependencies, i use MVI or MVVM so my preference is a UseCase class which i can unit test sorting with any type and a sample list and desired outcomes.
You can do it with
Comparator
andModifier.animateItemPlacement()
so it would look even nicer!I will post an example from Google so you can understand the logic: