This is the second post on how to encapsulate a business process in Android. The first post is here. A business process is a collection of pages that have a specific responsibility to interact with and collect information from the user. Sometimes a business process is triggered by the user, and sometimes it is triggered because some conditions are not met. When the process is complete, sometimes it is simply to go back to the page that initiated the process and update that page with the results of the process. Sometimes it is to continue operations that were interrupted earlier by triggering the process; Other times it’s a transition to a new process.

review

In my last post, I outlined seven issues that a process framework should address based on my own practice on corporate projects. At the same time, several schemes investigated and tried in the selection process, their advantages and disadvantages, as well as the final reasons for giving up, are also recorded. These schemes are as follows:

  • Simple based onstartActivityForResult/onActivityResult
  • EventBus or other eventbus-based solutions
  • Simple based onFLAG_ACTIVITY_CLEAR_TOPOr set launchMode to singleTop/singleTask
  • Open a new Activity task stack

The last solution proposed in the last share — using Fragment framework to encapsulate the process is relatively satisfactory to me. It is not a perfect solution, at least it does not solve all the seven problems I raised about the process framework. However, from the current stage, in terms of complexity, ease of use and reliability, This solution is sufficient for our daily development needs, and I think it’s worth mentioning until I find a better one 🙂

Just to recap, an Activity corresponds to a process, and the Activity is the interface that the process exposes, startActivityForResult and onActivityResult, and any location that triggers the process, Only these two methods interact with activities that represent the process.

Each step of the process (page) is encapsulated as a Fragment that only interacts with the host Activity, usually passing data about the step to the host Activity and notifying the host Activity that the step has been completed. The host Activity is the process Activity.

In addition to acting as the external interface of the process, the process Activity also undertakes the flow between steps in the process. In fact, it is the addition and removal of fragments representing steps.

Take the login process as an example (consisting of two steps: user name and password verification and two-step verification requiring mobile phone verification code), the interaction between the whole process and process trigger points (such as “like” operation of information flow on home page) and the internal Fragment stack of the process are shown as follows:

The interaction between the process host Activity and the Fragment that represents each specific step of the process can be represented as follows:

How to optimize the process and external interaction interface

At the end of the last post, I mentioned that initiating and receiving a process based on startActivityForResult and onActivityResult is not elegant, and it does not allow the process to be reused elsewhere. For example, logins can be triggered when you like them or when you comment on them. Normal writing just makes the Activity/Fragment too bloated.

The desired result is that the initiating process can be encapsulated as a normal asynchronous operation, and then we can assign an observer to the process to listen for the asynchronous result as we would for a normal asynchronous task. But the difficulty with encapsulation is that it’s not easy to get an object at startActivityForResult, and that object can get a callback at onActivityResult, The reason for this is that onActivityResult is not part of the Activity/Fragment Lifecycle function, so neither Google’s official Lifecycle Component nor third-party RxLifecycle includes this callback.

But there are other ways to get an observer of the onActivityResult callback in the startActivityForResult location. One approach is to borrow the idea of Glide, which binds an Activity’s lifecycle to an asynchronous operation by adding an invisible Fragment to the Activity that initiates the request. The Fragment can also initiate the startActivityForResult operation and accept the result through onActivityResult.

At this point, our thinking becomes clear. Instead of initiating startActivityForResult, our Activity creates a new invisible Fragemnt and gives it the task. The Fragment acts as an observer. The Activity holds this Fragment object, and the Fragment can notify the Activity when it receives an onActivityResult.

This approach has two advantages: First,

  1. If we create an observer ourselves, it is usually placed in the global scope. Object life cycle binding needs to be carefully considered to prevent potential memory leaks. Fragments are part of the Android framework, and as long as they are used properly (e.g., don’t misuse static references, be careful with anonymous inner classes), they won’t cause memory leaks.

  2. The observer object created by yourself may not recover properly if the process is killed and re-created or the background Activity is recycled. On the one hand, it may not receive onActivityResult results, and on the other hand, it may cause the application to Crash (usually due to null Pointers). However, if we assign the observer’s task to the Fragment, since the Fragment is managed by the FragmentManager of the Activity, even if the Activity is destroyed and recreated for system reasons, the observer itself can still be restored correctly. The onActivityResult callback is normally received.

RxJava is used for encapsulation

As mentioned in the previous section, we want to treat the business process like a normal asynchronous task. For teams like ours that have already introduced RxJava into their projects, using RxJava encapsulation is a natural choice.

First, the three parameters passed back to us in the onActivityResult callback are wrapped separately into a class:

public class ActivityResult {

    private int requestCode;
    private int resultCode;
    private Intent data;

    public ActivityResult(int requestCode, int resultCode, Intent data) {
        this.requestCode = requestCode;
        this.resultCode = resultCode;
        this.data = data;
    }

    // getters & setters
}
Copy the code

In the first section of this paper, it is mentioned that processes are sometimes triggered because certain conditions are not met. Taking a simple example, like operation of social App can only be carried out after login, so the code for handling like event is likely to look like this:

likeBtn.setOnClickListener(v -> {
    if (LoginInfo.isLogin()) {
        doLike();
    } else {
        startLoginProcess()
            .subscribe(this::doLike); }})Copy the code

In the above code, we assume that startLoginProcess is a wrapped login process that returns Observable

. There is a lot of code like this that detects conditions and initiates a flow, an asynchronous task that splits a logically smooth flow of code into two parts. To make this more elegant, we can actually treat loginInfo.islogin () as true as the data emitted by the startLoginProcess Observable. So far the ActivityResult object has been wrapped separately and we can instantiate it ourselves without relying on the onActivityResult callback to construct the object:

public Observable<ActivityResult> loginState(a) {
    if (LoginInfo.isLogin()) {
        return Observable.just(new ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, new Intent()));
    } else {
        returnstartLoginProcess(); }}Copy the code

In this case, the code to perform the “like” operation becomes the following if the user is logged in or if the user is not logged in but successfully logged in after going through the login process:

likeBtn.setOnClickListener(v -> {
    loginState()
        .subscribe(this::doLike);
})
Copy the code

While there is no less code overall, the logic is clearer, and the loginState method is easier to reuse elsewhere.

Continuing our discussion, we have not yet provided an implementation of the startLoginProcess method assumed above, and it is not hard to guess from the previous discussion, The startLoginProcess method is implemented so that the Activity passes the task of initiating the process to logic like the Fragment. The Fragment assumes the responsibility of being the observer of the onActivityResult method. In order for the Fragment to have a cleaner and more consistent interface to the outside world when dealing with situations like condition detection-initiation processes, We allow the Fragment to manually insert an ActivityResult object, in addition to instantiating an ActivityResult when it gets its own onActivityResult callback. The specific code is as follows:

public class ActivityResultFragment extends Fragment {

    private final BehaviorSubject<ActivityResult> mActivityResultSubject = BehaviorSubject.create();

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        mActivityResultSubject.onNext(new ActivityResult(requestCode, resultCode, data));
    }

    public static Observable<ActivityResult> getActivityResultObservable(Activity activity) {
        FragmentManager fragmentManager = activity.getFragmentManager();
        ActivityResultFragment fragment = (ActivityResultFragment) fragmentManager.findFragmentByTag(
                ActivityResultFragment.class.getCanonicalName());
        if (fragment == null) {
            fragment = new ActivityResultFragment();
            fragmentManager.beginTransaction()
                    .add(fragment, ActivityResultFragment.class.getCanonicalName())
                    .commit();
            fragmentManager.executePendingTransactions();
        }
        return fragment.mActivityResultSubject;
    }

    public static void startActivityForResult(Activity activity, Intent intent, int requestCode) {
        FragmentManager fragmentManager = activity.getFragmentManager();
        ActivityResultFragment fragment = (ActivityResultFragment) fragmentManager.findFragmentByTag(
                ActivityResultFragment.class.getCanonicalName());
        if (fragment == null) {
            fragment = new ActivityResultFragment();
            fragmentManager.beginTransaction()
                    .add(fragment, ActivityResultFragment.class.getCanonicalName())
                    .commit();
            fragmentManager.executePendingTransactions();
        }
        fragment.startActivityForResult(intent, requestCode);
    }

    public static void insertActivityResult(Activity activity, ActivityResult activityResult) {
        FragmentManager fragmentManager = activity.getFragmentManager();
        ActivityResultFragment fragment= (ActivityResultFragment) fragmentManager.findFragmentByTag(
                ActivityResultFragment.class.getCanonicalName());
        if (fragment == null) {
            fragment = newActivityResultFragment(); fragmentManager.beginTransaction() .add(fragment, ActivityResultFragment.class.getCanonicalName()) .commit(); fragmentManager.executePendingTransactions(); } fragment.mActivityResultSubject.onNext(activityResult); }}Copy the code

In the ActivityResultFragment class:

  1. MActivityResultSubject is an Observable that emits ActivityResult.

  2. GetActivityResultObservable this method is used to capture invisible fragments in the Activity of observables < ActivityResult >, draw lessons from the thoughts of Glide;

  3. In the onActivityResult method, the Fragment encapsulates the data it receives as ActivityResult and passes it to the mActivityResultSubject.

  4. The startActivityForResult method is called by the Activity. The Activity sends the startActivityForResult method to the Fragment that should have originated it.

  5. InsertActivityResult is a method that provides a consistent interface for the callers of the calling process. It optimises the condition detection and process initiation scenarios.

Encapsulation of reusable processes

So far, the infrastructure needed to encapsulate the process is in place based on RxJava. Let’s try to encapsulate a process to login process as an example, according to the result of the previous discussion, the login process may contain multiple pages (user name, password authentication, mobile phone verification code two-step verification, etc.), may also have child process (forgot password), but for the process, “login” external exposure of only one representative it Activity in this process, No matter how complex the internal jump is, interacting with the login process externally is simple with the startActivityForResult and onActivityResult methods. These two methods can be easily encapsulated in the previous section. Let’s take the login process as an example. Assume that the login process only exposes the LoginActivity, and the code is as follows:

public class LoginActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // your other code

        loginBtn.setOnClickListener(v -> {
            // For simplicity, skip the request part and log in successfully
            this.setResult(RESULT_OK);
            finish();
        });
    }

    public static Observable<ActivityResult> loginState(Activity activity) {
        if (LoginInfo.isLogin()) {
            ActivityResultFragment.insertActivityResult(
                activity,
                new ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, new Intent())
            );
        } else {
            Intent intent = new Intent(activity, LoginActivity.class);
            ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);
        }
        returnActivityResultFragment.getActivityResultObservable(activity) .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN) .filter(ar -> ar.getResultCode() == RESULT_OK); }}Copy the code

The Activity provides a static loginState method that returns Observable

. An Observable sends an ActivityResult to indicate a successful login if it has already logged in. If the result of the logon process is a successful login, the Observable sends an ActivityResult to indicate a successful login. Therefore, any call to the logon process that is used in the process, The code should look like this (again using the like operation as an example) :

likeBtn.setOnClickListener(v -> {
    LoginActivity.loginState(this)
        .subscribe(this::doLike);
})
Copy the code

The startActivityForResult and onActivityResult methods were originally required to complete the interaction with the login process, and it was also necessary to confirm whether the login state was already established before initiating the process. Now just one line of loginactivity.loginstate () and specifying an Observer will achieve the same effect. More importantly, the simplicity of writing makes the whole login process very reusable. Anywhere you need to check the loginState before doing something, Only this line of code is needed to complete the login state detection, realizing the highly reusable process.

This allows the process to continue after completion (such as the “like” in this case) without requiring the user to redo the action that was interrupted by the process (such as the “like” in this case). But careful you may not think so, since the code above is essentially a problem, the problem is that in the above LoginActivity. LoginState () call in likeBtn. SetOnClickListener internal callback, then consider the extreme situation, If the login process is evoked and the Activity that initiated the login process is unfortunately reclaimed by the system, the Activity that initiated the login process will be recreated by the system when the login process is finished. The new Activity is never executed likeBtn setOnClickListener internal callback any code, so the subscribe () method won’t be any callback specified observer, this: : doLike will not be executed.

To make the encapsulated process compatible with this situation, the solution is to modify the loginState method so that it returns ObservableTransformer, and we rename the loginState method to ensureLogin:

public static ObservableTransformer<T, ActivityResult> ensureLogin(Activity activity) {
    return upstream -> {
        Observable<ActivityResult> loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)
            .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)
            .filter(ar -> ar.getResultCode() == RESULT_OK);

        upstream.subscribe(t -> {
            if (LoginInfo.isLogin()) {
                ActivityResultFragment.insertActivityResult(
                    activity,
                    new ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, new Intent())
                );
            } else {
                Intent intent = newIntent(activity, LoginActivity.class); ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN); }}returnloginOkResult; }}Copy the code

If you haven’t worked with ObservableTransformer before, here’s a quick introduction. ObservableTransformer is commonly used in conjunction with the compose operator to process, modify, or even replace one Observable with another.

The login process is now wrapped in the ensureLogin method. How does other code call the login process? Again, using the like operation as an example, the code now looks like this:

RxView.clicks(likeBtn)
    .compose(LoginActivity.ensureLogin(this))
    .subscribe(this::doLike);
Copy the code

Clicks uses the RxBinding library to convert View events into Observables, which you can wrap yourself. After change this kind of writing, just mentioned the extreme cases also can work normally, even launch process page in the process of being called by the system after recovery, return to a page, in the process is finished by page is created, a page of the Observer can still be normal receive process as a result, had been in the operation to continue.

Now we can summarize a bit and make recommendations based on the previous article and this one on how to encapsulate a business process:

  1. A business process corresponds to an Activity, which acts as an external interface and scheduler of steps within the process.
  2. A step inside a process corresponds to a Fragment that is only responsible for completing its own tasks and feeding its data back to the Activity.
  3. The exposed interface of the process should be encapsulated as oneObservableTransformer, the process initiator should provide the originator of the processObservable(such asRxView.clicksThe form is provided), both throughcomposeOperator.

This is my personal experience of a package process. It is not a perfect solution, but it is reliable, reusable and simple enough for my daily development, so I have two articles to share.

We have encapsulated one of the simplest processes, the login process, but real projects often encounter more serious challenges, such as process composition and process nesting.

Complex process practices: Process composition

For example, a certain fund sales App may have the following flow when users click to buy funds:

As can be seen from the figure above, the longest path for an unlogged user to buy a fund includes: login – card binding – risk assessment – investor suitability management. However, not all users need to go through all of these steps; for example, if the user is logged in and has done the risk assessment, then the user only needs to do the card-suitability management step.

Such a requirement, if written conventionally, can be expected to list a lot of if-else in the click event handler:

// ...
// Set the click event handler
buyFundBtn.setOnClickListener(v -> handleBuyFund());


// ...
// Process the result
public void onActivityResult(int requestCode, int resultCode, Intent data) {
   switch (requestCode) {
       case REQUEST_LOGIN:
       case REQUEST_ADD_BANKCARD:
       case REQUEST_RISK_TEST:
       case REQUEST_INVESTMNET_PROMPT:
           if (resultCode == RESULT_OK) handleBuyFund();  
           break; }}// ...

private void handleBuyFund(a) {
   // Check whether you are logged in
   if(! isLogin()) { startLogin();return;
   }
   // Check whether the card is bound
   if(! hasBankcard()) { startAddBankcard();return;
   }
   // Determine if risk testing has been done
   if(! isRisktestDone()) { startRiskTest();return;
   }
   // Determine whether it is necessary to provide users with necessary prompts in accordance with investor suitability management regulations
   if (investmentPrompt()) {
       startInvestmentPrompt();
       return;
   }

   startBuyFundActivity();
}
Copy the code

On the one hand, the code is long. On the other hand, the process initiation and result processing are scattered in two places, making the code difficult to maintain. Let’s analyze that the whole big process is a combination of several small processes. We can draw the process in the above picture in another way:

In accordance with the above idea, we have made each process expose an Activity, which has been wrapped with RxJava ObservableTransformer, so the previous complex code can be simplified as:

RxView.clicks(buyFundBtn)
    // Ensure that the login process is initiated in the case of no login, and automatically moves to the next process in the case of login
    .compose(ActivityLogin.ensureLogin(this))
    // If no card is tied, initiate the process of tying the card. If the card is tied, it will be automatically transferred to the next process
    .compose(ActivityBankcardManage.ensureHavingBankcard(this))
    // Ensure that the risk assessment process is initiated when there is no risk assessment, and automatically flows to the next process when there is risk assessment
    .compose(ActivityRiskTest.ensureRiskTestDone(this))
    // Ensure that an appropriateness prompt is initiated if it is needed and automatically moved to the next process if it is needed or not
    .compose(ActivityInvestmentPrompt.ensureInvestmentPromptOk(this))
    // If all conditions are met, go to the purchase fund page
    .subscribe(v -> startBuyFundActivity(this));
Copy the code

With the good encapsulation of RxJava, we can express more complex logic with less code. In the example above 4 by combination of the process, they have A common characteristic, is independent of each other, each other does not rely on other surplus process as A result, in reality, we may encounter such A situation: B process started, the result of the need to rely on A process to complete, in order to meet this situation, we only need to modify the encapsulation slightly above.

Assuming that the card binding process needs to rely on the user information after the completion of the login process, first, the user information is passed at the location where setResult is called at the end of the login process:

this.setResult(
    RESULT_OK, 
    IntentBuilder.newInstance().putExtra("user", user).build()
);
finish();
Copy the code

Then, modify the ensureLogin method so that the new Observable returned by ObservableTransformer emits User instead of ActivityResult:

public static ObservableTransformer<T, User> ensureLogin(Activity activity) {
    return upstream -> {
        Observable<ActivityResult> loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)
            .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)
            .filter(ar -> ar.getResultCode() == RESULT_OK)
            .map(ar -> (User)ar.getData.getParcelableExtra("user"));

        upstream.subscribe(t -> {
            if (LoginInfo.isLogin()) {
                ActivityResultFragment.insertActivityResult(
                    activity,
                    new ActivityResult(
                        REQUEST_CODE_LOGIN, 
                        RESULT_OK, 
                        IntentBuilder.newInstance().putExtra("user", LoginInfo.getUser()).build()
                    )
                );
            } else {
                Intent intent = newIntent(activity, LoginActivity.class); ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN); }}returnloginOkResult; }}Copy the code

Meanwhile, the ObservableTransformer method of the original ensureHavingBankcard method accepted an Observable of any type T. Since we now stipulate that the card binding process depends on the result User of the login process, So let’s change T to User:

public static ObservableTransformer<User, ActivityResult> ensureHavingBankcard(Activity activity) {
    return upstream -> {
        Observable<ActivityResult> bankcardOk = ActivityResultFragment.getActivityResultObservable(activity)
            .filter(ar -> ar.getRequestCode() == REQUEST_ADD_BANKCARD)
            .filter(ar -> ar.getResultCode() == RESULT_OK);

        upstream.subscribe(user -> {
            if (getBankcardNum() > 0) {
                ActivityResultFragment.insertActivityResult(
                    activity,
                    new ActivityResult(
                        REQUEST_ADD_BANKCARD, 
                        RESULT_OK, 
                        new Intent()
                    )
                );
            } else {
                Intent intent = new Intent(activity, AddBankcardActivity.class);
                intent.putExtra("user", user); ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_ADD_BANKCARD); }}returnbankcardOk; }}Copy the code

Thus, there is a dependency between the two processes, and binding depends on the result returned by the logon process, but the composition of the two processes remains unchanged:

RxView.clicks(someBtn)
    .compose(ActivityLogin.ensureLogin(this))
    .compose(ActivityBankcardManage.ensureHavingBankcard(this))
    .subscribe(v -> doSomething());
Copy the code

In addition, the binding process is reusable and depends on a process that can return User, so any other process that can return User as a result can be combined with the binding process.

Complex process practice: Process nesting

For example, in the login process, in addition to the user name and password login, the login page often provides other options, the most typical of which are registration and password forgetting functions:

Intuitively, we must think that registering and forgetting passwords should not be part of the login process. They are relatively separate processes, which means that there are other processes embedded within the login process. I call this process nesting.

Following the same routine, we should first use ObservableTransformer to encapsulate the two processes of registering and forgetting the password, and then we can organize the above process according to the ideas of this article as follows:

As you can see, the difference now is that the place where the process is initiated is no longer a normal Activity, but a step in another process that, as discussed earlier, is hosted by the Fragment. So there are two methods of handling the process: a Fragment sends the task of initiating a process to a host Activity, and the host Activity assigns its invisible Fragment to initiate the process and process the result, or the Fragmnet initiates the process directly. Since the Fragment also has its own ChildFragmentManager, it only needs to make a few changes to the methods in the section “Encapsulating with RxJava” to support processes initiated from within the Fragment. Specific changes for the activity. GetFragmentManager () into fragments. GetChildFragmentManager ().

In the specific application, I use the latter method, that is, the process initiated by Fragment directly. Because the nested process is often associated with the main process, that is, the result of the nested process may change the flow branch of the main process, so it is convenient to directly initiate the process by Fragment and process the result. If you hand it to the host Activity, you may need to write extra code for activity-fragment communication to achieve the same effect.

First, in the absence of nested processes, the login step (username, password authentication), the first step of the login process, should look like this:

public class LoginFragment extends Fragment {
    // UI references.
    private EditText mPhoneView;
    private EditText mPasswordView;

    LoginCallback mCallback;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, false);
        // Set up the login form.
        mPhoneView = view.findViewById(R.id.phone);
        mPasswordView = view.findViewById(R.id.password);

        Button signInButton = view.findViewById(R.id.sign_in);
        signInButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String phone = mPhoneView.getText().toString();
                String pwd = mPasswordView.getText().toString();

                if(mCallback ! =null) {
                    // mock login ok
                    mCallback.onLoginOk(true.new User(
                            UUID.randomUUID().toString(),
                            "Jack", mPhoneView.getText().toString() )); }}});return view;
    }

    public void setLoginCallback(LoginCallback callback) {
        this.mCallback = callback;
    }

    public interface LoginCallback {
        void onLoginOk(boolean needSmsVerify, User user); }}Copy the code

In the above code, the LoginCallback interface is used to log in to the step, collect information, and interact with the server, and then pass the results back to the host Activity, which determines the flow of subsequent steps. The onClick handler mocks the result of a successful request, rather than initiating an interaction with the server.

The requirement now is to embed two steps in the login step:

  1. One is the registration process, and the successful registration is directly regarded as a successful login, there is no need to go through the rest of the login process steps;
  2. The other is the process of password forgetting. In essence, the process of password forgetting is to reset the password. However, even if the password is successfully reset, the user still needs to use the new password to log in.

As required, we add code embedded in these two processes to the above code:

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, false);
        // Set up the login form.
        mPhoneView = view.findViewById(R.id.phone);
        mPasswordView = view.findViewById(R.id.password);

        Button signInButton = view.findViewById(R.id.sign_in);
        Button mPwdResetBtn = view.findViewById(R.id.pwd_reset);
        Button mRegisterBtn = view.findViewById(R.id.register);

        // Log in directly
        signInButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String phone = mPhoneView.getText().toString();
                String pwd = mPasswordView.getText().toString();

                if(mCallback ! =null) {
                    // mock login ok
                    mCallback.onLoginOk(true.new User(
                            UUID.randomUUID().toString(),
                            "Jack", mPhoneView.getText().toString() )); }}});// Initiate registration process
        RxView.clicks(mRegisterBtn)
            .compose(RegisterActivity.startRegister(this))
            .subscribe(user -> {
                if(mCallback ! =null) { mCallback.onRegisterOk(user); }});// Initiate the password forget process
        RxView.clicks(mPwdResetBtn)
            .compose(PwdResetActivity.startPwdReset(this))
            .subscribe();

        return view;
    }

    public interface LoginCallback {
        void onLoginOk(boolean needSmsVerify, User user);
        void onRegisterOk(User user);
    }
Copy the code

In the above code, RegisterActivity. StartRegister and PwdResetActivity startPwdReset two methods to use the ObservableTransformer encapsulation registration process and the forgot password process. As you can see, the LoginCallback interface has a method onRegisterOk, which means that the login step no longer only notifies the host Activity of onLoginOk. If the inline registration process succeeds, You can also notify the host Activity and let the host Activity decide the subsequent flow. Of course, in this case, the successful registration is also a kind of successful login. The host Activity marks the status of the whole login process as successful login through the setResult method. Finish yourself and pass the user information to the place that initiated the login process.

But why does the embedded registration process need to pass the results of the process back to the host Activity of the login process, while the embedded forget password process doesn’t have a similar method to call back to the host Activity of the login process? Because registered success it has affected the login process to (registration directly as a successful login, the login process state for success, and notify a login process where the registration results of success), and finally the forgot password process to reset the password of success does not affect the login process to (even if the reset password success still need to login in using the new password login screen).

According to the above analysis, the code associated with the logics that distribute the process steps, the host Activity of the login process, is as follows:

public class LoginActivity extends Activity {

    // ... 

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        / /...
        // User name password verification procedure
        loginFragment.setLoginCallback(new LoginFragment.LoginCallback() {
            @Override
            public void onLoginOk(boolean needSmsVerify, User user) {
                // The user name and password are successfully verified
                if (needSmsVerify) {
                    // Two-step verification of SMS verification code is required
                    push(loginBySmsFragment);
                } else {
                    // Login succeeded
                    setResult(
                        RESULT_OK, 
                        IntentBuilder.newInstance().putExtra("user", user).build() ); finish(); }}@Override
            public void onRegisterOk(User user) {
                // Register successfully, login directly
                setResult(
                    RESULT_OK, 
                    IntentBuilder.newInstance().putExtra("user", user).build() ); finish(); }}// SMS verification code Two-step verification step
        loginBySmsFragment.setSmsLoginCallback(new LoginBySmsFragment.LoginBySmsCallback() {
            @Override
            public void onSmsVerifyOk(User user) {
                // SMS authentication succeeded
                setResult(
                    RESULT_OK, 
                    IntentBuilder.newInstance().putExtra("user", user).build() ); finish(); }}); }// ...
}
Copy the code

As you can see, even in the case of process nesting, the process encapsulated in RxJava does not clutter up the process-jumping code, which is valuable because it means that the process-related code will not become a difficult module to maintain in the project, but will be clear and highly cohesive.

Process context saving

So far, we have one last problem to solve, which is the preservation of relevant context involving the process. Specifically, there are two parts: one is the process trigger point, the position of initiating the process, and the context before initiating the process needs to be saved; the other is the result of the intermediate steps of the process, which also needs to be saved.

1. Save the results of intermediate steps in the process

The reason for saving the results of intermediate steps in the process is that, as we discussed earlier, each step in the process (i.e. Fragment) interacts with the user and passes the results of that step to the host Activity, assuming that the process is not complete and the results of that step may be used by subsequent steps. Then it is necessary for the host Activity to save the result. Usually the result is saved as a member variable of the Activity. The problem is that once the Activity is put in the background, it can be reclaimed by the system at any time. If you do not save and restore the Activity’s member variables, the next time the Activity returns to the foreground, the state of the process is uncertain and may even crash.

The obvious solution is to inherit the Activity’s onSaveInstanceState and onRestoreInstanceState (or onCreate) methods and implement variable save and restore operations inside the two methods. If you think that implementing these two methods will make your code look ugly, I recommend using SaveState. With SaveState, you simply mark the @autoRestore annotation on the member variables that need to be saved and restored, and the framework will automatically save and restore them for you. You don’t need to write any extra code.

2. Save the context before initiating the process

Similar to reason 1, once the process is invoked, the Activity that initiated the process is in background state, a state that can be reclaimed by the system. For example, there is a list page of financial products, and the user is not logged in. Now the user is required to click any financial product and take the user to the login interface. After the login process is completed, the user will be taken to the page of purchasing specific financial products. The click event setting of the list is generally divided into two kinds. One is to set a click handler function for each Item in the list, and the other is to set the same click handler function for all items. Take setting the same click handler for all items in a list:


// The Observable corresponding to all Item click events emits the click location element
Observable<Integer> itemClicks = ...

itemClicks
    .compose(LoginActivity.ensureLogin(activity))
    .subscribe(/** */);
Copy the code

Subscribe to the inside of the observer. Because don’t know how to write the LoginActivity ensureLogin the ObservableTransformer will put observables < T > to Observable

, so the observer only knows that the login is successful, but does not know which financial product is clicked to trigger the login operation, so it does not know how to start the purchase page.

The dilemma we ran into was that when the process was complete, we didn’t know what the context was before the process was initiated, so we couldn’t do the proper follow-up logic in the observer. An intuitive solution is to wrap the context data at the time the process is initiated into the Intent of startActivityForResult, store it with a reserved Key value, and ensure that when the process completes, when setResult is called, The context data just passed in by the process is also passed back to the originating process with a reserved Key value.

If such after processing, we looking back just now, we will implement a LoginActivity ensureLoginWithContext method, its return value for ObservableTransformer < Bundle, Bundle > :

public static ObservableTransformer<Bundle, Bundle> ensureLoginWithContext(AppCompatActivity activity) {
    return upstream -> {
        upstream.subscribe(contextData -> {
            if (LoginInfo.isLogin()) {
                ActivityResultFragment.insertActivityResult(
                    activity,
                    new ActivityResult(REQUEST_LOGIN, RESULT_OK, null, contextData)
                );
            } else {
                Intent intent = newIntent(activity, LoginActivity.class); ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_LOGIN, contextData); }});return ActivityResultFragment.getActivityResultObservable(activity)
                .filter(ar -> ar.getRequestCode() == REQUEST_LOGIN)
                .filter(ar -> ar.getResultCode() == RESULT_OK)
                .map(ActivityResult::getRequestContextData);
    };
}
Copy the code

The ensureLoginWithContext method in the above code is different from the original ensureLogin method, except that the generic type of the return value is different. Both the ActivityResult constructor and the startActivityForResult method have a bundle-type contextData parameter, which is the context that needs to be saved before the process is initiated. Finally, the return statement of the entire method adds a map operator that retrieves the context of the process stored in ActivityResult. The logic here is just mentioned: before the process is initiated, the context information before the process is initiated is passed to the process through the Bundle, and at the end of the process, it is returned to the place where the process is initiated intact, so that the process initiating point can know its state before the process is initiated. You can refer to the Sq framework for specific implementations of these methods.

After this processing, the click event of the list Item initiates the login process as follows:

itemClicks
        .map(index -> BundleBuilder.newInstance().putInt("index", index).build())
        .compose(LoginActivity.ensureLoginWithContext(this))
        .map(bundle -> bundle.getInt("index"))
        .subscribe(index -> {
            // modification of item in position $index
            adapter.notifyItemChanged(index);
        });
Copy the code

The compose operator is followed by the map operator, which packages the context and unpacks the original context from the process result.

Save point and a note to the process context, is the process at the end of the call setResult, need to make sure that the previous context of incoming plug back into the results, only do this, the code above is effective, if always manually to do these jobs will be very tedious, you can choose their own packaging, Or just use the out-of-the-box tools described in the next section.

How to use it quickly

Here, to encapsulate all business process related experience sharing is introduced, if you see here, for the purpose of this article and this article on a proposed process scheme of interested in you there are two ways to integrate into your own project, it is a reference to the text of the code, their implementation (core code has been in the article, make a little change); Another way is to use the packaged version directly. The project name is Sq, and you just add the dependencies to Gradle, right out of the box.

conclusion

It’s a long article. Thank you for your patience. Due to my limited ability, the article may be flawed, welcome to correct. As for how to encapsulate business processes, I have not seen many technical articles discussing this area, so my personal opinions are not comprehensive, if you have a better solution, welcome to discuss. Thank you!

In addition, welcome to my personal account: Muggle Diary, update the original technology from time to time, thank you! 🙂