purpose

This article doesn’t cover anything deep in Flow, but you can use it even if you don’t know Flow.

There are two ways to encapsulate Retrofit+ coroutines for elegant and fast network requests

Recently, I was writing a new project independently, using package II. Although a few lines of code can make network requests, I still felt a little regretful in the process of use, and it was not very fast to write, because there were template codes.

In addition, many friends want to have a Flow version, so they take a break from busy work. I optimized this framework with Kotlin Flow and found that Flow is really sweet.

First, the regret of the previous package

It mainly focuses on the following two points:

  • The processing of Loading

  • Redundant LiveData

In short, there’s a lot of template code to write.

One of the biggest benefits of not having to write template code is that the less code you write, the less likely you are to go wrong.

1.1 Handling Loading

For encapsulation two, although decoupling is more complete than encapsulation one, I still have some regrets about Loading.

Imagine an Activity that has a lot of traffic and complex logic, and many network requests. You need to manually perform showLoading() wherever network requests are required, and then manually invoke stopLoading() in observer().

If the Activity code is complex and there are multiple APIS, there are many methods related to loading in the Activity.

Also, if the showLoading() and dismissLoading() methods of a network request are far apart. This results in a fragmentation of sequential processes.

ShowLoading () before the request starts –> request network –> stopLoading() after the request ends, this is a complete process, the code should try to be together, at a clear sight, should not separate existence.

ShowLoading () or stopLoading() can also cause problems if the code is too large for future maintenance.

There is also the need to manually call these two methods every time, trouble.

1.2 Duplicate LiveData declarations

In my opinion, common network requests fall into two categories:

  • The lost after use

  • You need to listen for data changes

For a common example, look at this page:

Once the user enters this page, the content in the green box will basically not change (not to worry about whether the wechat page is a Webview or not). In fact, this KIND of UI does not need to set a LiveData to monitor, because it will almost never update.

Typically, click on the login button and proceed to the next page.

But the UI inside the red box is different and needs to refresh data in real time, using LiveData listeners, where the benefits of the observer subscriber mode really come into play. And from other pages, LiveData will automatically update the latest data.

For throwaway network requests, LoginViewModel will have code like this:

// LoginViewModel.kt
valloginLiveData = MutableLiveData<User? > ()vallogoutLiveData = MutableLiveData<Any? > ()valforgetPasswordLiveData = MutableLiveData<User? > ()Copy the code

And the corresponding Activity also needs to listen to these three LiveData.

This kind of template code makes me really bored.

After Flow optimization, these two pain points can be perfectly solved.

“Talk is cheap. Show me the code.

Second, the use of integrated Flow

2.1 Request Loading&& Does not need to listen for data changes

Requirements:

  • Do not need to monitor data changes, corresponding to the above disposable throw

  • There is no need to declare LiveData member objects in the ViewModel

  • ShowLoading () automatically before the request is initiated and stopLoading() automatically after the request is completed

  • Similar to clicking the login button, finish the current page and jump to the next page

Sample code from TestActivity:

// TestActivity.kt
private fun login(a) {
    launchWithLoadingAndCollect({mViewModel.login("username"."password")}) {
        onSuccess = { data->
            showSuccessView(data)
        }
        onFailed = { errorCode, errorMsg ->
            showFailedView(code, msg)
        }
        onError = {e ->
            e.printStackTrace()
        }
    }
}
Copy the code

TestViewModel:

// Code in TestViewModel
suspend fun login(username: String, password: String): ApiResponse<User? > {return repository.login(username, password)
}
Copy the code

2.2 Requests without Loading&& Do not need to declare LiveData

Requirements:

  • There is no need to listen for data changes

  • There is no need to declare LiveData member objects in the ViewModel

  • There is no need to display Loading

// TestActivity.kt
private fun getArticleDetail(a) {
    launchAndCollect({ mViewModel.getArticleDetail() }) {
            onSuccess = {
                showSuccessView()
            }
            onFailed = { errorCode, errorMsg ->
                showFailedView(code, msg)
            }
            onDataEmpty = {
                showEmptyView()
            }
        }
}
Copy the code

TestViewModel has the same code as above, so I’m not going to write it here.

ShowLoading () and stopLoading() are no longer required.

In addition, the result of the request is directly received and processed in the callback. In this way, the network and the result are processed together, and it looks clear ata glance. There is no need to search around in the Activity to find where to listen to LiveData.

Also, like LiveData, it listens for the life cycle of an Activity without causing a memory leak. Because it runs in the Activity’s lifecycleScope coroutine scope.

2.3 Data changes need to be monitored

Requirements:

  • You need to listen for data changes, you need to update data in real time

  • LiveData member objects need to be declared in the ViewModel

  • For example, obtain the latest configuration and user information in real time

Sample code from TestActivity:

// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

    private fun initObserver(a) {
        mViewModel.wxArticleLiveData.observeState(this) {
        
            onSuccess = { data: List<WxArticleBean>? ->
                showSuccessView(data)
            }

            onDataEmpty = { showEmptyView() }

            onFailed = { code, msg -> showFailedView(code, msg) }

            onError = { showErrorView() }
        }
    }

    private fun requestNet(a) {
        / / need to Loading
        launchWithLoading {
            mViewModel.requestNet()
        }
    }
}
Copy the code

Example code in ViewModel:

class ApiViewModel : ViewModel() {

    private val repository by lazy { WxArticleRepository() }

    val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

    suspend fun requestNet(a) {
        wxArticleLiveData.value = repository.fetchWxArticleFromNet()
    }
}
Copy the code

The setValue() method of LiveData is essentially called through FLow, or the use of LiveData. Although it can be completely realized by Flow, I think it is troublesome to use Flow here and it is not easy to understand.

This approach is similar to encapsulation 2 in the previous article, except that Loading methods are not manually called.

Three, unpacking

If the generic method is not extracted, it is written like this:

// TestActivity.kt
private fun login(a) {
    lifecycleScope.launch {
        flow {
            emit(mViewModel.login("username"."password"))
        }.onStart {
            showLoading()
        }.onCompletion {
            dismissLoading()
        }.collect { response ->
            when (response) {
                is ApiSuccessResponse -> showSuccessView(response.data)
                is ApiEmptyResponse -> showEmptyView()
                is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
                is ApiErrorResponse -> showErrorView(response.error)
            }
        }
    }
}
Copy the code

A brief introduction to Flow:

Flow is similar to RxJava, with operators similar to RxJava, but much simpler. Kotlin uses Flow to implement sequential Flow and chained programming.

The flow keyword in curly braces is the execution of the method, and the result is sent downstream via emit.

OnStart represents the action performed before the method is first called. Here is a loading UI;

OnCompletion means that all execution is complete, and the callback will be executed with or without exceptions.

Collect represents a successful result callback, which is sent by the emit() method. Flow must execute collect to receive the result. Because there’s cold flow, there’s heat flow.

For more information on Flow, see other blogs and official documentation.

It can be seen that Flow perfectly solves the display and hiding of loading.

Here we are calling flow in the Activity, so we can extend BaseActivity.

Why extend BaseActivity?

Because startLoading() and stopLoading() are in BaseActivity. 😂

3.1 Solve the Loading template code of flow

fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend() - >ApiResponse<T>): Flow<ApiResponse<T>> {
    return flow {
        emit(block())
    }.onStart {
        showLoading()
    }.onCompletion {
        dismissLoading()
    }
}
Copy the code

Each time launchWithLoadingGetFlow is called, Loading is shown and hidden, and a FLow object is returned.

The next step is to process the template code in the flow result COLLECT.

3.2 Declare the result callback class

class ResultBuilder<T> {
    var onSuccess: (data: T?) -> Unit = {}
    var onDataEmpty: () -> Unit = {}
    var onFailed: (errorCode: Int? , errorMsg: String?) ->Unit= {_, _ ->}var onError: (e: Throwable) -> Unit = { e -> }
    var onComplete: () -> Unit= {}}Copy the code

All callbacks can be deleted according to the project features.

3.3 Parsing the ApiResponse object

private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
                                       listenerBuilder: ResultBuilder<T>. () - >Unit) {
    val listener = ResultBuilder<T>().also(listenerBuilder)
    when (response) {
        is ApiSuccessResponse -> listener.onSuccess(response.response)
        is ApiEmptyResponse -> listener.onDataEmpty()
        is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
        is ApiErrorResponse -> listener.onError(response.throwable)
    }
    listener.onComplete()
}
Copy the code

In the previous article, we used inheritance LiveData and Observer, but we don’t need it here, because inheritance can be used as little as possible.

3.4 Final extraction method

Connect the above steps as follows:

fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend() - >ApiResponse<T>, 
                                                listenerBuilder: ResultBuilder<T>. () - >Unit) {
    lifecycleScope.launch {
        launchWithLoadingGetFlow(block).collect { response ->
            parseResultAndCallback(response, listenerBuilder)
        }
    }
}
Copy the code

3.5 Convert Flow into LiveData objects

You get a Flow object, and if you want to become a LiveData, the Flow native supports converting a Flow object into an immutable LiveData object.

valloginFlow: Flow<ApiResponse<User? >> = launchAndGetFlow(requestBlock = { mViewModel.login("UserName"."Password")})valloginLiveData: LiveData<ApiResponse<User? >> = loginFlow.asLiveData()Copy the code

Call Flow’s asLiveData() method, which uses the liveData extension function:

@JvmOverloads
fun <T> Flow<T>.asLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
    collect {
        emit(it)
    }
}
Copy the code

What is returned here is LiveData

The previous way is inheritance, has the following disadvantages:

  • Must useStateLiveDataCan’t use nativeLiveDataVery invasive
  • Not just inheritanceLiveData, but also inheritObserverThe trouble,
  • A bunch of code was written to do this

This is implemented with the Kotlin extension, which extends LiveData directly:

@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
    owner: LifecycleOwner,
    listenerBuilder: ResultBuilder<T>. () - >Unit
) {
    val listener = ResultBuilder<T>().also(listenerBuilder)
    observe(owner) { apiResponse ->
        when (apiResponse) {
            is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
            is ApiEmptyResponse -> listener.onDataEmpty()
            is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
            is ApiErrorResponse -> listener.onError(apiResponse.throwable)
        }
        listener.onComplete()
    }
}
Copy the code

Thanks to the Flywith24 open source library for ideas, I feel like I’m still writing Kotlin in Java sometimes.

3.6 Further Improvement

Many network requests are not only related to the loading state, but also need to handle some specific logic before and after the request.

By default, loading is implemented as a callback.

fun <T> BaseActivity.launchAndCollect(
    requestBlock: suspend() - >ApiResponse<T>,
    startCallback: () -> Unit = { showLoading() },
    completeCallback: () -> Unit = { dismissLoading() },
    listenerBuilder: ResultBuilder<T>.() -> Unit
)
Copy the code

4. Targeting multiple data sources

Although most of the projects are single data sources, occasionally there are multiple data sources, which combined with the Flow operator is also very convenient.

The sample

If the same data can be retrieved from a database, it can be retrieved from a network request. The code for TestRepository is as follows:

// TestRepository.kt
suspend fun fetchDataFromNet(a): Flow<ApiResponse<List<WxArticleBean>>> {
    val response =  executeHttp { mService.getWxArticle() }
    return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(a): Flow<ApiResponse<List<WxArticleBean>>> {
    val response =  getDataFromRoom()
    return flow { emit(response) }.flowOn(Dispatchers.IO)
}
Copy the code

The return in Repository no longer returns the entity class directly, but the entity class object wrapped in Flow.

Why would you do that?

To use the magic of the flow operator.

The flow composite operator

  • Combine, combineTransform

The Combine operator can join two different flows.

  • merge

The merge operator is used to merge multiple streams.

  • zip

The ZIP operator takes values from both streams, and the zip process is complete when data is fetched from one stream.

About the basic operators of Flow, Dr. Xu dashen has written a great article, so there is no more to write here.

As you can see from the operator example, the operator can be used even if the object returned is not the same.

When I first started learning RxJava a few years ago, I gave up several times. There were too many operators and I was confused. Flow is much simpler than RxJava.

5. Flow’s bizarre techniques

flowWithLifecycle

Requirement: The Activity’s onSume() method requests the latest geolocation information.

Previously written:

// TestActivity.kt
override fun onResume(a) {
    super.onResume()
    getLastLocation()
}

override fun onDestory(a) {
    super.onDestory()
    // Release the code that gets the location to prevent memory leaks
}

Copy the code

That’s fine, that’s normal, but now that we use Flow, we have a new way of writing it.

Use the expression flow:

// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation(a) {
    if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
        lifecycleScope.launch {
            SharedLocationManager(lifecycleScope)
            .locationFlow()
            .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
            .collect { location ->
                Log.i(TAG, "The latest position is:$location")}}}}Copy the code

Write this function in onCreate, and then add to the chain call to flow:

.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)

FlowWithLifecycle listens to the Activity’s life cycle and stops automatically when the Activity’s onResume starts requesting location information and onStop, without causing a memory leak.

FlowWithLifecycle sends the project and cancels the internal producer as the lifecycle enters and leaves the target state.

This API need to introduce androidx. Lifecycle: lifecycle – runtime – KTX: 2.4.0 – rc01 dependent libraries.

callbackFlow

Did you find it easy to call the code in 5.1 to get location information?

SharedLocationManager(lifecycleScope)
    .locationFlow()
    .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
    .collect { location -> 
         Log.i(TAG, "The latest position is:$location")}Copy the code

Get location information in a few lines of code, and call it directly from anywhere, don’t write a bunch of code.

CallbackFlow is used to write callback code synchronously.

Here is the code for SharedLocationManager directly, the details are up to Google, because this is not the content of the web framework.

Here is the main code:

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private val _locationUpdates: SharedFlow<Location> = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?).{ result ? :return
            Log.d(TAG, "New location: ${result.lastLocation}")
            trySend(result.lastLocation)
        }

    }
    Log.d(TAG, "Starting location updates") fusedLocationClient.requestLocationUpdates( locationRequest,callback,Looper.getMainLooper()) .addOnFailureListener { e  ->close(e)} awaitClose { Log.d(TAG,"Stopping location updates")
        fusedLocationClient.removeLocationUpdates(callback)
    }
}.shareIn(
    externalScope,
    replay = 0,
    started = SharingStarted.WhileSubscribed()
)
Copy the code

See GitHub for the full code

conclusion

Previous article # Two ways to encapsulate Retrofit+ Coroutines for elegant and fast network requests

Together with this flow network request encapsulation, there are three network encapsulation methods for Retrofit+ coroutines.

Compare the three encapsulation modes:

  • Package one (corresponding to branch oneWay) passes UI references, allowing for deep UI customization by project, which is fast and convenient, but with high coupling

  • Wrapper two (corresponding to the branch master) has low coupling and very few dependencies, but a lot of template code to write

  • Encapsulation three (the equivalent of branching Dev) introduces the new flow programming (which has been around for a long time, but most people don’t use yet), chain calls, loading and network requests, and result handling together, and in many cases don’t even declare LiveData objects.

I have used the second kind of encapsulation in the company’s commercial project App for a long time, involving dozens of interfaces, and have not encountered any problems at present.

The third one IS something I’ve been working on recently for the company’s new project (not yet online) and haven’t had any problems with yet.

If someone has a different opinion on this article, or finds a bug in package 3, please point it out. Thank you very much!

The project address

FastJetpack

The project continues to update…