compose UI(五)Lazy布局替换ListView,RecyclerView实现数据展示

本文示例代码API基于compose UI 1.0.0-bate08

简单应用

比如,我们现在有一个数据库映射类文件如下(room库):

@Entity(
    tableName = "user",
    indices = [Index("id", unique = true)]
)
data class User(
    @ColumnInfo(name = "account")
    val account: String,
    @ColumnInfo(name = "password")
    val password: String,
    @ColumnInfo(name = "operator")
    val name: String,
    @ColumnInfo(name = "create_date")
    val createDate: Long,
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Long = 0
    }

只做示例,不考虑password应该加密存入,用原来listview,RecyclerView,太过复杂,这里只讨论compose ui方式的实现。
room的示例代码,贴一波,不讲解

@Dao
interface UserDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(user: User)

    @Query("SELECT * FROM user ORDER BY id")
    fun getAll(): Flow<List<User>>

}

class DbRepository @Inject constructor(
    private val userDao: UserDao
)  {
	@WorkerThread
    fun insertUser(user: User) = userDao.insert(user)

    @WorkerThread
    fun selectAllUser() = userDao.getAll().flowOn(Dispatchers.IO)
    }

@HiltViewModel
class MainViewModel @Inject constructor(
    private val dbRepository: DbRepository,
) : BaseViewModel() {
	private val _userList: MutableStateFlow<List<User>> = MutableStateFlow(listOf())
    val userList: StateFlow<List<User>> = _userList
	init {
        viewModelScope.launch(Dispatchers.IO) {
            dbRepository.selectAllUser().collect {
                _userList.value = it
            }
        }
    }
}

用了hilt,di代码不贴了,跑题!不用hilt,大家自己new一下。
如果我们想要横向展示我们的部分数据库信息(不展示密码,日期需要格式化),我们可以用LazyColumn实现。
首先我们需要一个展示用@Composable函数,这个业务逻辑是属于User类的,我们让它提供自己@Composable业务逻辑(有点类似重写toString的思想)
User.kt

	@Composable
    fun RowText(
        modifier: Modifier = Modifier,
        horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
        verticalAlignment: Alignment.Vertical = Alignment.CenterVertically
    ) {
        Row(
            modifier = modifier,
            horizontalArrangement = horizontalArrangement,
            verticalAlignment = verticalAlignment
        ) {
            Text(text = id.toString(),modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
            Text(text = account,modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
            Text(text = name,modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
            //生产环境,日期格式化对象应该缓存,这里方便大家理解
            Text(text = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).format(createDate),modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
        }
    }

然后我们在要展示数据的地方用Lazy布局,比如:

@Composable
fun UserTab(mainViewModel: MainViewModel) {
    val userList: List<User> by mainViewModel.userList.collectAsState()

    LazyColumn(
        contentPadding = PaddingValues(8.dp)
    ) {
        item {
            Row(
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(text = "序号",modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
                Text(text = "账户",modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
                Text(text = "姓名",modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
                Text(text = "创建时间",modifier = Modifier.weight(1f),textAlign = TextAlign.Center)
            }
        }
        items(items = userList) { item ->
            item.RowText()
        }
    }
}

如果需要更多的灵活性,就利用传入@Composable函数 element: @Composable RowScope.(String) -> Unit,然后改为

			item.RowText(){
                Text(text = it,textAlign = TextAlign.Center,modifier = Modifier.weight(1f))
            }

Text可以自定义传入其他各种@Composable函数
这种写法比RecyclerView节省几百行代码有没有?

双向滑动

老的Android ui实现双向滑动,需要解决事件冲突,但是在compose中变得异常简单,手势滑动约束horizontalScroll,verticalScroll,配合Lazy布局,或者另一个滑动约束,就可以实现效果(示例代码只是功能,不考虑美观)。

    val listTile0 = listOf<String>("标  题","标  题","标  题","标  题","标  题","标  题","标  题","标  题","标  题","标  题","标  题",)
    val listTile1 = listOf<String>("2A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile2 = listOf<String>("2A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile3 = listOf<String>("3A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile4 = listOf<String>("4A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile5 = listOf<String>("5A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile6 = listOf<String>("6A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile7 = listOf<String>("7A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile8 = listOf<String>("8A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile9 = listOf<String>("9A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile10 = listOf<String>("10A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile11 = listOf<String>("11A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile12 = listOf<String>("12A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile13 = listOf<String>("13A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile14 = listOf<String>("14A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile15 = listOf<String>("15A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val listTile16 = listOf<String>("16A B C","A B C D","A B E","A B C F","A B C G","A B C G","A B C J","A B C L","A B C","A B C","A B C",)
    val showItems = listOf(listTile1,listTile2,listTile3,listTile4,listTile5,listTile6,listTile7,listTile8,listTile9,listTile10,
    	listTile11,listTile12,listTile13,listTile14,listTile15,listTile16,)
    Box(modifier = Modifier.horizontalScroll(rememberScrollState())){
        Row {
            repeat(listTile0.size){ index->
                Text(text = listTile0[index],modifier = Modifier.padding(20.dp),textAlign = TextAlign.Center)
            }
        }
        LazyColumn(modifier = Modifier.padding(20.dp)) {
            items(items = showItems){ item->
                Row {
                    repeat(item.size){ index->
                        Text(text = item[index],modifier = Modifier.padding(20.dp),textAlign = TextAlign.Center)
                    }
                }
            }
        }
    }

当然使用LazyRow,LazyColumn嵌套也可以实现双向滑动,不过里外层会只发生自身滑动,大家改一下demo运行一下体会。

真实应用

以上2各demo都有各自的问题,第一个简单应用demo能保证没一列都在一条直线,但是如果展示内容过多,weight分配有限,单列的内容放不下,只能用上Text的overflow甚至maxLines等其他自适应属性。第二个双向滑动demo,无法保证一列的内容在一条直线,你可以修改一个String变得更长,它会写到旁边去。你可以指定每一个width,如果统一指定除非每条数据宽度类似,如果自定义,根本无法使用repeat,只能硬编。给第一个demo加Modifier.horizontalScroll(rememberScrollState()),加上这个你将无法使用weight,只能指定width,然后限制Text , maxLines = 1,overflow = TextOverflow.Ellipsis 等(举例)条件实现。

公司设计扔过来的图是这样的:
数据展示页面
大家看不见的右边,还有一堆数据要展示。
简单讲一下我的思路,大致的用了这么些类,因为@Composable函数其实是onCreate种跑的方法,所以我放在MainActivity类里面:
数据展示UML图
Form运行时注解提供一些数据配置信息:

/**
 * Form展示用运行时注解
 * @param title 展示时标题
 * @param serial 序号 0表示无所谓 serial 1==数组0
 * @param format %s 数据占位符,
 */
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class Form(
    val title: String="",
    val serial: Int = 0,
    /**
     *  %s 数据占位符,直接取field的value覆盖举例 ABC%sDEF==ABC${value}DEF
     *  %b 表示布尔类型占位符,替换举例 %b,true:真,false:假
     *  %f 小数位格式化,举例 %f,2 to 0.01
     *  %date 时间可格式化占位符,替换举例 %date,yyyyMMdd
     */
    val format: String = "%s"
)

interface IPagesForm 主要是暴露方法给PagesForm,PagesForm入参是

@Composable
fun <T> PagesForm(
    pagesViewModel: IPagesForm<T>,
    FormHead: @Composable (() -> Unit)? = null,
    OperationWidget: @Composable (() -> Unit)? = null,
    Item: @Composable ((item: List<String>, it: Int) -> Unit)? = null,
)

interface IPagesFormPrivate<T,R> 其实就是为了保证用我这个模块人不要暴露这些属性。

PagesFormUtils提供给默认实现给viewModel,其中最关键是mapToShowItem方法,mapToShowItem就是通过反射Form注解,完成ShowItem的生产,把一个横向的展示的数据,按照Form中serial 的排序,同时添加上标题和格式化转换为竖向展示的数据。入参和出参展示:

	@WorkerThread
    @Throws(Exception::class)
    inline fun <reified T> mapToShowItem(
        items: List<T>?,
        cacheId: ArrayList<Long>
    ): List<List<String>>? {
    }

重点是UI,不能歪,在展示时代码有一个坑,如果你用LazyRow去右滑展示时,你切页时,不会刷新页面,当你左右滑动时,左右2边会被刷新,而中间的不会。转换成竖向的ShowItem会获得一个好处,就是你可以在Column的约束中使用**Modifier.width(IntrinsicSize.Max)**自适应每一个列的最大宽度。
PagesForm 部分代码

	Scaffold(
        topBar = FormHead ?: {},
        bottomBar = {
            FormBottom(
                page,
                pages,
                OperationWidget = OperationWidget,
                changePage = pagesViewModel::changePages
            ) { check ->
                for (i in 0 until pagesViewModel.pageNumber){
                    pagesViewModel.changeChecked(i,check)
                }
            }
        }
    ) {
        Card(modifier = Modifier.padding(bottom = 67.dp)) {
            FormBody(
                items,
                checkedList,
                Item = Item,
                onCheckedChange = { pagesViewModel.changeChecked(it,null) },
                size = pagesViewModel.pageNumber + 1
            )
        }
    }

FormBody

@Composable
internal fun FormBody(
    showItems: List<List<String>>,
    checkedList: List<Boolean>,
    onCheckedChange: (Int) -> Unit,
    size: Int,
    Item: @Composable ((item: List<String>, it: Int) -> Unit)? = null
) {
    if (showItems.isEmpty()) {
        Text(text = "没有数据")
        return
    } else {
        //不能用LazyRow
        Row(
            modifier = Modifier
                .padding(8.dp)
                .horizontalScroll(rememberScrollState())
        ) {
            CheckList(showItems, checkedList, size = size, onCheckedChange)
            repeat(showItems.size) {
                ShowItems(item = showItems[it], size = size, Item)
            }
        }
    }
}

ShowItems

@Composable
internal fun ShowItems(
    item: List<String>,
    size: Int,
    Item: @Composable ((item: List<String>, it: Int) -> Unit)? = null
) {
    Column(
        Modifier.width(IntrinsicSize.Max),
        verticalArrangement = Arrangement.Center,
    ) {
        Timber.d("item title ${item[0]}")
        repeat(item.size) { index ->
            Item?.let { it(item, index) } ?: DefaultItem(
                item, index, Modifier.weight(1f)
            )
        }
        repeat(size - item.size) {
            Box(modifier = Modifier.weight(1f)) {}
        }
    }
}

这个模块我写完了,直接在别的项目基本上可以直接用,当然优化方向,是把反射改apt,忙。。。


8月3日更新

有人问单item自定义操作,我在另一个项目有把上面的简单应用扩展一下,设计给的效果是:
校验员管理
当然这不是最终效果图,是初稿,不过最终也差不多,具体的我贴一下关于lazy布局的代码:

  1. 首先,在同样在user的里面定义自身的compose方法。但是,具体样式实现交给父类管理,并且整加一个checked属性(不需要被room持久化到数据库)
	@Ignore
    var checked: MutableLiveData<Boolean> = MutableLiveData(false)

    @Composable
    fun RowTextItem(
            modifier: Modifier = Modifier,
            //是否需要展示复选框
            hasCheckbox: Boolean = false,
            allCheckState: Boolean = false,
            //改变父组件全选状态方法
            changeAllCheckState: (() -> Unit)? = null,
            horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
            verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
            //具体样式由父组件传入
            element: @Composable RowScope.(String, Modifier) -> Unit
    ) {
        val checkedState by checked.observeAsState()

        Row(
                modifier = modifier.clickable {
                    checked.value = !checked.value!!
                    Timber.i("checked = $checked")
                    if (!checked.value!! && allCheckState && changeAllCheckState != null) {
                        changeAllCheckState()
                    }
                },
                horizontalArrangement = horizontalArrangement,
                verticalAlignment = verticalAlignment
        ) {
            if (hasCheckbox) {
                Checkbox(
                        checked = checkedState ?: false,
                        onCheckedChange = {
                            checked.value = !checked.value!!
                            if (!checked.value!! && allCheckState && changeAllCheckState != null) {
                                changeAllCheckState()
                            }
                        },
                        modifier = Modifier.weight(0.8f)
                )
            }
            element(id.toString(), Modifier.weight(1f))
            element(account, Modifier.weight(1f))
            element(operator, Modifier.weight(1f))
            element(createDate, Modifier.weight(1.5f))
        }
    }
  1. 使用,(展示的是一种比较简单的应用,实际环境比如在数据展示等页面使用,lazy布局会更复杂):
Column {
        FormTitle(
                checked = checked,
                onCheckedChange = {
                    checked = it
                    viewModel.changeUserChecked(it)
                },
                titleList = listOf(
                        Pair("用户编号", Modifier.weight(1f)),
                        Pair("用户账号", Modifier.weight(1f)),
                        Pair("操作员", Modifier.weight(1f)),
                        Pair("创建时间", Modifier.weight(1.5f)),
                )) { pair ->
            Text(
                    text = pair.first, textAlign = TextAlign.Center,
                    modifier = pair.second,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    style = MaterialTheme.typography.h5
            )
        }
        if (userList.isEmpty()) {
            Text(
                    text = "没有用户信息",
                    modifier = Modifier
                            .fillMaxSize()
                            .padding(top = 50.dp),
                    textAlign = TextAlign.Center,
                    style = MaterialTheme.typography.h4
            )
            //room数据库查询返回flow转化为StateFlow,不推荐livedata,livedata适合简单的数据。
            viewModel.loadUserList()
        } else {
            LazyColumn {
                items(count = userList.size) { i ->
                	//通过i你也可以不展示当前RowTextItem,插入别的compose等等
                    userList[i].RowTextItem(
                            hasCheckbox = true,
                            allCheckState = checked,
                            changeAllCheckState = { checked = false },
                            modifier = Modifier
                                    .background(
                                    		//通过i完成不同的背景色。
                                            color = when (i % 4) {
                                                0 -> primaryLightColor.copy(0.9f)
                                                1 -> primaryColor.copy(0.8f)
                                                2 -> primaryDarkColor.copy(0.7f)
                                                3 -> secondaryColor.copy(0.6f)
                                                else -> primaryColor.copy(0.6f)
                                            }
                                    )
                                    .padding(end = 100.dp)
                    ) { string, modifier ->
                        Text(
                                text = string, textAlign = TextAlign.Center,
                                modifier = modifier.padding(top = 10.dp, bottom = 10.dp),
                                maxLines = 1,
                                overflow = TextOverflow.Ellipsis,
                                style = MaterialTheme.typography.body2,
                                fontSize = 20.sp
                        )
                    }
                }
            }
        }
    }
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值