本文示例代码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类里面:
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布局的代码:
- 首先,在同样在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))
}
}
- 使用,(展示的是一种比较简单的应用,实际环境比如在数据展示等页面使用,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
)
}
}
}
}
}