如何应对 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 中的数据;

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

欢迎三连


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

相关推荐
消失的旧时光-19432 天前
Android Jetpack 组件库 ->Jetpack Navigation (下)
android·android jetpack
ljt27249606612 天前
Compose笔记(三十八)--CompositionLocal
笔记·android jetpack
_一条咸鱼_3 天前
Android Runtime性能计数器实现深度剖析(95)
android·android jetpack
搬砖不得颈椎病4 天前
Compose 中的 Side-effects
android·android jetpack
_一条咸鱼_4 天前
Android Runtime死代码消除原理深度剖析(93)
android·面试·android jetpack
equationl5 天前
安卓开发中使用 kotlin Object 和 lazy 关键字以及 Room 踩坑记录
前端·数据库·android jetpack
刘龙超5 天前
如何应对 Android 面试官 -> 玩转 Jetpack Paging
android jetpack
_一条咸鱼_6 天前
Android Runtime常量折叠与传播源码级深入解析(92)
android·面试·android jetpack
alexhilton6 天前
揭密Jetpack Compose中的PausableComposition
android·kotlin·android jetpack