We present here a useful pattern for creating those Intents in a way that makes code more robust and easier to maintain.
For the purpose of simpler description we'll talk about starting Activities using Intents, though the same pattern makes most sense when used (with due care and discretion) for all components.
Implicit Intents
Implicit Intents do not name the specific Activity they trigger. Instead Android looks for the best match between an Intent's content and any Intent Filters declared by the Activity. This is what happens when your application's main Activity is started by an Intent that has its action set to ACTION_MAIN and its category set to CATEGORY_LAUNCHER.
Implicit Intents and their matching by IntentFilters is the key way that applications are able to interact with the entire ecosystem of applications and services in an Android system. It allows us to run an application that (for example) makes use of a web-browser or email client without having to embed all the functionality of a browser or email client in the application itself; we can merely assume that a suitable application exists in the ecosystem, and all our application has to do to leverage that capability is to fire the right Intent. This system of implicit Intents is one of the most powerful aspect of Android, and one which many applications fail to use well. How many applications have we seen that try to provide their own web-browsing capability, ending up providing some half-baked, crippled web-page fetcher-and-renderer, while we know perfectly well that the system almost certainly hosts one or more full-fledged, powerful web-browsers, if only they would just fire an appropriate Intent instead of trying to go it alone.
Explicit Intents
Explicit Intents are Intents that name a specific component – an Activity, Service or BroadcastReceiver – as the thing to be activated using the components class name. As soon as an Intent names a specific component, all other forms of Intent matching against filters is abandoned, and Android activates only the specific component, without any regard for the other bits of data the Intent may carry.
In terms of Activities, explicit Intents are used to implement in-application navigation between Activities, and many of the target Activities will not have any associated IntentFilter at all, so there is no other way to activate them other than with explicit Intents fired form other parts of the application.
Pattern
Each Activity, Service or BroadcastReceiver should provide a factory that returns template Intents for starting that Activity, Service or BroadcastReceiver. In the simplest case – say an Activity started by an explicit Intent – this might be a simple factory method:
public class ListManagerActivity extends Activity {
public static Intent getStartIntent( Context context ){
return new Intent( context, ListManagerActivity.class );
}
...
}
In this most elementary form such a factory method is not, by itself, very useful. It becomes ''much'' more compelling when the Activity in question expects or requires certain additional data to be present in its starter Intent:
public class ListManagerActivity extends Activity {
public static Intent getStartIntent( Context context, UserInfo userData ){
checkNotNull( userData );
final Intent startIntent = new Intent( context, ListManagerActivity.class );
startIntent.putExtra( EXTRA_USERINFO, userData );
return startIntent;
}
...
}
In this case we achieve two worthy objectives with this simple factory method:
- We make sure that the Intent that starts the ListManagerActivity always contains the userInfo extra that (presumably) the activity absolutely requires in order to behave correctly, provided, of course, that we only ever manufacture start Intents for ListManagerActivity using this method, and never by calling new Intent(...) – something that can quite easily be checked using various code-quality audit tools.
- We keep the code that builds start Intents (with their extra data) close to the code that consumes those Intents (and that extra data) so that, if we change the notion of what data an Activity requires, we're already in the right place in the code to make the corresponding fixes to the manufacture of those Intents, rather than having to hunt around all over our codebase.
There might be more complex cases where components get started using a variety of different Intents. Perhaps Intents might carry different extra objects or flags affecting the component's behaviour. Perhaps the component might be activates using implicit intents with varying options for their data URI. Indeed, a common mistake when passing a data URI in an implicit Intent is forgetting to correctly set the mime-type for the data URI, in which case matching the Intent against an IntentFilter mysteriously fails.
In such complex cases it may be appropriate to supply several Intent-factory methods in the class that gets activated. It may even be desirable to provide an Intent Builder class:
public class ListManagerActivity extends Activity {
public static class Builder {
private Intent startIntent;
public Builder( Context context, Foo mandatoryData ){
startIntent = new Intent( ListManagerActivity.class );
startIntent.addExtra( EXTRA_FOO, mandatoryData );
startIntent.addFlags( FLAG_ALLOW_NEW_ITEMS );
}
public Builder addVitamins( final Vitamin nutrient ){
startIntent.addExtra( EXTRA_VITAMIN_ID, nutrient.getId() );
return this;
}
...
public Intent build(){
validateIntent();
final Intent constructedIntent = startIntent;
startIntent = null;
return constructedIntent;
}
}
TL;DR:
Don't create start Intents for Activities, Services and BroadcastListeners willy-nilly in myriad and random places all over your codebase. Rather have each such component class provide factory methods or builders to manufacture those start Intents.
This ensures that
- the Intents carry all the necessary data that the component expects,
- that the data is added in close proximity to the places where it is consumed, so that changes in the requirement are easily mirrored in the corresponding changes in the creation and structuring of that data, and
- mandatory data required by the component can be reflected by mandatory arguments in the signature of factory methods, ensuring that required data cannot be easily forgotten.
Nice one Mike, thank you for sharing!
ReplyDelete