Foreword
I have been practicing Kotlin in seclusion recently. To be honest, it is really good and easy to use. The company just happened to be giving me a new project, so I planned to use Kotlin to build the project directly. The overall architecture has just been built, so I will share the network request part with you first. This time, the coroutine + retrofit + mvvm model is used. Let me use a simple demo to see the specific implementation. The article only describes the implementation ideas. If you need a demo, jump directly to the end of the article.
Project configuration
First introduce the required dependencies
implementation 'android.arch.lifecycle:extensions:1.1.1' //coroutine 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'
Implementation ideas
Regardless of the design patterns, let’s first make a simple network request and see what steps are required for the basic implementation of retrofit.
1. Create retrofit
~~~ val retrofit = Retrofit.Builder() .baseUrl(RetrofitClient.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build() ~~~
2. Create service interface
~~~ interface RequestService {<!-- --> @GET("wxarticle/chapters/json") fun getDatas() : Call<DataBean> } ~~~
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. } }) ~~~
This just describes a simple request method of retrofit. In actual projects, it is basically encapsulated before use. In order to improve the readability of the code and reduce the coupling of each part, in layman’s terms, only when each performs its own duties can the Well done at work. Next we will focus on fulfilling our duties one by one.
Coroutine implementation
Next, change the above request into a coroutine method to implement it.
1. Create RetrofitClient
object allows 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) } } ~~~
2. Create service interface class
~~~ interface RequestService {<!-- --> @GET("wxarticle/chapters/json") fun getDatas() : Deferred<DataBean> } ~~~
Because we will use coroutines later, we will replace Call with Deferred.
3. Initiate a request
~~~ GlobalScope.launch(Dispatchers.Main) {<!-- --> withContext(Dispatchers.IO){<!-- --> val dataBean = RetrofitClient.reqApi.getDatas().await() } //update ui } ~~~
Coroutines are used above. Here we only describe their applications. For more details, please refer to the official documents. The network request is in the coroutine and in the IO scheduling unit, so there is no need to worry about blocking the main thread.
Coroutine + ViewModel + LiveData implementation
The above is just a simple implementation, but it is replaced by a coroutine. In the project, it can be further encapsulated for ease of use. MVVM was also mentioned earlier, so the newly introduced component architecture of Android, ViewModel and LiveData, are also used. Let’s take a look first. 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 } }
ViewModel will serve as the middleman between View and data, and Repository is dedicated to data acquisition. Let’s take a look at the code of Repository, which is used to initiate network requests to obtain data.
class ArticleRepository {<!-- --> suspend fun getDatas(): DataBean {<!-- --> return RetrofitClient.reqApi.getDatas().await() } }
The code in Activity is as follows
private fun initData() {<!-- --> model.getActicle().observe(this, Observer{<!-- --> //Get data toolbar.setBackgroundColor(Color.RED) }) }
Follow-up optimization
1. Solution to memory leak problem
Based on the opinions of all the experts, the memory leak problem that may occur when using GlobalScope has been optimized. Because during the process of the coroutine making a request, if the ViewModel is destroyed at this time and the coroutine inside is making requests, it will not be destroyed and a memory leak will occur. Therefore, in ViewModel onCleared, even if the coroutine task is ended, the reference code is as follows.
open class BaseViewModel : ViewModel(), LifecycleObserver{<!-- --> private val viewModelJob = SupervisorJob() private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) //Coroutine running on UI thread fun launchUI( block: suspend CoroutineScope.() -> Unit) {<!-- --> try {<!-- --> uiScope.launch(Dispatchers.Main) {<!-- --> block() } }catch (e:Exception){<!-- --> e.printStackTrace() } } override fun onCleared() {<!-- --> super.onCleared() viewModelJob.cancel() } }
Of course, the best way is to use viewModelScope, but when I introduce this package, I will get an error. Since I have been busy recently, I haven’t been able to solve it urgently. I will continue to modify subsequent problems when I have time. I hope you guys can help. Give advice
2. Optimize request code
Let’s take a look at the previous request code first.
private suspend fun getData() {<!-- --> val result = withContext(Dispatchers.IO){<!-- --> // delay(10000) repository.getDatas() } datas.value = result }
You need to write a withContext() every time. In actual use, it feels a bit inconvenient, so I thought for a while, how can I seal it in the request method? code show as below
open class BaseRepository {<!-- --> suspend fun <T : Any> request(call: suspend () -> ResponseData<T>): ResponseData<T> {<!-- --> return withContext(Dispatchers.IO){<!-- --> call.invoke()} } }
By writing a special request method in BaseRepository, you only need to execute the request every time. The request reference is as follows
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() } } }
Note: This delay(10000) is only used by me for testing. It means to hibernate the current coroutine to prevent newbies from adding it to their own projects. It is still necessary to mention it.
If you look at ViewModel again, it’s too simple.
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. } }
Pay attention to the request part. There are only two sentences. One sentence initiates the request val result = repository.getDatas(), and then assigns a value to our LiveData. It looks like synchronized code. This is the charm of coroutines. In order to verify Our request did not block the main thread and I printed the log
06-19 12:26:35.736 13648-13648/huaan.com.mvvmdemo I/ScrollingViewModel: loadDatas start run in main 06-19 12:26:45.743 13648-13684/huaan.com.mvvmdemo I/ScrollingViewModel: request run in DefaultDispatcher-worker-1 06-19 12:26:46.227 13648-13648/huaan.com.mvvmdemo I/ScrollingViewModel: loadDatas end run in main
You see, everyone performs their duties, and the effect is great
Exception handling
After working on it for a long time, I discovered that there was no exception handling. When the request failed, the project collapsed. This was not the result we wanted. Since we couldn’t think of a better way to handle it, we could only use a tyr catch outside to support it. Yes, please refer to the following
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>() } //Coroutine running on UI thread 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 error } /** * The request is completed, do some closing operations here */ fun getFinally(): LiveData<Int> {<!-- --> return finally } }
Conclusion
The above only describes some implementation processes. You must refer to the demo for specific use. It can basically meet most needs. If you are interested, you can download the demo for reference. If you feel good, just give it a like and you will be satisfied. If you are not good at what you have learned, you may use it inappropriately. I hope you guys can point out the inappropriate areas. Thank you very much.
Finally, I would like to share with you a set of “Advanced Materials on the Eight Modules of Android” written by Alibaba’s senior architects to help you systematically organize messy, scattered, and fragmented knowledge, so that you can master Android development systematically and efficiently. Various knowledge points.
Due to the large content of the article and the limited space, the information has been organized into PDF documents. If you need the complete document of “Advanced Materials on Eight Modules of Android”, you can add WeChat to get it for free!
“Advanced Notes on the Eight Modules of Android”
Compared with the fragmented content we usually read, the knowledge points in this note are more systematic, easier to understand and remember, and are strictly arranged according to the knowledge system.