用Compose实现轻量版网易云音乐
簡述
這是一個幾乎全部使用Compose實現UI各組成部分的純Kotlin Android App,應用取名Compose Many是因為最初想實現集各種小功能的工具軟件,當然主要還是想也借此來學習Compose的整個開發流程。不過目前只做好了音樂部分。
Compose目前正式版已經發布到了1.0.2(Alpha版本是1.1.0),目前來看官方更新速度不算太快,第一個正式版應該還是以穩定性為主。希望后續大版本更新時,能像Flutter2.0一樣,功能更全面的同時也帶來更豐富的生態。
效果
已經完成的功能主要就是歌單列表的播放和評論查看,由于接口眾多,而APP主要利用空閑時間開發的,時間有限只做了推薦歌單和個人歌單的獲取,常聽的歌曲可以先聽聽了~ 然后評論功能暫時只能查看,點贊、回復這些后面有時間再做。
??
已實現的功能
- 網易云手機賬號+密碼登錄
- 推薦歌單、個人歌單的顯示
- 歌單歌曲的播放
- 歌曲評論查看、樓中回復評論查看
主要實現
服務器端
使用Binaryify大佬整理的網易云API NeteaseCloudMusicApi,可以非常方便地通過RESTful API訪問各個數據接口,倉庫里也提供了開箱即用的部署方案,這里就選用其中的Vercal方案:
于是借助寶藏網站 Vercel,就免費擁有自己的域名,并且可以在上面部署自己的代碼。當然,Vercel也不是完全免費的,它對于一段時間內的訪問量有所限制,達到比較高的訪問量時會認為超出了個人使用用途,域名入口可能會被關停。因此作為學習目的,最好就是自己注冊一個Vercel賬號,然后App調用自己專屬的API地址
客戶端架構
- 界面表現層
應用的界面不多,界面表現層使用MVVM,音樂功能為單Activity+多Fragment,Fragment內容使用Compose構建。
其中PlaySongsViewModel生命周期跟隨Activity:
private val playSongsViewModel by activityViewModels<PlaySongsViewModel>() 復制代碼這樣就能實現無論是首頁、歌單頁底部的播放器小控件(PlayWidget),還是歌曲播放界面,他們的音樂播放狀態都一致來源于PlaySongsViewModel,任何一處的播放操作都能在其他頁面得到正確的展示。依靠ViewModel作用域合理劃分,自然地實現了狀態單一來源,而不必使用類似EventBus這樣容易造成狀態混亂的通知。
項目中使用Compose構建的界面,盡量遵循了官網提出的狀態提升達成“單向數據流”,具體參考官網:developer.android.google.cn/jetpack/com…
- 依賴注入
項目使用了Jetpack Hilt管理所有依賴,它與其他大部分Jetpack組件都能提供很好支持,無論View、ViewModel還是Repository層都能很輕松地獲取到需要的依賴項。
- 數據倉庫層
網絡數據源使用Retrofit、數據庫ORM使用Jetpack ROOM、應用持久化數據使用Jetpack DataStore(ProtoBuf實現)
界面導航
界面跳轉使用Jetpack Navigation,方案選擇經過幾次迭代:
- 只使用navigation的compose集成(ComposeNavigator)
最開始打算單純使用navigation的compose集成,可以像以下代碼這樣非常方便地實現兩個composable界面的跳轉:
val navController = rememberNavController() NavHost(navController = navController, startDestination = ScreenRoutes.AboutUs.path) {composable(ScreenRoutes.AboutUs.path) {AboutUsPage(onBackClick = { finish() }) {navController.navigate(ScreenRoutes.Privacy.path)}}composable(ScreenRoutes.Privacy.path) {HtmlDocumentViewer(title = "隱私政策")}... ... } 復制代碼完全使用ComposeNavigator的好處是可以做到Compose界面跳轉的過渡,并且后續版本還能實現界面間共享元素。
- FragmentNavigator與ComposeNavigator混用
但實際使用中發現上述方式目的地之間無法傳遞自定義類型的參數,于是想把Fragment/Activity的Navigator和ComposeNavigator混用,但發現跳轉Fragment返回時(Navigation對于Fragment導航跳轉默認使用replace,因此返回時重新調用onCreateView)重新創建的ComposeView中的總是空白。通過查看源碼和調試,發現NavHost中:
// NavHost.kt // // lifecycle#currentState狀態大于STARTED時才做渲染 val backStackEntry = transitionsInProgress.lastOrNull { entry ->entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } ?: backStack.lastOrNull { entry ->entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } ... ... if (backStackEntry != null) {Crossfade(backStackEntry, modifier) { currentEntry ->...} } 復制代碼而從回退棧返回是重組NavHost時狀態是CREATED,無法獲得backStackEntry,因此也無法顯示。解決的方法想到的是在lifecycle進入到STARTED時改變NavHost的狀態主動觸發重組,比如透明度從0f -> 1f:
var navAlpha by remember { mutableStateOf(0f) } LaunchedEffect(key1 = true, block = {lifecycle.whenStarted { navAlpha = 1.0f } }) 復制代碼- 最終方案,只使用FragmentNavigator
這樣一來返回時界面就能正常顯示了,不過為了統一導航最終還是選擇了全部使用單純的FragmentNavigator做界面導航,暫時放棄ComposeNavigator,當前的navigation版本上(2.4.0-alpha06)對于compose的支持似乎還沒有完全穩定。
可折疊標題欄
Compose暫時沒有類似View系統中的CollapsingToolbarLayout和CoordinatorLayout,或者Flutter中CustomScrollView+SliverAppBar那樣方便實現定制滑動可折疊標題的控件,因此最后找到一些其他的實現方式:
主要就是嵌套滑動算出的偏移關聯到TopAppBar中,問題主要是上拉時只要開始上拉就把折疊展開,而不是上拉到列表頂部后才展開。也許可以通過其他的計算方式達到效果,但整體比較復雜。
使用graphicsLayer實現關聯偏移、折疊、透明度等等可以避免頻繁重組。
onebone/compose-collapsing-toolbar: A simple implementation of collapsing toolbar for Jetpack Compose (github.com)
CollapsingToolbarScaffold(state = rememberCollapsingToolbarScaffoldState(), // provide the state of the scaffoldtoolbar = {// contents of toolbar go here...} ) {// main contents go here... } 復制代碼唱片動畫
Compose中實現控件過渡動畫會發現比View系統的簡單很多,并且表現力也更強。比如圖片的無限旋轉動畫使用下面的代碼就可以很容易實現:
val infiniteTransition = rememberInfiniteTransition() val rotation by infiniteTransition.animateFloat(initialValue = 0f, targetValue = 360f,animationSpec = infiniteRepeatable(animation = tween(15 * 1000, easing = LinearEasing)) ) Image(modifier = Modifier.graphicsLayer {rotationZ = rotation} ) 復制代碼但是唱片動畫有個特點,就是歌曲可以暫停,動畫也需要可暫停并且原地續播。以Flutter為例,它可以通過AnimateController的stop、forward方法暫停、繼續動畫,但Compose的動畫系統用起來更方便了卻也缺少了這種可以直接控制動畫流程的API,為了實現這樣的需求,用了更底層的Animatable。因為無限動畫的起點和終點必須相差360度才能有無限循環效果,并且起始角度是當前角度值,所以通過角度的取余讓它在0~720度范圍內,達到視覺上無縫的無限旋轉動畫:
/*** 無限循環的旋轉動畫*/ @Composable private fun infiniteRotation(startRotate: Boolean,duration: Int = 15 * 1000 ): Animatable<Float, AnimationVector1D> {var rotation by remember { mutableStateOf(Animatable(0f)) }LaunchedEffect(key1 = startRotate, block = {if (startRotate) {//從上次的暫停角度 -> 執行動畫 -> 到目標角度(+360°)rotation.animateTo((rotation.value % 360f) + 360f, animationSpec = infiniteRepeatable(animation = tween(duration, easing = LinearEasing)))} else {rotation.stop()//初始角度取余是為了防止每次暫停后目標角度無限增大rotation = Animatable(rotation.value % 360f)}})return rotation } 復制代碼圖片圓角、模糊
圖片的圓角、圓形裁切和模糊都能通過Coil很容易地實現:
Image(painter = rememberImagePainter(song?.picUrl?.limitSize(200), builder = {transformations(CircleCropTransformation(),BlurTransformation(LocalContext.current, 16f),)}) ) 復制代碼這里之前在實現模糊背景時存在一個缺陷,就是白色的圖標(按鈕)在淺色背景下會與背景融在一起而無法看清。觀察了網易云音樂的原版App,發現即使白色背景它也會被調暗,以適應淺色的前景按鈕和圖標。因此順著這個思路,我這兒采用的方法是在模糊背景上遮蓋一層半透明的灰黑色蒙層:
//模糊虛化的封面作為背景 Image(...modifier = Modifier.drawWithContent {drawContent()//背景遮上半透明顏色,改善明亮色調的背景下,白色操作按鈕的顯示效果drawRect(Color.Gray, alpha = 0.7f)},... ) 復制代碼這樣即使模糊背景整體偏亮色,上面的淺色按鈕也能比較容易看清。
除此之外,應該也能通過Image的colorFilter混合減暗顏色來達到更好的效果(未測試過):colorFilter = ColorFilter.tint(Color.Gray, BlendMode.Darken)
底部彈窗
底部彈窗在Compose中實現起來也是非常簡單
ModalBottomSheetLayout(//彈窗內容sheetContent = { ReplySheet(floorComment) },sheetState = sheetState,sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) ) {//主內容CommentMain(song, commentCount, commentList, sortType) {viewModel.loadFloorReply(it.commentId)scope.launch { sheetState.show() }} } //需要返回時收起彈窗,這里處理返回鍵行為 BackHandler(sheetState.currentValue != ModalBottomSheetValue.Hidden) {scope.launch { sheetState.hide() } } 復制代碼最后
第一次掘金上發文,文章排版比較散亂。這個項目作為自己的第一個Compose應用,并且也是主要練習為目的,APP中很多功能都不完善,并且對于Compose的了解還不是非常深入,有些部分實現可能不是最佳實踐。整體使用開發下來的直觀感受還是與傳統View很大不同,尤其通過各種修飾符就能將內置控件定制為自己想要的樣式,然后進行組合排布,重復的樣板代碼少了很多。
Compose開發的界面在某些部分還是能看出不完善的地方,比如LayzColumn列表滑動的流暢性上是和RecyclerView還是有差距,還有很多API都還有試驗性注解(即API還不穩定,后續可能變動)。性能方面也是官方著重在后續版本優化的點。
可以預見的是,現代的聲明式UI未來應該會成為一個高效的UI開發模式,但能不能在Android中成為主流就看官方的開發力度和開發者們的接受度了~
最后還有本APP的源碼地址,有空還會補充更多功能:
Mr-lin930819/ComposeMany: 使用jetpack compose構建的app (github.com)
?
總結
以上是生活随笔為你收集整理的用Compose实现轻量版网易云音乐的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软ERP Axapta 开发环境编辑器
- 下一篇: 郑州市养老院解决方案,苏州新导助力养老院