Home > Net >  Sorting List Items in LazyColumn - Android Jetpack Compose
Sorting List Items in LazyColumn - Android Jetpack Compose

Time:10-01

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 }
    }
}

CodePudding user response:

You have tons of codes and I only see lists, so its hard to guess what's going on, but based on

the list of breweries does not sort

I would advice creating a very simple working app with 1 data class 1 Viewmodel 1 Screen with a LazyColumn and 1 Button.

So consider this quote-unquote "working sample code"~ish

Your data class

data class Person(
   val id: Int,
   val name: String
)

Your ViewModel

class MyViewModel {
    val peopleList = mutableStateListOf<Person>() // SnapshotStateList
    
    fun onSortButtonClicked() {
         // do your sorting logic here 
         // update your mutable`State`List
    }
}

Your Composable Screen

@Composable // I just put the view model as an argument, don't follow it  you don't need to
fun MyScreen(viewModel: MyViewModel) {
      val myList = viewModel.peopleList

       LazyColumn {
           items(items = myList, key = { person -> person.id }) { person ->
              //.. your item composable //
       }

       Button(
           onClick = { viewModel.onSortButtonClicked() }
       )
   }
}

It works but its not advisable to use MutableList inside a State or mutableState as any changes you make on its elements won't trigger re-composition, your best way to do it is to manually copy the entire List and create a new one which is again not advisable, thats why SnapshotStateList<T> or mutableStateListOf<T> is not only recommended but the only way to deal with lists in Jetpack Compose development efficiently.

Using SnapshotStateList, any updates (in my experience) such as deleting an element, modifying by doing .copy(..) and re-inserting to the same index will guarantee a re-composition on a Composable that reads that element, in the case of the "working sample code", if I change the name of a person in the SnapshotStateList like peopleList[index] = person.copy(name="Mr Person"), the item composable that reads that person object will re-compose. As for your Sorting 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 the SnapshotStateList.

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 said

I am new at this

So am I. I'm confident this is a good starting point dealing with collections/list in jetpack compose.

CodePudding user response:

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 one MutableState 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.

  • Related