Google has been pushing JetPack for some time, along with the MVVM architecture, using tools like ViewModel LiveData to implement data-binding.

JetPack is also equipped with a Navigation function, which, as its name implies, is mainly used to realize the single Activity architecture. I wrote an article before, which realized the single Activity architecture with fragmentation. With a learning attitude, This project uses Navigation to realize single Activity architecture.

The MVVM architecture diagram of the project is attached first:

  • Green represents the View layer
  • Blue represents the ViewModel layer
  • Red represents the Model layer

There is one-way dependence among all layers, that is, V layer sends requests to VM layer, VM layer obtains data from M layer, and then acts as a bridge through LiveData. V layer monitors LiveData data and updates UI when data changes.

Here’s a code example of the login process:

The first is the V layer code:

class LoginFragment : BaseFragment(), View.OnClickListener { private val viewModel by lazy { ViewModelProviders.of(this).get(LoginViewModel::class.java) } ...  . override fun bindView(savedInstanceState: Bundle? , rootView: View) { observe() fragmentLoginSignInLayoutSignIn.setOnClickListener(this) } override fun onClick(v: View?) { when (v? . Id) {R.i d.f ragmentLoginSignInLayoutSignIn - > {/ / call log viewModel. Login ( fragmentLoginSignInLayoutAccount.text.toString(), fragmentLoginSignInLayoutPassword.text.toString() ) } ... . }} / monitoring data changes * * * * / private fun observe () {/ / login to monitor the viewModel. LoginLiveData. Observe (this, the Observer {the if (it errorCode! = 0) { toast(it.errorMsg) } else { Navigation.findNavController(fragmentLoginSignInLayoutSignIn) . Navigate (da ction_loginFragment_to_mainFragment R.i)}}) / / registered to monitor the viewModel registerLiveData. Observe (this, Observer { if (it.errorCode ! = 0) {it. ErrorMsg)} else {toast(" Login to return to the login page ") getSignInAnimation (fragmentLoginSignUpLayout)}}) / / loading window to monitor the viewModel isLoading. Observe (this, Observer {if (it) {dismissAllLoading()} else {dismissAllLoading()}}}Copy the code

Get LoginViewModel instance, click login button, call its login (userName: String, password: String) method, the observe () method gets its LiveData data observe method, monitoring the data changes, Perform UI updates in the Observer anonymous class.

VM layer code:

class LoginViewModel(application: Application) : BaseViewModel(application) {/** * login data */ var loginLiveData = MutableLiveData<LoginResponse>() /** * registration data */ var RegisterLiveData = MutableLiveData<LoginResponse>() /** * logOutLiveData = MutableLiveData<LoginResponse>() private val repository = LoginRepository.getInstance(PackRatNetUtil.getInstance()) fun login(username: String, password: String) { launch { loginLiveData.postValue(repository.login(username, password)) } } fun register(username: String, password: String, repassword: String) { launch { registerLiveData.postValue(repository.register(username, password, repassword)) } } fun logOut() { launch { logOutLiveData.postValue(repository.logOut()) } } }Copy the code

Simply instantiate LiveData and Repository data dependencies, call Repository to fetch data, and assign postValue to the repository. I’ve wrapped a layer of BaseViewModel, which inherits AndroidViewModel, which differs from a regular ViewModel in that you can pass in an application parameter, which is to get a global context reference.

abstract class BaseViewModel(application: Application) : AndroidViewModel(application){
  /** * Load changes */
  var isLoading = MutableLiveData<Boolean>()
  /** * unified coroutine processing */
  fun launch(block:suspend() -> Unit) = viewModelScope.launch {
    try {
        isLoading.value = true
        withContext(Dispatchers.IO){
            block()
        }
        isLoading.value = false
    }catch (t:Throwable){
        t.printStackTrace()
        getApplication<PackRatApp>().toast(t.message)
        isLoading.value = false}}}Copy the code

A coroutine method is removed, and the time-consuming operation is unified to the IO thread operation. Loading is set to false when the time-consuming method is completed, notifying the page to close the popup window.

M layer code:

class LoginRepository private constructor( private val net: PackRatNetUtil ) { companion object { @Volatile private var instance: LoginRepository? = null fun getInstance(net: PackRatNetUtil) = instance ? : synchronized(this) { instance ? : LoginRepository(net).apply { instance = this } } } suspend fun login(username: String, password: String) = net.fetchLoginResult(username, password) suspend fun register(username: String, password: String, repassword: String) = net.fetchRegisterResult(username, password, repassword) suspend fun logOut() = net.fetchQuitResult() }Copy the code

Get data from the network layer. Of course, if you need to store a local database, you can do the following:

class CollectRepository private constructor( private var collectDao: CollectDao, private var net: PackRatNetUtil ) { companion object { @Volatile private var instance: CollectRepository? = null fun getInstance(collectDao: CollectDao, net: PackRatNetUtil) = instance ? : synchronized(this) { instance ? : CollectRepository(collectDao, Net).apply {instance = this}}} /** * Get collectworks list data */ suspend fun getcollectworks () = try {net.fetchCollectList()} catch (t: Throwable) {t.p rintStackTrace () collectDao. GetCollectList ()} / * * * * set collection list is stored into the database/suspend fun setCollects (collects: List<Collect>) { collects.forEach { collectDao.insert(it) log(content = it.content) } } }Copy the code

Pass in instances of the local database and network layer, and get the data separately, depending on the situation.

class PackRatNetUtil private constructor(a){
  companion object {
    @Volatile
    private var instance: PackRatNetUtil? = null

    fun getInstance(a) = instance ? :synchronized(this) { instance ? : PackRatNetUtil().apply { instance =this}}}private val collectService = ServiceCreator.create(CollectService::class.java)

  private val loginService = ServiceCreator.create(LoginService::class.java/** * Get the favorites list from the server */suspend fun fetchCollectList(a)= collectService.getCollectAsync().await()

  /** * Get the login result */
  suspend fun fetchLoginResult(username: String, password: String) =
    loginService.loginAsync(username, password).await()

  / * * * this method is used to retrofit the use of [Call] [Callback] Callback and coroutines [await] Callback connected * retrofit follow-up provides [CoroutineCallAdapterFactory], however, Can return [Deferred] as a callback *@DeprecatedIntroduction [com jakewharton. Retrofit: retrofit2 - kotlin - coroutines - adapter] package can use Deferred as a callback * /
  private suspend fun <T> Call<T>.await(): T = suspendCoroutine { continuation ->
    enqueue(object : Callback<T> {
        override fun onFailure(call: Call<T>, t: Throwable) {
            continuation.resumeWithException(t)
        }

        override fun onResponse(call: Call<T>, response: Response<T>) {
            val body = response.body()
            if(body ! =null) {
                continuation.resume(body)
            } else {
                continuation.resumeWithException(NullPointerException("response body is null")}}})}}Copy the code

If retrofit did not provide a coroutines-Adapter dependency package, it could not use Deferred as a callback. Instead, it could rewrite the await method of its Call to match the coroutine’s resume method with the resumeWithException method. So retroFIT works better with coroutines, but retroFIT later provides

implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofitCoroutineVersion"
Copy the code

So you can use Deferred as a callback in its apiService.

interface LoginService {
  @FormUrlEncoded
  @POST("user/login")
  fun loginAsync(
    @Field("username") username: String,
    @Field("password") password: String
  ): Deferred<LoginResponse>

  @FormUrlEncoded
  @POST("user/register")
  fun registerAsync(
    @Field("username") username: String,
    @Field("password") password: String,
    @Field("repassword") repassword: String
  ): Deferred<LoginResponse>

  @FormUrlEncoded
  @GET("user/logout/json")
  fun quitAsync(a): Deferred<LoginResponse>
}
Copy the code

The core idea of MVVM lies in the one-way communication between each layer, and the V layer cannot directly request data and other operations. Later, I will write Navigation to implement a single Activity architecture.

First post the project code: github.com/512DIDIDI/P…