kotlin + MVVM + hint + viewBindi

作者: 虞_18bd | 来源:发表于2021-09-19 12:51 被阅读0次

    前言

    很久没更新简书了,之前写的

    这套东西是基于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, 但是那属于复杂的需求,可以使用三方框架或是自己封装业务组件来减少代码,总体来说这套结构作为单人或是小团队开发,效率还是能得到保证的。

    相关文章

      网友评论

        本文标题:kotlin + MVVM + hint + viewBindi

        本文链接:https://www.haomeiwen.com/subject/mkiugltx.html