前期回顾
如何应对 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()
}
}
好了,整体大体功能就完成了
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~