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

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

欢迎三连


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

相关推荐
Frank_HarmonyOS3 天前
Android APP 的压力测试与优化
android jetpack
QING6184 天前
Jetpack Compose 条件布局与 Layout 内在测量详解
android·kotlin·android jetpack
Lei活在当下4 天前
【现代 Android APP 架构】09. 聊一聊依赖注入在 Android 开发中的应用
java·架构·android jetpack
bqliang5 天前
Jetpack Navigation 3:领航未来
android·android studio·android jetpack
用户69371750013848 天前
🚀 Jetpack MVI 实战全解析:一次彻底搞懂 MVI 架构,让状态管理像点奶茶一样丝滑!
android·android jetpack
俩个逗号。。11 天前
ViewPager+Fragment 切换主题崩溃
android·android studio·android jetpack
alexhilton12 天前
Compose CameraX现已稳定:给Composer的端到端指南
android·kotlin·android jetpack
在狂风暴雨中奔跑15 天前
使用 Compose 权限请求模板高效搭建应用权限流程
android jetpack
H10019 天前
SharedFlow和StateFlow的方案选择-屏幕旋转设计
android jetpack
alexhilton19 天前
理解retain{}的内部机制:Jetpack Compose中基于作用域的状态保存
android·kotlin·android jetpack