This article is a summary of content sharing by GDG Android Meetup
Annotations are technology that we often come into contact with. Java has annotations and Android has annotations. This article will try to introduce annotations in Android, as well as some working principles of annotation-based libraries such as ButterKnife and Otto.
In summary, the annotations in Android have the following benefits
- Improve our development efficiency
- Discovering problems or errors in the program earlier
- Better increase code description ability
- Some of our norms and constraints are more conducive to our
- Provide better solutions to problems
Preparation
By default, the annotation package in Android is not included in the framework. It is independent as a separate package. Usually we need to introduce this package.
dependencies { compile ':support-annotations:22.2.0' }
But if we have introduced appcompat, there is no need to refer to support-annotations again, because appcompat contains references to it by default.
Alternative enum
At the earliest times, when we wanted to do something worth limiting the implementation of enumeration, it is usually
- Define several constants for qualifying
- Select the value from the above constant for use
- A sample code that compares the above problem is as follows
public static final int COLOR_RED = 0; public static final int COLOR_GREEN = 1; public static final int COLOR_YELLOW = 2; public void setColor(int color) { //some code here } //CallsetColor(COLOR_RED)
However, there are still some imperfections on it
setColor(COLOR_RED) has the same effect as setColor(0), which is very poorly readable, but can run normally
The setColor method can accept values other than enumerations, such as setColor(3). In this case, the program may have problems.
A relatively better solution is to use Enum in Java. The effect of using enum is as follows
// public enum ColorEmun { RED, GREEN, YELLOW } public void setColorEnum(ColorEmun colorEnum) { //some code here } setColorEnum();
However, Enum is not the best. Enum takes up a lot of memory compared to the constants of Solution One and has been listed as not recommended by Google. For this reason, Google has specially introduced some related annotations to replace enumeration.
The newly introduced alternative enumeration annotations in Android include IntDef and StringDef. Here, let's use IntDef as an example to illustrate.
public class Colors { @IntDef({RED, GREEN, YELLOW}) @Retention() public @interface LightColors{} public static final int RED = 0; public static final int GREEN = 1; public static final int YELLOW = 2; }
- Declare the necessary int constants
- Declare an annotation as LightColors
- Use @IntDef to modify LightColors, and the parameter is set to the collection to be enumerated
- Use @Retention() to specify that the annotation only exists in the source code and is not added to the class file
Null related annotations
There are two comments related to Null
@Nullable The element annotated can be Null
@NonNull The annotated element cannot be Null
The above two elements can be modified as follows
Member Attributes
Method parameters
The return value of the method
@Nullable private String obtainReferrerFromIntent(@NonNull Intent intent) { return ("apps_referrer"); }
Conditions for the NonNull detection to take effect
Explicitly pass null
When the parameter is null before calling the method
setReferrer(null);//Check warning //No warningString referrer = getIntent().getStringExtra("apps_referrer"); setReferrer(referrer); //Check warningString referrer = getIntent().getStringExtra("apps_referrer"); if (referrer == null) { setReferrer(referrer); } private void setReferrer(@NonNull String referrer) { //some code here }
Interval range annotation
IntRange and FloatRange in Android are two annotations used to limit the interval range.
float currentProgress; public void setCurrentProgress(@FloatRange(from=0.0f, to=1.0f) float progress) { currentProgress = progress; }
If we pass in an illegal value, as shown below
setCurrentProgress(11);
You will get such an error
Value must be >=0.0 and <= 1.0(was 11)
Length and array size limitations
Limit the length of the string
private void setKey(@Size(6) String key) {
}
Qualify the size of the array collection
private void setData(@Size(max = 1) String[] data) { } setData(new String[]{"b", "a"});//error occurs
Limited special array length, such as multiples of 3
private void setItemData(@Size(multiple = 3) String[] data) {
}
Permission related
In Android, there are many scenarios that require permissions, whether it is dynamic permission management before or after Marshmallow. They all need to be declared in manifest. If you forget it, it will cause the program to crash. Fortunately, there is an annotation that can help us avoid this problem. Just use the RequiresPermission annotation.
@RequiresPermission(.SET_WALLPAPER) public void changeWallpaper(Bitmap bitmap) throws IOException { }
Resource Notes
Almost all resources in Android can have corresponding resource ids. For example, we can use the following method to obtain the defined string.
public String getStringById(int stringResId) { return getResources().getString(stringResId); }
Using this method, we can easily obtain the defined string, but this writing also has risks.
getStringById(.ic_launcher)
If we pass such a value without knowing or negligence, problems will arise. However, if we modify the parameters using resource-related annotations, we can avoid errors to a large extent.
public String getStringById(@StringRes int stringResId) { return getResources().getString(stringResId); }
In Android, the resource annotation is as follows
- AnimRes
- AnimatorRes
- AnyRes
- ArrayRes
- AttrRes
- BoolRes
- ColorRes
- DimenRes
- DrawableRes
- FractionRes
- IdRes
- IntegerRes
- InterpolatorRes
- LayoutRes
- MenuRes
- PluralsRes
- RawRes
- StringRes
- StyleRes
- StyleableRes
- TransitionRes
- XmlRes
Color value limit
The above part mentions ColorRes, used to define the color resource id. Here we will use ColorInt, an annotation used to define the Color value. This is how the setTextColor in an earlier TextView was implemented.
public void setTextColor(int color) { mTextColor = (color); updateTextColors(); }
However, this often occurs when the above method is called
();
As mentioned above, if the resource id with the past parameter is color will have a color error, and this problem was still relatively serious in the past. Fortunately, ColorInt appeared, which changed this problem.
public void setTextColor(@ColorInt int color) { mTextColor = (color); updateTextColors(); }
When we pass in the Color resource value again, we will get an error message.
CheckResult
This is an annotation about returning the result, used to annotate the method. If a method gets the result but does not use the result, an error will occur. Once this error occurs, it means that you have not used the method correctly.
@CheckResult public String trim(String s) { return (); }
Thread related
Four thread-related annotations are provided in Android
- @UiThread, it can usually be equivalent to the main thread. The annotation method needs to be executed in UIThread. For example, the View class uses this annotation.
- @MainThread The main thread, the first thread created after startup
- @WorkerThread Worker threads are generally some background threads, such as doInBackground in AsyncTask.
- @BinderThread The annotation method must be executed in the BinderThread thread and is generally used less.
Some examples
new AsyncTask<Void, Void, Void>() { //doInBackground is already annotated with @WorkerThread @Override protected Void doInBackground(Void... params) { return null; updateViews();//error } }; @UiThread public void updateViews() { (LOGTAG, "updateViews ThreadInfo=" + ()); }
Note that there will be no error prompt in this case
new Thread(){ @Override public void run() { (); updateViews(); } }.start();
Although updateViews will be executed in a new worker thread, there is no error prompt when compiling.
Because its judgment is that if the thread annotation of updateView (here is @UiThread) and run (no thread annotation) are inconsistent, the error message will be prompted. If the run method does not have thread annotation, it will not be prompted.
CallSuper
The overridden method must call the super method
Using this annotation, we can force the method of the parent class to be called when rewriting the method, such as Application's onCreate, onConfigurationChanged, etc.
Keep
In the process of compiling and generating APKs on Android, we usually need to set minifyEnabled to true to achieve the following two effects
Obfuscating code
Delete useless code
However, for some purposes, we need not to obfuscate some code or delete some code. In addition to configuring complex Proguard files, we can also use @Keep annotation.
@Keep public static int getBitmapWidth(Bitmap bitmap) { return (); }
ButterKnife
ButterKnife is an efficient tool for binding Views, resources and callbacks. Authored by Jake Wharton. The benefits of ButterKnife
- Use BindView instead of tedious findViewById and type conversion
- Use the OnClick annotation method to replace anonymous inner classes declared explicitly
- Use BindString, BindBool, BindDrawable and other annotations to achieve resource acquisition
An example taken from Github
class ExampleActivity extends Activity { @BindView() EditText username; @BindView() EditText password; @BindString(.login_error) String loginErrorMessage; @OnClick() void submit() { // TODO call server... } @Override public void onCreate(Bundle savedInstanceState) { (savedInstanceState); setContentView(.simple_activity); (this); // TODO Use fields... } }
How ButterKnife works
Taking BindView annotation as an example, the example code is
public class MainActivity extends AppCompatActivity { @BindView() TextView myTextView; @Override protected void onCreate(Bundle savedInstanceState) { (savedInstanceState); setContentView(.activity_main); (this); } }
1. When the program compiles, two classes will be automatically generated based on the annotation. Here are MainActivity_ViewBinder.class and MainActivity_ViewBinding.class
2. When we call (this);, we will look up the ViewBinder class corresponding to the current class and call the bind method. Here we will call the MainActivty_ViewBinder.bind method.
3. MainActivty_ViewBinder.bind method actually calls findViewById and then performs type conversion, assigns to the myTextView property of MainActivity
ButterKnife's bind method
public static Unbinder bind(@NonNull Activity target) { return getViewBinder(target).bind(, target, target); }
ButterKnife's getViewBinder and findViewBinderForClass
@NonNull @CheckResult @UiThread static ViewBinder<Object> getViewBinder(@NonNull Object target) { Class<?> targetClass = (); if (debug) (TAG, "Looking up view binder for " + ()); return findViewBinderForClass(targetClass); } @NonNull @CheckResult @UiThread private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) { //If the memory collection BINDERS is included, then no more searches ViewBinder<Object> viewBinder = (cls); if (viewBinder != null) { if (debug) (TAG, "HIT: Cached in view binder map."); return viewBinder; } String clsName = (); if (("android.") || ("java.")) { if (debug) (TAG, "MISS: Reached framework class. Abandoning search."); return NOP_VIEW_BINDER; } //noinspection TryWithIdenticalCatches Resolves to API 19+ only type. try { //Create an instance using reflection Class<?> viewBindingClass = (clsName + "_ViewBinder"); //noinspection unchecked viewBinder = (ViewBinder<Object>) (); if (debug) (TAG, "HIT: Loaded view binder class."); } catch (ClassNotFoundException e) { //If not found, search for the parent class if (debug) (TAG, "Not found. Trying superclass " + ().getName()); viewBinder = findViewBinderForClass(()); } catch (InstantiationException e) { throw new RuntimeException("Unable to create view binder for " + clsName, e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to create view binder for " + clsName, e); } //Add to the memory collection to facilitate subsequent search (cls, viewBinder); return viewBinder; }
Decompiled source code of MainActivity_ViewBinder
➜ androidannotationsample javap -c MainActivity_ViewBinder Warning: Binary file MainActivity_ViewBinder contains .MainActivity_ViewBinder Compiled from "MainActivity_ViewBinder.java" public final class .MainActivity_ViewBinder implements <> { public .MainActivity_ViewBinder(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public bind(, , ); Code: 0: new #2 // class com/example/admin/androidannotationsample/MainActivity_ViewBinding 3: dup 4: aload_2 5: aload_1 6: aload_3 // Create ViewBinding instance 7: invokespecial #3 // Method com/example/admin/androidannotationsample/MainActivity_ViewBinding."<init>":(Lcom/example/admin/androidannotationsample/MainActivity;Lbutterknife/internal/Finder;Ljava/lang/Object;)V 10: areturn public bind(, , ); Code: 0: aload_0 1: aload_1 2: aload_2 3: checkcast #4 // class com/example/admin/androidannotationsample/MainActivity 6: aload_3 //Calling the overloaded method above 7: invokevirtual #5 // Method bind:(Lbutterknife/internal/Finder;Lcom/example/admin/androidannotationsample/MainActivity;Ljava/lang/Object;)Lbutterknife/Unbinder; 10: areturn } MainActivity_ViewBindingDecompiling source code ➜ androidannotationsample javap -c MainActivity_ViewBinding Warning: Binary file MainActivity_ViewBinding contains .MainActivity_ViewBinding Compiled from "MainActivity_ViewBinding.java" public class .MainActivity_ViewBinding<T extends > implements { protected T target; public .MainActivity_ViewBinding(T, , ); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: aload_1 6: putfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity; 9: aload_1 10: aload_2 11: aload_3 //Call the View, type conversion, and copy it to the variable in MainActivity 12: ldc #4 // int 2131427412 14: ldc #5 // String field 'myTextView' 16: ldc #6 // class android/widget/TextView // FindViewById is actually called internally 18: invokevirtual #7 // Method butterknife/internal/:(Ljava/lang/Object;ILjava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; 21: checkcast #6 // class android/widget/TextView 24: putfield #8 // Field com/example/admin/androidannotationsample/:Landroid/widget/TextView; 27: return public void unbind(); Code: 0: aload_0 1: getfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity; 4: astore_1 5: aload_1 6: ifnonnull 19 9: new #9 // class java/lang/IllegalStateException 12: dup 13: ldc #10 // String Bindings already cleared. 15: invokespecial #11 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V 18: athrow 19: aload_1 20: aconst_null // Unbind, set the corresponding variable to null 21: putfield #8 // Field com/example/admin/androidannotationsample/:Landroid/widget/TextView; 24: aload_0 25: aconst_null 26: putfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity; 29: return }
Finder's source code
package ; import ; import ; import ; import ; import ; @SuppressWarnings("UnusedDeclaration") // Used by generated code. public enum Finder { VIEW { @Override public View findOptionalView(Object source, @IdRes int id) { return ((View) source).findViewById(id); } @Override public Context getContext(Object source) { return ((View) source).getContext(); } @Override protected String getResourceEntryName(Object source, @IdRes int id) { final View view = (View) source; // In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources if (()) { return "<unavailable while editing>"; } return (source, id); } }, ACTIVITY { @Override public View findOptionalView(Object source, @IdRes int id) { return ((Activity) source).findViewById(id); } @Override public Context getContext(Object source) { return (Activity) source; } }, DIALOG { @Override public View findOptionalView(Object source, @IdRes int id) { return ((Dialog) source).findViewById(id); } @Override public Context getContext(Object source) { return ((Dialog) source).getContext(); } }; //Find the corresponding Finder, such as ACTIVITY, DIALOG, VIEW above public abstract View findOptionalView(Object source, @IdRes int id); public final <T> T findOptionalViewAsType(Object source, @IdRes int id, String who, Class<T> cls) { View view = findOptionalView(source, id); return castView(view, id, who, cls); } public final View findRequiredView(Object source, @IdRes int id, String who) { View view = findOptionalView(source, id); if (view != null) { return view; } String name = getResourceEntryName(source, id); throw new IllegalStateException("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'" + " (methods) annotation."); } //Call from ViewBinding public final <T> T findRequiredViewAsType(Object source, @IdRes int id, String who, Class<T> cls) { View view = findRequiredView(source, id, who); return castView(view, id, who, cls); } public final <T> T castView(View view, @IdRes int id, String who, Class<T> cls) { try { return (view); } catch (ClassCastException e) { String name = getResourceEntryName(view, id); throw new IllegalStateException("View '" + name + "' with ID " + id + " for " + who + " was of the wrong type. See cause for more info.", e); } } @SuppressWarnings("unchecked") // That's the point. public final <T> T castParam(Object value, String from, int fromPos, String to, int toPos) { try { return (T) value; } catch (ClassCastException e) { throw new IllegalStateException("Parameter #" + (fromPos + 1) + " of method '" + from + "' was of the wrong type for parameter #" + (toPos + 1) + " of method '" + to + "'. See cause for more info.", e); } } protected String getResourceEntryName(Object source, @IdRes int id) { return getContext(source).getResources().getResourceEntryName(id); } public abstract Context getContext(Object source); }
Otto
Otto Bus It's a specialAndroidModifiedEvent Bus,It has application in many projects.Depend onSquareOpen source sharing. public class EventBusTest { private static final String LOGTAG = "EventBusTest"; Bus mBus = new Bus(); public void test() { (this); } class NetworkChangedEvent { } @Produce public NetworkChangedEvent sendNetworkChangedEvent() { return new NetworkChangedEvent(); } @Subscribe public void onNetworkChanged(NetworkChangedEvent event) { (LOGTAG, "onNetworkChanged event=" + event); } }
How Otto works
- Tag methods using @Produce and @Subscribe
- When calling the method, search the tag method of the registered object and cache the relationship
- When post event, add the event to the handler method to the event queue
- Extract the event queue and then call handler to handle it
The following is an analysis of how Otto uses annotations
Register's source code
public void register(Object object) { if (object == null) { throw new NullPointerException("Object to register must not be null."); } (this); //Find Subscriber in object Map<Class<?>, Set<EventHandler>> foundHandlersMap = (object); for (Class<?> type : ()) { Set<EventHandler> handlers = (type); if (handlers == null) { //concurrent put if absent Set<EventHandler> handlersCreation = new CopyOnWriteArraySet<EventHandler>(); handlers = (type, handlersCreation); if (handlers == null) { handlers = handlersCreation; } } final Set<EventHandler> foundHandlers = (type); if (!(foundHandlers)) { throw new IllegalArgumentException("Object already registered."); } } for (<Class<?>, Set<EventHandler>> entry : ()) { Class<?> type = (); EventProducer producer = (type); if (producer != null && ()) { Set<EventHandler> foundHandlers = (); for (EventHandler foundHandler : foundHandlers) { if (!()) { break; } if (()) { dispatchProducerResultToHandler(foundHandler, producer); } } } } }
HandlerFinder source code
interface HandlerFinder { Map<Class<?>, EventProducer> findAllProducers(Object listener); Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener); //Otto annotation finder HandlerFinder ANNOTATED = new HandlerFinder() { @Override public Map<Class<?>, EventProducer> findAllProducers(Object listener) { return (listener); } @Override public Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) { return (listener); } };
Detailed search implementation
/** This implementation finds all methods marked with a {@link Subscribe} annotation. */ static Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) { Class<?> listenerClass = (); Map<Class<?>, Set<EventHandler>> handlersInMethod = new HashMap<Class<?>, Set<EventHandler>>(); Map<Class<?>, Set<Method>> methods = SUBSCRIBERS_CACHE.get(listenerClass); if (null == methods) { methods = new HashMap<Class<?>, Set<Method>>(); loadAnnotatedSubscriberMethods(listenerClass, methods); } if (!()) { for (<Class<?>, Set<Method>> e : ()) { Set<EventHandler> handlers = new HashSet<EventHandler>(); for (Method m : ()) { (new EventHandler(listener, m)); } ((), handlers); } } return handlersInMethod; }
The above are some summary of annotations in Android. Some of the contents of the article are referenced from Support Annotations. I hope it can help everyone have a basic understanding of annotations and apply them to actual daily development.
Through this article, I hope it can help you fully understand the Android annotation mechanism. Thank you for your support for this website!