如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(二)音乐列表

前言


本章继续,运用前面讲过的 Jetpack 组件结合 MVVM + Kotlin 在完成一个音乐播放器;

数据填充


我们接下来进行 Base 类和 Activity 的数据填充;

SharedViewModel 功能填充

kotlin 复制代码
class SharedViewModel : ViewModel() {

    // 添加监听(可以弹上来的监听)
    val timeToAddSlideListener = MutableLiveData<Boolean>()

    // 可以控制播放详情 点击/back 掉下来
    // 播放详情中左手边滑动图标(点击的时候),与 MainActivity back 是会 set(true)
    val closeSlidePanelIfExpanded = MutableLiveData<Boolean>()

    // 活动关闭的一些记录(播放条缩小一条与扩大展开)通知给控制者的(扩展的)
    val activityCanBeClosedDirectly = MutableLiveData<Boolean>()

    // openMenu 打开菜单的时候会 set触发---> 改变 openDrawer.setValue(aBoolean); 的值
    val openOrCloseDrawer = MutableLiveData<Boolean>()

    // 开启和关闭卡片相关的状态,如果发生改变会和 allowDrawerOpen 挂钩
    val enableSwipeDrawer = MutableLiveData<Boolean>()
}

MainActivity 功能填充

我们首先来初始化 MainActivityViewModel 和 ActivityMainBinding

kotlin 复制代码
class MainActivity : BaseActivity() {

    var mainBinding: ActivityMainBinding? = null // 当前 MainActivity 的布局
    var mainActivityViewModel: MainActivityViewModel? = null  // 当前 Activity 关联的 ViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 绑定 DataBinding 与 ViewModel 结合
        mainActivityViewModel = getActivityViewModelProvider(this).get(MainActivityViewModel::class.java)
        mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        mainBinding?.lifecycleOwner = this
        mainBinding?.setVm(mainActivityViewModel)
        
        // 
        setOberve()
    }
}

接下来,我们来给 SharedViewModel 中定义的各个状态设置 observe

kotlin 复制代码
private fun setObserve() {

    // 共享 (观察) 活动关闭的一些记录(播放条 缩小一条 与 扩大展开)
    mSharedViewModel.activityCanBeClosedDirectly.observe(this, {
        // ... 业务逻辑的
    })

    // 监听(发送 打开菜单的指令 1)
    mSharedViewModel.openOrCloseDrawer.observe(this, { aBoolean ->
        mainActivityViewModel?.allowDrawerOpen.value = aBoolean // 触发,就会改变 --> 观察(打开菜单)
    })

    // 监听(发送 打开菜单的指令 2)
    mSharedViewModel.enableSwipeDrawer.observe(this, { aBoolean ->
        mainActivityViewModel?.allowDrawerOpen.value = aBoolean // 触发抽屉控件关联的值
    })
}

接下来,我们来给 MainActivityViewModel 中定义变量

kotlin 复制代码
class MainActivityViewModel : ViewModel() {

    // 首页需要记录抽屉布局的情况
    @JvmField
    val openDrawer = MutableLiveData<Boolean>()

    // 响应的效果,都让抽屉控件干了
    @JvmField // @JvmField消除了变量的getter方法
    val allowDrawerOpen = MutableLiveData<Boolean>()

    // 构造代码块
    init {
        allowDrawerOpen.value = true
    }
}

接下来,我们需要在 Activity 可见的时候,触发 timeToAddSlideListener 的状态变化,这里我们复现下 onWindowFoucsChanged 方法

kotlin 复制代码
override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    // 此字段只要发生了改变,就会添加监听(可以弹上来的监听)
    mSharedViewModel.timeToAddSlideListener.value = true // 触发改变
}

BindingAdapter

上一章中,我们有在 activity_main.xml 中设置了一些并不属于 xml 范围内的属性,例如:

这是 DataBinding 的能力,我们来进行 DrawerLayout 的 DataBinding 填充

kotlin 复制代码
object DrawerBindingAdapter {

    // 『打开抽屉』与『关闭抽屉』
    @JvmStatic // 代表是静态函数
    @BindingAdapter(value = ["isOpenDrawer"], requireAll = false)
    fun openDrawer(drawerLayout: DrawerLayout, isOpenDrawer: Boolean) {
        if (isOpenDrawer && !drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.openDrawer(GravityCompat.START)
        } else {
            drawerLayout.closeDrawer(GravityCompat.START)
        }
    }

    // 允许抽屉的 打开 与 关闭
    @JvmStatic // 代表是静态函数
    @BindingAdapter(value = ["allowDrawerOpen"], requireAll = false)
    fun allowDrawerOpen(drawerLayout: DrawerLayout, allowDrawerOpen: Boolean) {
        drawerLayout.setDrawerLockMode(if (allowDrawerOpen) DrawerLayout.LOCK_MODE_UNLOCKED else DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
    }
}

PS:这里需要注意,因为是 Kotlin,需要我们引入 apply plugin: 'kotlin-kapt' 才能编译通过;

MainFragment

我们先来填充 xml 中的布局:

ini 复制代码
<layout 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">

    <data>

        <!-- 点击事件 -->
        <variable
            name="click"
            type="com.xiangxue.puremusic.ui.page.MainFragment.ClickProxy" />

        <variable
            name="vm"
            type="com.xiangxue.puremusic.bridge.state.MainViewModel" />

    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:background="@color/black">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:theme="@style/AppTheme">

            <!-- 折叠工具栏布局 -->
            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:id="@+id/collapse_layout"
                android:layout_width="match_parent"
                android:layout_height="275dp"
                android:fitsSystemWindows="true"
                app:contentScrim="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                <!-- 背景 图片
                   android:src="@drawable/bg_home"
                 -->
                <androidx.appcompat.widget.AppCompatImageView
                    android:id="@+id/iv_bg"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:fitsSystemWindows="true"
                    android:scaleType="centerCrop"
                    android:src="@drawable/bg_home"
                    app:layout_collapseMode="parallax" />

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:id="@+id/toolbar"
                    drawable_radius="@{8}"
                    drawable_solidColor="@{0x88ffffff}"
                    drawable_strokeColor="@{0x33666666}"
                    drawable_strokeWidth="@{1}"
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:layout_marginStart="12dp"
                    android:layout_marginTop="37dp"
                    android:layout_marginEnd="12dp"
                    android:layout_marginBottom="12dp">

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/iv_menu"
                        android:layout_width="24dp"
                        android:layout_height="24dp"
                        android:layout_marginStart="12dp"
                        android:background="?attr/selectableItemBackgroundBorderless"
                        android:onClick="@{()->click.openMenu()}"
                        android:scaleType="centerInside"
                        android:src="@drawable/ic_menu_black_48dp"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintLeft_toLeftOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/iv_icon"
                        onClickWithDebouncing="@{()->click.search()}"
                        android:layout_width="24dp"
                        android:layout_height="24dp"
                        android:background="?attr/selectableItemBackgroundBorderless"
                        android:scaleType="centerInside"
                        android:src="@drawable/ic_music_note_black_48dp"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintRight_toLeftOf="@+id/tv_app"
                        app:layout_constraintTop_toTopOf="parent" />

                    <TextView
                        android:id="@+id/tv_app"
                        onClickWithDebouncing="@{()->click.search()}"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/app_name"
                        android:textSize="18sp"
                        android:textStyle="bold"
                        android:textColor="@color/my_c2"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintLeft_toLeftOf="parent"
                        app:layout_constraintRight_toRightOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/iv_search"
                        onClickWithDebouncing="@{()->click.search()}"
                        android:layout_width="24dp"
                        android:layout_height="24dp"
                        android:layout_marginEnd="12dp"
                        android:background="?attr/selectableItemBackgroundBorderless"
                        android:scaleType="centerInside"
                        android:src="@drawable/ic_search_black_48dp"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintRight_toRightOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                </androidx.constraintlayout.widget.ConstraintLayout>

            </com.google.android.material.appbar.CollapsingToolbarLayout>

            <!-- MainFragment 初始化页面的标记,初始化选项卡和页面 -->
            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tab_layout"
                initTabAndPage="@{vm.initTabAndPage}"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:fitsSystemWindows="true"
                app:tabBackground="@color/my_c1"
                app:tabIndicatorColor="@color/gray"
                app:tabIndicatorHeight="4dp"
                app:tabSelectedTextColor="@color/gray"
                app:tabTextColor="@color/light_gray">

                <com.google.android.material.tabs.TabItem
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/recently" />

                <com.google.android.material.tabs.TabItem
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/best_practice" />

            </com.google.android.material.tabs.TabLayout>

        </com.google.android.material.appbar.AppBarLayout>

        <!-- 下面展示 横向切换:
             1."最近播放区域" 其实就是 音乐列表 系列Item
             2."其他信息区域" 其实就是 WebView 展示网页信息而已
        -->
        <androidx.viewpager.widget.ViewPager
            android:id="@+id/view_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:overScrollMode="never"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <!-- 1."我的歌曲区域" 其实就是『 音乐列表 』系列Item
                 RecyclerView 的每一个 Item 布局 == adapter_play_item.xml
             -->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clipToPadding="false"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                tools:listitem="@layout/adapter_play_item"
                android:visibility="visible"
                />

            <!-- 2."其他信息区域" 其实就是 WebView 展示网页信息而已 -->
            <androidx.core.widget.NestedScrollView
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <WebView
                    android:id="@+id/web_view"
                    pageAssetPath="@{vm.pageAssetPath}"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:clipToPadding="false"
                    android:visibility="visible"
                    android:background="#B4D9DD"/>

            </androidx.core.widget.NestedScrollView>

        </androidx.viewpager.widget.ViewPager>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

接下来,我们在 MainFragment 中初始化 DataBinding 和 ViewModel

kotlin 复制代码
class MainFragment  : BaseFragment(){

    // 首页 DataBinding
    private var mainBinding: FragmentMainBinding? = null
    // 首页 Fragment的ViewModel
    private var mainViewModel : MainViewModel? = null 
    // 音乐资源相关的 VM Request ViewModel
    private var musicRequestViewModel: MusicRequestViewModel? = null
    // 适配器
    private var adapter: SimpleBaseBindingAdapter<TestAlbum.TestMusic?, AdapterPlayItemBinding?>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 初始化 ViewModel
        mainViewModel = getFragmentViewModelProvider(this).get(MainViewModel::class.java)
        musicRequestViewModel = getFragmentViewModelProvider(this).get(MusicRequestViewModel::class.java)
    }

    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view: View = inflater.inflate(R.layout.fragment_main, container, false)
        // 绑定DataBinding 与 ViewModel 关联
        mainBinding = FragmentMainBinding.bind(view)
        // 设置点击事件,布局就可以直接绑定
        mainBinding?.click = ClickProxy()
        // 设置 VM,就可以实时数据变化
        mainBinding?.setVm(mainViewModel) 
        return view
    }
}

这里用到了 ClickProxy 这个是一个点击代理类,我们可以把它定义在 MainFramgent 中最为一个内部类来处理;

kotlin 复制代码
// 处理所有点击事件
inner class ClickProxy {
    // 当在首页点击 "菜单" 的时候,直接导航到 ---> 菜单的Fragment界面
    // 间接通过共享 VM 触发到 openDrawer 触发到 @BindingAdapter
    fun openMenu() { 
        sharedViewModel.openOrCloseDrawer.value = true 
    } 

    // 当在首页点击 "搜索图标" 的时候,直接导航到 ---> 搜索的Fragment界面
    fun search() = nav().navigate(R.id.action_mainFragment_to_searchFragment)
}

接下来,我们来填充 MainViewModel 支持 initTabAndPage

less 复制代码
class MainViewModel : ViewModel() {

    // MainFragment 初始化页面的标记 初始化选项卡和页面
    @JvmField
    val initTabAndPage = ObservableBoolean()

    // MainFragment "其他信息" 里面的 WebView需要加载的网页链接路径
    @JvmField
    val pageAssetPath = ObservableField<String>()

}

PS:ViewModel 中什么时候使用 LiveData 什么时候使用 ObservableField,

LiveData 应用于更新并不频繁的场景,而 ObservableField 使用于较为频繁的场景来防止抖动;

我们给 TabLayout 和 WebView 设置了 BindingAdapter,那么我们来完善这个 BindingAdapter

initTabAndPage

kotlin 复制代码
object TabPageBindingAdapter {

    // MainFragment 初始化页面的标记,初始化选项卡和页面
    @JvmStatic
    @BindingAdapter(value = ["initTabAndPage"], requireAll = false)
    fun initTabAndPage(tabLayout: TabLayout, initTabAndPage: Boolean) {
        if (initTabAndPage) {
            val count = tabLayout.tabCount
            val title = arrayOfNulls<String>(count)
            for (i in 0 until count) {
                val tab = tabLayout.getTabAt(i)
                if (tab != null && tab.text != null) {
                    title[i] = tab.text.toString()
                }
            }
            val viewPager: ViewPager = tabLayout.rootView.findViewById(R.id.view_pager)
            if (viewPager != null) {
                viewPager.adapter = CommonViewPagerAdapter(count, false, title)
                tabLayout.setupWithViewPager(viewPager)
            }
        }
    }

    // 在选项卡上添加选定的侦听器(备用的功能)
    @BindingAdapter(value = ["tabSelectedListener"], requireAll = false)
    fun tabSelectedListener(tabLayout: TabLayout, listener: OnTabSelectedListener?) {
        tabLayout.addOnTabSelectedListener(listener)
    }
}

pageAssetPath

kotlin 复制代码
object WebViewBindingAdapter {

    /**
     * 加载WebView,固定加载 Assets 目录下的资源
     */
    @JvmStatic
    @SuppressLint("SetJavaScriptEnabled")
    @BindingAdapter(value = ["pageAssetPath"], requireAll = false)
    fun loadAssetsPage(webView: WebView, assetPath: String) {
        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView,
                request: WebResourceRequest
            ): Boolean {
                val uri = request.url
                val intent = Intent(Intent.ACTION_VIEW, uri)
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                Utils.getApp().startActivity(intent)
                return true
            }
        }
        webView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
        val webSettings = webView.settings
        webSettings.javaScriptEnabled = true
        webSettings.defaultTextEncodingName = "UTF-8"
        webSettings.setSupportZoom(true)
        webSettings.builtInZoomControls = true
        webSettings.displayZoomControls = false
        webSettings.useWideViewPort = true
        webSettings.loadWithOverviewMode = true
        val url = "file:///android_asset/$assetPath"
        webView.loadUrl(url)
    }
}

接下来,我们来填充 MainFragment 音乐列表界面,按照多状态管理原则,这个 Fragment 除了 MainViewModel 之后,还需要一个请求音乐数据列表的 MusicRequestViewModel

kotlin 复制代码
class MusicRequestViewModel : ViewModel() {

    var freeMusicesLiveData : MutableLiveData<TestAlbum>? = null
        get() {
            if (field == null) {
                field = MutableLiveData()
            }
            return field
        }
        private set

    fun requestFreeMusics() {
        HttpRequestManager.getFreeMusic(freeMusicesLiveData)
    }
}

接下来,我们进行适配器的填充,让数据在 View 上渲染出来;

kotlin 复制代码
class MainFragment  : BaseFragment() {
    
    
    // 我们操作布局,不去传统方式操作,全部使用 Databind
    private var mainBinding: FragmentMainBinding? = null
    // 首页Fragment的 ViewModel
    private var mainViewModel : MainViewModel? = null 
    // 音乐资源相关的 ViewModel 用来做网络请求,获取音乐数据
    private var musicRequestViewModel: MusicRequestViewModel? = null

    // 适配器
    private var adapter: SimpleBaseBindingAdapter<TestAlbum.TestMusic?, AdapterPlayItemBinding?>? = null
   
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // MainFragment初始化页面的标记,初始化选项卡和页面
        mainViewModel?.initTabAndPage.set(true)
        // 加载 WebView
        mainViewModel?.pageAssetPath.set("JetPack 之 WorkManager.html")

        // 展示数据,适配器里面的的数据 展示出来
        // 设置设配器(item 的布局和适配器的绑定)
        adapter = object : SimpleBaseBindingAdapter<TestAlbum.TestMusic?, AdapterPlayItemBinding?>(context, R.layout.adapter_play_item) {
            override fun onSimpleBindItem(
                binding: AdapterPlayItemBinding?,
                item: TestAlbum.TestMusic?,
                holder: RecyclerView.ViewHolder?) {
                
                // 标题
                binding?.tvTitle?.text = item?.title 
                // 名字
                binding?.tvArtist?.text = item?.artist?.name
                // 左右的图片
                Glide.with(binding?.ivCover?.context)
                     .load(item?.coverImg).into(binding.ivCover)

                // 点击Item
                binding.root.setOnClickListener { v ->
                    Toast.makeText(mContext, "播放音乐", Toast.LENGTH_SHORT).show()
                }
            }
        }
        mainBinding?.rv.adapter = adapter

        // 请求数据
        musicRequestViewModel?.requestFreeMusics()

        // observe
        // 音乐资源的 VM
        musicRequestViewModel?.freeMusicesLiveData?.observe(viewLifecycleOwner, { musicAlbum: TestAlbum? ->
            if (musicAlbum != null && musicAlbum.musics != null) {
                // 这里特殊:直接更新UI
                // 数据加入适配器
                adapter?.list = musicAlbum.musics 
                adapter?.notifyDataSetChanged()
            }
        })
    }
}

接下来,我们来看下适配器是如何实现的 SimpleBaseBindingAdapter 可以看到这个适配器是一个抽象类,实现了 onSimpleBindItem 方法;

scala 复制代码
public abstract class SimpleBaseBindingAdapter<M, B extends ViewDataBinding> extends BaseBindingAdapter {

    private final int layout;

    public SimpleBaseBindingAdapter(Context context, int layout) {
        super(context);
        this.layout = layout;
    }

    @Override
    protected @LayoutRes int getLayoutResId(int viewType) {
        return this.layout;
    }

    protected abstract void onSimpleBindItem(B binding, M item, RecyclerView.ViewHolder holder);

    @Override
    protected void onBindItem(ViewDataBinding binding, Object item, RecyclerView.ViewHolder holder) {
        //noinspection unchecked
        onSimpleBindItem((B) binding, (M) item, holder);
    }
}

AdapterPlayItemBinding 则是 xml 生成的 DataBinding adapter_play_item.xml

ini 复制代码
<layout 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">

    <!-- 为了以后扩展 -->
    <data>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/root_view"
        android:layout_width="match_parent"
        android:layout_height="72dp"
        android:orientation="vertical"
        tools:background="@color/light_gray">

        <!-- 正方形的图片 -->
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_cover"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:layout_marginStart="12dp"
            android:scaleType="centerCrop"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@drawable/bg_home" />

        <!-- 歌曲名字信息,与,歌曲描述信息等 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/iv_cover"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:textSize="18sp"
                android:textColor="@color/white"
                tools:text="title" />

            <TextView
                android:id="@+id/tv_artist"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginTop="4dp"
                android:textSize="14sp"
                android:textColor="@color/white"
                tools:text="summary" />

        </LinearLayout>

        <!-- 右的图标,播放的状态图标哦 -->
        <net.steamcrafted.materialiconlib.MaterialIconView
            android:id="@+id/iv_play_status"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:layout_marginEnd="12dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:scaleType="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:materialIcon="music_note"
            app:materialIconColor="@color/gray"
            app:materialIconSize="28dp" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

接下来,我们来完善下『网络请求』模块 HttpRequestManager

kotlin 复制代码
object HttpRequestManager : ILoadRequest, IRemoteRequest {

    // 仓库:liveData: MutableLiveData<TestAlbum>?  已经和  Request VM 的 LiveData是 同一份了
    override fun getFreeMusic(liveData: MutableLiveData<TestAlbum>?) {
        val gson = Gson()
        val type = object : TypeToken<TestAlbum?>(){}.type
        // 这里模拟网络请求了,加载了本地的一个 json 文件,后面有条件的可以修改成实际的网络请求;
        val testAlbum =
            gson.fromJson<TestAlbum>(Utils.getApp().getString(R.string.free_music_json), type)
        liveData?.value = testAlbum
    }

    override fun getLibraryInfo(liveData: MutableLiveData<List<LibraryInfo>>?) {
        val gson = Gson()
        val type = object : TypeToken<List<LibraryInfo?>?>() {}.type
        // 这里模拟网络请求了,加载了本地的一个 json 文件,后面有条件的可以修改成实际的网络请求;
        val list =
            gson.fromJson<List<LibraryInfo?>>(Utils.getApp().getString(R.string.library_json), type)
        liveData?.value = list as List<LibraryInfo>?
    }
}

我们还差一项需要进行完善,点击事件的 BindingAdapter

less 复制代码
object CommonBindingAdapter {

    @JvmStatic
    @BindingAdapter(value = ["onClickWithDebouncing"], requireAll = false)
    fun onClickWithDebouncing(view: View?, clickListener: View.OnClickListener?) {
        ClickUtils.applySingleDebouncing(view, clickListener)
    }
}

到这,首页的音乐列表,我们就填充完了;

欢迎三连


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

相关推荐
Wgllss1 天前
完整案例:Kotlin+Compose+Multiplatform跨平台之桌面端实现(一)
android·架构·android jetpack
alexhilton3 天前
学会说不!让你彻底学会Kotlin Flow的取消机制
android·kotlin·android jetpack
_一条咸鱼_5 天前
Android Runtime冷启动与热启动差异源码级分析(99)
android·面试·android jetpack
柿蒂5 天前
一次Android下载优化,CDN消耗占比从50+%到1%
android·android jetpack
bytebeats6 天前
Android 开发者的 Jetpack Compose 学习路线图
android·android jetpack
bytebeats6 天前
Android 的未来: 为什么 `Navigation 3` 是行业变革者!
android·android jetpack
bytebeats6 天前
如何在 Jetpack Compose 中创建逐字动画
android·android jetpack
柿蒂7 天前
Android图片批量添加处理优化:从「30」秒缩短至「4.4」秒
android·android jetpack
alexhilton7 天前
学会用最优雅的姿式在Compose中显示富文本
android·kotlin·android jetpack