Refactor Android asynchronous code using promises

Author: Wushan Old Demon

Background

Writing Android asynchronous tasks in business has always been a challenge. The previous callback and thread management methods were complex and cumbersome, making the code difficult to maintain and read. JavaScript actually faces the same problem in the front-end field, and Promise is one of its more mainstream solutions. Before trying to use Promise, we also made a detailed comparison of some existing asynchronous functions in Android.

Article mind map

What: What is a Promise?

For Android development students, many may not be familiar with Promise. It is mainly a front-end practice, so let’s analyze the concept first. Promise is a standardized asynchronous management method provided by the JavaScript language. Its general idea is that functions that require io, waiting or other asynchronous operations do not return real results, but return a “Promise” , the caller of the function can choose to wait for this promise to be fulfilled at the appropriate time (through the callback of the then method of Promise).

The simplest example (JavaScript)

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* asynchronous operation successful */){
    resolve(value);
  } else {
    reject(error);
  }
}).then(function(value) {
    console.log('resolved.');
}).catch(function(error) {
    console.log('An error occurred!', error);
});

Instantiate a Promise object. The constructor accepts a function as a parameter, which are resolve and reject.

resolve function: changes the Promise object status from pending to resolved

Reject function: Change the Promise object status from pending to rejected

then function: callback the result of the resolved state catch function: callback the result of the rejected state

You can see that the state of Promise is very simple and clear, which also reduces a lot of cognitive load when implementing asynchronous programming.

Why: Why should you consider introducing Promise

Isn’t the Promise mentioned earlier just an idea of JavaScript asynchronous programming? So what does this have to do with Android development? Although the fields of front-end and terminal are different, the problems faced are actually similar, such as common asynchronous callbacks leading to callback hell, incoherent logical processing and other issues. Students engaged in Android development should be familiar with the following asynchronous programming scenarios:

  • single network request
  • Racing multiple network requests
  • Wait for multiple asynchronous tasks to return results
  • Asynchronous task callback
  • Timeout processing
  • Regular polling

You can pause and think about it here. What would you do if you used the conventional Android method to implement the above scenario? You may have the following solutions in mind:

  • Create using Thread
  • Use Thread + Looper + Handler
  • Use Android native AsyncTask
  • Use HandlerThread
  • Use IntentService
  • Use thread pool
  • Using the RxJava framework

The above solutions can all implement asynchronous task processing in Android, but there are more or less problems and applicable scenarios. We will analyze their respective advantages and disadvantages in detail:

By comparing different asynchronous implementation methods, we can find that each implementation method has applicable scenarios, and the business complexity we face is also different. Each solution is to reduce business complexity and use a lower-cost method. To code, but we also know that the code is written for people to read, and it requires continuous iteration and maintenance. Frameworks like RxJava are too complex for us, and cumbersome operators make it easy to write code that is not easy to maintain. Simple and easy Understanding should be a better pursuit, rather than showing off skills, so we will explore using lighter and more concise coding methods to improve the team’s code consistency. For now, using Promise to write code will have the following benefits:

  • Solution to callback hell: Promise can turn nested callbacks into .then().then()... , making code writing and reading more intuitive
  • Easy to handle errors: Promise is clearer and more intuitive in error handling than callback
  • Very easy to code multiple asynchronous operations

How: How to use Promise to refactor business code?

Since our Java version of the Promise component is not open source, this section only analyzes the refactoring Case use cases.

Refactor case1: How to implement a network interface request with timeout?

This is a piece of asynchronous code to obtain the payment code before reconstruction:

You can see that the above code has the following problems:

  • Need to define an asynchronous callback interface
  • Many if-else judgments, high cyclomatic complexity
  • The business implements a timeout class in order not to be affected by the default timeout of the network library.
  • The logic is not coherent enough and difficult to maintain

After refactoring using Promise:

You can see the following changes:

  • The asynchronous callback interface is eliminated, and chain calls make the logic more coherent and clear.
  • The network request call is packaged through Promise and returns Promise uniformly.
  • Promise timeout is specified, eliminating the need to implement additional cumbersome timeout logic
  • Use the validate method to replace if-else judgment, and you can also define verification rules if necessary
  • Exception errors are handled uniformly, and the logic becomes more complete.

Refactoring case 2: How to downgrade long links to short links more elegantly?

What to do before refactoring:

The code has the following problems:

  • Handle long link request timeout and handle downgrade logic through callback
  • Use Handler to implement timer polling to request asynchronous results and handle callbacks
  • Handling various logical judgments makes the code difficult to maintain
  • It is not easy to simulate timeout degradation and the code has poor testability.

After refactoring using Promise:

The first Promise handles the long link Push monitoring, sets a 5s timeout, and calls back the except method when a timeout exception occurs. It determines the throwable type. If it is a PromiseTimeoutException instance object, the short link is downgraded. Short links are another Promise. In this way, the logic is completely resolved, the code will not be fragmented, and the logic will be more coherent. The short link training query logic is implemented using Promise:

  • The outermost Promise controls the overall timeout, that is, regardless of the polling result, a failure result will be given directly after the limited time.
  • Promise.delay(), for this comparison, we believe that 500ms polling will definitely not return a result, so we will reduce one polling request by delaying it.
  • Promise.retry(), the real retry logic, limits the maximum number of retries and delay logic. RetryStrategy defines the retry strategy, how much delay (delay) and what conditions are met before retrying is allowed.

This code implements complex delays, conditional judgments, and retry strategies through the Promise framework. There are fewer temporary variables, less code, and clearer logic.

Refactoring case 3: Implementing iLink Push payment messages and short link training order racing

Later, the downgrade strategy was restructured into a racing model, and Promise.any was used to easily implement code reconstruction. The code is shown in the figure below.

Summary

This article provides an idea of asynchronous programming, drawing on Promise ideas to reconstruct Android’s asynchronous code. The various concurrency models provided by the Promise component can solve most scenario requirements more elegantly.

Guidelines for preventing pitfalls

If it is bound to the Activity or Fragment life cycle, you need to cancel the thread running of the promise at the end of the life cycle, otherwise there may be a memory leak; here you can use AbortController to implement a more graceful interruption of Promise.

Concurrency model

● Multi-task parallel requests

Promise.all(): Accept any number of Promise objects and execute asynchronous tasks concurrently. If all tasks are successful, if one fails, the entire task is considered a failure.

Promise.allSettled(): Task priority, all tasks must be executed and will never enter a failed state.

Promise.any(): Accept any number of Promise objects and execute asynchronous tasks concurrently. Waiting for one of them to succeed is considered successful. If all tasks fail, it will enter the error state and output an error list.

●Multi-task racing scene

Promise.race(): Accept any number of Promise objects and execute asynchronous tasks concurrently. Time is the first priority. For multiple tasks, the result returned first shall prevail. The success of this result is the overall success, and the failure of this result is the overall failure.

Extended thinking

  1. Promise Best Practices
  1. Avoid long chain calls: Although Promise can avoid callback hell through chain calls, if the Promise chain is too long, the readability and maintainability of the code will also become worse.
  2. Perform abort operation on Promise in a timely manner: Improper use of Promise may cause memory leaks. For example, if abort is not called, the promise is not destroyed in time when the page is canceled.
  3. Need to handle except exception callback and handle PromiseException.
  4. You can use validation to implement rule verification and reduce if-else rule judgment.
  1. Java Promise component implementation principle
  1. State machine implementation (pending, fulfilled, rejected)
  2. The ForkJoinPool thread pool is used by default, which is suitable for computationally intensive tasks. For blocking IO types, you can use the built-in ThreadPerTaskExecutor simple thread pool model.
  1. Promise vs Kotlin coroutine

Promise chain calls, the code is clear, and the cost of getting started is low; the underlying implementation is still threads, and the thread pool is used to manage thread scheduling and Koitlin coroutines. Lighter threads are more flexible to use and can be controlled by developers, such as suspending and resuming.

  1. Thinking about testability

According to the characteristics of Promise, simulation success, rejection, and timeout can be realized through Mock status (resolve, reject, outTime); Implementation ideas: ● Custom annotation class to assist in positioning Hook points ● Use ASM bytecode to instrument Promise

Android learning notes

Android performance optimization: https://qr18.cn/FVlo89
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android audio and video: https://qr18.cn/Ei3VPD
Jetpack family bucket article (including Compose): https://qr18.cn/A0gajp
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap