preface

In front of the article, there is a big guy to remind the article some problems can be optimized. So this article has been written to optimize the article, I hope you pointed out.

This set of things is based on Java, and emMM has not been optimized for a long time. As my friend pointed out earlier, there are indeed many problems. Although the current set of framework based on Kotlin also has some problems, but optimization is also a matter of time.

Introduction to the

This is a new project, and I used some official Google demos to build this structure

Like a sunflower

Because the current project is not very complicated

UI: databinding + rXBinding4
Data transfer: UnPeekLiveData
Web request: OKhttp3 + Retrofit + Rxjava/flow

Structure that

Let’s take a look at the official demo code

# AndroidEntryPoint # AndroidEntryPoint # AndroidEntryPoint # AndroidEntryPoint # Fragment() { private val adapter = GalleryAdapter() private val args: GalleryFragmentArgs by navArgs() private var searchJob: Job? = null private val viewModel: GalleryViewModel by 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/bind adapter (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 {// Initiate a network request adapter.submitData(it) } } } }Copy the code

What is implemented here is to obtain the list of images through the network interface and display them through Paging, and only the UI logic is concerned here

// viewModel (galleryViewModel.kt) @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) PagingData return newResult}}Copy the code

It’s much simpler here, where the view is linked to the Models and the models are managed by 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 } }Copy the code

We only care about the input and output, the input is the request parameters, the query output is the Flow, so we can think of this as a data management center, it is responsible for processing the data that needs to be retrieved asynchronously, whether it is network requests, whether it is from the database, or local files.

My code

The sunflower code above shows the simple structure of dependency injection throughout the program

The View (Activity | | fragments) + ViewModel (LiveData | | Date | | Flow | | Flowable (RxJava)) + Repository (request data Whether through Service || Room)Copy the code

With that in mind, the architecture given to Kotlin at this stage is easy to build

In view of the previous data inversion with LiveData

Arch :unpeek- LiveData :$version const val unpeek = “com.kunminx.arch:unpeek- Livedata :$version” is used to avoid the previous problem

Without further ado, let’s demonstrate the structure given to Kotlin by logging in to the business

/** * phone number login */ @androidEntryPoint class LoginActivity: AppCompatActivity() { private lateinit var binding: ActivityLoginBinding val viewModel: LoginViewModel by viewModels() 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 {the if (position = = 0) {if (LoginUtils. CheckAliPayInstalled (this)) {/ / alipay val intent = intent (this, PayDemoActivity::class.java) intent.putExtra("OrderInfo", "123") intent.putExtra("OrderType", 1) // startActivity(intent)}else{ToastUtil. ShowToast (this, "Please install pay treasure")}} else {/ / WeChat}}}) / / login button binding. LoginBtn. SetOnClickListener {checkLoginInfo ()} / / / / / / registration to the registration page immediately binding.loginTv.setOnClickListener { // startActivity(Intent(this, RegisterActivity: : class. Java)) / / finish () / /} / / through the account password Enter the account login page binding. SwitchTv. SetOnClickListener { startActivity(Intent(this, LoginAccountActivity: : class. Java)) finish ()} / / access authentication code binding. SendCheckNumTv. SetOnClickListener {checkPhoneNum ()} / / So the captcha comes back and I'm going to store the LiveData that I'm returning in the viewModel but you can also, if you don't want to store the LiveData that I'm returning when I request it, I like is used separately the viewModel. RequestCaptchaDate. Observe (this, The Observer {the if (it. Code = = 0) {binding. CheckNumEdt. SetText (it. Data)}}) the setContentView (binding. Root)} / / here is the logic of input jiaoyan Business logic belonging to user interaction @ObsoletecoroutinesAPI Private Fun checkPhoneNum(){if the ObsoleteCoroutinesApi should be considered (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) {/ / here is to realize the fall to the ground, 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. / / the user information 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. The data. User. Uuid) / / if the login is tourists Here update memory cache ApplicationRepository. Instance. SetUserInfo (the userInfo) ToastUtil. ShowToast (this, StartActivity (Intent(this, Intent)) MainActivity::class.java)) Finish ()}}} @ObsoletecoroutinesAPI Fun startTimer(){// The ObsoleteCoroutinesApi fun startTimer should be considered again for 60 seconds TickerUtils.TickerBuilder() .apply { delayMillis = 1000 finishTime = 60 scope = viewModel.viewModelScope func = { Binding. SendCheckNumTv. Text = "get verification code" binding.sendCheckNumTv.setTextColor(resources.getColor(R.color.register_login_text_color, Null))} progress = {binding. SendCheckNumTv. Text = (60 - it). The toString () + "empress again request"}}. The build () startTicker ()}}Copy the code

As you can see, all the user UI operation logic is in the Activity, which is not too much of a burden, but other structures such as ViewModel or Repository don’t need to worry about the fuckup logic

// LoginViewModel.kt (ViewModel) @HiltViewModel class LoginViewModel @Inject constructor( val loginRepository: LoginRepository) : ViewModel() {var requestDate: UnPeekLiveData<CustomerLoginRsp> = UnPeekLiveData.Builder<CustomerLoginRsp>() .setAllowNullValue(false) .create() // Verification code returns var requestCaptchaDate: UnPeekLiveData<CustomerCaptchaRsp> = UnPeekLiveData.Builder<CustomerCaptchaRsp>() .setAllowNullValue(false) .create() // Val loginDrawableList = arrayListOf<Int>(r.map.alipay_icon, R.ipmap.wechat_icon) /** * Mobile login request */ fun phoneLoginRequest(lifecycleOwner: lifecycleOwner, phoneNum: String, code: String){ loginRepository.customerLogin(lifecycleOwner, CustomerLoginReq(phoneNum, code), RequestDate)} /** * Fun accountLoginRequest(lifecycleOwner: lifecycleOwner, account: String, password: String){} /** * Get verification code */ fun customerCaptcha(lifecycleOwner: lifecycleOwner, phoneNum: String){ loginRepository.customerCaptcha(lifecycleOwner , CustomerCaptchaReq(phoneNum), Fun saveUserInfo(info: CustomerLoginrsp. User, token: customerLoginrsp. User, token: String){ GlobalScope.launch { val userInfo = UserInfo( info.ID, info.nickName, info.userName, token, System.currenttimemillis (), // User info info.createdat, info.updatedat, info.activecolor, info.authorityId, info.basecolor, info.headerImg, info.sideMode, info.uuid ) loginRepository.deleteUserInfo(userInfo) loginRepository.insertUserInfo(userInfo) } } }Copy the code

As you can see from the viewModel, you only care about the parts that need to interact with data, such as network requests and data local persistence

// loginrepository.kt (Repository) /** * login Repository * login related */ @singleton class LoginRepository @inject Constructor (private val service: CustomerUserService, // Data from network private val userDao: UserInfoDao // Data from local) {/** * user register */ 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()})} /** * User login */ 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()})} /** * Get captcha */ 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()})} /** * 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) } }Copy the code

A ViewModel can bind any number of Repositories to achieve all the data services required by the UI

// loginRepository. kt (Service && Dao) interface CustomerUserService {/** * Register interface */ @post ("/customerUser/register") fun  customerRegister(@Body req: CustomerRegisterReq): Flowable<CustomerRegisterRsp> /** * Login interface */ @post ("/customerUser/login") fun customerLogin(@body req: CustomerLoginReq): Flowable<CustomerLoginRsp> /** * Validation code interface */ @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) }Copy the code

A Service and a Dao define interfaces for data retrieval, respectively. A variety of business requirements can be realized directly through simple interface configuration

/ / there's a Module has to be said is di / / DatabaseModule kt @ InstallIn (SingletonComponent: : class) @ the 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) } }Copy the code

Here the singleton initializes the Dao and Service

conclusion

This structure, modeled after the Sunflower framework, makes the project much less cumbersome. Although much of the UI interaction logic is in the Activity/fragment, it is a complex requirement to reduce code by using a tripartite framework or encapsulating business components yourself, and is generally developed as a single person or as a small team. Efficiency can still be guaranteed.

Public id: Programmer Cat