Jetpack Compose出來有一段時間了,一直都沒有去嘗試,這次有點想法去玩一玩這個聲明性界面工具,就以“原神”為主題寫個列表吧。
整體設計參考DisneyCompose
效果圖:
數據源
因為數據比較簡單,也就只包含圖片、姓名、描述等。所以在后臺數據存儲上選擇的是Bmob后端云,一個方便前端開發(fā)的后端服務平臺。
主要數據也是從原神各大網站搜集下來的,新建表結構并且將數據填充,我們簡單看一下Bmob的后臺。
數據準備好了,那就開始我們的Compose之旅。
首頁UI繪制
整體結構
從上面的項目效果圖來看,首頁總布局屬于是一個網格列表,平分兩格,列表中的每個Item上方帶有頭像,頭像下面是角色名稱以及角色其他信息。
網格布局
因為整體分成兩列,所以選擇的是網格布局,Compose提供了一個實現-LazyVerticalGrid。
fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
)
LazyVerticalGrid中有幾個重要參數先說明一下:
- GridCells :主要控制如何將單元格構建為列,如GridCells.Fixed(2),表示兩列平分。
- Modifier : 主要用來對列表進行額外的修飾。
- PaddingValues :主要設置圍繞整個內容的padding。
- LazyListState :用來控制或觀察列表狀態(tài)的狀態(tài)對象
首頁布局是平分兩列的網格布局,那相應的代碼如下:
LazyVerticalGrid(cells = GridCells.Fixed(2)) {}
單個Item
看過了外部框架,那現在來看每個Item的布局。每個Item為卡片式,外邊框為圓角,且?guī)в嘘幱?。內部上方是一張圖片Image,圖片下方是兩行文字Text。那Item具體該怎樣布局?
我們先來看看在Compose之前,在xml中是怎么寫?例如使用ConstraintLayout布局,頂部放一個ImageView,再來一個TextView layout_constraintTop_toBottomOf ImageView,最后在來個TextView也TopToBottomOf第一個TextView。
那使用Compose應該怎么寫?
其實在Compose里也存在著ConstraintLayout布局并且具體Api的調用思路與在xml中使用也是一致的。我們就來看看具體操作。
ConstraintLayout() {
Image()
Text()
Text()
}
一共兩個元素:Image,Text,分別代表著xml里的ImageView和TextView。
- Image:
Image(
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
})
Image加載的是網絡圖片,則使用painter加載圖片鏈接,contentScale與xml中的scaleType相似,modifier主要設置圖片的樣式,點擊事件、寬高等。里面有一個需要注意的點constrainAs(image)
constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
}
這段代碼主要表示Image在父布局中的位置,例如相對父布局,相對其他子控件等,有點xml中layout_constraintTop_toBottomOf內味。下面Text也是相同的道理。
- Text
Text(text = item.name,
color = Color.Black,
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)
top.linkTo(image.bottom)
}
)
Text的設置主要包含Text內容、文字類型、大小、顏色等。在constrainAs(title)里有一句top.linkTo(image.bottom),這句代碼指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView。
在Image和Text中發(fā)現了一個點,constrainAs(?)中傳入了一個值,且設置相對位置時也是以此值為控件的代表。這是在進行相對位置的設定之前,利用createRefs創(chuàng)建多個引用,在ConstraintLayout中作為Modifier.constrainAs的一部分分配給布局。
val (image, title, content) = createRefs()
具體代碼:
ConstraintLayout() {
val (image, title, content) = createRefs()
//頭像
Image(
//圖片地址
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
//圖片縮放規(guī)則
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {//點擊事件
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent) //水平居中
top.linkTo(parent.top)//位于父布局的頂部
})
//文字
Text(text = item.name,
color = Color.Black,//顏色
style = MaterialTheme.typography.h6,//字體格式
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)//水平居中
top.linkTo(image.bottom)//位于圖片的下方
}
)
Text(text = item.from,
color = Color.Black,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(4.dp)
.constrainAs(content) {
centerHorizontallyTo(parent)
top.linkTo(title.bottom)
})
}
數據填充
UI已經畫好了,接下來就是數據展示的事情。還是以ViewModel-LiveData-Repository為整體請求方式。
因為數據都存儲到了Bmob后臺,就直接使用Bmob的方式查詢數據:
private val bmobQuery: BmobQuery<GcDataItem> = BmobQuery()
fun queryRoleData(successLiveData: MutableLiveData<List<GcDataItem>>) {
bmobQuery.findObjects(object : FindListener<GcDataItem>() {
override fun done(list: MutableList<GcDataItem>?, e: BmobException?) {
if (e == null) {
successLiveData.value = list
}
}
})
}
具體的請求方式可參考Bmob的完檔,這里就不在贅述。
ViewModel中還是拋出一個LiveData,而UI層相對之前有一些變化。
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomePoster(navController: NavController, model: HomeViewModel = viewModel()) {
model.queryGcData()
val data: List<GcDataItem> by model.getDataLiveData().observeAsState(listOf())
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}
}
Compose提供了一個viewModel()方法來獲取ViewModel實例,至于怎么拿到數據,Compose提供了LiveData的一個擴展方法 observeAsState(listOf()) 。它的主要作用是用來觀察這個LiveData,并通過State表示它的值,每次有新值提交到LiveData時,返回的狀態(tài)將被更新,從而導致每個狀態(tài)的重新組合。
拿到List數據后,網格LazyVerticalGrid就開始使用items(data){}添加列表,
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}
而ItemPoster就是我們設置Item布局的地方,將每個Item的數據傳遞給ItemPoster,利用Image、Text等控件設置imageUrl、text內容等。
@Composable
fun ItemPoster(navController: NavController, item: GcDataItem) {
Surface(
modifier = Modifier
.padding(4.dp),
color = Color.White,
elevation = 8.dp,
shape = RoundedCornerShape(8.dp)
) {
ConstraintLayout() {
val (image, title, content) = createRefs()
Image(
//設置圖片Url-item.url
painter = rememberCoilPainter(request = item.url),
...)
Text(text = item.name
...)
Text(text = item.from
...)
}
}
跳轉
樣例中還有一個從列表跳轉到詳情頁的功能,Compose提供了一個跳轉組件-navigation。這個navigation與之前管理Fragment的navigation思路也是一致的,利用NavHostController進行不同頁面的管理。我們先使用 rememberNavController()方法創(chuàng)建一個NavHostController實例。
val navController = rememberNavController()
接著將navController與NavHost相關聯,且設置導航圖的起始目的地startDestination。
NavHost(navController = navController, startDestination = "Home") {}
我們將起始目的地暫時先標記為“Home”。
那如何對頁面進行管理?這就需要在NavHost中使用composable添加頁面,例如該項目有兩個頁面,一個首頁列表頁,一個詳情頁。我們就可以這樣寫:
NavHost(
navController = navController, startDestination = "Home"
) {
composable(
route = "Home",
){
HomePoster(navController)
}
composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
}
第一個composable則代表的是列表頁,并且將到達目的地的路線route設置為“Home”,其實類似于ARouter框架中在每個Activity上設置Path,做一個標識作用,后面做跳轉時也是依據該route進行跳轉。
第二個composable則代表的是詳情頁,同樣設置route="detail"。
那如何從列表頁跳到詳情頁?只需要在點擊事件里使用navController.navigate("detail"),傳入想要跳轉的route即可。
攜帶參數跳轉
因為詳情頁需要根據所點擊列表Item的Id進行數據查詢,點擊時要將id傳到詳情頁,這就需要攜帶參數。
在Compose中,向route添加參數占位符,如"detail/{objectId}",從composable()函數提取 NavArguments。
如下修改詳情頁:
composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
跳轉時將objectId傳到route的占位符中即可。
clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")})
當然,compose navigation還支持launchMode設置、深層鏈接等,具體可查看官方文檔。
一點感受
對于用習慣了xml編寫UI的我來說,首次上手Compose其實還是蠻不習慣,Compose打破了原有的格局,給了我們一個全新的視角去看待Android,學完后有種“哦,原來UI還可以這么干?。 钡母袊@。對于Android開發(fā)者來說,其實需要這些新的路線去突破自己的固有化思維。
Compose的風格其實和Flutter有點像,估計是出于同一個爸爸的原因。但是Compose沒有Flutter的無限套娃,對Android開發(fā)者來說還是比較友好的。如果想要學習Flutter,可以用Compose作為過渡。
以上便是本篇內容,感謝閱讀,如果對你有幫助,歡迎點贊收藏關注三連走一波??
項目地址:genshin-compose
歡迎關注公 z 號:9點大前端,每天9點推薦更多前端、Android、Flutter文章