androidandroid-studiokotlinandroid-jetpack-compose

Sorting List Items in LazyColumn - Android Jetpack Compose


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

Solution

  • You can do it with Comparatorand Modifier.animateItemPlacement() so it would look even nicer!

    I will post an example from Google so you can understand the logic:

    @Preview
    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun PopularBooksDemo() {
        MaterialTheme {
            var comparator by remember { mutableStateOf(TitleComparator) }
            Column {
                Row(
                    modifier = Modifier.height(IntrinsicSize.Max),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Text(
                        "Title",
                        Modifier.clickable { comparator = TitleComparator }
                            .weight(5f)
                            .fillMaxHeight()
                            .padding(4.dp)
                            .wrapContentHeight(Alignment.CenterVertically),
                        textAlign = TextAlign.Center
                    )
                    Text(
                        "Author",
                        Modifier.clickable { comparator = AuthorComparator }
                            .weight(2f)
                            .fillMaxHeight()
                            .padding(4.dp)
                            .wrapContentHeight(Alignment.CenterVertically),
                        textAlign = TextAlign.Center
                    )
                    Text(
                        "Year",
                        Modifier.clickable { comparator = YearComparator }
                            .width(50.dp)
                            .fillMaxHeight()
                            .padding(4.dp)
                            .wrapContentHeight(Alignment.CenterVertically),
                        textAlign = TextAlign.Center
                    )
                    Text(
                        "Sales (M)",
                        Modifier.clickable { comparator = SalesComparator }
                            .width(65.dp)
                            .fillMaxHeight()
                            .padding(4.dp)
                            .wrapContentHeight(Alignment.CenterVertically),
                        textAlign = TextAlign.Center
                    )
                }
                Divider(color = Color.LightGray, thickness = Dp.Hairline)
                LazyColumn(
                    Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    val sortedList = PopularBooksList.sortedWith(comparator)
                    items(sortedList, key = { it.title }) {
                        Row(
                            Modifier.animateItemPlacement()
                                .height(IntrinsicSize.Max),
                            horizontalArrangement = Arrangement.spacedBy(8.dp)
                        ) {
                            Text(
                                it.title,
                                Modifier.weight(5f)
                                    .fillMaxHeight()
                                    .padding(4.dp)
                                    .wrapContentHeight(Alignment.CenterVertically),
                                textAlign = TextAlign.Center
                            )
                            Text(
                                it.author,
                                Modifier.weight(2f)
                                    .fillMaxHeight()
                                    .padding(4.dp)
                                    .wrapContentHeight(Alignment.CenterVertically),
                                textAlign = TextAlign.Center
                            )
                            Text(
                                "${it.published}",
                                Modifier.width(55.dp)
                                    .fillMaxHeight()
                                    .padding(4.dp)
                                    .wrapContentHeight(Alignment.CenterVertically),
                                textAlign = TextAlign.Center
                            )
                            Text(
                                "${it.salesInMillions}",
                                Modifier.width(65.dp)
                                    .fillMaxHeight()
                                    .padding(4.dp)
                                    .wrapContentHeight(Alignment.CenterVertically),
                                textAlign = TextAlign.Center
                            )
                        }
                    }
                }
            }
        }
    }
    
    private val TitleComparator = Comparator<Book> { left, right ->
        left.title.compareTo(right.title)
    }
    
    private val AuthorComparator = Comparator<Book> { left, right ->
        left.author.compareTo(right.author)
    }
    
    private val YearComparator = Comparator<Book> { left, right ->
        right.published.compareTo(left.published)
    }
    
    private val SalesComparator = Comparator<Book> { left, right ->
        right.salesInMillions.compareTo(left.salesInMillions)
    }
    
    private val PopularBooksList = listOf(
        Book("The Hobbit", "J. R. R. Tolkien", 1937, 140),
        Book("Harry Potter and the Philosopher's Stone", "J. K. Rowling", 1997, 120),
        Book("Dream of the Red Chamber", "Cao Xueqin", 1800, 100),
        Book("And Then There Were None", "Agatha Christie", 1939, 100),
        Book("The Little Prince", "Antoine de Saint-Exupéry", 1943, 100),
        Book("The Lion, the Witch and the Wardrobe", "C. S. Lewis", 1950, 85),
        Book("The Adventures of Pinocchio", "Carlo Collodi", 1881, 80),
        Book("The Da Vinci Code", "Dan Brown", 2003, 80),
        Book("Harry Potter and the Chamber of Secrets", "J. K. Rowling", 1998, 77),
        Book("The Alchemist", "Paulo Coelho", 1988, 65),
        Book("Harry Potter and the Prisoner of Azkaban", "J. K. Rowling", 1999, 65),
        Book("Harry Potter and the Goblet of Fire", "J. K. Rowling", 2000, 65),
        Book("Harry Potter and the Order of the Phoenix", "J. K. Rowling", 2003, 65)
    )
    
    private class Book(
        val title: String,
        val author: String,
        val published: Int,
        val salesInMillions: Int
    )