前言
很久没更新简书了,之前写的
这套东西是基于java的,然后emmm已经很久没优化了,8月份也看到朋友的指正,确实有很多问题,虽然现在这套基于kotlin的框架也有些同样的问题,但优化也是时间问题。
简介
现在是一个新项目,在建立这套结构的时候也参考了一些google官方的demo
如sunflower
由于当前项目也不是很复杂
UI:databinding + rxbinding4
数据传递: UnPeekLiveData
网络请求:okhttp3 + Retrofit + Rxjava/flow
结构说明
我们先来看一下官方demo的代码
// demo来自sunflower
// view(GalleryFragment.kt)
@AndroidEntryPoint // 这里通过依赖注入
class GalleryFragment : Fragment() {
private val adapter = GalleryAdapter()
private val args: GalleryFragmentArgs by navArgs()
private var searchJob: Job? = null
private val viewModel: GalleryViewModel by viewModels() // 不用手动初始化viewModels
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentGalleryBinding.inflate(inflater, container, false)
context ?: return binding.root
binding.photoList.adapter = adapter // 绑定适配器
search(args.plantName)
binding.toolbar.setNavigationOnClickListener { view ->
view.findNavController().navigateUp()
}
return binding.root
}
private fun search(query: String) {
// Make sure we cancel the previous job before creating a new one
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModel.searchPictures(query).collectLatest { // 发起网络请求
adapter.submitData(it)
}
}
}
}
这里实现的是通过网络接口,获取图片列表,并通过Paging展示,这里就完全只关心ui逻辑
// viewModel (GalleryViewModel.kt)
@HiltViewModel // 依赖注入
class GalleryViewModel @Inject constructor(
private val repository: UnsplashRepository // 需要拿到数据管理器
) : ViewModel() {
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<UnsplashPhoto>>? = null
fun searchPictures(queryString: String): Flow<PagingData<UnsplashPhoto>> {
currentQueryValue = queryString
val newResult: Flow<PagingData<UnsplashPhoto>> =
repository.getSearchResultStream(queryString).cachedIn(viewModelScope)
currentSearchResult = newResult
// 这里通过数据管理器 获取pagingData
return newResult
}
}
这里就更简单了,这里只是负责链接view和models,而这里的models又通过Repository(数据管理器)来进行管理
// Repository(UnsplashRepository.kt)
class UnsplashRepository @Inject constructor(private val service: UnsplashService) {
fun getSearchResultStream(query: String): Flow<PagingData<UnsplashPhoto>> {
return Pager(
config = PagingConfig(enablePlaceholders = false, pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { UnsplashPagingSource(service, query) }
).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 25
}
}
这里我们只关心输入和输出, 输入的是请求的参数, query 输出的是 Flow, 那这里就可以看成一个数据管理中心,他负责处理需要异步获取的数据,无论是网络请求,还是来自数据库,亦或是本地文件。
我的代码
通过上面的sunflower的代码,我们可以看到这个结构其实很简单,通过依赖注入,将整个程序贯穿成
View(Activity||Fragment) + ViewModel (LiveData || Date || Flow || Flowable(RxJava)) + Repository (请求数据的方法 无论是通过Service || Room)
建立起上述的想法后,现阶段给予kotlin的架构就很容易搭建了
鉴于之前用livedata出现过数据倒灌
这里使用了 const val unpeek = "com.kunminx.arch:unpeek-livedata:$version" 来避免之前的问题
废话不多说 还是通过登录业务 来展示一下给予kotlin的结构
// LoginActivity.kt (View) 这里屏蔽一下非关键代码
/**
* phone number login
* 手机登录页面
*/
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
val viewModel: LoginViewModel by viewModels() // 通过依赖注入生成的viewModel
// private lateinit var loading: LoadingDialog
@ObsoleteCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
//loading = //LoadingDialog.Builder(this).setMessage(getString(R.string.logging)).create()
binding.loginView.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
binding.loginView.recyclerView.adapter = EasyBaseAdapter(viewModel.loginDrawableList,
layoutCallBack = { R.layout.item_image },
convertCallBack = { holder, data, position ->
holder.getView<ImageView>(R.id.goods_img)?.setImageResource(data)
holder.getView<ImageView>(R.id.goods_img)?.setOnClickListener {
if(position == 0){
if(LoginUtils.checkAliPayInstalled(this)){
// 支付宝
val intent = Intent(this, PayDemoActivity::class.java)
intent.putExtra("OrderInfo", "123")
intent.putExtra("OrderType", 1) // 登录
startActivity(intent)
}else{
ToastUtil.showToast(this, "请先安装支付宝")
}
}else{
// 微信
}
}
})
// 登录按钮
binding.loginBtn.setOnClickListener {
checkLoginInfo()
}
// // 立即注册 进入注册页面
// binding.loginTv.setOnClickListener {
// startActivity(Intent(this, RegisterActivity::class.java))
// finish()
// }
// 通过账号密码登陆 进入账号登陆页面
binding.switchTv.setOnClickListener {
startActivity(Intent(this, LoginAccountActivity::class.java))
finish()
}
// 获取验证码
binding.sendCheckNumTv.setOnClickListener {
checkPhoneNum()
}
// 验证码返回 这里我把返回的LiveData存放在viewModel 其实你也可以不存 直接在请求的时候,将LiveData作为返回的参数, 我这样分开纯属习惯
viewModel.requestCaptchaDate.observe(this, Observer {
if( it.code == 0) {
binding.checkNumEdt.setText(it.data)
}
})
setContentView(binding.root)
}
// 这里是输入的逻辑交验 属于用户交互的业务逻辑
@ObsoleteCoroutinesApi
private fun checkPhoneNum(){
if (binding.phoneInputEdt.text.toString().trim().isEmpty()) {
ToastUtil.showToast(this, getString(R.string.sign_please_enter_phone_number))
return
}
if (!RegexUtils.checkPhoneNum(binding.phoneInputEdt.text.toString().trim())) {
ToastUtil.showToast(this, getString(R.string.sign_please_enter_correct_phone_number))
return
}
viewModel.customerCaptcha(this, binding.phoneInputEdt.text.toString())
binding.sendCheckNumTv.setTextColor(resources.getColor(R.color.main_menu_select_text_color, null))
startTimer()
}
/**
* check login
* 校验登录
*/
private fun checkLoginInfo() {
if (binding.phoneInputEdt.text.toString().trim().isEmpty()) {
ToastUtil.showToast(this, getString(R.string.sign_please_enter_phone_number))
return
}
if (!RegexUtils.checkPhoneNum(binding.phoneInputEdt.text.toString().trim())) {
ToastUtil.showToast(this, getString(R.string.sign_please_enter_correct_phone_number))
return
}
if (binding.checkNumEdt.text.toString().trim().isEmpty()) {
ToastUtil.showToast(this, getString(R.string.sign_please_enter_verification_code))
return
}
loading.show()
viewModel.phoneLoginRequest(
this,
binding.phoneInputEdt.text.toString().trim(),
binding.checkNumEdt.text.toString().trim()
)
}
override fun onResume() {
super.onResume()
viewModel.requestDate.observe(this) {
loading.dismiss()
binding.phoneInputEdt.setText("")
binding.checkNumEdt.setText("")
if (it.code == 0) {
// 这里是实现落地,存放进数据库
viewModel.saveUserInfo(it.data.user, it.data.token)
val userInfo = UserInfo(
it.data.user.ID,
it.data.user.nickName,
it.data.user.userName,
it.data.token,
System.currentTimeMillis(),
// 用户信息
it.data.user.CreatedAt,
it.data.user.UpdatedAt,
it.data.user.activeColor,
it.data.user.authorityId,
it.data.user.baseColor,
it.data.user.headerImg,
it.data.user.sideMode,
it.data.user.uuid
)
// 如果是游客登录 这里更新一下内存缓存
ApplicationRepository.instance.setUserInfo(userInfo)
ToastUtil.showToast(this, it.msg)
// 登录成功
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
}
@ObsoleteCoroutinesApi
fun startTimer(){
// 计时器启动 倒计60秒可再次请求获取验证码
TickerUtils.TickerBuilder()
.apply {
delayMillis = 1000
finishTime = 60
scope = viewModel.viewModelScope
func = {
binding.sendCheckNumTv.text = "获取验证码"
binding.sendCheckNumTv.setTextColor(resources.getColor(R.color.register_login_text_color, null))
}
progress = {
binding.sendCheckNumTv.text = (60 - it).toString() + "秒后再次请求"
}
}.build()
.startTicker()
}
}
上面可以看的出来,所有的用户ui的操作逻辑,我都放到了Activity,其实这并没有太减负,但是好在其他结构 ViewModel 或是 Repository 就不需要关心这些操蛋的逻辑了
// LoginViewModel.kt (ViewModel)
@HiltViewModel
class LoginViewModel @Inject constructor( val loginRepository: LoginRepository) : ViewModel() {
// 登录返回
var requestDate: UnPeekLiveData<CustomerLoginRsp> = UnPeekLiveData.Builder<CustomerLoginRsp>()
.setAllowNullValue(false)
.create()
// 验证码返回
var requestCaptchaDate: UnPeekLiveData<CustomerCaptchaRsp> = UnPeekLiveData.Builder<CustomerCaptchaRsp>()
.setAllowNullValue(false)
.create()
// 图片
val loginDrawableList = arrayListOf<Int>(
R.mipmap.alipay_icon,
R.mipmap.wechat_icon
)
/**
* 手机登录请求
*/
fun phoneLoginRequest(lifecycleOwner: LifecycleOwner, phoneNum: String, code: String){
loginRepository.customerLogin(lifecycleOwner, CustomerLoginReq(phoneNum, code), requestDate)
}
/**
* 账号密码登录请求
*/
fun accountLoginRequest(lifecycleOwner: LifecycleOwner, account: String, password: String){
}
/**
* 获取验证码
*/
fun customerCaptcha(lifecycleOwner: LifecycleOwner, phoneNum: String){
loginRepository.customerCaptcha(lifecycleOwner , CustomerCaptchaReq(phoneNum), requestCaptchaDate)
}
// 数据库储存数据
fun saveUserInfo(info: CustomerLoginRsp.User, token: String){
GlobalScope.launch {
val userInfo = UserInfo(
info.ID,
info.nickName,
info.userName,
token,
System.currentTimeMillis(),
// 用户信息
info.CreatedAt,
info.UpdatedAt,
info.activeColor,
info.authorityId,
info.baseColor,
info.headerImg,
info.sideMode,
info.uuid
)
loginRepository.deleteUserInfo(userInfo)
loginRepository.insertUserInfo(userInfo)
}
}
}
从viewModel可以看的出来,这里只关心需要与数据交互的部分,比如说网络请求,数据本地持久化
// LoginRepository.kt (Repository)
/**
* login repository
* 登录相关
*/
@Singleton
class LoginRepository @Inject constructor(
private val service: CustomerUserService, // 来自网络的数据
private val userDao: UserInfoDao // 来自本地的数据
) {
/**
* 用户注册
*/
fun customerRegister(
lifecycleOwner: LifecycleOwner,
req: CustomerRegisterReq,
requestDate: UnPeekLiveData<CustomerRegisterRsp>
){
service.customerRegister(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
.subscribe(
{
// login success
requestDate.value = it
}, {
// request error
ToastUtil.showToast(GlobalApplication.instance.baseContext, it.message.toString())
it.printStackTrace()
}
)
}
/**
* 用户登录
*/
fun customerLogin(
lifecycleOwner: LifecycleOwner,
req: CustomerLoginReq,
requestDate: UnPeekLiveData<CustomerLoginRsp>
) {
service.customerLogin(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
.subscribe(
{
// login success
requestDate.value = it
}, {
// request error
ToastUtil.showToast(GlobalApplication.instance.baseContext, it.message.toString())
it.printStackTrace()
}
)
}
/**
* 获取验证码
*/
fun customerCaptcha(
lifecycleOwner: LifecycleOwner,
req: CustomerCaptchaReq,
requestDate: UnPeekLiveData<CustomerCaptchaRsp>
) {
service.customerCaptcha(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
.subscribe(
{
requestDate.value = it
}, {
ToastUtil.showToast(GlobalApplication.instance.baseContext, it.message.toString())
it.printStackTrace()
}
)
}
/**
* 校验token
*/
fun checkToken(
lifecycleOwner: LifecycleOwner,
requestDate: UnPeekLiveData<AnyResponseBean>
) {
service.checkToken()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
.subscribe(
{
requestDate.value = it
}, {
it.printStackTrace()
}
)
}
fun hasUser(info: UserInfo): Boolean{
return userDao.hasUser(info.id).value == 1
}
fun insertUserInfo(info: UserInfo) {
userDao.insertUserInfo(info)
}
fun updateUserInfo(info: UserInfo){
userDao.updateUserInfo(info)
}
fun deleteUserInfo(info: UserInfo) {
userDao.deleteUserInfo(info.id)
}
}
Repository这部分则只处理数据相关的业务,而这个部分,是可以很方便的组合复用,一个ViewModel 可以绑定任意多个Repository,以达到ui所需的所有数据业务
// LoginRepository.kt (Service && Dao)
interface CustomerUserService {
/**
* 注册接口
*/
@POST("/customerUser/register")
fun customerRegister(@Body req: CustomerRegisterReq): Flowable<CustomerRegisterRsp>
/**
* 登录接口(手机验证码登录)
*/
@POST("/customerUser/login")
fun customerLogin(@Body req: CustomerLoginReq): Flowable<CustomerLoginRsp>
/**
* 验证码接口
*/
@POST("/customerUser/captcha")
fun customerCaptcha(@Body req: CustomerCaptchaReq): Flowable<CustomerCaptchaRsp>
/**
* 密码登录
*/
@POST("/customerUser/loginPwd")
fun customerLoginPwd(@Body req: LoginPwdReq): Flowable<CustomerCaptchaRsp>
}
interface UserInfoDao {
@Query("SELECT * FROM userInfo WHERE id = :userId")
fun getUser(userId: Int): LiveData<UserInfo>
@Query("SELECT * FROM userInfo ORDER BY userInfo.changeTime DESC")
fun getUserList(): LiveData<List<UserInfo>>
@Query("SELECT EXISTS(SELECT 1 FROM userInfo WHERE id = :userId LIMIT 1)")
fun hasUser(userId: Int): LiveData<Int>
@Insert
fun insertUserInfo(userinfo: UserInfo): Long
@Query("DELETE FROM userInfo WHERE id = :userId")
fun deleteUserInfo(userId: Int)
@Update
fun updateUserInfo(userinfo: UserInfo)
}
Service和Dao分别定义了数据获取的接口,直接通过简单的接口配置,就能实现各种各样的业务需求
//这里还有个不得不说的模块 就是di
// DatabaseModule.kt
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getInstance(context)
}
@Provides
fun provideUserInfoDao(appDatabase: AppDatabase): UserInfoDao {
return appDatabase.userDao()
}
}
// NetworkModule.kt
@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {
@Singleton
@Provides
fun providePageSearchService(): InfoSearchService {
return HttpEngine.instance.create(InfoSearchService::class.java)
}
}
这里单例初始化了Dao和Service
总结
这套结构模仿了sunflower的大致框架,使得项目更加减负,虽然大量的ui交互逻辑在Activity / fragment, 但是那属于复杂的需求,可以使用三方框架或是自己封装业务组件来减少代码,总体来说这套结构作为单人或是小团队开发,效率还是能得到保证的。