Annotations to support your contracts

    Annotations to support your contracts
    Cover image: “Boaters at Argenteuil” by Claude Monet — on WikiArt

    Learn what the support annotations are, and why you should use them

    [tl;dr: use the support annotations and code like a bawss]

    You have surely noticed that sometimes Android Studio complains about something being annotated in the signature of the method you’re overriding, but not in your code:

    What does that @NonNull parameter annotation mean, and where does it come from?


    On Support Annotations

    The @NonNull annotation is included in the Android Support Annotations package. That package is among those that are installed in the local repository by the SDK Manager as part of the Support Library bundle.

    What those annotations do is help you write meaningful contracts for your APIs. @NonNull is just an example — there are a lot of other annotations in the package. There are annotations to specify that an int parameter is supposed to take a resource ID of a given type, the thread a method is supposed to be run on, and more.

    Android Studio can use those annotations to help you avoid bugs (NPEs, unresolved resources, threading issues), and from v1.3.0 of the Android Gradle plugin, so will the CLI Lint. The Android SDK has been annotated with the support annotations to help you avoid pitfalls and common issues.

    Before diving into the annotations themselves, you’ll need to add the annotations to your app’s compile dependencies:

    compile 'com.android.support:support-annotations:23.0.1'

    Protip

    Note that, starting with version 20 of the support library, the support-v4 library depends on the support-annotations, so you don’t have to include them explicitly. If you have any dependencies that include support-v4, such as appcompat-v7, in your app, then you’re all set.


    Show us the goods!

    Now that you have a brand-spanking-new dependency in your project, you want to jump straight to using this newly found arsenal of annotations.

    Let’s take your APIs from zero to hero!

    Nullability

    There are two nullability annotations, our dearest @NonNull and its counterpart @Nullable. They can be applied to fields, parameters and methods (in which case they refer to the return value).

    What they do is pretty obvious: @NonNull means that that symbol is expected to never be null, whereas @Nullable means that you should guard against null values for it. Note that the @Nullable annotation should only be used if that symbol can be null in normal conditions, not if it can be null if misused.

    Android Studio uses them as hint for its static analysis tools and the Infer nullity analysis.

    Resources

    When you pass around a resource ID, it looks like a normal integer value. There’s no way of telling if you’re passing, say, a string ID to a method that expects a colour. This is, unless you use the resources annotations.

    Using these annotations you can explicitly state that an int value is supposed to be any resource ID (@AnyRes) or a specific resource ID (@*Res), and the tools can check the contract at compile time.

    There are a bunch of resources annotations, one for each type of resource: @AnimatorRes, @AnimRes, @AnyRes, @ArrayRes, @AttrRes, @BoolRes, @ColorRes, @DimenRes, @DrawableRes, @FractionRes, @IdRes, @IntegerRes, @InterpolatorRes, @LayoutRes, @MenuRes, @PluralsRes, @RawRes, @StringRes, @StyleableRes, @StyleRes, @XmlRes.

    Values

    Similar to the way you can mark integers to be resource IDs, you can also specify that they are representing a colour (with the @ColorInt annotation), or that their values must fall within a certain range.

    To specify a range for an integer, you can use @IntRange; for a float, its counterpart @FloatRange. You can even specify the expected size of arrays using the @Size annotation.

    TypeDefs

    As you probably already noticed, the Android framework prefers integer flags to enums. This is done to save memory, as enum values are object instances and take up way more memory than a simple int.

    On the other hand, you lose any kind of type safety and check over which kind of values you can bundle up together.

    You can use the @interface and @IntDef (or @StringDef, if you want to use strings instead) annotations to define a “virtual enum” annotation that you can then use to mark int symbols as if they were an enum.

    Refer to the Creating Enumerated Annotations writeup on the Android Developers website for further details on how to implement TypeDefs. Please note that by default these annotated types are assumed to only assume one of the allowed types. If you want to be able to combine more @IntDef values together, you need to set the flag property to true for the @IntDef.

    Proguard

    In the future you’ll be able to auto-generate Proguard rules by simply annotating symbols and classes with the @Keep annotation.

    At the time of writing the feature is still being developed, because the Android Gradle plugin is not picking those annotations up yet.

    Thread safety

    There are annotations that you can use to mark some method (or whole class) that need to be run on a specific thread: @UiThread, @MainThread, @WorkerThread, @BinderThread.

    The difference between @MainThread and @UiThread is that an app has only one main thread, which is also a UI thread, but can spawn multiple UI threads (one per window). Usually you want to tie lifecycle events to the main thread, and all UI logic to a UI thread.

    Bits and bobs

    In this section we’ll quickly look at the rest of the annotations in the package, that can’t be easily split into categories.

    This set of annotations is what I call architecture annotations. They’re defining a contract that your code should respect when calling into some methods, or just providing some context on some aspects of a class’ API.

    The @CallSuper annotation is making explicit that when you override a method from the superclass, you have to call through to super. This really just makes it obvious and shows a warning, but should be always the case.

    The @CheckResult annotation is used to remark the fact that the return value of a method should really not be ignored, and it’s neat as it allows you to specify a suggested usage for the return value that the IDE will show in the warning should this contract be violated. This is how the checkSelfPermission() method is annotated, for example.

    Lastly, we have the @VisibleForTesting annotation. This isn’t really used by the IDE in any way, as far as I could tell when looking into it. What this does is explicit the reason why a method, or a class, has a broader scope than strictly needed: you’re using it for testing. While you should always minimise the need for this, it’s good to let other devs know that you didn’t mistakenly leave that method as public instead of package private.


    Want to know more?

    You can watch my Tools of the Trade talk recording where I go through the XML tools namespace, the Support Annotations, and more. You can find it here:

    Sebastiano Poggi

    Sebastiano Poggi

    “It depends” 🤷‍♂️ — Google Developer Expert for Android, Flutter and Identity. A geek 🤓 who has a serious thing for good design ✨ and for emojis 🤟working at JetBrains (opinions my own)

    London and elsewhere https://sebastiano.dev