如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(一)基础搭建

前言


本章开始,运用前面讲过的 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 构建如下:

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)
    }
}

主要是获取 ViewModelProvidershowToastgetResources 方法,供子 Activity 来进行 ViewModel 的初始化,信息提示,获取资源等等;

接下来,我们构建 BaseFragment;

BaseFragment

BaseFragment 的构建和 BaseActivity 差不多,也是提供获取 ViewModelProvidershowToastgetResources 等等

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() {}

然后 BaseActivityBaseFragment 中都持有这个 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 继承 BaseActivityFragment 继承 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 中的数据;

好了,基础构建就写到这里吧,下一章节,开始内容的填充;

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

相关推荐
alexhilton1 天前
学会在Jetpack Compose中加载Lottie动画资源
android·kotlin·android jetpack
ljt27249606614 天前
Compose笔记(六十一)--SelectionContainer
android·笔记·android jetpack
QING6185 天前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
惟恋惜5 天前
Jetpack Compose 的状态使用之“界面状态”
android·android jetpack
喜熊的Btm5 天前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
惟恋惜5 天前
Jetpack Compose 界面元素状态(UI Element State)详解
android·ui·android jetpack
惟恋惜6 天前
Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel
android·ui·架构·android jetpack
alexhilton7 天前
Jetpack Compose 2025年12月版本新增功能
android·kotlin·android jetpack
モンキー・D・小菜鸡儿8 天前
Android Jetpack Compose 基础控件介绍
android·kotlin·android jetpack·compose
darryrzhong10 天前
FluxImageLoader : 基于Coil3封装的 Android 图片加载库,旨在提供简单、高效且功能丰富的图片加载解决方案
android·github·android jetpack