Because the article involves only a lot of points, the content may be too long, according to their own ability level and familiarity with the stage jump read. If there is an incorrect place to tell you trouble private letter to the author, very grateful
Due to time reasons, the author can only write during the day and in his free time at night, so the update frequency should be every 2-3 days. Of course, I will try my best to make use of time and try to release it in advance. In order to facilitate reading, this article is divided into multiple chapters, and the corresponding chapters are selected according to their own needs. Now it is only a general catalog in the author’s mind, and the final update shall prevail:
- Basic usage of Kotlin coroutines
- Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
- Kotlin coroutine exception handling
- Use Kotlin coroutine to develop Android applications
- Network request encapsulation for Kotlin coroutines
- [Kotlin Coroutine for Android (6) -> Kotlin coroutine combined with Jetpack and the use of third-party frameworks]
- [Kotlin coroutine introduction for Android (7) -> In-depth Kotlin Coroutine principle (1)]
- [Kotlin coroutine introduction for Android version in detail (8) -> In-depth Kotlin coroutine principle (2)]
- [Kotlin coroutine introduction for Android version in detail (9) -> In-depth Kotlin coroutine principle (3)]
- [Kotlin coroutine introduction for Android (10) -> In-depth Kotlin coroutine principle (4)]
Extend the series
- Encapsulating DataBinding saves you thousands of lines of code
- A ViewModel wrapper for everyday use
This chapter, the preface
Other than the introduction of coroutines, no other framework will be covered in this section. This article is a guide to how to use kotlin coroutines in conjunction with these frameworks, based on the user’s familiarity with them. The whole length will be a bit long. We will make some encapsulation on the architecture while using it in combination, which is also for the convenience of subsequent actual combat, so that everyone can understand the code more conveniently and intuitively.
The author is just an ordinary developer, the architecture of the design is not necessarily reasonable, we can absorb the essence of the article, to dross.
Kotlin coroutines use encapsulation
In the previous chapter, we looked at the basic use of coroutines in activities, fragments, Lifecycle, viewModels, and how to easily customize a coroutine. In this chapter, we mainly do some basic encapsulation work. We will build on the previous chapter by introducing DataBinding, LiveData, Flow, and so on to do some basic encapsulation. For example: the definition of Base class, the use of coroutine encapsulation, commonly used extension functions.
Let’s first introduce the relevant libraries used in this chapter:
// Kotlin
implementation "Org. Jetbrains. Kotlin: kotlin - stdlib: 1.4.32"
// Coroutine core library
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.4.3"
// Coroutine Android support library
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.4.3"
implementation "Androidx. Activity: activity - KTX: 1.2.2." "
implementation "Androidx. Fragments: fragments - KTX: 1.3.3." "
implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.3.1." "
implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.3.1." "
implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.3.1." "
// ok http
implementation "Com. Squareup. Okhttp3: okhttp: 4.9.0"
implementation 'com. Squareup. Okhttp3: logging - interceptor: 4.9.0'
// retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
Copy the code
We are now ready to do some basic encapsulation and start using dataBinding in our app’s Bulid. gradle file
android {
buildFeatures {
dataBinding = true
}
/ / to omit...
}
Copy the code
This article is based on the use of DataBinding, see encapsulating DataBinding to save you thousands of lines of code
The ViewModel can be used in a ViewModel wrapper for everyday use
These two articles were written specifically for this chapter’s extensibility reading.
Recently because of a computer bombing signal output is not stable, thought is the graphics card is broken, for a few days or the whole no well, finally found a motherboard is corrosion led to a line fault, the current use of motherboard production for a long time, finally can only find a compatible, waited for a couple of days the arrival of the goods, and eventually lead to this section is a little delay for a few days. Electronic products are too fragile, we must pay attention to anti-fall, anti-knock, anti-corrosion! Without further ado, let’s move on to our main topic today.
The common environment for coroutines
In the actual development process, we often need to move the time-consuming processing to the non-main thread for execution, and then return to the main thread to refresh the interface after the asynchronous completion of time-consuming operation. Based on these requirements, we can roughly divide the environments where coroutines are used into the following five environments:
Network request
The callback processing
Database operations
File operations
Other Time-consuming Operations
Let’s start with the network request. At present, most apps on the market use RxJava combined with Retrofit and OkHttp to process network requests. Our ultimate goal is also to use coroutines in conjunction with Retrofit and okHttp for network request processing.
We are here to explain the use of Retrofit and OkHttp with coroutines, viewModels, and LiveData. If you need to understand the principles of Retrofit and OkHttp, check out the principle decomposition articles by other authors.
The encapsulation and use of coroutines in network request
In order to demonstrate the effect, the author applied for a free weather API in Weiwei. The interface address we used:
http[s]://route.showapi.com/9-2?showapi_appid= replace your own value &showapi_sign= Replace your own value
Copy the code
Showapi_res_body returns a large number of JSON fields, from which we have selected a few fields that we mainly focus on:
The parameter name | type | describe |
---|---|---|
showapi_res_body |
String | JSON encapsulation of the message body, in which all application-level return parameters will be embedded. |
showapi_res_code |
int | Viewing error codes |
showapi_res_error |
String | Display of error messages |
{
"showapi_res_error":""."showapi_res_code":0."showapi_res_body": {"time":"20210509180000".// Forecast release time
"now": {"wind_direction":"West wind"./ / the wind
"temperature_time":"01:30".// The time to get the temperature
"wind_power":"0"./ / wind
"aqi":"30".// Air index, the smaller the better
"sd":"40%".// Air humidity
"weather_pic":"http://app1.showapi.com/weather/icon/day/00.png".// The weather icon
"weather":"Fine"./ / the weather
"rain":"0.0".// Precipitation (mm)
"temperature":"15" / / the temperature}}}Copy the code
Of course, we also need an object to receive the data, so to avoid confusion with other libraries, we’ll call it CResponse, which is a familiar structure:
data class CResponse<T>(
@SerializedName("showapi_res_code")
val code: Int.@SerializedName("showapi_res_error")
val msg: String? = null.@SerializedName("showapi_res_body")
val data: T
)
Copy the code
The API returns field names that are not exactly my cup of tea and are not aesthetically pleasing to use. So I rename the attribute via Gson’s annotation SerializedName. This problem is often encountered in real development and can be handled in this way as well.
data class Weather(
val now: WeatherDetail,
val time: String
)
data class WeatherDetail(
val aqi: String,
val rain: String,
val sd: String,
val temperature: String,
@SerializedName("temperature_time")
val temperatureTime: String,
val weather: String,
@SerializedName("weather_pic")
val weatherPic: String,
@SerializedName("wind_direction")
val windDirection: String,
@SerializedName("windPower")
val windPower: String
)
Copy the code
Then let’s create okHttp, Retrofit. Retrofit’s Coroutine-Adapter library is no longer needed after Retrofit2.6, we can just use it:
object ServerApi {
val service: CoroutineService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
build()
}
private fun build(a):CoroutineService{
val retrofit = Retrofit.Builder().apply {
baseUrl(HttpConstant.HTTP_SERVER)
client(OkHttpClientManager.mClient)
addConverterFactory(ScalarsConverterFactory.create())
addConverterFactory(GsonConverterFactory.create())
}.build()
return retrofit.create(CoroutineService::class.java)
}
}
object HttpConstant {
internal val HTTP_SERVER = "https://route.showapi.com"
}
Copy the code
object OkHttpClientManager {
val mClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
buildClient()
}
private fun buildClient(a): OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder().apply {
addInterceptor(CommonInterceptor())
addInterceptor(logging)
followSslRedirects(true)
}.build()
}
}
Copy the code
Since showAPI_appID and showAPI_sign are mandatory values when we call the weather API, we added a CommonInterceptor interceptor to handle them all:
class CommonInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val oldRequest = chain.request()
val httpUrl = oldRequest.url
val host = httpUrl.host
if(HttpConstant.HTTP_SERVER ! = host ) {return chain.proceed(oldRequest)
}
val urlBuilder = httpUrl.newBuilder()
// Enter your own appID and sign here
urlBuilder.addQueryParameter("showapi_appid", SHOW_API_APPID)
urlBuilder.addQueryParameter("showapi_sign", SHOW_API_SIGN)
val request = oldRequest
.newBuilder()
.url(urlBuilder.build())
.build()
return chain.proceed(request)
}
}
Copy the code
For quick demonstration purposes, we have extracted only one parameter from the request list for demonstration purposes. Next, we define the interface CoroutineService that we need to use in the request via Retrofit:
Request parameters | type | describe |
---|---|---|
area |
String | Name of the region to be queried. |
interface CoroutineService {
@FormUrlEncoded
@POST("/ 9-2")
suspend fun getWeather(
@Field("area") area: String
): CResponse<Weather>
Copy the code
You can see that when we use Retrofit in conjunction with coroutines, we simply add the suspend keyword before the function, and the return result can be defined as the data object that we need to parse from the request result, instead of Call
as before.
That’s the end of our basic data-based definitions, and now we’re ready to move on to today’s topic. For a clearer understanding, the author will not adopt a direct one-step approach here. A lot of people might have trouble reading and understanding that. The author will encapsulate the request process step by step, requiring a little patience.
We will create a Repository to request data:
class WeatherRepository {
suspend fun getWeather(
area: String
): CResponse<Weather>{
return ServerApi.service.getWeather(area)
}
}
Copy the code
Also create a MainViewModel to use the Repository
class MainViewModel(private val repository: WeatherRepository):ViewModel() {
private val _weather:MutableLiveData<Weather> = MutableLiveData()
val mWeather: LiveData<Weather> = _weather
fun getWeather( area: String){
requestMain {
val result = repository.getWeather(area)
_weather.postValue(result.data)}}}Copy the code
Now we can create the MainViewModel in the MainActivity to call the method to get the weather data. When we are creating a ViewModel objects no longer use ViewModelProviders. Of (this). The get (MainViewModel: : class. Java) this way. Instead, use the viewModels method in the activity-ktx library to create:
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
valfactoryPromise = factoryProducer ? : { defaultViewModelProviderFactory }return ViewModelLazy(VM::class.{ viewModelStore }.factoryPromise)
}
Copy the code
This method requires us to pass in a Factory and define our own implementation:
object ViewModelUtils {
fun provideMainViewModelFactory(a): MainViewModelFactory {
return MainViewModelFactory(MainRepository())
}
}
class MainViewModelFactory(
private val repository: MainRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
}
Copy the code
Next we use it in MainActivity. We get the MainViewModelFactory by using ViewModelUtils. Then we use viewModels to create the viewModel object we need:
class MainActivity : BaseActivity<ActivityMainBinding>() {
private val viewModel by viewModels<MainViewModel> {
ViewModelUtils.provideMainViewModelFactory()
}
override fun initObserve(a) {
viewModel.mWeather.observe(this) {
mBinding.contentTv.text = "$it"}}override fun ActivityMainBinding.initBinding(a) {
this.mainViewModel = viewModel
}
}
Copy the code
InitObserve is the abstract method we defined in BaseActivity. We simply defined a Textview in activity_main.xml to display the data, and although mainViewModel was introduced in the XML, we didn’t use DataBinding to do the DataBinding directly for demonstration purposes. In real development, you would use DataBinding to bind data directly in XML.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mainViewModel"
type="com.carman.kotlin.coroutine.request.viewmodel.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<TextView
android:id="@+id/content_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Here is the data that was obtained."
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Copy the code
We successfully requested the data and displayed it on our interface. But there is one problem with our current request that does not have exception handling. Now let’s handle the exception in the request:
fun getWeather(area: String){
requestMain {
val result = try {
repository.getWeather(area)
} catch (e: Exception) {
when(e) {
is UnknownHostException -> {
/ /...
}
/ /... Various exceptions that need to be handled separately
is ConnectException -> {
/ /...
}
else- > {/ /...}}null} _weather.postValue(result? .data)}}Copy the code
This handled exceptions, but it was ugly, and it would be a nightmare if we had to write it on every request. What follows is our focus, and I will encapsulate three forms of invocation, starting with the appropriate scenario.
Higher order function mode
At this point we need to create a BaseRepository for encapsulation. We use onSuccess to get the result of success, onError to handle the exception specific to the request, and onComplete to handle the completed operation:
open class BaseRepository {
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend() - >CResponse<T>,
noinline onSuccess: ((T?). ->Unit)? = null.noinline onError: ((Exception) - >Unit)? =null.noinline onComplete: (() -> Unit)? = null ){
try {
valresponse = block() onSuccess? .invoke(response? .data)}catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
/ /...
}
/ /... Various exceptions that need to be handled separately
is ConnectException -> {
/ /...
}
else- > {/ /...} } onError? .invoke(e) }finally{ onComplete? .invoke() } } }Copy the code
The WeatherRepository getWeather method needs to be wrapped in launchRequest:
suspend fun getWeather(
area: String,
onSuccess: (Weather?). ->Unit,
onError: (Exception) - >Unit,
onComplete: () -> Unit.){
launchRequest({
ServerApi.service.getWeather(area)
}, onSuccess,onError, onComplete)
}
Copy the code
Then we can modify the getWeather method in MainViewModel. We can handle this interface-specific exception in the same place as the exception, and we can do some finishing work after the request ends:
fun getWeather(area: String) {
requestMain {
repository.getWeather(area, {
_weather.postValue(it)
}, {
it.printStackTrace()
Log.d(Companion.TAG, "Exception Handling")
}, {
Log.d(TAG, "Request closed, wrap up.")}}}Copy the code
And the next three parameters can be passed to a null implementation, except for the first one that executes the request. Avoid this ugly code when you don’t need to handle success, exception, completion, etc. If we send a data through sendData server, we don’t care whether the data is processed successfully or not. At this time, we can do as follows:
fun sendData(data: String) {
requestMain {
repository.launchRequest({
repository.sendData(data)}}}Copy the code
Going back to the launchRequest method, we return response directly when we process the result returned by the request. But in the actual development, when we request the interface to return data, we usually need to judge the interface data status code value is successful before returning data.
In our case, the state value is 0. So at this point we’re going to have to do something and add a method that handles response. Let’s modify the launchRequest method again:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend() - >CResponse<T>,
noinline onSuccess: ((T?). ->Unit)? = null.noinline onError: ((Exception) - >Unit)? = null.noinline onComplete: (() -> Unit)? = null
) {
try {
val response = block()
when (response.code) {
HttpConstant.OK -> {
val isListType = T::class.isSubclassOf(List::class)
if (response.data= =null&& isListType) { onSuccess? .invoke(Collections.EMPTY_LISTas? T)
} else{ onSuccess? .invoke(response? .data)}}else-> onError? .invoke(CException(response)) } }catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
/ /... Various exceptions that need to be handled separately
is ConnectException -> {
}
else-> { } } onError? .invoke(e) }finally{ onComplete? .invoke() } }Copy the code
It can be seen that when we process response, we first determine whether the returned poem type is a List collection type. If it is a collection type and the data returns a NULL, we try to convert an empty collection to the result.
val isListType = T::class.isSubclassOf(List::class)
if (response.data= =null&& isListType) { onSuccess? .invoke(Collections.EMPTY_LISTas? T)
} else{ onSuccess? .invoke(response? .data)}Copy the code
Multistate function return value mode
The above encapsulation is achieved through Kotlin’s higher-order functions. If we want to directly through the request results, then combined with other requests to handle the data notification interface refresh, the above appears to be very cumbersome, and seems to go into the pit of infinite nesting.
In this case, we need to directly handle the return value of the function. Now we first create a DataResult to encapsulate the returned result. We will classify the returned data as success or failure:
sealed class DataResult<out T> {
data class Success<out T>(val data: T) : DataResult<T>()
data class Error(val exception: Exception) : DataResult<Nothing>()
}
Copy the code
Then create a launchRequestForResult and copy the launchRequest code to modify it:
suspend inline fun <reified T : Any> launchRequestForResult(
noinline block: suspend() - >CResponse<T>): DataResult<T> {
return try {
val response = block()
if (0 == response.code) {
val isListType = T::class.isSubclassOf(List::class)
if (response.data= =null && isListType) {
DataResult.Success(Collections.EMPTY_LIST as? T) as DataResult<T>
} else {
DataResult.Success(response.data)}}else {
DataResult.Error(CException(response))
}
} catch (e: Exception) {
when (e) {
is UnknownHostException -> {
}
/ /... Various exceptions that need to be handled separately
is ConnectException -> {
}
else -> {
}
}
DataResult.Error(e)
}
}
Copy the code
We added the getWeather method to WeatherRepository to handle requests with launchRequestForResult:
suspend fun getWeather(area: String): DataResult<Weather> {
return launchRequestForResult {
ServerApi.service.getWeather(area)
}
}
Copy the code
Then we also add a getWeatherForResult method to the MainViewModel, where we can process the results in our usual code order:
fun getWeatherForResult(area: String) {
requestMain {
val result = repository.getWeather(area)
when(result){
is DataResult.Success ->{
_weather.postValue(result.data)}is DataResult.Error ->{
Log.d(TAG, "${(result? .exception}")}}}}Copy the code
Of course, this approach is still relatively cumbersome, because when we have multiple requests, we need to write multiple WHEN to determine the result. What if we don’t want to write template code
The way to return a value directly
At this point we need to further process the launchRequestForResult:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend() - >CResponse<T>): T? {
return try {
block()
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
/ /... Various exceptions that need to be handled separately
is ConnectException -> {
}
else- > {}}throwe }? .run {if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
return if (data= =null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data}}else {
throw CException(this)}}}Copy the code
The exception is rethrown with a throw because in a real development environment, we might still need to handle the exception hint externally. If the external interface does not want to handle CException, it can return null on catch as follows:
suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend() - >CResponse<T>): T? {
return try {
block()
} catch (e: Exception) {
null}? .run {if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
if (data= =null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data}}else {
throw CException(this)}}? : let {null}}Copy the code
Also add the getWeather method to WeatherRepository to handle the request by getting the launchRequest return value:
suspend fun getWeather(area: String): Weather? {
return launchRequest{
ServerApi.service.getWeather(area)
}
}
Copy the code
Since we rethrew an exception on the launchRequest, we need to catch it at the requested location:
fun getWeather(area: String) {
requestMain {
val weather = try {
repository.getWeather(area)
} catch (e: Exception) {
// Secondary exception handling...}}}Copy the code
The above three approaches are a kind of primer, but we can also further abstract the ViewModel to handle the internal interface request exception uniformly.
If you have better methods or ideas, welcome to exchange. The evolution of architecture and the encapsulation of code requires constant learning and communication, and every exchange and collision of knowledge is meaningful.
Need source code to see here: Demo source
Originality is not easy. If you like this article, you can move your little hands to like collection, your encouragement will be transformed into my motivation to move forward.
- Basic usage of Kotlin coroutines
- Kotlin coroutine introduction to Android version in detail (ii) -> Kotlin coroutine key knowledge points preliminary explanation
- Kotlin coroutine exception handling
- Use Kotlin coroutine to develop Android applications
- Network request encapsulation for Kotlin coroutines
Extend the series
-
Encapsulating DataBinding saves you thousands of lines of code
-
A ViewModel wrapper for everyday use