【问题标题】:Sorting List Items in LazyColumn - Android Jetpack Compose在 LazyColumn 中对列表项进行排序 - Android Jetpack Compose
【发布时间】:2022-11-05 07:39:32
【问题描述】:

尝试实现一项功能,其中用户可以选择一种选择下拉菜单中的某个项目(名称,城市,地址)时,可以选择一种方法来对啤酒厂列表进行排序。我很难弄清楚我做错了什么。每当我在下拉菜单中选择示例“名称”时,啤酒厂列表都不会排序。我是新手,所以任何建议都会有帮助。谢谢!

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

【问题讨论】:

  • 只是想对所有帮助我解决这个问题的人说声谢谢。我真的很感激!

标签: android android-studio kotlin android-jetpack-compose


【解决方案1】:

跟踪这么多代码真的很困难,尤其是在我的 13 英寸屏幕上。

您应该将排序逻辑移动到 ViewModel 或 useCase 并创建一个排序函数,您可以调用用户交互,例如 viewmodel.sort(sortType) 并使用 new 更新一个 MutableState 的值或使用 mutableStateListOf 并使用排序列表进行更新。

排序是业务逻辑,我会在不包含任何 Android 相关依赖项的类中进行排序,我使用 MVI 或 MVVM,所以我的偏好是 UseCase 类,我可以使用任何类型和示例列表和期望结果对排序进行单元测试.

【讨论】:

    【解决方案2】:

    你可以用ComparatorModifier.animateItemPlacement()来做到这一点,这样看起来会更好!

    我将发布一个来自 Google 的示例,以便您了解逻辑:

    @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
    )
    

    【讨论】:

      【解决方案3】:

      你有很多代码,我只看到lists,所以很难猜出发生了什么,但基于

      啤酒厂列表未排序

      我建议使用 1 data class 1 Viewmodel 1 ScreenLazyColumn 和 1 Button 创建一个非常简单的工作应用程序。

      考虑一下:

      你的data class

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

      你的ViewModel

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

      您的Composable 屏幕

      @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() }
             )
         }
      }
      

      它有效,但不建议在StatemutableState 中使用MutableList,因为您对其元素所做的任何更改都不会触发re-composition,最好的方法是manually copy 整个@ 987654338@ 并创建一个新的也是不可取的,这就是为什么SnapshotStateList&lt;T&gt;mutableStateListOf&lt;T&gt;not only recommended 但在Jetpack Compose 开发中有效处理lists 的唯一方法。

      使用SnapshotStateList,任何更新(根据我的经验),例如deleting 一个元素,modifying 通过执行.copy(..) 并重新插入到相同的索引将保证re-compositionComposable 上读取元素,在“工作示例代码”的情况下,如果我在SnapshotStateList 中更改一个人的姓名,如peopleList[index] = person.copy(name="Mr Person"),则读取该人对象的item composablere-compose。至于你Sorting的问题,我还没试过所以不确定,但我认为如果你只是在SnapshotStateList中执行排序操作,列表就会排序。

      尽管我对将SnapshotStateList 用于“工作示例代码”很有信心,但我对我所说的一切都持保留态度,但是在引擎盖下发生了大量细微差别和事情,我小心不要只是简单地扔掉,但正如你所说

      我是新来的

      我也是。我相信这是在 jetpack compose 中处理 collections/list 的一个很好的起点。

      【讨论】:

      • 谢谢你这个非常详细的建议!我会试试这个,我相信这会有所帮助。
      猜你喜欢
      • 2021-12-28
      • 2021-11-30
      • 2021-06-01
      • 1970-01-01
      • 2022-10-04
      • 2022-07-29
      • 2022-09-26
      • 2023-02-04
      • 1970-01-01
      相关资源
      最近更新 更多