前期回顾
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(一)基础搭建
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(二)音乐列表
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(三)播放能力
前言

本章继续前面的章节来完善音乐播放器;
本章我们使用 Jetpack 全家桶 + RxJava 来完善下『注册』+『登录』的能力;
注册
我们依然使用 DataBinding 来布局 xml
activity_user_register.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- ViewModel 的绑定操作 -->
<variable
name="vm"
type="com.llc.puremusic.bridge.state.RegisterViewModel" />
<!-- 布局的点击事件,全部交给 DataBinding 管理 -->
<variable
name="click"
type="com.llc.puremusic.RegisterActivity.ClickClass" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/root_bg_color"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/title_bar_height"
android:background="@color/top_bg_color">
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="注册"
android:textColor="@color/white"
android:textSize="@dimen/title_main_text_size" />
</RelativeLayout>
<!-- 面向 ViewModel 开发 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#f00"
android:textSize="20dp"
android:text="@{vm.registerState}"
android:layout_gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="30dp"
android:layout_marginRight="15dp"
android:background="@drawable/corner_login_bg"
android:gravity="center_vertical"
android:padding="10dp">
<EditText
android:id="@+id/user_phone_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:hint="账户"
android:inputType="text"
android:padding="10dp"
android:textSize="12sp"
android:text="@={vm.userName}"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:background="@drawable/corner_login_bg"
android:gravity="center_vertical"
android:padding="10dp">
<EditText
android:id="@+id/user_password_et"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:hint="密码"
android:inputType="textPassword"
android:padding="10dp"
android:textSize="12sp"
android:text="@={vm.userPwd}"
/>
<CheckBox
android:id="@+id/check_password_cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@drawable/check_user_password_selector" />
</LinearLayout>
<Button
android:id="@+id/user_register_bt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:background="@drawable/corners_login_bg"
android:text="注 册"
android:textColor="@color/white"
android:onClick="@{()->click.registerAction()}"/>
<TextView
android:id="@+id/user_agreement_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:drawableLeft="@drawable/consent_icon"
android:drawablePadding="5dp"
android:text="建议:把注册的信息牢牢记住,以免忘记!" />
</LinearLayout>
</layout>
布局的创建比较简单,就是两个输入框,一个按钮,进行用户名,密码的注册填入;
RegisterViewModel
接下来我们来完善注册唯一的 ViewModel
kotlin
class RegisterViewModel : ViewModel() {
// 用户名
@JvmField
val userName = MutableLiveData<String>()
// 密码
@JvmField
val userPwd = MutableLiveData<String>()
// 注册成功,注册失败 等等
val registerState = MutableLiveData<String>()
// 默认初始化
init {
userName.value = ""
userPwd.value = ""
registerState.value = ""
}
}
注册的 ViewModel 还是比较简单的,用来管理用户名和密码,以及注册后的状态;
RequestRegisterViewModel
接下来,我们来晚上发起『注册』请求的 ViewModel,用来发起网络请求,并返回注册结果;
kotlin
class RequestRegisterViewModel : ViewModel() {
// 注册成功的状态 LiveData
var registerSuccessData : MutableLiveData<LoginRegisterResponse>? = null
get() {
if (field == null) {
field = MutableLiveData()
}
return field
}
private set
// 注册失败的状态 LiveData
var registerFailData : MutableLiveData<String>? = null
get() {
if (field == null) {
field = MutableLiveData()
}
return field
}
private set
// 发起注册的网络请求,并通过 LiveData 观察注册结果
fun requestRegister(context: Context, username: String, userpwd: String, reuserpwd: String) {
HttpRequestManager.instance.register(context, username, userpwd, reuserpwd, registerSuccessData, registerFailData)
}
}
LoginRegisterResponse
注册返回结果 bean,登录和注册这两个能力可以使用同一个;
kotlin
data class LoginRegisterResponse(val admin: Boolean,
val chapterTops: List<*>,
val collectIds: List<*>,
val email: String ?,
val icon: String?,
val id: String?,
val nickname: String?,
val password: String?,
val publicName: String?,
val token: String?,
val type: Int,
val username: String?
)
register
接下来,我们来完善注册的网络请求能力;
我们借助 Retrofit + RxJava 来完成网络请求;
API
先来声明创建 Retrofit 的 interface
less
interface WanAndroidAPI {
/**
* 登录API
* username=Derry-vip&password=123456
*/
@POST("/user/login")
@FormUrlEncoded
fun loginAction(@Field("username") username: String,
@Field("password") password: String)
: Observable<LoginRegisterResponseWrapper<LoginRegisterResponse>> // 返回值
/**
* 注册的API
*/
@POST("/user/register")
@FormUrlEncoded
fun registerAction(@Field("username") username: String,
@Field("password") password: String,
@Field("repassword") repassword: String)
: Observable<LoginRegisterResponseWrapper<LoginRegisterResponse>> // 返回值
}
LoginRegisterResponseWrapper
这里对返回结果进行了一层包装,是为了兼容服务端返回的异常不规范信息;
kotlin
data class LoginRegisterResponseWrapper<T>(val data: T, val errorCode: Int, val errorMsg: String)
APIResponse
接下来,我们对 Retrofit 返回的 RxJava 数据进行处理和解析;
kotlin
abstract class APIResponse<T>(val context: Context) : Observer<LoginRegisterResponseWrapper<T>> {
private var isShow: Boolean = true
// 次构造
constructor(context: Context, isShow: Boolean = false) : this(context) {
this.isShow = isShow
}
// 成功 怎么办
abstract fun success(data: T?)
// 失败 怎么办
abstract fun failure(errorMsg: String?)
// 启点分发的时候
override fun onSubscribe(d: Disposable) {
// 弹出 加载框
if (isShow) {
LoadingDialog.show(context)
}
}
// 上游流下了的数据 我当前层 获取到了 上一层 流下来的 包装Bena == t: LoginRegisterResponseWrapper<T>
override fun onNext(t: LoginRegisterResponseWrapper<T>) {
if (t.data == null) {
// 失败
failure("登录失败了,请检查原因:msg:${t.errorMsg}")
} else {
// 成功
success(t.data)
}
}
// 上游流下了的错误
override fun onError(e: Throwable) {
// 取消加载
LoadingDialog.cancel()
failure(e.message)
}
// 停止
override fun onComplete() {
// 取消加载
LoadingDialog.cancel()
}
}
Retrofit 封装
kotlin
class APIClient {
private object Holder {
val INSTANCE = APIClient()
}
// 派生
companion object {
val instance = Holder.INSTANCE
}
fun <T> instanceRetrofit(apiInterface: Class<T>) : T {
// OKHttpClient请求服务器
val mOkHttpClient = OkHttpClient().newBuilder().myApply {
// 添加读取超时时间
readTimeout(10000, TimeUnit.SECONDS)
// 添加连接超时时间
connectTimeout(10000, TimeUnit.SECONDS)
// 添加写出超时时间
writeTimeout(10000, TimeUnit.SECONDS)
}.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com")
// 请求方 ←
.client(mOkHttpClient)
// 响应方 →
// Response 回来
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // RxJava来处理
.addConverterFactory(GsonConverterFactory.create()) // Gson 来解析 --- JavaBean
.build()
return retrofit.create(apiInterface)
}
}
// 默认 无参数的
fun <T> T.myApply(mm: T.() -> Unit) : T {
// T == this
mm()
return this
}
很基础的封装,接下来我们来调用这个发起网络请求,并处理响应结果;
register 发起请求
kotlin
fun register(
context: Context,
username: String,
password: String,
repassword: String,
dataLiveData1: MutableLiveData<LoginRegisterResponse>,
dataLiveData2: MutableLiveData<String>) {
// RxJava封装网络模型
APIClient.instance.instanceRetrofit(WanAndroidAPI::class.java)
.registerAction(username, password, repassword)
.subscribeOn(Schedulers.io()) // 给上面的代码分配异步线程
.observeOn(AndroidSchedulers.mainThread()) // 给下面的代码分配 安卓的主线程
.subscribe(object : APIResponse<LoginRegisterResponse>(context) {
override fun success(data: LoginRegisterResponse?) { // RxJava自定义操作符过滤后的
dataLiveData1.value = data // MVVM
}
override fun failure(errorMsg: String?) { // RxJava自定义操作符过滤后的
dataLiveData2.value = errorMsg // MVVM
}
})
}
RegisterActivity
我们接下来初始化 ViewModel,以及观察 LiveData 的变化并做出相应的处理;
kotlin
class RegisterActivity : BaseActivity() {
var mainBinding : ActivityUserRegisterBinding? = null // 当前Register的布局
var registerViewModel: RegisterViewModel? = null // ViewModel
// Reqeust ViewModel
var requestRegisterViewModel : RequestRegisterViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
hideActionBar()
registerViewModel = getActivityViewModelProvider(this).get(RegisterViewModel::class.java) // 状态VM
requestRegisterViewModel = getActivityViewModelProvider(this).get(RequestRegisterViewModel::class.java) // 请求VM
mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_register) // 初始化DB
mainBinding?.lifecycleOwner = this
mainBinding?.vm = registerViewModel // DataBinding绑定 ViewModel
mainBinding?.click = ClickClass() // 布局建立点击事件
// 盯着 requestRegisterViewModel 监听成功了吗
requestRegisterViewModel?.registerData1?.observe(this, {
registerSuccess(it)
})
// 盯着 requestRegisterViewModel 监听失败了吗
requestRegisterViewModel?.registerData2?.observe(this, {
registerFailed(it)
})
}
private fun registerSuccess(registerBean: LoginRegisterResponse?) {
Toast.makeText(this, "注册成功😀", Toast.LENGTH_SHORT).show()
registerViewModel?.registerState ?.value = "恭喜 ${registerBean?.username} 用户,注册成功"
// 注册成功,直接进入登录界面
startActivity(Intent(this, LoginActivity::class.java))
}
private fun registerFailed(errorMsg: String?) {
Toast.makeText(this, "注册失败~ 呜呜呜", Toast.LENGTH_SHORT).show()
registerViewModel?.registerState?.value = "骚瑞 注册失败,错误信息是:${errorMsg}"
}
inner class ClickClass {
// 点击事件,注册的函数
fun registerAction() {
if (registerViewModel?.userName?.value.isNullOrBlank() || registerViewModel?.userPwd?.value.isNullOrBlank()) {
registerViewModel?.registerState ?.value = "用户名或密码为空,请你好好检查"
return
}
requestRegisterViewModel?.requestRegister(
this@RegisterActivity,
registerViewModel?.userName?.value,
registerViewModel?.userPwd?.value,
registerViewModel?.userPwd?.value
)
}
}
}
Activity 中只需要进行 ViewModel 的初始化,以及 LiveData 的 observe 监听,并做出 UI 上的改变;
登录
登录和注册流程其实是一样,只是调用的 api 不一样;
activity_user_login.xml
ini
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!-- ViewModel 的绑定操作 -->
<variable
name="vm"
type="com.llc.puremusic.bridge.state.LoginViewModel" />
<!-- 布局的点击事件集 -->
<variable
name="click"
type="com.llc.puremusic.LoginActivity.ClickClass" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/root_bg_color"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/title_bar_height"
android:background="@color/top_bg_color">
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="登录"
android:textColor="@color/white"
android:textSize="@dimen/title_main_text_size" />
<TextView
android:id="@+id/user_register_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:padding="10dp"
android:text="注册"
android:onClick="@{()->click.startToRegister()}"
android:textColor="@color/white"
android:textSize="14dp" />
</RelativeLayout>
<!-- 登录成功 登录失败 的 各种状态 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#f00"
android:textSize="20dp"
android:text="@{vm.loginState}"
android:layout_gravity="center"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="30dp"
android:layout_marginRight="15dp"
android:background="@drawable/corner_login_bg"
android:gravity="center_vertical"
android:padding="10dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/login_user_name_icon" />
<EditText
android:id="@+id/user_phone_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:hint="账户"
android:inputType="text"
android:padding="10dp"
android:textSize="12sp"
android:text="@={vm.userName}"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:background="@drawable/corner_login_bg"
android:gravity="center_vertical"
android:padding="10dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/login_user_password_icon" />
<EditText
android:id="@+id/user_password_et"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:hint="密码"
android:inputType="textPassword"
android:padding="10dp"
android:textSize="12sp"
android:text="@={vm.userPwd}"
/>
<CheckBox
android:id="@+id/check_password_cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@drawable/check_user_password_selector" />
</LinearLayout>
<Button
android:id="@+id/user_login_bt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:background="@drawable/corners_login_bg"
android:text="登 录"
android:onClick="@{()->click.loginAction()}"
android:textColor="@color/white" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="忘记密码?"
android:textColor="@color/main_color" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:paddingRight="15dp"
android:text="手机快捷登录"
android:textColor="@color/main_color" />
</RelativeLayout>
</LinearLayout>
</layout>
区别就是这里需要一个『注册』的点击事件;
LoginViewModel
ini
class LoginViewModel : ViewModel() {
@JvmField
val userName = MutableLiveData<String>()
val userPwd = MutableLiveData<String>()
val loginState = MutableLiveData<String>()
init {
userName.value = ""
userPwd.value = ""
loginState.value = ""
}
}
RequestLoginViewModel
kotlin
class RequestLoginViewModel : ViewModel() {
var registerData1: MutableLiveData<LoginRegisterResponse>? = null
get() {
if (field == null) {
field = MutableLiveData<LoginRegisterResponse>()
}
return field
}
private set
var registerData2: MutableLiveData<String>? = null
get() {
if (field == null) {
field = MutableLiveData<String>()
}
return field
}
private set
fun requestLogin(context: Context, username: String, userpwd: String, reuserpwd: String) {
HttpRequestManager.instance.login(context, username, userpwd, registerData1, registerData2)
}
}
Login
kotlin
fun login(
context: Context,
username: String,
password: String,
dataLiveData1: MutableLiveData<LoginRegisterResponse>,
dataLiveData2: MutableLiveData<String>) {
// RxJava封装网络模型
APIClient.instance.instanceRetrofit(WanAndroidAPI::class.java)
.loginAction(username, password)
.subscribeOn(Schedulers.io()) // 给上面的代码分配异步线程
.observeOn(AndroidSchedulers.mainThread()) // 给下面的代码分配 安卓的主线程
.subscribe(object : APIResponse<LoginRegisterResponse>(context) {
override fun success(data: LoginRegisterResponse?) {
dataLiveData1.value = data // MVVM
}
override fun failure(errorMsg: String?) {
dataLiveData2.value = errorMsg // MVVM
}
})
}
接下来依然是在 Activity 中初始化 ViewModel 以及 LiveData 的 observe 监听;
LoginActivity
kotlin
class LoginActivity : BaseActivity() {
private var mainBinding: ActivityUserLoginBinding? = null // 当前Register的布局
private var loginViewModel: LoginViewModel? = null // ViewModel
var requestLoginViewModel : RequestLoginViewModel? = null // TODO Reqeust ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
hideActionBar()
loginViewModel = getActivityViewModelProvider(this).get(LoginViewModel::class.java) // State ViewModel初始化
requestLoginViewModel = getActivityViewModelProvider(this).get(RequestLoginViewModel::class.java) // Request ViewModel初始化
mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_login) // DataBinding初始化
mainBinding?.lifecycleOwner = this
mainBinding?.vm = loginViewModel // 绑定ViewModel与DataBinding关联
mainBinding?.click = ClickClass() // DataBinding关联 的点击事件
// 登录成功 眼睛监听 成功
requestLoginViewModel?.registerData1?.observe(this, {
loginSuccess(it)
})
// 登录失败 眼睛监听 失败
requestLoginViewModel?.registerData2?.observe(this, {
loginFailure(it)
})
}
// 响应的两个函数
private fun loginSuccess(registerBean: LoginRegisterResponse?) {
Toast.makeText(this@LoginActivity, "登录成功😀", Toast.LENGTH_SHORT).show()
loginViewModel?.loginState?.value = "恭喜 ${registerBean?.username} 用户,登录成功"
// 登录成功 在跳转首页之前,需要 保存 登录的会话
// 保存登录的临时会话信息
mSharedViewModel.session.value = Session(true, registerBean)
// 跳转到首页
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
}
private fun loginFailure(errorMsg: String?) {
Toast.makeText(this@LoginActivity, "登录失败 ~ 呜呜呜", Toast.LENGTH_SHORT).show()
loginViewModel?.loginState?.value = "骚瑞 登录失败,错误信息是:${errorMsg}"
}
inner class ClickClass {
// 点击事件,登录的函数
fun loginAction() {
if (loginViewModel?.userName?.value.isNullOrBlank() || loginViewModel?.userPwd?.value.isNullOrBlank()) {
loginViewModel ?.loginState ?.value = "用户名或密码为空,请你好好检查"
return
}
requestLoginViewModel?.requestLogin(
this@LoginActivity,
loginViewModel !!.userName.value!!,
loginViewModel !!.userPwd.value!!,
loginViewModel !!.userPwd.value!!
)
}
// 跳转到 注册界面
fun startToRegister() = startActivity(Intent(this@LoginActivity, RegisterActivity::class.java))
}
}
保存&获取登录会话
接下来,我们就需要保存登录成功的信息,以及贯穿整个场景,不需要每个页面都触发登录,所以我们需要在 SharedViewModel 中更新会话信息;
接下来构建 Session 信息;
Session
kotlin
data class Session constructor(val isLogin: Boolean, val loginRegisterResponse: LoginRegisterResponse?)
SharedViewModel
然后 SharedViewModel 中创建 Session 的 LiveData;
kotlin
class SharedViewModel : ViewModel() {
... 省略部分代码
// 保存登录信息的临时会话需要贯穿整个项目,所以是共享的,这里不能剔除粘性
val session = MutableLiveData<Session>()
}
loginSuccess
接下来,LoginActivity 的 loginSuccess 方法中,更新 Session 信息;
kotlin
private fun loginSuccess(registerBean: LoginRegisterResponse?) {
....省略部分代码
// 登录成功 在跳转首页之前,需要 保存 登录的会话
// 保存登录的临时会话信息
mSharedViewModel.session.value = Session(true, registerBean)
....省略部分代码
}
MainViewModel
然后,我们可以在 MainFragment 中获取登录信息,并展示,修改我们的 framgent_main.xml 和 MainViewModel 来展示信息;
kotlin
class MainViewModel : ViewModel() {
....省略部分代码
// 登录信息的临时数据
@JvmField
val loginSessionInfo = ObservableField<String>()
}
framgent_main.xml
xml 中增加一个 textView 用来展示信息;
ini
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.loginSessionInfo}"
android:textColor="#E91E63"
android:layout_marginStart="12dp"
android:layout_gravity="center"
/>
MainFragment
然后,在 MainFragment 中监听 SharedViewModel 中的 session 的变化;
kotlin
sharedViewModel.session.observe(viewLifecycleOwner, {
Log.d("Mars", "登录Session的变化,只要发生改变,就需要更新到界面UI中")
// 更新UI,面向 ViewModel 实例修改,UI就变了
mainViewModel?.loginSessionInfo?.set(
if (it.isLogin) "登录成功,欢迎${it.loginRegisterResponse ?.username}来到此系统"
else "你未登录")
})
好了,登录注册就写到这里吧~
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~