前言
最近在学习Android推荐应用架构,抽空写了一个登录例子。
首先,请查看下图,该图显示了所有模块应如何彼此交互
- View:Activity/Fragment
- ViewModel: ViewModel + LivaData
- Repository:Repository一般用来进行复杂的数据转换和处理,依赖于DataSource提供 本地持久性数据 和 服务端数据
注意点:
- 分离关注点、在应用的各个模块之间设定明确定义的职责界限。
- ViewModel用于持有和管理界面相关的数据,让数据可在发生屏幕旋转等配置更改后继续
- ViewModel 不能持有 View层引用,包括Context也不能持有,如果需要建议从构造注入,或者使用AndroidViewModel。
- 通过模型驱动界面(最好是持久性模型),应用所基于的模型类应明确定义数据管理职责
一、 Repository实现
1.1 User
定义User的data class
data class LoggedInUser(
@SerializedName("id")
var id: Int = 0,
@SerializedName("email")
var email: String = "",
@SerializedName("nickname")
var nickname: String = "",
@SerializedName("username")
var username: String = ""
)
1.2 ApiService
interface ApiService {
@POST("user/login")
@FormUrlEncoded
suspend fun login(
@Field("username") username: String,
@Field("password") password: String
): ApiResponse<LoggedInUser>
@GET("user/logout/json")
suspend fun logout(): ApiResponse<Any>
companion object {
private const val BASE_URL = "https://www.wanandroid.com"
fun create(): ApiService {
val logger = HttpLoggingInterceptor().also {
it.level =
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
val context = App.instance.applicationContext
val cookieJar =
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context))
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.cookieJar(cookieJar)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
}
1.3 ApiResult
网络返回进行了包装和处理,网络异常转换成业务提示
sealed class ApiResult<T> {
data class SuccessResult<T>(
val data: T?,
val errorCode: Int = -1,
val errorMsg: String = ""
) : ApiResult<T>()
data class ErrorResult<T>(val errorCode: Int, val errorMsg: String) : ApiResult<T>()
inline fun <reified T> ApiResult<T>.onSuccess(action: (T?) -> Unit): ApiResult<T> {
if (this is SuccessResult) action(data)
return this
}
inline fun <reified T> ApiResult<T>.onError(action: (code: Int, message: String) -> Unit): ApiResult<T> {
if (this is ErrorResult) action(errorCode, errorMsg)
return this
}
companion object {
const val SUCCESS_CODE = 0
fun <T> create(throwable: Throwable): ApiResult<T> {
// 网络异常转换成业务提示
val errorMsg = ErrorMsgFactory.create(throwable = throwable)
return ErrorResult(errorMsg.code, errorMsg.message)
}
fun <T> create(response: ApiResponse<T>): ApiResult<T> {
return if (response.succeeded) {
// Tips 这里可以根据Code发送EventBus
SuccessResult(response.data, response.errorCode, response.errorMsg)
} else {
ErrorResult(response.errorCode, response.errorMsg)
}
}
}
}
1.4 DataSource
提供DataSource提供数据
class LoginDataSource(
private val service: ApiService,
private val preferences: SharedPreferences
) {
suspend fun login(username: String, password: String): ApiResult<LoggedInUser> {
return try {
val response = service.login(username = username, password = password)
ApiResult.create(response)
} catch (e: Throwable) {
ApiResult.create(e)
}
}
}
1.5 Repository
Repository 一般用来进行复杂的数据转换和处理,如果数据复杂推荐使用Flow
class LoginRepository(val dataSource: LoginDataSource) {
suspend fun login(username: String, password: String): Result<LoggedInUser> {
// handle login
val result = dataSource.login(username, password)
return when (result) {
is ApiResult.SuccessResult -> {
setLoggedInUser(result.data)
Result.Success(result.data)
}
is ApiResult.ErrorResult -> {
Result.Error(result.errorMsg)
}
}
}
}
二、 ViewModel实现
2.1 ViewModel
以前在 ViewModel 里使用 LiveData。我们需要以下步骤
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login(username: String, password: String) {
viewModelScope.launch {
// can be launched in a separate asynchronous job
val result = loginRepository.login(username, password)
result.run {
onSuccess {
_loginResult.value =
LoginResult(success = LoggedInUserView(displayName = it?.username.toString()))
}
onError {
_loginResult.value = LoginResult(error = it)
}
}
}
}
- 准备一个 ViewModel 私有的 MutableLiveData (MLD)
- 暴露一个不可变的 LiveData
- 启动协程,然后将其操作结果赋给 MLD
在 LifeCycle 2.2.0 之后,提供了LiveData 协程构造方法 (coroutine builder),以更加精简的方式实现
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
fun login(username: String, password: String) = liveData {
// can be launched in a separate asynchronous job
val result = loginRepository.login(username, password)
Log.e("LoginViewModel", Thread.currentThread().name)
result.run {
onSuccess {
emit(LoginResult(success = LoggedInUserView(displayName = it?.username.toString())))
}
onError {
emit(LoginResult(error = it))
}
}
}
fun loginDataChanged(username: String, password: String) = liveData {
if (!isUserNameValid(username)) {
emit(LoginFormState(usernameError = R.string.invalid_username))
} else if (!isPasswordValid(password)) {
emit(LoginFormState(passwordError = R.string.invalid_password))
} else {
emit(LoginFormState(isDataValid = true))
}
}
// A placeholder username validation check
private fun isUserNameValid(username: String): Boolean {
return if (username.contains("@")) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank()
}
}
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.length > 5
}
}
2.1 ViewModelFactory
构造ViewModel需要Repository,所以通过ViewModelFactory注入必要的依赖
class ViewModelFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
val service = ApiService.create()
return LoginViewModel(
loginRepository = Injection.provideDataRepository()
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
UI实现
表单验证数据状态数据类
data class LoginFormState(
val usernameError: Int? = null,
val passwordError: Int? = null,
val isDataValid: Boolean = false
)
登录返回数据类
data class LoginResult(
val success: LoggedInUserView? = null,
val error: String? = null
)
class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
private val loginViewModel: LoginViewModel by viewModels {
ViewModelFactory()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setEvent()
}
private fun setEvent() {
registerEditTextChanged(binding.username, binding.password)
binding.password.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
login()
}
false
}
binding.login.setOnClickListener {
binding.loading.visibility = View.VISIBLE
login()
}
}
private fun registerEditTextChanged(vararg edits: EditText) {
for (e in edits) {
e.doAfterTextChanged {
validator()
}
}
}
private fun login() {
loginViewModel.login(
binding.username.text.toString(),
binding.password.text.toString()
).observe(viewLifecycleOwner) { loginResult ->
binding.loading.visibility = View.GONE
loginResult.error?.let {
showLoginFailed(it)
}
loginResult.success?.let {
updateUiWithUser(it)
}
}
}
private fun validator() {
loginViewModel.loginDataChanged(
binding.username.text.toString(),
binding.password.text.toString()
).observe(viewLifecycleOwner) { loginFormState ->
binding.login.isEnabled = loginFormState.isDataValid
loginFormState.usernameError?.let {
binding.username.error = getString(it)
}
loginFormState.passwordError?.let {
binding.password.error = getString(it)
}
}
}
private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome) + model.displayName
Toast.makeText(requireContext(), welcome, Toast.LENGTH_LONG).show()
}
private fun showLoginFailed(errorString: String) {
Toast.makeText(requireContext(), errorString, Toast.LENGTH_LONG).show()
}
}
总结
使用了WanAndroid的open Api,在此先感谢大佬无私贡献!
示例代码地址:github.com/Li-Lee-Roc/…
记得点赞哦!