preface

Recently, I have been practicing Kotlin in secluse. to be honest, it really smells good. The company is preparing to give me a new project, so I decided to build the project directly with Kotlin. Just completed the overall architecture, so the network request part of the first to share with you. This time, we will use the coroutine + Retrofit + MVVM pattern, and I will directly use a simple demo to see how to implement it. The article just describes the implementation of ideas, demo need to jump directly to the end of the article

Project configuration

First introduce the required dependencies

        implementation 'android. Arch. Lifecycle: extensions: 1.1.1'/ / coroutines implementation'org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.1.1'
        implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.1.1'
        //retrofit + okHttp3
        implementation 'com. Squareup. Retrofit2: retrofit: 2.4.0'
        implementation 'com. Squareup. Retrofit2: converter - gson: 2.4.0'
        implementation 'com. Jakewharton. Retrofit: retrofit2 - kotlin coroutines -- adapter: 0.9.2'
Copy the code

Implementation approach

Regardless of the design patterns, let’s start with a simple web request to see what steps are required for a basic retrofit implementation

1. Create a retrofit

~~~
    val retrofit = Retrofit.Builder()
                .baseUrl(RetrofitClient.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()
~~~
Copy the code

2. Create a Service interface

~~~
    interface RequestService {
        @GET("wxarticle/chapters/json")
        fun getDatas() : Call<DataBean>
    }
~~~
Copy the code

3. Initiate a request

~~~
    val service = retrofit.create(RequestService::class.java)
    service.getDatas().enqueue(object : Callback<DataBean> {
        override fun onFailure(call: retrofit2.Call<DataBean>, t: Throwable) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
        override fun onResponse(call: retrofit2.Call<DataBean>, response: Response<DataBean>) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
    })
~~~
Copy the code

This just describes a simple retrofit request that is almost always encapsulated and reused in real projects. In order to improve the readability of the code and reduce the coupling of the parts, in plain English, only each part can do the job well, so let’s implement each part one by one

Coroutines implementation

Then replace the above request with a coroutine

1. Create RetrofitClient

Object To enable RetrofitClient to have only one instance ~~~ object RetrofitClient {val BASE_URL = "https://wanandroid.com/" val reqApi by lazy { val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build() return@lazy retrofit.create(RequestService::class.java) } } ~ ~ ~Copy the code

Create a Service interface class

~~~
interface RequestService {
    @GET("wxarticle/chapters/json")
    fun getDatas() : Deferred<DataBean>
}
~~~
Copy the code

Since we will use coroutines later, we have changed Call to Deferred

3. Initiate a request

~~~ GlobalScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO){ val dataBean = RetrofitClient. ReqApi. GetDatas (). Await ()}} ~ ~ ~ a / / update the UICopy the code

The coroutine is used above, here is only about his application, the specific official documentation for further understanding. The network request is in the coroutine and in the IO scheduling unit, so it doesn’t have to block the main thread

Coroutine + ViewModel + LiveData implementation

The above is just a simple implementation, but is changed into a coroutine, in the project, but also can be further encapsulated, convenient use of MVVM mentioned before, so also used the Android component architecture of the new introduction of ViewModel and LiveData, first look at the implementation of ViewModel

class ScrollingViewModel  : ViewModel() {
    private val TAG = ScrollingViewModel::class.java.simpleName
    private val datas: MutableLiveData<DataBean> by lazy { MutableLiveData<DataBean>().also { loadDatas() } }
    private val repository = ArticleRepository()
    fun getActicle(): LiveData<DataBean> {
        return datas
    }
    private fun loadDatas() {
        GlobalScope.launch(Dispatchers.Main) {
            getData()
        }
        // Do an asynchronous operation to fetch users.
    }
    private suspend fun getData() {
        val result = withContext(Dispatchers.IO){
//            delay(10000)
            repository.getDatas()
        }
       datas.value = result
    }
}
Copy the code

The ViewModel will act as the middleman between the View and the data, and Repository is dedicated to data retrieval. Let’s look at the Repository code for making network requests to retrieve data

 class ArticleRepository {
     suspend fun getDatas(): DataBean {
          return RetrofitClient.reqApi.getDatas().await()
      }
  }
Copy the code

The code in the Activity is as follows

    private fun initData() {model. GetActicle (). Observe (this, the Observer {/ / get data toolbar. SetBackgroundColor (Color. RED)})}Copy the code

The follow-up to optimize

1. Solution to memory leak problem

The potential for memory leaks using GlobalScope has been optimized in response to your comments. If the ViewModel is destroyed while the coroutine is making a request, it will not be destroyed and there will be a memory leak. Therefore, in the ViewModel onCleared, even if the coroutine task is terminated, the reference code is as follows.

open class BaseViewModel : ViewModel(), LifecycleObserver{ private val viewModelJob = SupervisorJob() private val uiScope = CoroutineScope(Dispatchers.Main + ViewModelJob) // Run on UI thread coroutine Fun launchUI(block:suspend CoroutineScope.() -> Unit) {
        try {
            uiScope.launch(Dispatchers.Main) {
                block()
            }
        }catch (e:Exception){
            e.printStackTrace()
        }
    }
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}
Copy the code

Of course, the best way is to use viewModelScope, but WHEN I introduce this package, I will report an error. Because I am busy recently, I haven’t come to solve the problem immediately. I will continue to modify the subsequent problems when I have time, and I hope you can help me

2. Optimize request code

Take a look at the previous request code

private suspend fun getData() {
        val result = withContext(Dispatchers.IO){
//            delay(10000)
            repository.getDatas()
        }
       datas.value = result
    }
Copy the code

Every time you have to write a withContext (), it’s kind of inconvenient to actually use it, so I thought, how do I block it in the request method? The following code

open class BaseRepository {
    suspend fun <T : Any> request(call: suspend () -> ResponseData<T>): ResponseData<T> {
        return withContext(Dispatchers.IO){ call.invoke()}
    }
}
Copy the code

By writing a special request method in BaseRepository, you can simply execute a request each time

class ArticleRepository : BaseRepository() {
    suspend fun getDatas(): ResponseData<List<Data>> {
       return request {
           delay(10000)
           Log.i(ScrollingViewModel::class.java.simpleName,"loadDatas1 run in  ${Thread.currentThread().name}")
           RetrofitClient.reqApi.getDatas().await() }
    }
}
Copy the code

Note: This delay(10000) is only used by me for testing, which means to hibernate the current coroutine, so as to prevent the new coroutine from being added to my project. It is necessary to mention it

If you look at the ViewModel, it’s too easy

class ScrollingViewModel : BaseViewModel() {
    private val TAG = ScrollingViewModel::class.java.simpleName
    
    private val datas: MutableLiveData<List<Data>> by lazy { MutableLiveData<List<Data>>().also { loadDatas() } }
    
    private val repository = ArticleRepository()
    fun getActicle(): LiveData<List<Data>> {
        return datas
    }
    
    private fun loadDatas() {
        launchUI {
            Log.i(TAG,"loadDatas1 run in  ${Thread.currentThread().name}")
            val result = repository.getDatas()
            Log.i(TAG,"loadDatas3 run in  ${Thread.currentThread().name}")
            datas.value = result.data
        }
        // Do an asynchronous operation to fetch users.
    }
}
Copy the code

Val result = repository.getdatas (), then assign value to our LiveData, to see if it is synchronized. That’s the magic of coroutines. To verify that our request is not blocking the main thread, I printed the log

06-19 12:26:35.736 13648-13648/huaan.com.mvvmdemo I/ScrollingViewModel: loadDatas start run inMain 06-19 12:26:45.743 13648-13684/huaan.com.mvvmdemo I/ScrollingViewModel: Request RuninDefaultDispatcher- Worker -1 06-19 12:26:46.227 13648-13648/huaan.com.mvVMDemo I/ScrollingViewModel: loadDatas End runin  main
Copy the code

See? They do their job. It’s great

Exception handling

After a long time, we found that there was no exception processing. When the request failed, the project collapsed, which was not what we wanted. Since we didn’t come up with a better way to handle it, we had to cover it with tyR catch

open class BaseViewModel : ViewModel(), LifecycleObserver{ private val viewModelJob = SupervisorJob() private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) private val error by lazy { MutableLiveData<Exception>() } private val finally by lazy { MutableLiveData<Int>()} // Run on UI thread coroutine fun launchUI(block:suspend CoroutineScope.() -> Unit) {
        uiScope.launch(Dispatchers.Main) {
            try {
                block()
            }catch (e:Exception){
                error.value = e
            }finally {
                finally.value = 200
            }
        }
    }
    override fun onCleared() {super.oncleared () viewModeljob.cancel ()} /** * The request failed and an Exception occurred */ fun getError(): LiveData<Exception> {return*/ Fun getFinally(): LiveData<Int> {return finally
    }
}
Copy the code

Update the Retrofit server

Updated Retrofit to 2.6.0, updated viewModelScope to manage coroutines, and changed the project to AndroidX, which looks pretty good. Post some changes below

// Run on the UI thread coroutine fun launchUI(block:suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        try {
            block()
        } catch (e: Exception) {
            error.value = e
        } finally {
            finally.value = 200
        }
    }
Copy the code

So when we change it to viewModelScope, we don’t need to fetch the onCleared as we did before, because he’s already done that for us

interface RequestService {
    @GET("wxarticle/chapters/json")
   suspend fun getDatas() : ResponseData<List<Data>>
}

suspend fun getDatas(): ResponseData<List<Data>> = request {
    RetrofitClient.reqApi.getDatas()
}
Copy the code

The request interface declaration can be declared suspend and cancel to return Deferred, and the request method can remove await () because retrofit2.6.0 supports coroutines internally, so we don’t need to process them

conclusion

The above is just a description of some implementation process, the specific use of the demo, basically can meet most of the needs, if interested partners, you can download the demo reference, feel good, easily click a “like” is very satisfied. If you are not good at what you have learned, there may be some improper use. I hope you can point out the improper place. Thank you very much.

Attach the project address

Github.com/qingtian521…