前言

本章开始,运用前面讲过的 Jetpack 组件结合 MVVM + Kotlin 在完成一个音乐播放器;
整体采用一个 Activity + 多个 Fragment 的模式,架构图如下:

按照 Google 官方推荐的架构图,一个 Activity/Fragment 对应一个 ViewModel,所以我们也采用这种方式;
框架搭建
接下来我们来搭建整体框架
单 Activity
我们的 MainActivity 构建如下:
kotlin
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
activity_main.xml
ini
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:sothree="http://schemas.android.com/apk/res-auto">
<!-- 数据区域 -->
<data>
<!-- 考虑到后面横竖屏切换数据不丢失 -->
<variable
name="vm"
type="com.mars.puremusic.bridge.state.MainActivityViewModel" />
</data>
<!--
抽屉控件左右滑动,DrawerLayout只支持左右滑动的菜单,但是并不支持上下滑动的菜单
allowDrawerOpen="@{vm.allowDrawerOpen}" 允许抽屉打开与关闭
isOpenDrawer="@{vm.openDrawer}" 打开抽屉与关闭抽屉
-->
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/dl"
allowDrawerOpen="@{vm.allowDrawerOpen}"
isOpenDrawer="@{vm.openDrawer}"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--
这是正面的唯一的遗憾是 DrawerLayout 只支持左右滑动的菜单,但是并不支持上下滑动的菜单,
我们使用开源的 SlidingUpPanelLayout,相当于竖向的 DrawerLayout
https://www.jianshu.com/p/b6fb08a5b604
-->
<com.sothree.slidinguppanel.SlidingUpPanelLayout
android:id="@+id/sliding_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
sothree:umanoDragView="@+id/slide_fragment_host"
sothree:umanoOverlay="false"
sothree:umanoPanelHeight="@dimen/sliding_up_header"
sothree:umanoShadowHeight="5dp">
<!--
主页效果的 fragment 显示
就是 main 的效果,当点击 main 上面的菜单图标的时候跳转到"搜索界面"
nav_main (首页界面,搜索界面)
-->
<fragment
android:id="@+id/main_fragment_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
<!--
底部播放条 底部播放项
nav_slide(播放条播放界面)
-->
<fragment
android:id="@+id/slide_fragment_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_slide" />
</com.sothree.slidinguppanel.SlidingUpPanelLayout>
<!--
点击菜单图标后,弹出左侧的"黑色半边框界面"
nav_drawer(左侧的黑色半边框界面)
-->
<fragment
android:id="@+id/drawer_fragment_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="330dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
Activity 关联的 ViewModel-> MainActivityViewModel
kotlin
class MainActivityViewModel : ViewModel() {
}
Navigation
三个 Navigation 构建如下:
nav_main.xml
ini
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_main"
app:startDestination="@id/mainFragment">
<!-- 首页界面,不包含播放条 -->
<fragment
android:id="@+id/mainFragment"
android:name="com.mars.puremusic.ui.page.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main">
<!-- 跳转到 "测试搜索界面 app:destination="@id/searchFragment"-->
<action
android:id="@+id/action_mainFragment_to_searchFragment"
app:destination="@id/searchFragment"
app:enterAnim="@anim/h_fragment_enter"
app:exitAnim="@anim/h_fragment_exit"
app:popEnterAnim="@anim/h_fragment_pop_enter"
app:popExitAnim="@anim/h_fragment_pop_exit" />
</fragment>
<!-- "搜索界面 -->
<fragment
android:id="@+id/searchFragment"
android:name="com.mars.puremusic.ui.page.SearchFragment"
android:label="fragment_search"
tools:layout="@layout/fragment_search"/>
</navigation>
nav_slide.xml
ini
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_slide"
app:startDestination="@id/playerFragment">
<fragment
android:id="@+id/playerFragment"
android:name="com.mars.puremusic.ui.page.PlayerFragment"
android:label="fragment_player"
tools:layout="@layout/fragment_player" />
</navigation>
nav_drawer.xml
ini
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_drawer"
app:startDestination="@id/drawerFragment">
<fragment
android:id="@+id/drawerFragment"
android:name="com.mars.puremusic.ui.page.DrawerFragment"
android:label="fragment_drawer"
tools:layout="@layout/fragment_drawer" />
</navigation>
多 Fragment
接下来,我们就需要构建真正的界面了,也就是多个 Fragment;
MainFragment -> MainViewModel
SearchFragment -> SearchViewModel
DrawerFragment -> DrawerViewModel
PlayerFragment -> PlayerViewModel
MainFragment
kotlin
class MainFragment : Fragment(){
/** DataBinding */
private var mainBinding: FragmentMainBinding? = null
/** 首页Fragment的ViewModel */
private var mainViewModel : MainViewModel? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mainBinding = FragmentMainBinding.inflate(inflater, container, false)
return mainBinding?.root
}
}
fragement_main.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MainViewModel
kotlin
class MainViewModel : ViewModel() {}
SearchFragment
kotlin
class SearchFragment : Fragment(){
private var searchBinding: FragmentSearchBinding? = null
private var searchViewModel: SearchViewModel? = null // 搜索界面 相关的 VM
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
searchBinding = FragmentSearchBinding.inflate(inflater, container, false)
return searchBinding?.root
}
}
fragment_search.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
SearchViewModel
kotlin
class SearchViewModel : ViewModel() {}
DrawerFragment
kotlin
class DrawerFragment : Fragment(){
private var drawerBinding: FragmentDrawerBinding? = null
private var drawerViewModel: DrawerViewModel? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
drawerBinding = FragmentDrawerBinding.inflate(inflater, container, false)
return drawerBinding?.root
}
}
fragment_drawer.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DrawerViewModel
kotlin
class DrawerViewModel : ViewModel() {}
PlayerFragment
kotlin
class PlayerFragment : Fragment(){
private var playerBinding: FragmentPlayerBinding? = null
private var playerViewModel: PlayerViewModel? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
playerBinding = FragmentPlayerBinding.inflate(inflater, container, false)
return playerBinding?.root
}
}
fragment_player.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
PlayerViewModel
kotlin
class PlayerViewModel : ViewModel() {}
整体的架子就是这样,一个 Activity 通过 NavHostFragment 来管理另外的四个 Fragment
我们接着来构建 Base 基类;
Base 基类
主要是针对Application、Activity、Fragment、ViewModel 的基类
Application
我们定义一个 Application 来接管一下
kotlin
class App : Application(), ViewModelStoreOwner {
private var mAppViewModelStore: ViewModelStore? = null
private var mFactory: ViewModelProvider.Factory? = null
override fun onCreate() {
super.onCreate()
Utils.init(this)
mAppViewModelStore = ViewModelStore()
}
// 关键函数,只暴露给 BaseActivity 与 BaseFragment 用的,保证共享 ViewModel 初始化的单例
// 专门给 BaseActivity 与 BaseFragment 用的
fun getAppViewModelProvider(activity: Activity): ViewModelProvider {
return ViewModelProvider(
(activity.applicationContext as App),
(activity.applicationContext as App).getAppFactory(activity) !!
)
}
// AndroidViewModelFactory 工程是为了创建 ViewModel,给上面的 getAppViewModelProvider 函数用的
private fun getAppFactory(activity: Activity): ViewModelProvider.Factory? {
val application = checkApplication(activity)
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application)
}
return mFactory
}
// 监测下 Application 是否为 null
private fun checkApplication(activity: Activity): Application {
return activity.application
?: throw IllegalStateException(
"Your activity/fragment is not yet attached to "
+ "Application. You can't request ViewModel before onCreate call."
)
}
// 监测下 Activity 是否为null
private fun checkActivity(fragment: Fragment): Activity? {
return fragment.activity
?: throw IllegalStateException("Can't create ViewModelProvider for detached fragment")
}
// 此函数只给 NavHostFragment 使用
override fun getViewModelStore(): ViewModelStore = mAppViewModelStore !!
}
BaseActivity
BaseActivity 中主要是一些全局处理的能力,封装一些全局方法等等;
kotlin
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BarUtils.setStatusBarColor(this, Color.TRANSPARENT)
BarUtils.setStatusBarLightMode(this, true)
lifecycle.addObserver(NetworkStateManager.instance)
}
// 工具函数
fun isDebug(): Boolean {
return applicationContext.applicationInfo != null &&
applicationContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
}
// BaseActivity的 Resource信息给 暴露给外界用
override fun getResources(): Resources? {
return if (ScreenUtils.isPortrait) {
AdaptScreenUtils.adaptWidth(super.getResources(), 360)
} else {
AdaptScreenUtils.adaptHeight(super.getResources(), 640)
}
}
// 工具函数 提示Toast而已
fun showLongToast(text: String?) {
Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
}
// 工具函数 提示Toast而已
fun showShortToast(text: String?) {
Toast.makeText(applicationContext, text, Toast.LENGTH_SHORT).show()
}
// 此 getAppViewModelProvider 函数,只给共享的 ViewModel 用
protected fun getAppViewModelProvider(): ViewModelProvider {
return (applicationContext as App).getAppViewModelProvider(this)
}
// 此 getActivityViewModelProvider 函数,给所有的 BaseActivity 子类用的『ViewModel非共享区域』
protected fun getActivityViewModelProvider(activity: AppCompatActivity): ViewModelProvider {
return ViewModelProvider(activity, activity.defaultViewModelProviderFactory)
}
}
主要是获取 ViewModelProvider
,showToast
,getResources
方法,供子 Activity 来进行 ViewModel 的初始化,信息提示,获取资源等等;
接下来,我们构建 BaseFragment;
BaseFragment
BaseFragment 的构建和 BaseActivity 差不多,也是提供获取 ViewModelProvider 、 showToast
、getResources
等等
kotlin
open class BaseFragment : Fragment() {
// 为了让所有的子类持有 Activity
protected var mActivity: AppCompatActivity? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mActivity = context as AppCompatActivity
}
// 测试用的
fun isDebug(): Boolean {
return mActivity!!.applicationContext.applicationInfo != null &&
mActivity!!.applicationContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
}
// 提示
fun showLongToast(text: String?) {
Toast.makeText(mActivity!!.applicationContext, text, Toast.LENGTH_LONG).show()
}
// 提示
fun showShortToast(text: String?) {
Toast.makeText(mActivity!!.applicationContext, text, Toast.LENGTH_SHORT).show()
}
// 提示
fun showLongToast(stringRes: Int) {
showLongToast(mActivity!!.applicationContext.getString(stringRes))
}
// 提示
fun showShortToast(stringRes: Int) {
showShortToast(mActivity!!.applicationContext.getString(stringRes))
}
// 给当前 BaseFragment 用的【共享区域的 ViewModel】
protected fun getAppViewModelProvider(): ViewModelProvider {
return (mActivity!!.applicationContext as App).getAppViewModelProvider(mActivity!!)
}
// 给所有的子 fragment 提供的函数,可以顺利的拿到 ViewModel 【非共享区域的ViewModel】
protected fun getFragmentViewModelProvider(fragment: Fragment): ViewModelProvider {
return ViewModelProvider(fragment, fragment.defaultViewModelProviderFactory)
}
// 备用的
// 给所有的子 fragment 提供的函数,可以顺利的拿到 ViewModel 【非共享区域的ViewModel】
protected fun getActivityViewModelProvider(activity: AppCompatActivity): ViewModelProvider {
return ViewModelProvider(activity, activity.defaultViewModelProviderFactory)
}
/**
* 为了给所有的子 fragment,导航跳转 fragment 的
* @return
*/
protected fun nav(): NavController {
return NavHostFragment.findNavController(this)
}
}
数据共享
我们有时候需要在各个 Fragment 之间做数据共享,这里我们采用一个公共的 ViewModel 来处理;所有的 Activity 和 Framgent 公用这个 ViewModel
kotlin
class SharedViewModel : ViewModel() {}
然后 BaseActivity 和 BaseFragment 中都持有这个 ViewModel
kotlin
open class BaseActivity : AppCompatActivity() {
// 贯穿整个项目的(只会让App(Application)初始化一次)
protected lateinit var mSharedViewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 注意,这里使用的是 getAppViewModelProvider 来获取 ViewModel
mSharedViewModel = getAppViewModelProvider().get<SharedViewModel>(SharedViewModel::class.java)
...
}
}
BaseFragemnt
kotlin
open class BaseFragment : Fragment() {
// 贯穿整个项目的(只会让App(Application)初始化一次)
protected lateinit var mSharedViewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 注意,这里使用的是 getAppViewModelProvider 来获取 ViewModel
mSharedViewModel = getAppViewModelProvider().get<SharedViewModel>(SharedViewModel::class.java)
...
}
}
通过这个 SharedViewModel 保证项目的一致性;
修改 MainActivity 继承 BaseActivity ,Fragment 继承 BaseFragment
数据 Bean
数据 bean 比较简单,就是根据业务功能提供相关数据 bean
kotlin
class TestAlbum : BaseAlbumItem<TestMusic?, TestArtist?>() {
// 专辑 Mid
var albumMid: String? = null
// 歌曲 Mid
class TestMusic : BaseMusicItem<TestArtist?>() {
var songMid: String? = null
}
// 歌手相关
class TestArtist : BaseArtistItem() {
var birthday: String? = null
}
}
这个是用来装载音乐列表的数据 bean
javascript
class LibraryInfo {
var title // XiangxeMusic
: String? = null
var summary // "享学VIP之JetPack项目"
: String? = null
var url // 本来是用来跳转到 WebView要加载的网页路径的
: String? = null
constructor() {}
constructor(title: String?, summary: String?, url: String?) {
this.title = title
this.summary = summary
this.url = url
}
}
这个是用来展示 DrawerFragment 中的数据;
好了,基础构建就写到这里吧,下一章节,开始内容的填充;
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~