如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(五)完结

前期回顾


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

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

如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(三)播放能力

如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(四)登录注册

前言


本章继续 Jetpack 全家桶实战音乐播放器;

协程


上一章,我们用 RxJava + Retrofit 实现了网络请求,本章我们用协程实现一版本;

API

首先,让我们的 API 来支持协程;

less 复制代码
interface WanAndroidAPI {

    /**
     * 登录API
     */
    @POST("/user/login")
    @FormUrlEncoded
    suspend fun loginActionCoroutine(@Field("username") username: String,
                                     @Field("password") password: String)
            : LoginRegisterResponseWrapper<LoginRegisterResponse> // 返回值

    /** 
     * 注册的API
     */
    @POST("/user/register")
    @FormUrlEncoded
    suspend fun registerActionCoroutine(@Field("username") username: String,
                                        @Field("password") password: String,
                                        @Field("repassword") repassword: String)
            : LoginRegisterResponseWrapper<LoginRegisterResponse> // 返回值
}

实现很简单,只需要使用 suspend 关键字修饰即可,这样 Retrofit 就能自动帮我们进行线程的切换了;

HttpReqeustManager

kotlin 复制代码
suspend fun loginCoroutine(
    username: String,
    password: String)
=
    APIClient.instance.instanceRetrofit(WanAndroidAPI::class.java)
        .loginActionCoroutine(username, password).data

这里我们就不需要使用 RxJava 来处理线程切换以及自定义 Response 处理数据了

RequestLoginViewModel

ViewModel 中我们直接调用 loginCoroutine 方法;

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

    .... 省略部分代码

    // 协程函数
    fun requestLoginCoroutine(context: Context, username: String, userpwd: String) {

        // GlobalScope(Dispatchers.Main) 全局作用域 默认是异步线程
        // viewModelScope.launch 默认是主线程 == (Dispatchers.Main)
        viewModelScope.launch {
            // 当前是主线程,可以弹框
            LoadingDialog.show(context)
            // 左边的是:主线程,右边:异步线程
            val dataResult =  HttpRequestManager.instance.loginCoroutine(username, userpwd)
            // 当前是主线程,可以用 setValue 更新状态
            if (dataResult != null) {
                loginData1?.value = dataResult
            } else {
                loginData2?.value = "登录失败,发送了意外,请检查用户名与密码 或者你的代码"
            }
            // 当前是主线程,可以取消弹框
            LoadingDialog.cancel()
        }
    }
}

因为继承了 ViewModel ,所以我们可以直接使用 viewModelScope 来启动一个协程;

Activity 中直接调用这个 requestLoginCoroutine 方法;

less 复制代码
requestLoginViewModel?.requestLoginCoroutine( this@LoginActivity,
    loginViewModel?.userName?.value!!, loginViewModel?.userPwd?.value!!)

这里要注意 Retrofit 的版本号,要使用支持协程的版本

SearchFragment


我们接下来完善『搜索』模块;

fragment_search.xml

ini 复制代码
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="click"
            type="com.xiangxue.puremusic.ui.page.SearchFragment.ClickProxy" />

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

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black">

        <net.steamcrafted.materialiconlib.MaterialIconView
            android:id="@+id/btn_back"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:layout_marginStart="12dp"
            android:layout_marginTop="37dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:onClick="@{()->click.back()}"
            android:scaleType="center"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:materialIcon="arrow_left"
            app:materialIconColor="@color/white"
            app:materialIconSize="28dp" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginTop="37dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:text="@string/relearn_android"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="@color/white"
            app:layout_constraintBottom_toTopOf="@+id/tv_content"
            app:layout_constraintLeft_toRightOf="@+id/btn_back"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="18dp"
            android:layout_marginBottom="4dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:text="@string/learn_more_friends"
            android:textColor="@color/white"
            android:textSize="12sp"
            app:layout_constraintLeft_toRightOf="@+id/btn_back"
            app:layout_constraintTop_toBottomOf="@+id/tv_title" />

        <TextView
            android:id="@+id/btn_subsribe"
            drawable_radius="@{25}"
            drawable_solidColor="@{0xffFF7055}"
            android:layout_width="100dp"
            android:layout_height="32dp"
            android:layout_marginTop="37dp"
            android:layout_marginEnd="20dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:gravity="center"
            android:onClick="@{()->click.subscribe()}"
            android:text="@string/learn_more"
            android:textColor="@color/white"
            android:textSize="13sp"
            android:textStyle="bold"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <net.steamcrafted.materialiconlib.MaterialIconView
            android:id="@+id/ic"
            android:layout_width="242dp"
            android:layout_height="242dp"
            android:layout_marginTop="24dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:onClick="@{()->click.testNav()}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_content"
            app:materialIcon="magnify"
            app:materialIconColor="@color/white"
            app:materialIconSize="28dp" />

        <TextView
            android:id="@+id/tv_tip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="?attr/selectableItemBackground"
            android:onClick="@{()->click.testNav()}"
            android:text="@string/search_page_tip"
            android:textSize="16sp"
            android:visibility="gone"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/ic" />
        <TextView
            android:id="@+id/tv_test_download"
            drawable_radius="@{25}"
            drawable_solidColor="@{0xffFF7055}"
            android:layout_width="wrap_content"
            android:layout_height="32dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:onClick="@{()->click.testDownload()}"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            android:text="@string/test_download"
            android:textColor="@color/white"
            android:textSize="13sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_tip" />

        <TextView
            android:id="@+id/tv_test_lifecycle_download"
            drawable_radius="@{25}"
            drawable_solidColor="@{0xffFF7055}"
            android:layout_width="wrap_content"
            android:layout_height="32dp"
            android:layout_marginTop="12dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:onClick="@{()->click.testLifecycleDownload()}"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            android:text="@string/test_lifecycle_download"
            android:textColor="@color/white"
            android:textSize="13sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_test_download" />

        <!-- 频繁更新 刷新 -->
        <SeekBar
            android:id="@+id/pb"
            android:layout_width="0dp"
            android:layout_height="20dp"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:background="@color/white"
            android:progress="@{vm.progress}"
            android:progressDrawable="@drawable/progressbar_color"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_test_lifecycle_download" />

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

布局很简单~

SearchViewModel

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

    // LiveData 还是选择 DataBinding 的 ObservableField
    // 使用的是 DataBinding 的 ObservableField
    // 1.更新很频繁,因为要把进度更新到拖动条
    // 2.界面可见和不可见,都必须执行,所以不能用 LiveData
    @JvmField
    val progress = ObservableField<Int>()
}

状态 ViewModel 用来观察下载进度;

DownloadViewModel

用来模拟下载的 ViewModel;

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

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

    // 请求仓库 模拟下载功能
    fun requestDownloadFile() = 
        HttpRequestManager.instance.downloadFile(downloadFileLiveData)
}

这里就是调用 downloadFile 方法来模拟下载;

downloadFile

kotlin 复制代码
fun downloadFile(liveData: MutableLiveData<DownloadFile>?) {
    val timer = Timer()
    val task: TimerTask = object : TimerTask() {
        override fun run() {
            //模拟下载,假设下载一个文件要 10秒、每 100 毫秒下载 1% 并通知 UI 层
            var downloadFile = liveData?.value
            if (downloadFile == null) {
                downloadFile = DownloadFile()
            }
            if (downloadFile.progress < 100) {
                downloadFile.progress += 1
                Log.d("TAG", "下载进度 " + downloadFile.progress + "%")
            } else {
                timer.cancel()
                downloadFile.progress = 0
                return
            }
            if (downloadFile.isForgive) {
                timer.cancel()
                downloadFile.progress = 0
                return
            }
            liveData?.postValue(downloadFile)
            downloadFile(liveData)
        }
    }
    timer.schedule(task, 100)
}

接下来,在 Framgent 中初始化 ViewModel 和 DataBinding

SearchFragment

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

    private var mBinding: FragmentSearchBinding? = null
    private var mSearchViewModel: SearchViewModel? = null // 搜索界面 相关的 VM
    private var mDownloadViewModel: DownloadViewModel? = null  // 下载相关的 VM

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

        mDownloadViewModel = getActivityViewModelProvider(mActivity!!).get(DownloadViewModel::class.java)
        mSearchViewModel = getFragmentViewModelProvider(this).get(SearchViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val view: View = inflater.inflate(R.layout.fragment_search, container, false)
        mBinding = FragmentSearchBinding.bind(view)
        mBinding?.click = ClickProxy() // 设置监听
        mBinding?.vm = mSearchViewModel // 设置 自身VM // todo Status ViewModel
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 就是不需要Lifecycle观察 宿主生命周期变化
        mDownloadViewModel?.downloadFileLiveData?.observeForever({
            // 修改了 StatusViewModel
            // 让自身的VM 数据发送变化, 那么布局就会感应变化
            mSearchViewModel?.progress?.set(it.progress)
        })
    }

    inner class ClickProxy {

        private val PATH2 = "http://www.baidu.com/" // 网页

        // 跳转 加载一个网页
        fun testNav()  = startPath()
        // 跳转 加载一个网页
        fun subscribe() = startPath()
        // 返回
        fun back() {
            nav().navigateUp() // back键的时候,返回上一个界面
        }

        private fun startPath() {
            val uri = Uri.parse(PATH2)
            val intent = Intent(Intent.ACTION_VIEW, uri)
            startActivity(intent)
        }

        // 测试下载,返回页面依然有效
        fun testLifecycleDownload() {
            mDownloadViewModel ?.requestDownloadFile()
        }

        // 测试下载,返回页面依然有效
        fun testDownload() {
            mDownloadViewModel ?.requestDownloadFile()
        }
    }
}

DrawerFragment


接下来,晚上侧边栏 Framgent;

首先来完成 xml,fragment_drawer.xml

ini 复制代码
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

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

        <variable
            name="click"
            type="com.xiangxue.puremusic.ui.page.DrawerFragment.ClickProxy" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black">

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_logo"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="40dp"
            android:onClick="@{()->click.logoClick()}"
            android:src="@drawable/ic_launcher"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_app"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:background="?attr/selectableItemBackground"
            android:onClick="@{()->click.logoClick()}"
            android:text="@string/app_name"
            android:textColor="@color/white"
            android:textSize="20sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/iv_logo" />

        <TextView
            android:id="@+id/tv_summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:background="?attr/selectableItemBackground"
            android:onClick="@{()->click.logoClick()}"
            android:text="@string/app_summary"
            android:textColor="@color/white"
            android:textSize="12sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_app" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="24dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toTopOf="@+id/tv_copyright"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_summary" />

        <TextView
            android:id="@+id/tv_copyright"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:onClick="@{()->click.logoClick()}"
            android:text="@string/Copyright"
            android:textColor="@color/white"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/rv" />

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

还是之前的规则,我们需要两个 ViewModel,一个是状态 ViewModel,一个是业务 ViewModel

InfoRequestViewModel

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

    var libraryLiveData: MutableLiveData<List<LibraryInfo>>? = null
        get() {
            if (field == null) {
                field = MutableLiveData()
            }
            return field
        }
        private set

    fun requestLibraryInfo() {
        // 调用仓库
        HttpRequestManager.instance.getLibraryInfo(libraryLiveData)
    }
}

DrawerViewModel

kotlin 复制代码
class DrawerViewModel : ViewModel() {
    // todo
}

DrawerFragment

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

    private var mBinding: FragmentDrawerBinding? = null
    private var mDrawerViewModel: DrawerViewModel? = null // Status ViewModel 独一份
    private var infoRequestViewModel: InfoRequestViewModel? = null // Request ViewModel

    private var mAdapter: SimpleBaseBindingAdapter<LibraryInfo?, AdapterLibraryBinding?>? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        mActivity = context as AppCompatActivity
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mDrawerViewModel = getFragmentViewModelProvider(this).get(DrawerViewModel::class.java)
        infoRequestViewModel = getFragmentViewModelProvider(this).get(InfoRequestViewModel::class.java)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View { // DataBinding 与 ViewModel关联
        val view: View = inflater.inflate(R.layout.fragment_drawer, container, false)
        mBinding = FragmentDrawerBinding.bind(view)
        mBinding?.vm = mDrawerViewModel
        mBinding?.click = ClickProxy() // 点击
        return view
    }

    // 获取 Item数据集合
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mAdapter = object : SimpleBaseBindingAdapter<LibraryInfo?, AdapterLibraryBinding?>(
            context,
            R.layout.adapter_library
        ) {

            override fun onSimpleBindItem(
                binding: AdapterLibraryBinding?,
                item: LibraryInfo?,
                holder: RecyclerView.ViewHolder?
            ) {
                // 把数据 设置好,就显示数据了
                binding?.info = item
                binding?.root?.setOnClickListener {
                    Toast.makeText(mContext, "哎呀,还在研发中...", Toast.LENGTH_SHORT).show()
                }
            }
        }

        // 设置适配器 到 RecyclerView
        mBinding?.rv?.adapter = mAdapter
        infoRequestViewModel?.libraryLiveData?.observe(viewLifecycleOwner, { libraryInfos ->
            mAdapter?.list = libraryInfos
            mAdapter?.notifyDataSetChanged()
        })

        // 请求数据 调用 Request 的 ViewModel 加载数据
        infoRequestViewModel?.requestLibraryInfo()
    }

    inner class ClickProxy {
        fun logoClick() = Toast.makeText(mActivity, "哎呀,你能不能不要乱点啊,程序员还在玩命编码中...", Toast.LENGTH_SHORT).show()
    }
}

好了,整体大体功能就完成了

欢迎三连


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

相关推荐
MettBarr16 小时前
Jetpack Lifecycle 的本质
android jetpack
alexhilton1 天前
用Compose中的Shader实现一个雪花飘飘弹窗效果
android·kotlin·android jetpack
刘龙超2 天前
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(四)登录注册
android jetpack
刘龙超4 天前
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(三)播放能力
android jetpack
我命由我123455 天前
Android 开发问题:The specified child already has a parent.
android·java·开发语言·java-ee·android jetpack·android-studio·android runtime
Wgllss7 天前
完整案例:Kotlin+Compose+Multiplatform之桌面端音乐播放器,数据库使用实现(三)
android·架构·android jetpack
木子予彤8 天前
Compose 手势处理全面解析
android jetpack
alexhilton8 天前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小白马丶9 天前
Jetpack Compose开发框架搭建
android·前端·android jetpack