SoFunction
Updated on 2025-03-06

Detailed introduction to annotations in Android

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

  1. Improve our development efficiency
  2. Discovering problems or errors in the program earlier
  3. Better increase code description ability
  4. Some of our norms and constraints are more conducive to our
  5. 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

1. Define several constants for qualifying
2. 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;
}

  1. Declare the necessary int constants
  2. Declare an annotation as LightColors
  3. Use @IntDef to modify LightColors, and the parameter is set to the collection to be enumerated
  4. 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

  1. AnimRes
  2. AnimatorRes
  3. AnyRes
  4. ArrayRes
  5. AttrRes
  6. BoolRes
  7. ColorRes
  8. DimenRes
  9. DrawableRes
  10. FractionRes
  11. IdRes
  12. IntegerRes
  13. InterpolatorRes
  14. LayoutRes
  15. MenuRes
  16. PluralsRes
  17. RawRes
  18. StringRes
  19. StyleRes
  20. StyleableRes
  21. TransitionRes
  22. 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

  1. @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.
  2. @MainThread The main thread, the first thread created after startup
  3. @WorkerThread Worker threads are generally some background threads, such as doInBackground in AsyncTask.
  4. @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

  1. Use BindView instead of tedious findViewById and type conversion
  2. Use the OnClick annotation method to replace anonymous inner classes declared explicitly
  3. 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&lt;Object&gt; getViewBinder(@NonNull Object target) {
  Class&lt;?&gt; targetClass = ();
  if (debug) (TAG, "Looking up view binder for " + ());
  return findViewBinderForClass(targetClass);
 }

 @NonNull @CheckResult @UiThread
 private static ViewBinder&lt;Object&gt; findViewBinderForClass(Class&lt;?&gt; cls) {
  //If the memory collection BINDERS is included, then no more searches  ViewBinder&lt;Object&gt; 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&lt;?&gt; viewBindingClass = (clsName + "_ViewBinder");
   //noinspection unchecked
   viewBinder = (ViewBinder&lt;Object&gt;) ();
   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 &lt;&gt; {
 public .MainActivity_ViewBinder();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."&lt;init&gt;":()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."&lt;init&gt;":(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
}

Decompiled source code of MainActivity_ViewBinding

➜ androidannotationsample javap -c MainActivity_ViewBinding
Warning: Binary file MainActivity_ViewBinding contains .MainActivity_ViewBinding
Compiled from "MainActivity_ViewBinding.java"
public class .MainActivity_ViewBinding&lt;T extends &gt; implements  {
 protected T target;

 public .MainActivity_ViewBinding(T, , );
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."&lt;init&gt;":()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."&lt;init&gt;":(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 "&lt;unavailable while editing&gt;";
   }
   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 &lt;T&gt; T findOptionalViewAsType(Object source, @IdRes int id, String who,
   Class&lt;T&gt; 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 &lt;T&gt; T findRequiredViewAsType(Object source, @IdRes int id, String who,
   Class&lt;T&gt; cls) {
  View view = findRequiredView(source, id, who);
  return castView(view, id, who, cls);
 }

 public final &lt;T&gt; T castView(View view, @IdRes int id, String who, Class&lt;T&gt; 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 &lt;T&gt; 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 is an Event Bus specially modified for Android and has applications in many projects. It is shared open source by Square.

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

  1. Tag methods using @Produce and @Subscribe
  2. When calling the method, search the tag method of the registered object and cache the relationship
  3. When post event, add the event to the handler method to the event queue
  4. 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&lt;Class&lt;?&gt;, Set&lt;EventHandler&gt;&gt; foundHandlersMap = (object);
  for (Class&lt;?&gt; type : ()) {
   Set&lt;EventHandler&gt; handlers = (type);
   if (handlers == null) {
    //concurrent put if absent
    Set&lt;EventHandler&gt; handlersCreation = new CopyOnWriteArraySet&lt;EventHandler&gt;();
    handlers = (type, handlersCreation);
    if (handlers == null) {
      handlers = handlersCreation;
    }
   }
   final Set&lt;EventHandler&gt; foundHandlers = (type);
   if (!(foundHandlers)) {
    throw new IllegalArgumentException("Object already registered.");
   }
  }

  for (&lt;Class&lt;?&gt;, Set&lt;EventHandler&gt;&gt; entry : ()) {
   Class&lt;?&gt; type = ();
   EventProducer producer = (type);
   if (producer != null &amp;&amp; ()) {
    Set&lt;EventHandler&gt; foundHandlers = ();
    for (EventHandler foundHandler : foundHandlers) {
     if (!()) {
      break;
     }
     if (()) {
      dispatchProducerResultToHandler(foundHandler, producer);
     }
    }
   }
  }
 }

HandlerFinder source code

interface HandlerFinder {

 Map&lt;Class&lt;?&gt;, EventProducer&gt; findAllProducers(Object listener);

 Map&lt;Class&lt;?&gt;, Set&lt;EventHandler&gt;&gt; findAllSubscribers(Object listener);

 //Otto annotation finder HandlerFinder ANNOTATED = new HandlerFinder() {
  @Override
  public Map&lt;Class&lt;?&gt;, EventProducer&gt; findAllProducers(Object listener) {
   return (listener);
  }

  @Override
  public Map&lt;Class&lt;?&gt;, Set&lt;EventHandler&gt;&gt; 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.

The above will organize the Android annotation information and continue to be supplemented later. Thank you for your support for this site!