對政府網(wǎng)站一體化服務建設(shè)的需求百度投放廣告一天多少錢
Bilibili移動端APP
- 簡介
- 依賴
- 效果
- 登錄
- 效果
- WebView
- 自定義TobRow的Indicator大小
- 首頁
- 推薦
- LazyGridView使用Paging3
- 熱門
- 排行榜
- 搜索
- 模糊搜索
- 富文本
- 搜索結(jié)果
- 視頻詳情
- 合集
- 信息
- Coroutines進行網(wǎng)絡(luò)請求管理,避免回調(diào)地獄
- 添加suspend
- withContext
- Git項目鏈接
- 末
簡介
此Demo采用Android Compose聲明式UI編寫而成,主體采用MVVM設(shè)計框架,Demo涉及到的主要技術(shù)包括:Flow、Coroutines、Retrofit、Okhttp、Hilt以及適配了深色模式等;主要數(shù)據(jù)來源于Bilibili API。
依賴
Demo中所使用的依賴如下表格所示
庫名稱 | 備注 |
---|---|
Flow | 流 |
Coroutines | 協(xié)程 |
Retrofit | 網(wǎng)絡(luò) |
Okhttp | 網(wǎng)絡(luò) |
Hilt | 依賴注入 |
room | 數(shù)據(jù)存儲 |
coil | 異步加載圖片 |
paging | 分頁加載 |
media3-exoplayer | 視頻 |
效果
登錄
登錄在Demo中分為WebView嵌入B站網(wǎng)頁實現(xiàn)獲取Cookie和自主實現(xiàn)登錄,由于后者需要通過極驗API驗證,所以暫且采用前者獲取Cookie,后者繪制了基本view和基本邏輯
效果


WebView
由于登錄暫未實現(xiàn),故而此處就介紹使用WebView獲取Cookie。由于在Compose中并未直接提供WebView組件,故使用AndroidView進行引入。以下代碼對WebView進行了一個簡單的封裝,我們只需要在onPageFinished
方法中回調(diào)所獲的cookie即可,然后保存到緩存文件即可
@Composable
fun CustomWebView(modifier: Modifier = Modifier,url:String,onBack: (webView: WebView?) -> Unit,onProgressChange: (progress:Int)->Unit = {},initSettings: (webSettings: WebSettings?) -> Unit = {},onReceivedError: (error: WebResourceError?) -> Unit = {},onCookie:(String)->Unit = {}
){val webViewChromeClient = object: WebChromeClient(){override fun onProgressChanged(view: WebView?, newProgress: Int) {//回調(diào)網(wǎng)頁內(nèi)容加載進度onProgressChange(newProgress)super.onProgressChanged(view, newProgress)}}val webViewClient = object: WebViewClient(){override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {super.onPageStarted(view, url, favicon)onProgressChange(-1)}override fun onPageFinished(view: WebView?, url: String?) {super.onPageFinished(view, url)onProgressChange(100)//監(jiān)聽獲取cookieval cookie = CookieManager.getInstance().getCookie(url)cookie?.let{ onCookie(cookie) }}override fun shouldOverrideUrlLoading(view: WebView?,request: WebResourceRequest?): Boolean {if(null == request?.url) return falseval showOverrideUrl = request.url.toString()try {if (!showOverrideUrl.startsWith("http://")&& !showOverrideUrl.startsWith("https://")) {Intent(Intent.ACTION_VIEW, Uri.parse(showOverrideUrl)).apply {addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)view?.context?.applicationContext?.startActivity(this)}return true}}catch (e:Exception){return true}return super.shouldOverrideUrlLoading(view, request)}override fun onReceivedError(view: WebView?,request: WebResourceRequest?,error: WebResourceError?) {super.onReceivedError(view, request, error)onReceivedError(error)}}var webView:WebView? = nullval coroutineScope = rememberCoroutineScope()AndroidView(modifier = modifier,factory = { ctx ->WebView(ctx).apply {this.webViewClient = webViewClientthis.webChromeClient = webViewChromeClientinitSettings(this.settings)webView = thisloadUrl(url)}})BackHandler {coroutineScope.launch {onBack(webView)}}
}
自定義TobRow的Indicator大小
由于在compose中TobRow的指示器寬度被寫死,如果需要更改指示器寬度,則需要自己進行重寫,將源碼拷貝一份,然后根據(jù)自己需求進行定制,具體代碼如下
@ExperimentalPagerApi
fun Modifier.customIndicatorOffset(pagerState: PagerState,tabPositions: List<TabPosition>,width: Dp
): Modifier = composed {if (pagerState.pageCount == 0) return@composed thisval targetIndicatorOffset: Dpval indicatorWidth: Dpval currentTab = tabPositions[minOf(tabPositions.lastIndex, pagerState.currentPage)]val targetPage = pagerState.targetPageval targetTab = tabPositions.getOrNull(targetPage)if (targetTab != null) {val targetDistance = (targetPage - pagerState.currentPage).absoluteValueval fraction = (pagerState.currentPageOffset / max(targetDistance, 1)).absoluteValuetargetIndicatorOffset = lerp(currentTab.left, targetTab.left, fraction)indicatorWidth = lerp(currentTab.width, targetTab.width, fraction).value.absoluteValue.dp} else {targetIndicatorOffset = currentTab.leftindicatorWidth = currentTab.width}fillMaxWidth().wrapContentSize(Alignment.BottomStart).padding(horizontal = (indicatorWidth - width) / 2).offset(x = targetIndicatorOffset).width(width)
}
使用就變得很簡單了,因為是采用modifier的擴展函數(shù)進行編寫,而modifier在每一個compose組件都擁有,所以只需要在tabrow的指示器調(diào)用即可,具體代碼如下
TabRow(...indicator = { pos ->TabRowDefaults.Indicator(color = BilibiliTheme.colors.tabSelect,modifier = Modifier.customIndicatorOffset(pagerState = pageState,tabPositions = pos,32.dp))}...)
首頁
整個首頁頁面由BottomNavbar
構(gòu)成,包含四個子界面,其中第一個界面又由兩個子界面組成,通過TabRow
+HorizontalPager
完成子頁面滑動,子頁面分為推薦
和熱門
兩個頁面
推薦
推薦頁面由上面的Banner和下方的LazyGridView組成,由于Compose中不允許同向滑動,所以就將Banner作為LazyGridView的一個item,進而進行包裹


LazyGridView使用Paging3
由于在現(xiàn)在Compose版本中LazyGridView并不支持Paging3,所以如果有此類需求,則需要自己動手,具體代碼如下
fun <T : Any> LazyGridScope.items(items: LazyPagingItems<T>,key: ((item: T) -> Any)? = null,span: ((item: T) -> GridItemSpan)? = null,contentType: ((item: T) -> Any)? = null,itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
) {items(count = items.itemCount,key = if (key == null) null else { index ->val item = items.peek(index)if (item == null) {//PagingPlaceholderKey(index)} else {key(item)}},span = if (span == null) null else { index ->val item = items.peek(index)if (item == null) {GridItemSpan(1)} else {span(item)}},contentType = if (contentType == null) {{ null }} else { index ->val item = items.peek(index)if (item == null) {null} else {contentType(item)}}) { index ->itemContent(items[index])}
}
熱門
熱門頁面代碼與推薦頁面代碼類似,此處不在闡述


排行榜
排行界面與上述類似,Tab
+HorizontalPager
完成所有子頁面滑動切換,此處也不在繼續(xù)闡述


搜索
搜索界面主要分為四個模塊:搜索欄、熱搜內(nèi)容、搜索記錄、搜索列表;搜索框內(nèi)字符改變,搜索列表顯示并以富文本顯示,熱搜內(nèi)容展開與折疊、搜索記錄內(nèi)容展開與折疊、清空記錄等操作都在ViewModel中完成,然后view通過監(jiān)聽VM中狀態(tài)值進行重組


模糊搜索
在搜索框內(nèi)鍵入字符,然后通過字符的改變,獲取相應的網(wǎng)絡(luò)請求數(shù)據(jù),最后通過AnimatedVisibility
顯示與隱藏搜索建議列表


富文本
通過逐字匹配輸入框內(nèi)的字符與搜索建議item內(nèi)容,然后輸入框的字符存在搜索建議列表中的文字就加入高亮顯示列表中,因為采用buildAnnotatedString
,可以讓文本顯示多種不同風格,所以最后將字符內(nèi)容區(qū)別為高亮顏色和普通文本兩種文本,并讓其進行顯示
@Composable
fun RichText(selectColor: Color,unselectColor: Color,fontSize:TextUnit = TextUnit.Unspecified,searchValue: String,matchValue: String
){val richText = buildAnnotatedString {repeat(matchValue.length){val index = if (it < searchValue.length) matchValue.indexOf(searchValue[it]) else -1if (index == -1){withStyle(style = SpanStyle(fontSize = fontSize,color = unselectColor,)){append(matchValue[it])}}else{withStyle(style = SpanStyle(fontSize = fontSize,color = selectColor,)){append(matchValue[index])}}}}Text(text = richText,maxLines = 1,overflow = TextOverflow.Ellipsis,modifier = Modifier.fillMaxWidth(),)
}
搜索結(jié)果
搜索結(jié)果也是由ScrollableTabRow
+HorizontalPager
完成子頁面的滑動切換,但是與上述不同的是,所展現(xiàn)的Tab與內(nèi)容并不是固定,而是根據(jù)后端返回的數(shù)據(jù)進行自動生成的。由于其他子頁面的內(nèi)容都是由LazyColumn
進行展現(xiàn),而綜合界面有需要將其他界面的數(shù)據(jù)進行集中,所以就必須LazyColumn
嵌套LazyColumn
,然后這在Compose中是不被允許的,所以就將子Page的LazyColumn
,使用modifier.heightIn(max = screenHeight.dp)
進行高度限制,高度可以取屏幕高度,并且多個item之間都是取屏幕高度,之間不會存在間隙


視頻詳情
視頻播放功能暫未實現(xiàn)完成,因為獲取的API返回的URL進行播放一直為403,被告知權(quán)限不足,在網(wǎng)上進行多番查詢未果,所以暫且擱置。視頻庫采用的Google的ExoPlayer


合集
每個視頻返回的內(nèi)容數(shù)據(jù)格式一致,但具體內(nèi)容不一致,有的視頻存在排行信息、合集等,就通過AnimatedVisibility
進行顯示和隱藏,將所有結(jié)果進行列出,然后在ViewModel通過解析數(shù)據(jù),并改變相應的狀態(tài)值,view即可進行重組


信息


Coroutines進行網(wǎng)絡(luò)請求管理,避免回調(diào)地獄
在日常開發(fā)中網(wǎng)絡(luò)請求必不可少,在傳統(tǒng)View+java開發(fā)中使用Retrifit或者okhttp進行網(wǎng)絡(luò)請求最為常見,但大多數(shù)場景中,后一個API需要前一個API數(shù)據(jù)內(nèi)字段值,此時就需要callback
進行操作,回調(diào)一次獲取代碼依舊看起來簡潔,可讀,但次數(shù)一旦增多,則會掉入回調(diào)地獄。Google后續(xù)推出的協(xié)程完美解決此類問題,協(xié)程的主要核心就是“通過非阻塞的代碼實現(xiàn)阻塞功能”
,具體代碼如下
添加suspend
以下為示例代碼,通過給接口添加suspend
標志符,告知外界次方法需要掛起
@GET("xxxxx")suspend fun getVideoDetail(@Query("aid")aid:Int):BaseResponse<VideoDetail>
withContext
getVideoDetail
掛起函數(shù)返回一個字段值,然后通過withContext包裹,使其進行阻塞,然后將返回值進行返回,后續(xù)的getVideoUrl
掛起函數(shù)就可以使用前一個接口返回的數(shù)據(jù);需要注意的是,函數(shù)都需為suspend
修飾的方法,并且在統(tǒng)一協(xié)程域中,否則會出現(xiàn)異常
viewModelScope.launch(Dispatchers.Main) {try {withContext(Dispatchers.Main){val cid = withContext(Dispatchers.IO){getVideoDetail(_videoState.value.aid)}val url = withContext(Dispatchers.IO){getVideoUrl(avid = _videoState.value.aid, cid = cid)}if (url.isNotEmpty()){play(url)}getRelatedVideos(_videoState.value.aid)}}catch (e:Exception){Log.d("VDetailViewModel",e.message.toString())}}
Git項目鏈接
Git項目鏈接
末
此Demo并未完全完善,尤其是播放界面,由于采用Bilibili API獲取的視頻URL,在播放時一直返回403錯誤,被告知沒有權(quán)限,在根據(jù)文檔進行使用以及網(wǎng)上查詢未果之后,只能暫且擱置此功能。