关于Android界面组件重用 A Simple Control Flow Facility For Android Activities --Submitted by Fabrizio Giudici
Posted on 2010-10-22 00:22 charley_yang 阅读(678) 评论(0) 编辑 收藏 举报In blueBill Mobile for Android (BTW, I have published the first version to the Android Market) I've had to face with an internal reuse problem. Consider the following diagram, which depicts a couple of possible "screen" flows that the user may navigate within:
In Android, every "screen" is called Activity. The application starts with the ObservationsActivity, which shows the list of recorded observations. By pushing a button, the user can insert a new one. This task is accomplished by a wizard-like flow described by the upper sequence in the diagram:
- Eventually pick a bird species from a history list (containing the most recent picks) (PickTaxonActivity).
- Alternatively, the user can push a button and get to a new screen where it is possible to browse a taxonomy and drill down to a single species (TaxonBrowserActivity).
- Then the number of observed birds and their gender is entered (CountAndGenderActivity). By pressing an "Add more" button, the user might cycle and loop back to entering a new species.
- Instead, by pressing an "Ok" button, a final PickLocationActivity allows to enter the location of the observation.
- At the end, the user returns to the ObservationsActivity.
In a second function, the user might want to recall a fact sheet of a bird species, with information, media (photos and sounds), etc. It makes sense to reuse the same two activities (PickTaxonActivity and TaxonBrowserActivity) and, after the selection has been done, to get to a FactSheetActivity:
- Eventually pick a bird species from a history list (containing the most recent picks) (PickTaxonActivity).
- Alternatively, the user can push a button and get to a new screen where it is possible to browse a taxonomy and drill down to a single species (TaxonBrowserActivity).
- Then the FactSheetActivity is rendered.
How does one navigate through Activities in Android? By means of an asynchronous message-passing facility called "Intent". Basically, one Activity creates and sends a message to the runtime, which reacts by bringing on the screen the recipient Activity of the message, that gets activated.
Messages can be based on a sort of "publish and subscribe" pattern (they carry a "topic" and the proper Activity is found as it declared to be competent about the topic in the application manifest):
Intent intent = new Intent("topic"); // eventually put extra information into the intent startActivity(intent);
Alternatively it is possible to explicitly specify the recipient by referring its class (in this case we have a "point to point" messaging system):
Intent intent = new Intent(); intent.setClass(..., NextActivity.class); // eventually put extra information into the intent startActivity(intent);
A special kind of message is generated by calling the finish() method, which notifies that an Activity terminated its task, so the control is returned to the sender of the message.
As a plus, Android keeps track of the flow and implements a stack of the recently visited Activities, so the "back" button usually works automatically without needing further code; the "finish" message flushes the stack up to the caller point.
I'm a big fan of the "publish and subscribe" pattern because it does a great job in decoupling the various part of a design. In Android, the mechanism is very powerful because the various Activities needn't to be part of the same application: it's very easy to have multiple applications to cooperate. For instance, sending an Intent with a pre-defined topic can bring up the email client, or the phone contact list, etc.
Unfortunately, often you need the "point to point" variant of messaging when you operate inside an application as you require a specific sequences of Activities, as in my introductory example.
Now, the "point to point" pattern is very coupling because the originator needs to know the recipient. In other words, PickTaxonActivity and TaxonBrowserActivity need to know which is the next Activity to start: CountAndGenderActivity (flow #1) or FactShetActivity (flow #2). If I wanted to reuse them in further scenarios, I would have more coupling and complicated code, with PickTaxonActivity and TaxonBrowserActivity depending on a number of other Activities, while it should be really the opposite. This is a sign of a bad distribution of roles & responsibilities: Activities should only focus on their own task and know nothing of what happens after.
Who should be in charge of defining the sequence? A possible candidate could be the initial ObservationsActivity as it knows which command has been invoked by the user, thus it knows the whole sequence of steps to walk through. So, in a first time I thought of an approach where each Activity, when it completes, always sends a "finish" message, so it immediately returns to ObservationsActivity; this class would implement the logic for finding the next step and starting the related Activity.
Unfortunately, in this way you completely loose the stack-based back button implementation provided by Android, which is a big pitfall. In order to keep it, each Activity, when it completes, must keep on sending messages forward.
Self-routing messages
The elegant solution I've found to work is based on self-routing messages. The idea is to make messages smart enough to know the path they have to follow (it can be a fixed path or one based on rules). This means that when an Activity completes, it delegates the next thing to do to the message itself. Paths can be described by extending the class ControlFlow, as this simple example illustrates:
public class TaxonFactSheetControlFlow extends ControlFlow { public TaxonFactSheetControlFlow() { startFrom(PickTaxonActivity.class); when(PickTaxonActivity.class).completes().thenForwardTo(TaxonFactSheetActivity.class); when(TaxonBrowserActivity.class).completes().thenForwardTo(TaxonFactSheetActivity.class); } }
It should be readable enough to understand that it implements the flow #2 in our diagram. To start it, the originating Activity calls:
ControlFlow.start(this, TaxonFactSheetControlFlow.class);
The specified ControlFlow not only calls the first Activity of the sequence, but it also binds itself to the Intent; in this way, it will be possible to retrieve it in the next steps.
Whenever an Activity completes, it calls:
ControlFlow.from(this).toNextStep(intent); // an intent can be used to carry extra information
The originally instantiated ControlFlow is retrieved and invoked to find the next Activity to activate.
In case of rule-based paths, it is possible to pass arguments to be evaluated by the ControlFlow; for instance, CountAndGenderActivity in flow#1 calls one of the two following code fragments reacting to the pressure of the "Ok" and "Add more" buttons:
ControlFlow.from(this).toNextStep(); ControlFlow.from(this).toNextStep(ADD_MORE);
The flow#2 is described by this class:
public class AddObservationControlFlow extends ControlFlow { private final Condition addMore = new Condition() { public boolean compute(final @Nonnull Object... args) { return Arrays.asList(args).contains(CountAndGenderActivity.ADD_MORE); } }; public AddObservationControlFlow() { startFrom(PickTaxonActivity.class); when(PickTaxonActivity.class).completes().thenForwardTo(CountAndGenderActivity.class); when(TaxonBrowserActivity.class).completes().thenForwardTo(CountAndGenderActivity.class); when(CountAndGenderActivity.class).completesWith(addMore).thenForwardTo(PickTaxonActivity.class); when(CountAndGenderActivity.class).completesWithout(addMore).thenForwardTo(PickLocationActivity.class); } }
As you can see, each transition can be associated with a condition which is function, for instance, of the arguments passed to toNextStep().
This solution is very scalable, as I can reuse existing Activities as many times as I want, by just creating new subclasses of ControlFlow which describe the new sequences to implement.
The code can be checked out from the Mercurial repository at https://kenai.com/hg/bluebill-mobile~android-src, tag dzone20100517.