如何应对 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()
    }
}

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

欢迎三连


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

相关推荐
雨白1 天前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack
我命由我123451 天前
Android 开发 - Android JNI 开发关键要点
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton2 天前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
峰哥的Android进阶之路2 天前
viewModel机制及原理总结
android jetpack
我命由我123453 天前
Android WebView - loadUrl 方法的长度限制
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Coffeeee3 天前
面试被问到Compose的副作用不会,只怪我没好好学
android·kotlin·android jetpack
Frank_HarmonyOS6 天前
Android APP 的压力测试与优化
android jetpack
QING6187 天前
Jetpack Compose 条件布局与 Layout 内在测量详解
android·kotlin·android jetpack
Lei活在当下8 天前
【现代 Android APP 架构】09. 聊一聊依赖注入在 Android 开发中的应用
java·架构·android jetpack
bqliang8 天前
Jetpack Navigation 3:领航未来
android·android studio·android jetpack