Use the Suspend Function on View | Actual Combat

Problems encountered

We have a sample application: Tivi, which can show the detailed information of TV programs. Regarding program information, every season and every episode is listed in the app. When the user clicks on one of the episodes, the detailed information of the episode will be displayed with the animation that expands at the click (0.2x speed display):

  • Tivihttps://tivi.app/

image

The application uses the InboxRecyclerView library to process the expansion animation in the figure:

fun onEpisodeItemClicked(view: View, episode: Episode) {
  • InboxRecyclerViewhttps://github.com/saket/InboxRecyclerView

The working principle of InboxRecyclerView is to find the corresponding item in RecyclerView through the item ID we provide, and then execute the animation. Next let us look at the problems that need to be solved. Near the top of these same UI interfaces, items for watching the next episode are displayed. This uses the same view type as the standalone episode below, but has a different item ID. For ease of development, the two items here reuse the same onEpisodeItemClicked() method. Unfortunately, this caused an abnormal animation when clicked (0.2 times faster display):

image

The actual effect does not expand from the clicked item, but expands a seemingly random item from the top. This is not our expected effect. The reasons for this problem are as follows:

  • The ID we use in the listener of the click event is obtained directly through the Episode class. This ID is mapped to a certain episode in the season list;

  • The items in this episode may not have been added to the RecyclerView. The user needs to expand the list of the season and then slide it to the screen, so that the view we need can be loaded by the RecyclerView.

Due to the above reasons, the dependent library is rolled back and the first entry is used for expansion.

Ideal solution

What is our expected behavior? We want to get this effect (0.2 times faster display):

image

It is implemented with pseudo code, which is roughly like this:

fun onNextEpisodeToWatchItemClick(view: View, nextEpisodeToWatch: Episode) {

But in reality, it should be more like the following implementation:

fun onNextEpisodeToWatchItemClick(view: View, nextEpisodeToWatch: Episode) {

We can find that there is a lot of code waiting for the completion of the asynchronous operation. The pseudo-code here seems not too complicated, but as soon as you start to implement these functions, you will immediately fall into callback hell. Here is the architecture tried to achieve using chained callbacks:

fun expandEpisodeItem(itemId: Long) {

This code is flawed and may not work properly, which aims to show that callbacks will greatly increase the complexity of UI programming. In general, this code has the following problems:

Serious coupling

Since we have to complete the transition animation through callbacks, each animation needs to be clear about the method that needs to be called next: Callback #1 calls Animation #2, Callback #2 calls Animation #3, and so on. These animations are not related, but we force them to be coupled together. Difficult to maintain/update

Two months later, the animator requested to add a fade-in and fade-out transition animation. You may need to track this part of the transition animation, check each callback to find the exact position to trigger the new animation, and then you have to test...

test

In any case, testing animations is very difficult, and the use of messy callbacks makes the problem even worse. In order to use assertions in callbacks to determine whether certain actions have been performed, your test must include all animation types. This article does not really involve testing, but using coroutines can make it easier.

Use coroutines to solve problems

In the previous article, we have learned how to use the suspend function to encapsulate the callback API. Let's use this knowledge to optimize our bloated callback code:

viewLifecycleOwner.lifecycleScope.launch {    

Readability has been greatly improved! The new suspend function hides all complex operations, resulting in a linear sequence of calling methods. Let us explore deeper details...

MotionLayout.awaitTransitionComplete()

Currently, there is no ktx extension method of MotionLayout for us to use, and MotionLayout does not support adding multiple monitors for the time being. This means that the implementation of awaitTransitionComplete() is much more complicated than other methods. Here we use a subclass of MotionLayout to implement multi-listener support: MultiListenerMotionLayout.

  • MotionLayout

    https://developer.android.google.cn/reference/android/support/constraint/motion/MotionLayout

  • MultiListenerMotionLayouthttps://gist.github.com/chrisbanes/a7371683c224464bf6bda5a25491aee0

Our awaitTransitionComplete() method is defined as follows:

/**

Adapter.awaitItemIdExists()

This method is very elegant, but also very effective. In the TV program example, several different asynchronous states are actually handled:

// 确保指定的季份列表已经展开,目标剧集已经被加载

This method uses the AdapterDataObserver of RecyclerView to monitor changes in the adapter data set:

/**
  • AdapterDataObserverhttps://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView.AdapterDataObserver.html

RecyclerView.awaitScrollEnd()

Need to pay special attention to the method of waiting for the scroll to complete: RecyclerView.awaitScrollEnd()

suspend fun RecyclerView.awaitScrollEnd() {
  • RecyclerViewhttps://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView.html

I hope so far, this code is easy to understand. The most tricky part of this method is that you need to call awaitAnimationFrame() before the fail-fast check. As mentioned in the comment, since the actual start of SmoothScroller is the next frame of the animation, we wait for one frame before judging the sliding state.

  • SmoothScrollerhttps://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView.SmoothScroller.html

The awaitAnimationFrame() method encapsulates postOnAnimation() to realize the next action of waiting for the animation, which usually occurs in the next rendering. The implementation here is similar to doOnNextLayout() in the previous article:

suspend fun View.awaitAnimationFrame() = suspendCancellableCoroutine<Unit> { continuation ->
  • postOnAnimation ()

    https://developer.android.google.cn/reference/android/view/View.html#postOnAnimation(java.lang.Runnable)

final effect

Finally, the effect of the operation sequence is shown in the figure below (0.2 times faster display):

image

Break the callback chain

Migrating to coroutines allows us to get rid of the huge callback chain, and too many callbacks make it difficult for us to maintain and test. For all APIs, the way to encapsulate callbacks, listeners, and observers into suspended functions is basically the same. Hope you can already feel the repetitiveness of the examples in our article. Then please make persistent efforts to free your UI code from chain callbacks!

This article  has been included in the open source project: https://github.com/Android-Alvin/Android-LearningNotes , which contains self-learning programming routes in different directions, interview question collection/face sutras, and a series of technical articles, etc. The resources are continuously being updated …

Guess you like

Origin blog.csdn.net/weixin_43901866/article/details/113997629
Recommended