Front row alerts, which will be covered in this articleAndroidXIf you’re confused, please leave a comment in the comments section and I’ll write about it next time, but it shouldn’t affect your understanding of the semantics of the code.

The code for this article can be vieweddemo

This is a web framework thinking driven by coroutines, Retrofit and LiveData, and Google demo.

The reason for this thought is that I have used too many network frameworks. For example, my dish 🐔, used OkGo, used RxJava version, my previous coroutine version, and so on, found that they did not win her heart.

First, I ask the essential question, why do we use third party encapsulated network open source libraries??

Think about it for a few seconds.

From my point of view: simplicity, convenience, safety.

Think about it. Is that right?

I’ve had a second thought about coroutines’ web requests since using the Kotlin gift pack, see The idea – Kotlin coroutines Are elegant with Retrofit – Text, is the web encapsulation elegant in this article? I’ll leave it to you. In terms of calling, it might be elegant.

After reading and reread the code for the web framework part of Google’s official sample project, Github Browser, over time, I had an Epiphany one day.

What is the essence? The KISS principle!

KISS principle

KISS is short for “Keep it Simple and Stupid,” meaning “Keep it Simple and Stupid.” There are other variants of the acronym, such as: “Keep it short and simple,” “Keep it stupid simple” and “Keep it simple and straightforward.”

The core message is the same: be as simple and simple as possible. The fewer things there are, the more secure and easy to maintain.

It was the idea of engineers at lockheed Martin, the aircraft manufacturer.

The pilots, of all people, must understand the KISS principle.

So let me ask you another question:

With Retrofit, can we do the following at the same time:

1, do not use any of the Retrofit CallAdapterFactory, for example: RxJava2CallAdapterFactory, LiveDataCallAdapterFactory;

Retrofit’s Call interface returns the original method without using the CallAdapterFactory.

3, the code uses a sequential process, simple to understand, just use the extension method to complete the encapsulation;

4, the network request automatically closed, no risk of leakage;

5. Don’t use any third-party libraries except for Kotlin, AndroidX and Retrofit!

Do you think it’s possible to meet all of these conditions?

No, let’s get started…

Packages that need to be imported

// The Activity Lifecycle extension that includes the coroutine
implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0." "

// The coroutine network requests the poured packet
implementation "Com. Squareup. Retrofit2: retrofit: 2.7.1." "
/ / the json conversion
implementation "Com. Squareup. Retrofit2: converter - moshi: 2.7.1." "

/ / kotlin coroutines
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.3." "
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: "$kotlin_version"
Copy the code

As you can see, the packages used here are all from three official sources:

  • squareupThe Retrofit network framework, as wellmoshiConverter, if you likeGson, then replace Gson’s converter package (Note: inkotlinRecommended to use inmoshi, very friendly to Kt.GsonNot supported in Ktdata classDefault parameter!! No solution, there are other pits, later free to write an article)
  • kotlinThe official coroutine package
  • AndroidXThe officiallifecycleExtension, which contains the Activity, Fragment and other coroutine encapsulation

Use of coroutines in Retrofit (skip it if you know it)

In the new version of Retrofit, you can use it directly, so I’m just going to go over it briefly, without going into too much detail, but there are more tutorials online.

The construction of a Retrofit

fun getRetrofit(a): Retrofit {
    // Build Retrofit normally, no difference
    val builder = OkHttpClient.Builder()

    return Retrofit.Builder()
        .baseUrl("https://api.apiopen.top")
        .addConverterFactory(MoshiConverterFactory.create()) // Json converter
        .client(builder.build())
        .build()
}
Copy the code

Api

interface NewsApi {
    /** * Interfaces need to be added [suspend]! * The return value is your datatype, no need to wrap anything else */
    @GET("/getWangYiNews")
    suspend fun getNews(a): NewsBean
}
Copy the code

Activity simple use

class MainActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityMainBinding

    private val mAdapter = MainRvAdapter()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        viewBinding.recyclerView.adapter = mAdapter
 // The network request starts  // Use the Scope of the activity to create a coroutine that is lifecycle aware and automatically destroyed lifecycleScope.launch { try { // Normal creation API, no difference val newsApi = getRetrofit().create(NewsApi::class.java)  // Call the API method directly, do not need to do any asynchronous operations themselves val newsBean: NewsBean = newsApi.getNews()  // Refresh the interface directly, because this is a UI thread coroutine mAdapter.setList(newsBean.list)  // These are the three key lines of code to get the data. Retrofit also doesn't need to set up Rx or anything like that. } catch (e: Throwable) { // This is where network errors are handled if (e is HttpException) { // HTTP status code when (e.code()) { 400 -> { } 500-> {}}else if (e is SocketTimeoutException) { // The connection timed out } else if (e is SocketException || this is UnknownHostException || this is SSLException ) { // Various other network errors...}}}}}Copy the code

Ok, so that’s the basic use, and the basic meaning is written in the code comments. After seeing is scalp hairpin, need slow?

I have a question for you. In the codelifecycleScopeHow did it come about?

LifecycleScope is androidx. Lifecycle: lifecycle – runtime – KTX: 2.2.0 expansion pack, the official has achieved a lifecycle of perception coroutines scope, can be directly open coroutines, and closed the page automatically destroyed.

lifecycleScopeextension

In lifecycleScope, you can also use:

// Open a coroutine directly, the most common
lifecycleScope.launch {
}

// Open the coroutine content after the lifecycle reaches Create
lifecycleScope.launchWhenCreated {
}

// Open the coroutine content after the lifecycle goes to Start
lifecycleScope.launchWhenStarted {
}

// Open the coroutine content after the life cycle reaches Resume
lifecycleScope.launchWhenResumed {
}
Copy the code

What f**k? What the hell is this? I’ve never seen this before. What a surprise! Hey, hey, Google is taking coroutines so seriously. (Interested to see the source code, not expanded here)

All right, let’s get back to our web request.

The above basic usage, while very simple, clear, and easy to read, is certainly not elegant in a project.

Using DSL encapsulation

The general content is similar to the text I wrote earlier about thinking -kotlin coroutines elegant and Retrofit lingering – but no more nonsense. (Code written in the demo, you can view)

Direct use is as follows:

lifecycleScope.retrofit<NewsBean> {
    api = api.getNews()

    onComplete {
    }

    onSuccess { bean ->
    }
onFailed { error, code -> } }Copy the code

If you don’t need a design pattern or MVVM to request the network directly from your Activity, then using DSL encapsulation can handle most network requests

This article highlights: with LiveData encapsulation

You need to add packages to import

/ / LiveData package
implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0." "

// Extension of the activity
implementation "Androidx. Activity: activity - KTX: 1.1.0." "
Copy the code

In the MVVM design pattern (we are only talking about the ViewModel layer here), is it ok to directly use DSL encapsulation in the ViewModel and then convert it to LiveData values? Sure. But I think it’s a lot of work.

In the official Google sample project dead simple Browser, ViewModel LiveData object, getting the result of just network and official use coroutines is cooperate with LiveDataCallAdapterFactory converter, In cases where Retrofit does not support coroutines, this is a better solution. But now, to be frank, this is an outdated way of writing, extremely inelegant, Chen Ping dare to question the classical economic theory, why can’t we question Google’s demo, just because he is a foreigner, just because he is Google written?

What exactly does a ViewModel need?

Look at the essence, look at the essence, look at the essence, look at the essence, the Google demo is telling us the idea that what a ViewModel needs is a network data encapsulated in LiveData, and that’s it.

So in the absence ofLiveDataCallAdapterFactoryHow do we encapsulate

-the easiest way to think about it

At the beginning, I have copied Google demo no less than 4 times, each copy has new progress, and finally found that it is actually a way to write: put the results of network requests into LiveData.

The simplified code model is as follows:


fun CoroutineScope.getHttpLiveData(a): LiveData<NewsBean> {
  // Define LiveData first
  val requestLive = MutableLiveData<NewsBean>()

  this.launch(Dispatchers.Main) {
     / / create API
     val newsApi = getRetrofit().create(NewsApi::class.java)

     // Call the API method
     val newsBean: NewsBean = newsApi.getNews()

     // Give the value to LiveData
     requestLive.value = newsBean
  }

  return requestLive
}
Copy the code

Okay, this way of writing it, it doesn’t seem to be a problem. Yes, the code logically works and there’s nothing wrong with it, but it’s, it’s, it’s almost inside, it feels like this LiveData is pieced together, and I want to do it in a more authentic, authentic way.

– liveData mode (finally the theme)

Notice, I said liveData, not liveData, and I started with lower case, not upper case.

Finished, some classmate giddy again, “this TM what ghost??? Can you fucking speak Human?”

Listen to me come slowly.

LiveData, uppercase, is an abstract class, is the base class for all LiveData, is a real class.

And liveData in lowercase, that’s a method that comes with the AndroidX package, and notice, it’s a method that generates a CoroutineLiveData, which is a coroutine that comes with it, and I was shocked when I saw that.

This dish chicken is also inadvertently, found that the system provides a method, after looking at the relevant source code, found that this is simply a perfect model to solve the problem.

Let’s take off. The simplified model is as follows:

fun CoroutineScope.getHttpLiveData(a): LiveData<NewsBean> {
    // use coroutineContext to generate a LiveData
    return liveData(this.coroutineContext) {
        / / create API
        val newsApi = getRetrofit().create(NewsApi::class.java)
        // Call the API method
        val newsBean: NewsBean = newsApi.getNews()
        // Submit data
        emit(newsBean)
    }
}
Copy the code

The core idea is just a few lines of code above. Ok, that’s it. Now we can continue to abstract the Api and encapsulate network requests.

“Stop, stop! Wait, I TM good meng, this is what operation, the steering wheel I welding dead, you do not say a don’t want to go “.

Don’t worry, don’t worry, I’m going to leave the liveData method explanation to you later, so we can continue the encapsulation.

Preliminary packaging molding

Before we start, we need to get things ready

Define a RequestStatus to distinguish network RequestStatus
enum class RequestStatus {
    START,
    SUCCESS,
    COMPLETE,
    ERROR
}
Copy the code

The four states correspond to the start, success, completion and failure of network requests

Defines ResultData, which encapsulates network data
data class ResultData<T>(val requestStatus: RequestStatus,
                         val data: T?
                         val error: Throwable? = null) {
    companion object {

        fun <T> start(a): ResultData<T> {
            return ResultData(RequestStatus.START, null.null)}
        fun <T> success(data: T? , isCache:Boolean = false): ResultData<T> {
            return ResultData(RequestStatus.SUCCESS, data.null)}
        fun <T> complete(data: T?).: ResultData<T> {
            return ResultData(RequestStatus.COMPLETE, data.null)}
        fun <T> error(error: Throwable?).: ResultData<T> {
            return ResultData(RequestStatus.ERROR, null, error)
        }
    }
Copy the code
Define an ApiResponse that identifies which network status is being returned
internal sealed class ApiResponse<T> {
    companion object {
        fun <T> create(error: Throwable): ApiErrorResponse<T> {
            return ApiErrorResponse(error)
        }

        fun <T> create(body: T?).: ApiResponse<T> {
            return if (body == null) {
                ApiEmptyResponse()
            } else {
                ApiSuccessResponse(body)
            }
        }
    }
}

internal class ApiEmptyResponse<T> : ApiResponse<T>(a)
internal data class ApiSuccessResponse<T>(val body: T) : ApiResponse<T>()

internal data class ApiErrorResponse<T>(val throwable: Throwable) : ApiResponse<T>()
Copy the code
And then define aRequestActionClass for DSLS that wraps the methods that need to be manipulated.
open class RequestAction<ResponseType> {
    var api: (suspend () -> ResponseType)? = null

    fun api(block: suspend() - >ResponseType) {
        this.api = block
    }
}
Copy the code

I’m sure the API is going to be passed in dynamically, so you can’t just write it to death. So let’s come up with DSL.

For the sake of simplicity, let’s define just one API-related method, and the rest will follow.

Everything is ready to go!
/** * DSL network request */
inline fun <ResultType> CoroutineScope.requestLiveData(
    dsl: RequestAction<ResultType>. () - >Unit
): LiveData<ResultData<ResultType>> {
    val action = RequestAction2<ResultType>().apply(dsl)
    return liveData(this.coroutineContext) {
        // Notify the network request to start
        emit(ResultData.start<ResultType>())

        val apiResponse = try {
            // Get the network request data
            valresultBean = action.api? .invoke() ApiResponse.create<ResultType>(resultBean) }catch (e: Throwable) {
            ApiResponse.create<ResultType>(e)
        }

        // Process the object according to the ApiResponse type
        val result = when (apiResponse) {
            is ApiEmptyResponse -> {
                null
            }
            is ApiSuccessResponse -> {
                apiResponse.body.apply {
                    // Submit the successful data to LiveData
                    emit(ResultData.success<ResultType>(this))}}is ApiErrorResponse -> {
                // Submit the incorrect data to LiveData
                emit(ResultData.error<ResultType>(apiResponse.throwable))
                null}}
        // Submit the successful information
        emit(ResultData.complete<ResultType>(result))
    }
}
Copy the code

As you can see, all the logic is executed sequentially! Simple and clear to understand, do not need to deal with thread conversion. Using emit, you can submit the data to LiveData

ViewModel

It’s pretty much the same as in the Google demo, but it doesn’t change much

class MainViewModel : ViewModel() {

    private val newsApi = getRetrofit().create(NewsApi::class.java)

    private val _newsLiveData = MediatorLiveData<ResultData<NewsBean>>()

    // only the abstract LiveData is exposed to the outside, preventing the outside from arbitrarily changing the data
    val newsLiveData: LiveData<ResultData<NewsBean>>
        get() = _newsLiveData

    fun getNews(a) {
        // viewModelScope is the coroutine scope of the ViewModel provided by the system extension
        val newsLiveData = viewModelScope.requestLiveData<NewsBean> {
            api { newsApi.getNews() }
        }
 // Listen for data changes _newsLiveData.addSource(newsLiveData) { if (it.requestStatus == RequestStatus.COMPLETE) { _newsLiveData.removeSource(newsLiveData) } _newsLiveData.value = it } } } Copy the code

ViewModelScope is the coroutine scope of the ViewModel provided by the system extension and automatically manages the coroutine life cycle.

An invocation in an Activity
class MainActivity : AppCompatActivity() {
    // Use the system extension proxy to generate viewModel quickly
    private val viewModel by viewModels<MainViewModel>()

    private lateinit var viewBinding: ActivityMainBinding

    private val mAdapter = MainRvAdapter()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        viewBinding.recyclerView.adapter = mAdapter
 / / subscribe LiveData viewModel.newsLiveData.observe(this, Observer { // Process the data according to the state when (it.requestStatus) { RequestStatus.START -> { } RequestStatus.SUCCESS -> { it.data? .let { newsBean ->// Refresh the interface directly mAdapter.setList(newsBean.result) } } RequestStatus.COMPLETE -> { } RequestStatus.ERROR -> { Toast.makeText(this."There is a network error.", Toast.LENGTH_SHORT).show() } } })  }  override fun onStart(a) { super.onStart() // Call the method to get the data where you need it viewModel.getNews() } }  Copy the code

That’s a simple encapsulation that includes the core idea. You can go further and encapsulate it (for example, when subscribing to Livedata, state processing can be encapsulated), but I’m just trying to get you started here. More complete code can be viewed in the demo, a little change can be used in the project.

conclusion

Now that Retrofit supports coroutines, we can throw out the CallAdapterFactory converter.

Overall, we can see that Google also makes coroutines very convenient! Activity, Fragment, ViewModel, all come with coroutine scope, and even designed a coroutine scope specifically for LiveData. Google is trying to push coroutine to the top.

For us developers, writing code is getting easier and dumb-ass, so let’s focus more on the business side of development.

In that case, should we think about whether a network request framework really needs Rx and other third-party libraries? The official has been simplified to such a point, why don’t we make good use of it, besides, the quality of the official library is guaranteed, won’t suddenly stop more.

Those of you who haven’t got on the kotlin bus yet, hurry up, and those of you who don’t know coroutines yet, hurry up, or you’ll be out of sync with AndroidX.

digression

I don’t know how to write, so I just do hard code, and I don’t explain it in detail, but please forgive me, this article will not be finished if I have to explain all the official AndroidX extensions.

If you have a lot of questions about the new content, I will write the corresponding article.

Again: The complete code can be seen in the demo

This article was typeset using MDNICE