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

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

欢迎三连


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

相关推荐
未来猫咪花7 小时前
LiveData "数据倒灌":一个流行的错误概念
android·android jetpack
alexhilton2 天前
借助RemoteCompose开发动态化页面
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
QING6183 天前
Jetpack Compose 中 Flow 收集详解 —— 新手指南
android·kotlin·android jetpack
ljt27249606614 天前
Compose笔记(五十七)--snapshotFlow
android·笔记·android jetpack
QING6184 天前
kotlin 协程: GlobalScope 和 Application Scope 选择和使用 —— 新手指南
android·kotlin·android jetpack
QING6184 天前
Kotlin 协程中Job和SupervisorJob —— 新手指南
android·kotlin·android jetpack
天花板之恋4 天前
Compose中的协程:rememberCoroutineScope 和 LaunchedEffect
android jetpack
我命由我123455 天前
Android 开发问题:布局文件中的文本,在预览时有显示出来,但是,在应用中没有显示出来
android·java·java-ee·android studio·android jetpack·android-studio·android runtime