Skip to content

Instantly share code, notes, and snippets.

@madki
Last active February 7, 2016 04:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save madki/9475dd405c0307d9f95e to your computer and use it in GitHub Desktop.
Save madki/9475dd405c0307d9f95e to your computer and use it in GitHub Desktop.
Proposal for a annotation processor that generates wrappers for intents

AutoIntent

The problem

Data transfer in android happens a lot via intents. Even in case of single activity pattern (to which square is a strong proponent of) there are still Android's system activities that are needed to be started or passing messages to services. So intents are probably going to stay for a while and if there's a way to make them better it's worth taking a look at.

  • Type-safety One of the advantages Intents offer is the completely decoupling of the calling code and the recieving activity. It has one major caveat that compilers are now clueless whether the passed in values are correct. Any key can be mapped with any of the types that can be put into the intent.

  • Definition There's no standard way to specify intent requirements. Some activities need some additional extras that are required for it to start. In some cases additional flags or action might have to be set. There should be an easier and straight forward way of defining these requirements such that misuse can be avoided or spotted on the calling side (compile time errors or fail-fast: during execution of calling code)

These happen to be the major problems with intents.

The ideal solution

The ideal solution for this problem is to write wrappers for all intents and expose helpers/builders for starting activities. But the amount of code actually needed for this is very huge. A sample DemoActivity with three extras two of which are definitely required(id and name) will look as follows:

public class DemoActivity extends Activity {
	private static final String ID     = "DemoActivity#id";
	private static final String NAME   = "DemoActivity#name";
	private static final String GENDER = "DemoActivity#gender";
	

	private static class Parser {
		private final Intent intent;

		private Parser(Intent intent) {
			this.intent = intent;
		}

		private int id(int defValue) {
			if (intent == null) return defValue;
			return intent.getIntExtra(ID, defValue);
		}

		private int name() {
			if (intent == null) return null;
			return intent.getStringExtra(NAME);
		}

		// @TypeDef support annotation
		@Gender
		private int gender(int defValue) {
			if (intent == null) return defValue;
			return intent.getIntExtra(GENDER, defValue);
		}
	}

	public static class Builder {
		private final int id;
		private final String name;
		@Gender
		private int gender;

		public Builder(int id, String name) {
			this.id = id;
			this.name = name;
		}

		public Builder gender(@Gender int g) {
			this.gender = g;
			return this;
		}

		public Intent intent(Context context) {
			return new Intent(context, DemoActivity.class)
						.putExtra(ID, id)
						.putExtra(NAME, name)
						.putExtra(GENDER, gender);
		} 

		public void start(Context context) {
			context.startActivity(intent(context));
		}
	}
}

As can be seen this quickly gets verbose with null checks and changing one field leads to additional changes in multiple places, maintainability becomes hard.

Annotation processing to rescue

The repeated code in the above can be generated by an annotation processor. Opting for a definition structure very similar to AutoValue. The code could look like:

public class DemoActivity extends Activity {
	
	@Override
	protected void onCreate(Bundle savedState) {
		// DemoActivity_Extras is the generated class
		Extras extras = DemoActivity_Extras.from(this);
	} 

	public static Builder withExtras(int id, String name) {
		// DemoActivity_Builder is the generated class
		// all non-optional parameters are taken in constructor
		return new DemoActivity_Builder(id, name);
	}	

	// generated DemoActivity_Extras
	@Args(DemoActivity.class)
	static abstract class Extras {
		protected abstract int id();
		protected abstract String name();

		@Optional @Gender
		protected abstract int gender(int defValue);

		// methods to check if the extra was passed in the intent
		@Helpers
		protected abstract boolean hasId();
		protected abstract boolean hasName();
		protected abstract boolean hasGender();
	}

	// This is optional
	// the info above given by @Extras is enough
	// to generate DemoActivity_Builder
	@ArgFactory(DemoActivity.class)
	interface Builder {
		Builder gender(@Gender int gender);

		Intent intent(Context context);
		void start(Context context);
		void startActivityForResult(Context context, int requestCode);
	}
}

Now this activity can be started from the calling code in a very clean way:

DemoActivity.withExtras(1, "Name")
			.gender(1)
			.start();

Compiler knows what the activity needs to start now. We get awesome autocompletion and the completion would suggest us the required fields and the optional ones. Notice the absence of intent key definitions. The keys can also be generated and if some of the extras need to have a partcular key, it can be provided with a @Key annotation.

The same method can be used for generating Builders for activities that use Intents with uris or action and data. The code in the above class can further be reduced. Once the optional parameters are declared with @Optional the Builder can actually be generated without the interface.

There is no need for both @Args and @ArgFactory to be defined as inner classes of Activity. So these annotations could be used for generating type safe wrappers for any existing activities (from other libraries or core android activities).

I have already made an attempt in this direction once and made the Bundler library. It's actually simpler to use but it has many issues that I discovered while using and it heavily exposes the generated classes which turned out to be a very bad idea (no surprises there).

I'd like inputs and would appreciate any advice and suggestions in re-doing the library (or starting a new one) with a better api and more focus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment