Anyone with some experience developing actual Android projects must have had to deal with a lot of repetitive business processes on their projects. Develop a social App, for example, so for the sake of user experience, will need to allow anonymous users (not login user) can browse the contents of the information flow (or can only be limited to browse content), when the user wants to further operation (thumb up, for example), prompt the user need to login or register, user just operation to complete this process can continue. If the user needs to conduct more in-depth interaction (such as comments, Posting status), the user needs to authenticate the real name or add a mobile phone number to complete the process before proceeding.
While the above list is only a relatively simple case, the processes can also be combined with each other. For example, if an anonymous user clicks on a comment, then it needs to be repeated:
- Login/registration
- Real-name authentication
These two processes can continue to comment on a message. In addition, the login process may also be nested with processes such as “forget password” or “retrieve password”, or the server may insert a two-step authentication/phone number authentication process when detecting the user’s remote login.
Problems that need to be solved
(1) The experience of the process should be smooth
According to my experience in using apps in the market, business processes can be divided into two categories according to the classification of experience. One is that after the triggering process is completed, the user returns to the original page without any reaction, and the user needs to click the button just now or re-operate the behavior that triggered the process just now, so that the original desired operation can be performed. On the other hand, after the process is complete, if certain conditions that were not previously met are now met, the user is automatically helped to continue the operation that was just interrupted. Clearly, the latter is more in line with user expectations and needs to be addressed if we need to develop a new process framework.
(2) The process needs to support nesting
If, during a process, certain conditions are not met and a new process needs to be triggered, it should be possible to start that process, complete the operation, and return to continue the current process.
(3) Data transfer between process steps should be simple
Traditional data transfer between activities is based on intents, so data types need to support Parcelable or Serializable and need to be filled with key-value in an Intent, which has limitations. In addition, some data is shared and some is unique between process steps. How can you easily read and write this data?
One might say that you can put the data in a common space, and the Activity that wants to read and write the data can access the data itself. But if it does, the new problem is that the application process can destroy the reconstruction at any time, and the data stored in memory will disappear. If you don’t want to see this, you need to consider data persistence, and the persistent data is only used by this process, when should it be destroyed? Persistent data needs to consider its own lifecycle, which introduces additional complexity. And it’s not much easier than using intents.
(4) The process needs to adapt to the Android component lifecycle
As mentioned above, the process is destroyed and rebuilt. Since many operations trigger the process, the process page is implemented based on the Activity. Therefore, the Activity instance returned after completing the process may not be the same as the original instance when the process was triggered. There must be a means in place to ensure that when the process is complete, the context can be correctly restored back to the page that triggered the process.
(v) The process needs to be simple and reusable
In addition, the process is often reusable. For example, the login process can be triggered in many places of the application, so the jump page after the process is triggered is different, and the jump page can not be written in the end of the process.
(vi) The process page needs to be easier to destroy after completion
After the process is complete, each step page of the process can simply be destroyed, returning to the interface that triggered the process in the first place.
(vii) Processing of rollback behavior in process
If a process consists of multiple intermediate steps, how should the behavior be defined when the user presses the return key at one of the intermediate steps? In most cases, a return to the previous step should be supported, but in some cases, a direct return to the beginning of the process should also be supported.
Solution 1: Based on startActivityForResult
The startActivityForResult method is an example of how to select a contact from your address book.
static final int PICK_CONTACT_REQUEST = 1; // The request code.private void pickContact(a) {
Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
pickContactIntent.setType(Phone.CONTENT_TYPE); // Show user only contacts w/ phone numbers
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check which request we're responding to
if (requestCode == PICK_CONTACT_REQUEST) {
// Make sure the request was successful
if (resultCode == RESULT_OK) {
// The user picked a contact.
// The Intent's data Uri identifies which contact was selected.
// Do something with the contact here (bigger example below)}}}Copy the code
In the example above, when the user clicks a button (or something), the pickContact method is triggered, the system launches the address book, and after the user selects a contact from the address book, the user goes back to the original page and continues with the logic. Selecting a user from the address book and returning the result can be seen as a process.
However, the above process is a relatively simple case, because the process logic only has one page, and sometimes a complex process may contain multiple pages: for example, registration, including the phone number verification interface (verification code receiving), set the nickname page, set the password page. Assuming that the registration process is started from the login interface, the change in the Activity task stack using startActivityForResult to implement the registration process is shown below:
The details of the registration process are as follows:
- Login page Yes
startActivityForResult
Start the registration page of the first interface —- verify the phone number; - After the verification succeeds, the verification screen is displayed
startActivityForResult
Launch the nickname setup page; - After the nickname check is valid, the nickname information passes
onActivityResult
Return to the verify phone number interface, verify the phone number interface throughstartActivityForResult
Start set password interface, because set password is the last process, verify the phone number interface before the collection of good phone number information, nickname information are transferred to the password interface, password check legal, according to the existing phone number, nickname, password to initiate registration; - After the registration is successful, the server returns the information about the registered user, and the password setting page succeeds
onActivityResult
Feedback the registration result to the interface of setting phone number; - Registration success, set the phone number interface to end their own, at the same time the registration success information through
onActivityResult
Feedback to the process initiator (in this case, the login interface);
It can be seen from this example that the mobile phone number verification interface not only takes on the function of verifying the mobile phone number in the registration process, but also takes on the responsibility of the external interface of the registration process. In other words, any location that triggers the registration process does not need to have any knowledge of the details of the registration process, but only needs to interact with the Activity exposed by the process through startActivityForResult and onActivityResult, as shown in the figure below:
One of the things that might make you wonder about the above example is why each step needs to go back to the verify phone number page, and then the verify phone number page is responsible for launching the next step. On the one hand, as the verification phone number is the first page of the process, it assumes the identity of the process scheduler, so it distributes the steps. The advantage of this is that each step (except the first step) is decoupled and cohesive, and each step only needs to do its own thing and return data through onActivityResult. If the steps of the subsequent process are added or deleted, the maintenance is relatively simple; On the other hand, since each step is returned after completion, when the last step is completed, the intermediate pages of the previous process will not exist, and there is no need to manually destroy the finished process pages, which is easier to code.
But this has a minor side effect: if you press the back key in the middle of the process, you go back to the first step in the process, and the user sometimes wants to go back to the previous step. In order for the user to return to the previous step by pressing the back key, the Activity of each step must be pushed down, but doing so poses the problem of destroying all the activities associated with the process after the last step has been completed.
To address the destruction of process-related activities, the above diagram needs to be modified as follows:
Previously, each step only needs to finish itself and return the result after completing its task. After modification, each step does not finish itself and does not return the result after completing its task. At the same time, it is responsible for starting the next step of the process (through startActivityForResult). When its next step finishes and returns its result, the step can be caught in its own onActivityResult. What it needs to do in onActivityResult is to combine its result with the result of its next step, pass it on to its previous step, and end itself.
In this way, the behavior required for the user to press the return key is implemented, but the disadvantage of this approach is the coupling between the steps within the process, on the one hand, between the start sequence, and on the other hand, due to the need to both carry the results of its next step and return the resulting data.
In addition, I have seen people use a separate stack to save the activities started in the process, and then manually destroy each Activity in turn after the process has finished. I don’t like this approach, it doesn’t solve the real problem, it requires an extra data structure to maintain, and it takes into account the life cycle.
In conclusion, the startActivityForResult method has its own advantages:
- Simple enough, native support.
- You can process the results that the process returns and continue with the actions that occurred before the process was triggered.
- The process is well encapsulated and reusable.
- Although the introduction of additional
requestCode
, but preserves the context of the request to some extent.
But the problem with this native solution is obvious:
- The writing is too Dirty, and the logic for making the request and processing the result is scattered in two places, making it difficult to maintain.
- If there are multiple requests on the page, the different process callbacks are mixed into one
onActivityResult
In, not easy to maintain.- If a process contains more than one page, the coding can be tedious and unwieldy.
- Data sharing between process steps is based on intents and does not solve problem (3).
- There is a contradiction between the self-destruction of the process page and the rollback behavior in the process, and issues (6) and (7) are not well resolved.
In actual development, these problems are very prominent and affect the development efficiency, so they cannot be used directly.
Solution 2: EventBus or another eventbus-based solution
Event-based decoupling is also an elegant solution, especially the EventBus framework, which implements the classic publis-subscribe model and achieves excellent decoupling:
I’m sure many Android developers have had a good time using the framework …………………………………… They either abandoned it or used it only on a small scale. I, for example, have been gradually removing code that uses EventBus from my project and using RxJava instead.
Take a look at the basic use of EventBus through the concrete code:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EventBus.getDefault().register(this);
EventBus.getDefault().post(new MessageEvent("hello"."world"));
}
@Subscribe(threadMode = ThreadMode.MainThread)
public void helloEventBus(MessageEvent message){
mText.setText(message.name);
}
@Override
protected void onDestroy(a) {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
class MessageEvent {
public final String name;
public final String password;
public MessageEvent(String name, String password) {
this.name = name;
this.password = password; }}Copy the code
So what’s wrong with it? First, a generic asynchronous task is initiated and the developer expects the result of the task in the callback, whereas in the EventBus concept, the “event” (MessageEvent in the example) is passed in the callback. In theory, the data type of the result of an asynchronous task could be the same as the data type of the event, thus unifying the two concepts. However, in practice, there are many cases where this cannot be done. For example: Both Activity A and Activity B need to request A network interface. If the object type of the response of the network request is directly provided to their Subscriber as the event type, confusion will occur, as shown in the figure below.
In the figure, A Activity and B Activity both initiate the same network request (perhaps with different parameters, such as weather interface, one is to check the weather of Beijing, the other is to check the weather of Shanghai), so their response result class is the same. If you provide this response as a callback to EventBus directly as the event type, the result is that both activities receive two messages. I call it the spatial confusion caused by event propagation.
The usual solution is to encapsulate an event with Response as the data carried by the event:
public class ResponseEvent {
String sender;
Response response;
}
Copy the code
After the response object is encapsulated as an event, a sender field is added to distinguish which Subscriber this response should correspond to, which solves the above problem.
Not only spatially, event propagation can also cause confusion in time. Imagine a situation where two requests of the same type are made successively, but the callbacks that handle them are different. If you use the traditional method of setting the callback, you only need to set two callbacks for the two requests. However, if you use EventBus, the request type is the same, so the data return type is the same. If you use EventBus as the event type, the return data type is the same. Then there is no way to distinguish between the two requests in EventBus’ event-handling callback (there is no guarantee that the first two requests will return first). The solution is similar to the one above, as long as the sender field is replaced with a field like TIMESTAMP.
Ultimately, the underlying reason for the spatial and temporal confusion of event propagation is the shift from the traditional “set a callback for each asynchronous request” pattern to the “set a callback in response to an event” pattern. Traditionally, a specific request is strongly associated with a specific callback, and a specific callback serves a specific request. EventBus decouple the two, with a weak relationship between the callback and the request and a strong relationship between the callback and the event type.
In addition to the above problems, there is actually a more serious problem, specific code:
// File: ActivityA.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
EventBus.getDefault().register(this);
findViewById(R.id.start).setOnClickListener(
v -> startActivity(new Intent(this, ActivityB.class))
)
}
@Subscribe(threadMode = ThreadMode.MainThread)
public void helloEventBus(MessageEvent message){
mText.setText(message.name);
}
@Override
protected void onDestroy(a) {
super.onDestroy();
EventBus.getDefault().unregister(this); }...// File: ActivityB.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);
findViewById(R.id.btn).setOnClickListener(v -> {
EventBus.getDefault().post(new MessageEvent("hello"."world")); finish(); })}Copy the code
The intent of the above code is primarily: Click the button in ActivityA to start ActivtyB, which hosts a business process. When the process task undertaken by ActivityB is complete, click the button in its page to end the process. Pass the resulting data back to ActivityA via EventBus, while ending itself and bringing the user back to ActivityA.
Ideally, this would be fine, but ActivityA won’t receive any messages from ActivityB if the “Developer Options – Don’t keep activities” option on Android is turned on. The “Do not retain activities” option simulates the feature that destroys activities in the Activity stack that are not visible to the user when the system runs out of memory. This is very common on low-end phones, where you’re playing with your phone, you take a call, you come back and the whole page has reloaded. The reason is obvious: Since ActivityA performed onDestroy when it was destroyed by the system, removing its own callback from EventBus, it no longer receives the callback from ActivityB. Can we not remove the callback? Of course not, because that would cause a memory leak, or worse.
Those familiar with EventBus should be familiar with the postSticky method. Indeed, in this case, the postSticky method allows the event to survive for a longer period of time until its consumer comes along and consumes it. However, this method also has some side effects. The event sent by postSticky needs to be manually removed by Subscriber. As a result, if the event has more than one consumer, it is not known when to remove the event when writing code, and it needs to increase a counter or other means. Additional complexity is introduced. PostSticky events are used to ensure that internal callbacks can still receive events after the Activity resumes its lifecycle, but they contaminate the global space, which I find very inelegant.
At this point, this article is going to become EventBus criticism. In fact, EventBus itself is ok, but we users should consider the scenario, can not abuse, or some occasions more applicable, but for business process processing this task, I don’t think it is a good application scenario.
In the above statement, I use “asynchronous task” as an example to illustrate many examples, mainly because I think that in fact, the business process we insert in the user operation can also be regarded as a kind of asynchronous task, anyway, the final result is returned to the caller asynchronously. So I think EventBus is not suitable for those points of asynchronous tasks, and also not suitable for business processes.
Other EventBus solutions are similar. Android’s native Broadcast is basically a low-spec EventBus when it comes to handling business processes, except for its cross-process features, so I won’t go into this again.
Option 3: FLAG_ACTIVITY_CLEAR_TOP might be one solution
Considering a third-party framework is always a problem with the Android lifecycle (the EventBus case in the previous section for Activity destruction and reconstruction of lost context). We still tend to look for features in the Android native framework that fit our requirements. FLAG_ACTIVITY_CLEAR_TOP = “FLAG_ACTIVITY_CLEAR_TOP”; FLAG_ACTIVITY_CLEAR_TOP = “activity_clear_top”;
If an Activity task stack has the following activities: A, B, C, D. If D calls startActivity() and the Intent as an argument resolves to startActivity B (FLAG_ACTIVITY_CLEAR_TOP), then both C and D are destroyed. B will receive the Intent, and the task stack should look like this: A, B.
This paragraph only describes the phenomenon, the document also describes the data flow in more detail, I recommend to read the document description carefully, I will only translate the most important part of it separately:
B in the example above would be one of two outcomes
- in
onNewIntent
The Intent passed from D is received in the callback- B will destroy and rebuild, and rebuild
Intent
That’s the one passed by DIntent
If B’s launchMode is declared multiple(standard) and FLAG_ACTIVITY_SINGLE_TOP is not included in the Intent Flags, then result 2 is shown above. The remaining case, where launchMode is declared as non-multiple or FLAG_ACTIVITY_SINGLE_TOP in the Intent Flags, is result 1.
In the above description, result 1 of B is a good fit for the encapsulation of our business process. Here is an example of why. Background: A social App, homepage information flow. Assuming that all activities are in a task stack, the task stack changes as shown in the figure below:
(1) After browsing for a while, the anonymous user gives a thumbs-up, which triggers the login process and the login interface pops up; (2) After the user enters the correct user name and password (assume the user is an old user), the server receives the login request, detects risks, and initiates two-step authentication (SMS authentication is required). The client displays the SMS authentication page for authentication. (3) The user enters the correct verification code and clicks login to return to the information flow page. Meanwhile, the “like” operation on the page has been successful.
How do you implement the phenomenon described in Step 3? Just add the start of Activity A logic to the logon success logic in Activity C, FLAG_ACTIVITY_CLEAR_TOP FLAG_ACTIVITY_SINGLE_TOP FLAG_ACTIVITY_SINGLE_TOP LaunchMode is standard) and the code is as follows:
Intent intent = new Intent(ActivityC.this, ActivityA.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// put your data here
// intent.putExtra("data", "result");
startActivity(intent);
Copy the code
The advantages of using this method are as follows:
- You can take the user back to the page before the process was triggered;
- Callbacks with data (in this case, the user’s login information);
- In the callback can help the user to continue to complete the operation that was interrupted before;
- Process pages all self-destruct, even Activity C’s own
finish()
Methods are not called; - Even if the openingUnreserved activityIt still works, Acitivity
onNewIntent
The callback will beonCreate
Is then called; - Page rollback halfway through the process is well supported;
This may seem like A much better approach than solution 1, but there is A problem with the above example: The last step, Activity C, explicitly starts Activity A. The process page should not be coupled in any way to the page that triggers the process, otherwise the process will not be reusable, so a mechanism should be found to decouple the two while passing the data that the process carries when it completes to the place where the process was triggered. So far I can think of startActivityForResult in scheme one, which is, Activity A interacts only with Activity B through startActivityForResult and onActivityResult, In the last page of the process, related data of the process end is brought back to the first page of the process (Activity B) through the above onNewIntent, and Activity B passes the data to the process trigger through onActivityResult. The specific logic is as shown in the figure below:
This solves the problem of process encapsulation and reuse, but there are still some disadvantages to this solution:
- and
startActivityForResult
Similarly, write Dirty, if the process is many, maintenance is not easy; - Even for the same process, there is a reuse situation in the same page, do not add a new field cannot be in
onNewIntent
Inside distinction; - Question (3)It’s not solved,
onNewIntent
Data transfer is also based on intents, and there are no measures to share data between steps. Shared data may need to be passed from start to finish. - There is a slight coupling between the steps: each step is responsible for starting the next step;
The disadvantage 2 is that a like will trigger a login, and a comment will also trigger a login, and both will return to the information flow page after a successful login. Without adding extra fields, onNewIntent just receives the user’s login information and doesn’t know whether you just liked or commented.
This solution and the pure startActivityForResult solution (Solution 1) have a complementary feel, one is good at the case where the process page does not support a fallback, and the other is good at the case where the process page does support a fallback, and neither is a good solution to the problem (3), we need to further explore whether there is a better solution.
Solution 4: Use the newly opened Activity stack to complete the business process
Since the process page in the project we are taking over is implemented based on activities, it is natural to think that the activities that process should be more cohesive. If the activities related to the process are all in a separate Activity task stack, then after the process is processed, Just destroy that task stack when you get the final result of the process, simple and crude.
If you still use the flow login example above, the Activity task stack changes should look like this:
To achieve the effect shown in the figure, two questions need to be considered:
- How do I start a new task stack and put all the activities involved in the process in it?
- How do I destroy the task stack occupied by the process after the process has finished and return the process results to the page that triggered the process?
Problem 1 is relatively simple, we set taskAffinity explicitly for all activities related to the process (e.g. Com.flowtest.flowa), be sure not to use the same packageName as the Application. Because the default taskAffinity for an Activity is packageName. To start a process, add FLAG_ACTIVITY_NEW_TASK to the Intent of the Activity that starts the process:
<! -- In AndroidManifest.xml -->
<activity android:name=".ActivityB" android:taskAffinity="com.flowtest.flowA"/>
Copy the code
// In ActivityA.java
Intent intent = new Intent(ActivityA.this, ActivityB.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Copy the code
The other activities in the process do not need to be started in any way, because they have the same taskAffinity as the process entry Activity, so they are automatically placed in the same task stack.
Question 2 is a little more complicated. As far as I know, Android does not provide a means of communicating between stacks of tasks, so we can only go back to the method of data transfer between activities. First, for the sake of process reusability, The process still exposes Activity B, and the process initiator (Activity A) interacts with the process through Activity B and the startActivityForResult and onActivityResult methods. Second, there are onNewIntent and onActivityResult callback methods for the steps inside the process to interact with Activity B.
Look this kind of thinking is more promising, but after a few tests, I gave up this approach, the reason is that once you create a new task stack, recent application on his mobile phone list, will be more than an App field that represents the process task stack (extra), which means the user when doing the process if the Home button switch, So when he wants to come back, he presses the recent Applications list, and he sees two tasks, and he doesn’t know which one to return to. Even when the process is complete, that place will remain in the list of recent applications, causing confusion for users later on. In addition, the default animation for task stack switching is different from the Activty default animation (although it can be changed to be the same), which makes it a little weird to use.
Solution 5: Use the Fragment framework to encapsulate the process
So far, of the above options, only options 1 and 3 are relatively available. In solution 1, there is another contradiction. If you want all steps in the process to be destroyed gracefully, and the coupling between steps is more loose, the rollback behavior cannot be guaranteed. When the rollback behavior is guaranteed, the destruction of process steps is less elegant and the steps are more tightly coupled; In Scenario 3, the problem of process step destruction and the rollback are elegantly resolved, but the coupling between the steps is not. We wanted a solution that had the best of both worlds, with loosely coupled steps, elegant fallbacks, and easy destruction.
After careful analysis of the advantages and disadvantages of the two solutions, it is not difficult to come to the conclusion that the interaction between activities alone is difficult to achieve the above goals, in essence, because the Activity task stack does not open enough apis to us, and what we can do with the task stack is limited. In fact, it is easy to think that in Android, in addition to activities, fragments also have a Back Stack. If we wrap the process page with fragments, we can complete the process by switching fragments in an Activity. Since the life cycle of the Activity and the Fragment Back Stack is the same, the Activity is the ideal place to save the Fragment Back Stack state (process state); In addition, simply call the Activity’s finish() method to clear the Fragment Back Stack!
Still taking two-step login verification as an example, after Fragment modification, the point that triggers the process will only start an Activity and only interact with this Activity, as shown in the figure below:
Activity A starts ActivityLogin with startActivityForResult, ActivityLogin completes the business process internally through Fragment, finishes itself, The process result is returned to Activity A via onActivityResult. The two steps of the process are encapsulated as two fragments, which interact with the host Activity as shown in the figure below:
-
ActivityLogin Start the first page of the process —- password login, use push method (the method in this example is all pseudo code) to display Fragment A in front of the user, the user login password verification is successful, use onLoginOk method to call back ActivityLogin, ActivityLogin Saves the necessary information for this step.
-
ActivityLogin start the second page of the process —- two-step verification, and pass the information of the previous step to Fragment B. Also through push method, mobile phone SMS authentication succeeds, and call back ActivityLogin through onValidataOk method. ActivityLogin packages the data from this step with the data from the previous step and passes it to the process trigger via onActivityResult.
Looking back at the beginning, we put forward seven problems to be solved for the new process framework. Looking back at this plan, we can find that all the other problems have been properly solved except problem (3).
Normally, adding a Fragment is done without animation. There is no default animation like an Activity switch. In order to make the switch between fragments feel the same as the Activity experience, I recommend setting the switch animation for fragments to be the same as the Activity. First, give the Activity a toggling animation (the default Activity toggling animation varies from ROM to ROM, and manually setting the toggling animation is highly recommended for a consistent App experience).
For example, if you swipe left in and right out, you can set the theme in styles.xml as follows:
<! -- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowAnimationStyle">@style/ActivityAnimation</item>
<! -- Customize your theme here. -->.</style>
<! -- Activity enter/exit animation -->
<style name="ActivityAnimation" parent="android:Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/push_in_left</item>
<item name="android:activityCloseEnterAnimation">@anim/push_in_right</item>
<item name="android:activityCloseExitAnimation">@anim/push_out_right</item>
<item name="android:activityOpenExitAnimation">@anim/push_out_left</item>
</style>
Copy the code
Define the approach and exit animations and place them in the res/anim folder:
<! -- file: push_in_left.xml -->
<?xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%p"
android:toXDelta="0"
android:duration="400"/>
</set>
<! -- file: push_in_right.xml -->
<?xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="-25%p"
android:toXDelta="0"
android:duration="400"/>
</set>
<! -- file: push_out_right.xml -->
<?xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0"
android:toXDelta="100%p"
android:duration="400"/>
</set>
<! -- file: push_out_left.xml -->
<?xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0"
android:toXDelta="-25%p"
android:duration="400"/>
</set>
Copy the code
Therefore, with the Fragment toggle animation, the above push method is implemented as follows:
protected void push(Fragment fragment, String tag) {
List<Fragment> currentFragments = fragmentManager.getFragments();
FragmentTransaction transaction = fragmentManager.beginTransaction();
if(currentFragments.size() ! =0) {
// In the process, the Fragment in the first step does not need to be animated, the rest of the steps do
transaction.setCustomAnimations(
R.anim.push_in_left,
R.anim.push_out_left,
R.anim.push_in_right,
R.anim.push_out_right
);
}
transaction.add(R.id.fragment_container, fragment, tag);
if(currentFragments.size() ! =0) {
// Start with the Fragment coming into play in the second step of the process. Hide the previous Fragment in order to see the toggle animation
transaction
.hide(currentFragments.get(currentFragments.size() - 1))
.addToBackStack(tag);
}
transaction.commit();
}
Copy the code
The responsibilities of each Fragment representing a specific step in the process are also clear: collect information, complete the step, and return the results of that step to the host Activity. This step itself is not responsible for initiating the next step and is loosely coupled to other steps. A specific example is as follows:
public class PhoneRegisterFragment extends Fragment {
PhoneValidateCallback mPhoneValidateCallback;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_simple_content, container, false);
Button button = view.findViewById(R.id.action);
EditText input = view.findViewById(R.id.input);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(mPhoneValidateCallback ! =null) { mPhoneValidateCallback.onPhoneValidateOk(input.getText().toString()); }}});return view;
}
public void setPhoneValidateCallback(PhoneValidateCallback phoneValidateCallback) {
mPhoneValidateCallback = phoneValidateCallback;
}
public interface PhoneValidateCallback {
void onPhoneValidateOk(String phoneNumber); }}Copy the code
At this point, the responsibilities of the host Activity as a series of process steps are also clear:
- As the exposed interface of the process, external data interaction (
startActivityForResult
和onActivityResult
) - Responsible for scheduling process steps and determining the sequence of calls between steps
- The channel through which data is shared between process steps
For example, the registration process consists of three steps: verifying the phone number, setting a nickname, and setting a password. The process Activity is as follows:
public class RegisterActivity extends BaseActivity {
String phoneNumber;
String nickName;
User mUser;
PhoneRegisterFragment mPhoneRegisterFragment;
NicknameCheckFragment mNicknameCheckFragment;
PasswordSetFragment mPasswordSetFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/** * Ensure that the Activity gets the correct Fragment instance */ when it first starts or when it destroys a rebuild
mPhoneRegisterFragment = findOrCreateFragment(PhoneRegisterFragment.class);
mNicknameCheckFragment = findOrCreateFragment(NicknameCheckFragment.class);
mPasswordSetFragment = findOrCreateFragment(PasswordSetFragment.class);
// If it is the first time to start, stack the Fragment represented by the first step of the process
if (savedInstanceState == null) {
push(mPhoneRegisterFragment);
}
// Responsible for verifying the mobile phone number after starting to set the nickname
mPhoneRegisterFragment.setPhoneValidateCallback(new PhoneRegisterFragment.PhoneValidateCallback() {
@Override
public void onPhoneValidateOk(String phoneNumber) {
RegisterActivity.this.phoneNumber = phoneNumber; push(mNicknameCheckFragment); }});// After setting the nickname, start setting the password
mNicknameCheckFragment.setNicknameCheckCallback(new NicknameCheckFragment.NicknameCheckCallback() {
@Override
public void onNicknameCheckOk(String nickname) {
RegisterActivity.this.nickName = nickName; mPasswordSetFragment.setParams(phoneNumber, nickName); push(mPasswordSetFragment); }});// After the password is set, the registration process is complete
mPasswordSetFragment.setRegisterCallback(new PasswordSetFragment.PasswordSetCallback() {
@Override
public void onRegisterOk(User user) {
mUser = user;
Intent intent = new Intent();
intent.putExtra("user", mUser); setResult(RESULT_OK, intent); finish(); }}); }}Copy the code
The findOrCreateFragment method is implemented as follows:
public <T extends Fragment> T findOrCreateFragment(@NonNull Class<T> fragmentClass) {
String tag = fragmentClass.fragmentClass.getCanonicalName();
FragmentManager fragmentManager = getSupportFragmentManager();
T fragment = (T) fragmentManager.findFragmentByTag(tag);
if (fragment == null) {
try {
fragment = fragmentClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch(IllegalAccessException e) { e.printStackTrace(); }}return fragment;
}
Copy the code
At this point, you might be a little skeptical about the findOrCreateFragment implementation, which is based on the line of code that instantiates the Fragment using the class.newinstance method. In general, Google recommends implementing its own newInstance method in the Fragment to take care of instantiating the Fragment. The Fragment should also include a no-argument constructor. Fragment initialization arguments should not be in the form of constructor arguments, but should instead be passed through the Fragment. SetArguments method, the newInstance method that follows the above should look like this:
public static MyFragment newInstance(int someInt) {
MyFragment myFragment = new MyFragment();
Bundle args = new Bundle();
args.putInt("someInt", someInt);
myFragment.setArguments(args);
return myFragment;
}
Copy the code
This is because using the fragment.setarguments method sets parameters that can be passed to the fragments that are being resumed when the Activity destroys a rebuild — a rebuild process that includes rebuilding any fragments that the old Activity manages.
But why is this code here doing this? First, an Activity can be killed at some point whenever it enters the background, so when we return to an Activity, we should be aware: This Activity may be the same one you just left, or it may be a new Activity that has been killed but created from scratch. In the case of re-creation, the state in the previous Activity may have been lost. This means that the callback (setPhoneValidateCallback, setNicknameCheckCallback, setRegisterCallback) set for each process step Fragment may no longer be valid. When the Activity is recreated, it is a new object in memory that only undergoes the onCreate, onStart, and onResume callbacks. If the Fragment callback is not in one of these lifecycle functions, Then the state is lost (this can be verified by the developer options do not retain the activity option).
However, one solution is to write the call to the Fragment callback in the onCreate function of the Activity (since both new and rebuilt activities go through the onCreate lifecycle). As in the onCreate method in this example. However, this requires that the onCreate function retrieve all instances of the Fragment (whether the Fragment is newly created for the first time or, in the case of the Fragment, the system automatically recovers the Fragment using the FragmentManager lookup).
However, it is very common in a process that the parameters required for one step to start depend on the previous step. If we use the best practices recommended by Google, it is obvious that we need to prepare all parameters during initialization. This is unrealistic. The onCreate function of the Activity certainly does not have the parameters required for the Fragment initialization later in the step.
This creates a contradiction: In order to ensure that the process continues to be available in the case of a destroy rebuild, you need to obtain all Fragment instances during onCreate. On the other hand, you cannot prepare all the parameters required for Fragment initialization during onCreate to instantiate the Fragment with Google best practices.
The solution here is the findOrCreateFragment method above, which does not entirely use Google best practices. Instantiate the Fragment using reflection, taking advantage of the fact that the Fragment should contain a constructor that takes no arguments.
fragment = fragmentClass.newInstance();
Copy the code
Arguments initialized with fragments should not be constructor arguments, but should be passed through the fragment.setarguments method before starting the code for the next step (the push method in this example) in the callback at the end of each step. Pass the value through the fragment. setArguments method. PasswordSetFragment. SetParams method is as follows (bottom is fragments. SetArguments method) :
public void setParams(String phone, String nickname) {
Bundle bundle = new Bundle();
bundle.putString("phone", phone);
bundle.putString("nickname", nickname);
setArguments(bundle);
}
Copy the code
In fact, static analysis of the code shows that the Fragment instances that call push are instances that do not exist in the FragmentManger, that is, instances that have been instantiated only by reflection. Quasi-new fragments that haven’t actually gone through any of the Fragment lifecycle functions. So, while our code may not be the same as Google’s recommended code, we’re still following Google’s recommended best practices.
Here, all the key details of the process framework implemented by the Fragment Back Stack are covered. This scheme is clearly a better one than option 1 and option 3 because it combines the advantages of both. Let’s summarize the advantages of this scheme:
- The process steps are decoupled, and each step has clear responsibilities. It only needs to do its own thing and notify the host.
- Good rollback support and smooth user experience.
- The destruction process simply calls the Activity’s
finish
Method, very lightweight; - Only one Activity represents the process exposed to the outside world, which is well encapsulated and easy to reuse;
- Sharing data between process steps becomes easier
Looking back at the seven issues that need to be addressed by the process framework proposed at the beginning of this article, it can be found that all the issues have been satisfactorily resolved except problem (3), which is not completely solved. Let’s take a look at problem (3), which is based on the assumption that each step of the process is implemented based on the Activity, although the Fragment callback to the Activity is no longer limited by the Bundle format. But starting the Fragment from the Activity push requires calling the setArguments method first, and the format this method supports is still constrained by the Bundle. If we want Android to properly restore the Fragment when the Activity is rebuilt after destruction, we have to accept this.
In addition, although there are no restrictions on the format of data that a Fragment can pass to an Activity, in order to maintain the state of the Activity, given that the Activity may be destroyed and rebuilt, We still need to implement the onSaveInstanceState and onRestoreInstanceState methods of the Activity, which are still dealing with the Bundle:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("phoneNumber", phoneNumber);
outState.putString("nickName", nickName);
outState.putSerializable("user", mUser);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState == null) return;
phoneNumber = savedInstanceState.getString("phoneNumber");
nickName = savedInstanceState.getString("nickName");
mUser = (User) savedInstanceState.getSerializable("user");
}
Copy the code
That is, if we want our Activity/Fragment to survive its lifecycle and be restored correctly, we need to ensure that our data can be packaged into the Bundle. We trade the convenience of coding for the correctness of code execution. Therefore, at present, although ** problem (3)** has not been solved or bypassed by us, its existence is actually acceptable in essence.
conclusion
After discussing and comparing so many solutions above, we finally found the most suitable solution relative to —- solution (5) : Fragment based encapsulating process framework. However, this is not the end of the road. Although the solution meets our needs in theoretical terms, there are still a few minor issues waiting to be solved in practical development. Such as:
- Process is exposed outside interface startActivityForResult/onActivityResult, based on the API for development, is hardly “elegant”;
- How the context that initiated the process should be saved,
requestCode
The amount of information that can be stored is limited, especially in ListView/RecyclerView Settings; - Maybe we should use a framework to help us implement the process framework instead of writing a lot of repetitive code;
And so on.
In my next post, I’ll continue to show you how to use and encapsulate process frameworks more elegantly, so stay tuned!