Jetpack Compose出來(lái)有一段時(shí)間了,一直都沒(méi)有去嘗試啃沪,這次有點(diǎn)想法去玩一玩這個(gè)聲明性界面工具粘拾,就以“原神”為主題寫(xiě)個(gè)列表吧。
整體設(shè)計(jì)參考DisneyCompose
效果圖:
數(shù)據(jù)源
因?yàn)閿?shù)據(jù)比較簡(jiǎn)單创千,也就只包含圖片缰雇、姓名入偷、描述等。所以在后臺(tái)數(shù)據(jù)存儲(chǔ)上選擇的是Bmob后端云械哟,一個(gè)方便前端開(kāi)發(fā)的后端服務(wù)平臺(tái)疏之。
主要數(shù)據(jù)也是從原神各大網(wǎng)站搜集下來(lái)的,新建表結(jié)構(gòu)并且將數(shù)據(jù)填充暇咆,我們簡(jiǎn)單看一下Bmob的后臺(tái)锋爪。
數(shù)據(jù)準(zhǔn)備好了,那就開(kāi)始我們的Compose之旅爸业。
首頁(yè)UI繪制
整體結(jié)構(gòu)
從上面的項(xiàng)目效果圖來(lái)看几缭,首頁(yè)總布局屬于是一個(gè)網(wǎng)格列表,平分兩格沃呢,列表中的每個(gè)Item上方帶有頭像年栓,頭像下面是角色名稱(chēng)以及角色其他信息。
網(wǎng)格布局
因?yàn)檎w分成兩列薄霜,所以選擇的是網(wǎng)格布局某抓,Compose提供了一個(gè)實(shí)現(xiàn)-LazyVerticalGrid。
fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
)
LazyVerticalGrid中有幾個(gè)重要參數(shù)先說(shuō)明一下:
- GridCells :主要控制如何將單元格構(gòu)建為列惰瓜,如GridCells.Fixed(2)否副,表示兩列平分。
- Modifier : 主要用來(lái)對(duì)列表進(jìn)行額外的修飾崎坊。
- PaddingValues :主要設(shè)置圍繞整個(gè)內(nèi)容的padding备禀。
- LazyListState :用來(lái)控制或觀(guān)察列表狀態(tài)的狀態(tài)對(duì)象
首頁(yè)布局是平分兩列的網(wǎng)格布局,那相應(yīng)的代碼如下:
LazyVerticalGrid(cells = GridCells.Fixed(2)) {}
單個(gè)Item
看過(guò)了外部框架奈揍,那現(xiàn)在來(lái)看每個(gè)Item的布局曲尸。每個(gè)Item為卡片式,外邊框?yàn)閳A角男翰,且?guī)в嘘幱傲砘肌?nèi)部上方是一張圖片Image,圖片下方是兩行文字Text蛾绎。那Item具體該怎樣布局昆箕?
我們先來(lái)看看在Compose之前,在xml中是怎么寫(xiě)租冠?例如使用ConstraintLayout
布局鹏倘,頂部放一個(gè)ImageView
,再來(lái)一個(gè)TextView layout_constraintTop_toBottomOf ImageView
顽爹,最后在來(lái)個(gè)TextView
也TopToBottomOf
第一個(gè)TextView
纤泵。
那使用Compose應(yīng)該怎么寫(xiě)?
其實(shí)在Compose里也存在著ConstraintLayout
布局并且具體Api的調(diào)用思路與在xml中使用也是一致的话原。我們就來(lái)看看具體操作夕吻。
ConstraintLayout() {
Image()
Text()
Text()
}
一共兩個(gè)元素: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加載的是網(wǎng)絡(luò)圖片归园,則使用painter加載圖片鏈接,contentScale
與xml中的scaleType
相似稚矿,modifier主要設(shè)置圖片的樣式庸诱,點(diǎn)擊事件、寬高等晤揣。里面有一個(gè)需要注意的點(diǎn)constrainAs(image)
constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
}
這段代碼主要表示Image在父布局中的位置桥爽,例如相對(duì)父布局,相對(duì)其他子控件等昧识,有點(diǎn)xml中layout_constraintTop_toBottomOf
內(nèi)味钠四。下面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的設(shè)置主要包含Text內(nèi)容跪楞、文字類(lèi)型缀去、大小、顏色等甸祭。在constrainAs(title)
里有一句top.linkTo(image.bottom)
缕碎,這句代碼指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView
池户。
在Image和Text中發(fā)現(xiàn)了一個(gè)點(diǎn)咏雌,constrainAs(?)
中傳入了一個(gè)值校焦,且設(shè)置相對(duì)位置時(shí)也是以此值為控件的代表赊抖。這是在進(jìn)行相對(duì)位置的設(shè)定之前,利用createRefs
創(chuàng)建多個(gè)引用斟湃,在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 = {//點(diǎn)擊事件
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)
})
}
數(shù)據(jù)填充
UI已經(jīng)畫(huà)好了檐薯,接下來(lái)就是數(shù)據(jù)展示的事情凝赛。還是以ViewModel-LiveData-Repository為整體請(qǐng)求方式。
因?yàn)閿?shù)據(jù)都存儲(chǔ)到了Bmob后臺(tái)坛缕,就直接使用Bmob的方式查詢(xún)數(shù)據(jù):
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
}
}
})
}
具體的請(qǐng)求方式可參考Bmob的完檔墓猎,這里就不在贅述。
ViewModel中還是拋出一個(gè)LiveData赚楚,而UI層相對(duì)之前有一些變化毙沾。
@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提供了一個(gè)viewModel()
方法來(lái)獲取ViewModel實(shí)例,至于怎么拿到數(shù)據(jù)宠页,Compose提供了LiveData的一個(gè)擴(kuò)展方法 observeAsState(listOf())
左胞。它的主要作用是用來(lái)觀(guān)察這個(gè)LiveData寇仓,并通過(guò)State表示它的值,每次有新值提交到LiveData時(shí)烤宙,返回的狀態(tài)將被更新遍烦,從而導(dǎo)致每個(gè)狀態(tài)的重新組合。
拿到List數(shù)據(jù)后躺枕,網(wǎng)格LazyVerticalGrid就開(kāi)始使用items(data){}
添加列表服猪,
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}
而ItemPoster就是我們?cè)O(shè)置Item布局的地方,將每個(gè)Item的數(shù)據(jù)傳遞給ItemPoster拐云,利用Image罢猪、Text等控件設(shè)置imageUrl、text內(nèi)容等叉瘩。
@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(
//設(shè)置圖片Url-item.url
painter = rememberCoilPainter(request = item.url),
...)
Text(text = item.name
...)
Text(text = item.from
...)
}
}
跳轉(zhuǎn)
樣例中還有一個(gè)從列表跳轉(zhuǎn)到詳情頁(yè)的功能膳帕,Compose提供了一個(gè)跳轉(zhuǎn)組件-navigation。這個(gè)navigation與之前管理Fragment的navigation思路也是一致的薇缅,利用NavHostController
進(jìn)行不同頁(yè)面的管理备闲。我們先使用 rememberNavController()
方法創(chuàng)建一個(gè)NavHostController
實(shí)例。
val navController = rememberNavController()
接著將navController與NavHost相關(guān)聯(lián)捅暴,且設(shè)置導(dǎo)航圖的起始目的地startDestination
恬砂。
NavHost(navController = navController, startDestination = "Home") {}
我們將起始目的地暫時(shí)先標(biāo)記為“Home”。
那如何對(duì)頁(yè)面進(jìn)行管理蓬痒?這就需要在NavHost中使用composable添加頁(yè)面泻骤,例如該項(xiàng)目有兩個(gè)頁(yè)面,一個(gè)首頁(yè)列表頁(yè)梧奢,一個(gè)詳情頁(yè)狱掂。我們就可以這樣寫(xiě):
NavHost(
navController = navController, startDestination = "Home"
) {
composable(
route = "Home",
){
HomePoster(navController)
}
composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
}
第一個(gè)composable則代表的是列表頁(yè),并且將到達(dá)目的地的路線(xiàn)route
設(shè)置為“Home”亲轨,其實(shí)類(lèi)似于ARouter框架中在每個(gè)Activity上設(shè)置Path趋惨,做一個(gè)標(biāo)識(shí)作用,后面做跳轉(zhuǎn)時(shí)也是依據(jù)該route進(jìn)行跳轉(zhuǎn)惦蚊。
第二個(gè)composable則代表的是詳情頁(yè)器虾,同樣設(shè)置route="detail"
。
那如何從列表頁(yè)跳到詳情頁(yè)蹦锋?只需要在點(diǎn)擊事件里使用navController.navigate("detail")
兆沙,傳入想要跳轉(zhuǎn)的route即可。
攜帶參數(shù)跳轉(zhuǎn)
因?yàn)樵斍轫?yè)需要根據(jù)所點(diǎn)擊列表Item的Id進(jìn)行數(shù)據(jù)查詢(xún)莉掂,點(diǎn)擊時(shí)要將id傳到詳情頁(yè)葛圃,這就需要攜帶參數(shù)。
在Compose中,向route添加參數(shù)占位符库正,如"detail/{objectId}"
曲楚,從composable()
函數(shù)提取 NavArguments
。
如下修改詳情頁(yè):
composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
跳轉(zhuǎn)時(shí)將objectId傳到route的占位符中即可褥符。
clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")})
當(dāng)然洞渤,compose navigation還支持launchMode設(shè)置、深層鏈接等属瓣,具體可查看官方文檔载迄。
一點(diǎn)感受
對(duì)于用習(xí)慣了xml編寫(xiě)UI的我來(lái)說(shuō),首次上手Compose其實(shí)還是蠻不習(xí)慣抡蛙,Compose打破了原有的格局护昧,給了我們一個(gè)全新的視角去看待Android,學(xué)完后有種“哦粗截,原來(lái)UI還可以這么干M锇摇!”的感嘆熊昌。對(duì)于Android開(kāi)發(fā)者來(lái)說(shuō)绽榛,其實(shí)需要這些新的路線(xiàn)去突破自己的固有化思維。
Compose的風(fēng)格其實(shí)和Flutter有點(diǎn)像婿屹,估計(jì)是出于同一個(gè)爸爸的原因灭美。但是Compose沒(méi)有Flutter的無(wú)限套娃,對(duì)Android開(kāi)發(fā)者來(lái)說(shuō)還是比較友好的昂利。如果想要學(xué)習(xí)Flutter届腐,可以用Compose作為過(guò)渡。
以上便是本篇內(nèi)容蜂奸,感謝閱讀犁苏,如果對(duì)你有幫助,歡迎點(diǎn)贊收藏關(guān)注三連走一波??
項(xiàng)目地址:genshin-compose
歡迎關(guān)注公 z 號(hào):9點(diǎn)大前端扩所,每天9點(diǎn)推薦更多前端围详、Android、Flutter文章