Library API guidelines

Introduction

s.android.com/api-guidelines, which covers standard and practices for designing platform APIs.

All platform API design guidelines also apply to Jetpack libraries, with any additional guidelines or exceptions noted in this document. Jetpack libraries also follow explicit API mode for Kotlin libraries.

Modules

Packaging and naming

Java packages within Jetpack follow the format androidx.<feature-name>. All classes within a feature's artifact must reside within this package, and may further subdivide into androidx.<feature-name>.<layer> using standard Android layers (app, widget, etc.) or layers specific to the feature.

Maven artifacts use the groupId format androidx.<feature-name> and artifactId format <feature-name> to match the Java package.

Sub-features that can be separated into their own artifact should use the following formats:

Java package: androidx.<feature-name>.<sub-feature>.<layer>

Maven groupId: androidx.<feature-name>

Maven artifactId: <feature-name>-<sub-feature>

Common sub-feature names

  • -testing for an artifact intended to be used while testing usages of your library, e.g. androidx.room:room-testing
  • -core for a low-level artifact that may contain public APIs but is primarily intended for use by other libraries in the group
  • -ktx for an Kotlin artifact that exposes idiomatic Kotlin APIs as an extension to a Java-only library
  • -java8 for a Java 8 artifact that exposes idiomatic Java 8 APIs as an extension to a Java 7 library
  • -<third-party> for an artifact that integrates an optional third-party API surface, e.g. -proto or -rxjava2. Note that a major version is included in the sub-feature name for third-party API surfaces where the major version indicates binary compatibility (only needed for post-1.x).

Artifacts should not use -impl or -base to indicate that a library is an implementation detail shared within the group. Instead, use -core.

Splitting existing modules

Existing modules should not be split into smaller modules; doing so creates the potential for class duplication issues when a developer depends on a new sub-module alongside the older top-level module. Consider the following scenario:

  • androidx.library:1.0.0
    • contains class androidx.library.A
    • contains class androidx.library.util.B

This module is split, moving androidx.library.util.B to a new module:

  • androidx.library:1.1.0
    • contains class androidx.library.A
    • depends on androidx.library.util:1.1.0
  • androidx.library.util:1.1.0
    • contains class androidx.library.util.B

A developer writes an app that depends directly on androidx.library.util:1.1.0 and also transitively pulls in androidx.library:1.0.0. Their app will no longer compile due to class duplication of androidx.library.util.B.

While it is possible for the developer to fix this by manually specifying a dependency on androidx.library:1.1.0, there is no easy way for the developer to discover this solution from the class duplication error raised at compile time.

Same-version (atomic) groups

Library groups are encouraged to opt-in to a same-version policy whereby all libraries in the group use the same version and express exact-match dependencies on libraries within the group. Such groups must increment the version of every library at the same time and release all libraries at the same time.

Atomic groups are specified in LibraryGroups.kt:

// Non-atomic library group
val APPCOMPAT = LibraryGroup("androidx.appcompat", null)
// Atomic library group
val APPSEARCH = LibraryGroup("androidx.appsearch", LibraryVersions.APPSEARCH)

Libraries within an atomic group should not specify a version in their build.gradle:

androidx {
    name = 'AppSearch'
    publish = Publish.SNAPSHOT_AND_RELEASE
    mavenGroup = LibraryGroups.APPSEARCH
    inceptionYear = '2019'
    description = 'Provides local and centralized app indexing'
}

There is one exception to this policy. Newly-added libraries within an atomic group may stay within the 1.0.0-alphaXX before conforming to the same-version policy. When the library would like to move to beta, it must match the version used by the atomic group (which must be beta at the time).

The benefits of using an atomic group are:

  • Easier for developers to understand dependency versioning
  • @RestrictTo(LIBRARY_GROUP) APIs are treated as private APIs and not tracked for binary compatibility
  • @RequiresOptIn APIs defined within the group may be used without any restrictions between libraries in the group

Potential drawbacks include:

  • All libraries within the group must be versioned identically at head
  • All libraries within the group must release at the same time

Choosing a minSdkVersion

The recommended minimum SDK version for new Jetpack libraries is currently 17 (Android 4.2, Jelly Bean). This SDK was chosen to represent 99% of active devices based on Play Store check-ins (see Android Studio distribution metadata for current statistics). This maximizes potential users for external developers while minimizing the amount of overhead necessary to support legacy versions.

However, if no explicit minimum SDK version is specified for a library, the default is 14.

Note that a library must not depend on another library with a higher minSdkVersion that its own, so it may be necessary for a new library to match its dependent libraries' minSdkVersion.

Individual modules may choose a higher minimum SDK version for business or technical reasons. This is common for device-specific modules such as Auto or Wear.

Individual classes or methods may be annotated with the @RequiresApi annotation to indicate divergence from the overall module's minimum SDK version. Note that this pattern is not recommended because it leads to confusion for external developers and should be considered a last-resort when backporting behavior is not feasible.

Platform compatibility API patterns

Static shims (ex. ViewCompat)

When to use?

  • Platform class exists at module's minSdkVersion
  • Compatibility implementation does not need to store additional metadata

Implementation requirements

  • Class name must be <PlatformClass>Compat
  • Package name must be androidx.<feature>.<platform.package>
  • Superclass must be Object
  • Class must be non-instantiable, i.e. constructor is private no-op
  • Static fields and static methods must match match signatures with PlatformClass
    • Static fields that can be inlined, ex. integer constants, must not be shimmed
  • Public method names must match platform method names
  • Public methods must be static and take PlatformClass as first parameter
  • Implementation may delegate to PlatformClass methods when available

Sample

The following sample provides static helper methods for the platform class android.os.Process.

/**
 * Helper for accessing features in {@link Process}.
 */
public final class ProcessCompat {
    private ProcessCompat() {
        // This class is non-instantiable.
    }

    /**
     * [Docs should match platform docs.]
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 24 and above, this method matches platform behavior.
     * <li>SDK 16 through 23, this method is a best-effort to match platform behavior, but may
     * default to returning {@code true} if an accurate result is not available.
     * <li>SDK 15 and below, this method always returns {@code true} as application UIDs and
     * isolated processes did not exist yet.
     * </ul>
     *
     * @param [match platform docs]
     * @return [match platform docs], or a value based on platform-specific fallback behavior
     */
    public static boolean isApplicationUid(int uid) {
        if (Build.VERSION.SDK_INT >= 24) {
            return Api24Impl.isApplicationUid(uid);
        } else if (Build.VERSION.SDK_INT >= 17) {
            return Api17Impl.isApplicationUid(uid);
        } else if (Build.VERSION.SDK_INT == 16) {
            return Api16Impl.isApplicationUid(uid);
        } else {
            return true;
        }
    }

    @RequiresApi(24)
    static class Api24Impl {
        static boolean isApplicationUid(int uid) {
            // In N, the method was made public on android.os.Process.
            return Process.isApplicationUid(uid);
        }
    }

    @RequiresApi(17)
    static class Api17Impl {
        private static Method sMethod_isAppMethod;
        private static boolean sResolved;

        static boolean isApplicationUid(int uid) {
            // In JELLY_BEAN_MR2, the equivalent isApp(int) hidden method moved to public class
            // android.os.UserHandle.
            try {
                if (!sResolved) {
                    sResolved = true;
                    sMethod_isAppMethod = UserHandle.class.getDeclaredMethod("isApp",int.class);
                }
                if (sMethod_isAppMethod != null) {
                    return (Boolean) sMethod_isAppMethod.invoke(null, uid);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
    }

    ...
}

Wrapper (ex. AccessibilityNodeInfoCompat)

When to use?

  • Platform class may not exist at module's minSdkVersion
  • Compatibility implementation may need to store additional metadata
  • Needs to integrate with platform APIs as return value or method argument
  • Note: Should be avoided when possible, as using wrapper classes makes it very difficult to deprecate classes and migrate source code when the minSdkVersion is raised

Sample

The following sample wraps a hypothetical platform class ModemInfo that was added to the platform SDK in API level 23:

public final class ModemInfoCompat {
  // Only guaranteed to be non-null on SDK_INT >= 23. Note that referencing the
  // class itself directly is fine -- only references to class members need to
  // be pushed into static inner classes.
  private final ModemInfo wrappedObj;

  /**
   * [Copy platform docs for matching constructor.]
   */
  public ModemInfoCompat() {
    if (SDK_INT >= 23) {
      wrappedObj = Api23Impl.create();
    } else {
      wrappedObj = null;
    }
    ...
  }

  @RequiresApi(23)
  private ModemInfoCompat(@NonNull ModemInfo obj) {
    mWrapped = obj;
  }

  /**
   * Provides a backward-compatible wrapper for {@link ModemInfo}.
   * <p>
   * This method is not supported on devices running SDK < 23 since the platform
   * class will not be available.
   *
   * @param info platform class to wrap
   * @return wrapped class, or {@code null} if parameter is {@code null}
   */
  @RequiresApi(23)
  @NonNull
  public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info) {
    return new ModemInfoCompat(obj);
  }

  /**
   * Provides the {@link ModemInfo} represented by this object.
   * <p>
   * This method is not supported on devices running SDK < 23 since the platform
   * class will not be available.
   *
   * @return platform class object
   * @see ModemInfoCompat#toModemInfoCompat(ModemInfo)
   */
  @RequiresApi(23)
  @NonNull
  public ModemInfo toModemInfo() {
    return mWrapped;
  }

  /**
   * [Docs should match platform docs.]
   *
   * Compatibility behavior:
   * <ul>
   * <li>API level 23 and above, this method matches platform behavior.
   * <li>API level 18 through 22, this method ...
   * <li>API level 17 and earlier, this method always returns false.
   * </ul>
   *
   * @return [match platform docs], or platform-specific fallback behavior
   */
  public boolean isLteSupported() {
    if (SDK_INT >= 23) {
      return Api23Impl.isLteSupported(mWrapped);
    } else if (SDK_INT >= 18) {
      // Smart fallback behavior based on earlier APIs.
      ...
    }
    // Default behavior.
    return false;
  }

  // All references to class members -- including the constructor -- must be
  // made on an inner class to avoid soft-verification errors that slow class
  // loading and prevent optimization.
  @RequiresApi(23)
  private static class Api23Impl {
    @NonNull
    static ModemInfo create() {
      return new ModemInfo();
    }

    static boolean isLteSupported(PlatformClass obj) {
      return obj.isLteSupported();
    }
  }
}

Note that libraries written in Java should express conversion to and from the platform class differently than Kotlin classes. For Java classes, conversion from the platform class to the wrapper should be expressed as a static method, while conversion from the wrapper to the platform class should be a method on the wrapper object:

@NonNull
public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info);

@NonNull
public ModemInfo toModemInfo();

In cases where the primary library is written in Java and has an accompanying -ktx Kotlin extensions library, the following conversion should be provided as an extension function:

fun ModemInfo.toModemInfoCompat() : ModemInfoCompat

Whereas in cases where the primary library is written in Kotlin, the conversion should be provided as an extension factory:

class ModemInfoCompat {
  fun toModemInfo() : ModemInfo

  companion object {
    @JvmStatic
    @JvmName("toModemInfoCompat")
    fun ModemInfo.toModemInfoCompat() : ModemInfoCompat
  }
}

API guidelines

Naming
  • Class name must be <PlatformClass>Compat
  • Package name must be androidx.core.<platform.package>
  • Superclass must not be <PlatformClass>
Construction
  • Class may have public constructor(s) to provide parity with public PlatformClass constructors
    • Constructor used to wrap PlatformClass must not be public
  • Class must implement a static PlatformClassCompat toPlatformClassCompat(PlatformClass) method to wrap PlatformClass on supported SDK levels
    • If class does not exist at module's minSdkVersion, method must be annotated with @RequiresApi(<sdk>) for SDK version where class was introduced

Implementation

  • Class must implement a PlatformClass toPlatformClass() method to unwrap PlatformClass on supported SDK levels
    • If class does not exist at module's minSdkVersion, method must be annotated with @RequiresApi(<sdk>) for SDK version where class was introduced
  • Implementation may delegate to PlatformClass methods when available (see below note for caveats)
  • To avoid runtime class verification issues, all operations that interact with the internal structure of PlatformClass must be implemented in inner classes targeted to the SDK level at which the operation was added.
    • See the sample for an example of interacting with a method that was added in SDK level 23.

Standalone (ex. ArraySet, Fragment)

When to use?

  • Platform class may exist at module's minSdkVersion
  • Does not need to integrate with platform APIs
  • Does not need to coexist with platform class, ex. no potential import collision due to both compatibility and platform classes being referenced within the same source file

Implementation requirements

  • Class name must be <PlatformClass>
  • Package name must be androidx.<platform.package>
  • Superclass must not be <PlatformClass>
  • Class must not expose PlatformClass in public API
  • Implementation may delegate to PlatformClass methods when available

Standalone JAR library (no Android dependencies)

When to use

  • General purpose library with minimal interaction with Android types
    • or when abstraction around types can be used (e.g. Room's SQLite wrapper)
  • Lib used in parts of app with minimal Android dependencies
    • ex. Repository, ViewModel
  • When Android dependency can sit on top of common library
  • Clear separation between android dependent and independent parts of your library
  • Clear that future integration with android dependencies can be layered separately

Examples:

The Paging Library pages data from DataSources (such as DB content from Room or network content from Retrofit) into PagedLists, so they can be presented in a RecyclerView. Since the included Adapter receives a PagedList, and there are no other Android dependencies, Paging is split into two parts - a no-android library (paging-common) with the majority of the paging code, and an android library (paging-runtime) with just the code to present a PagedList in a RecyclerView Adapter. This way, tests of Repositories and their components can be tested in host-side tests.

Room loads SQLite data on Android, but provides an abstraction for those that want to use a different SQL implementation on device. This abstraction, and the fact that Room generates code dynamically, means that Room interfaces can be used in host-side tests (though actual DB code should be tested on device, since DB impls may be significantly different on host).

Implementing compatibility

Referencing new APIs

Generally, methods on extension library classes should be available to all devices above the library's minSdkVersion.

Checking device SDK version

The most common way of delegating to platform or backport implementations is to compare the device's Build.VERSION.SDK_INT field to a known-good SDK version; for example, the SDK in which a method first appeared or in which a critical bug was first fixed.

Non-reflective calls to new APIs gated on SDK_INT must be made from version-specific static inner classes to avoid verification errors that negatively affect run-time performance. For more information, see Chromium's guide to Class Verification Failures.

Methods in implementation-specific classes must be paired with the @DoNotInline annotation to prevent them from being inlined.

public static void saveAttributeDataForStyleable(@NonNull View view, ...) {
  if (Build.VERSION.SDK_INT >= 29) {
    Api29Impl.saveAttributeDataForStyleable(view, ...);
  }
}

@RequiresApi(29)
private static class Api29Impl {
  @DoNotInline
  static void saveAttributeDataForStyleable(@NonNull View view, ...) {
    view.saveAttributeDataForStyleable(...);
  }
}

Alternatively, in Kotlin sources:

@RequiresApi(29)
object Api25 {
  @DoNotInline
  fun saveAttributeDataForStyleable(view: View, ...) { ... }
}

When developing against pre-release SDKs where the SDK_INT has not been finalized, SDK checks must use BuildCompat.isAtLeastX() methods.

@NonNull
public static List<Window> getAllWindows() {
  if (BuildCompat.isAtLeastR()) {
    return ApiRImpl.getAllWindows();
  }
  return Collections.emptyList();
}

Device-specific issues

Library code may work around device- or manufacturer-specific issues -- issues not present in AOSP builds of Android -- only if a corresponding CTS test and/or CDD policy is added to the next revision of the Android platform. Doing so ensures that such issues can be detected and fixed by OEMs.

Handling minSdkVersion disparity

Methods that only need to be accessible on newer devices, including to<PlatformClass>() methods, may be annotated with @RequiresApi(<sdk>) to indicate they will fail to link on older SDKs. This annotation is enforced at build time by Lint.

Handling targetSdkVersion behavior changes

To preserve application functionality, device behavior at a given API level may change based on an application's targetSdkVersion. For example, if an app with targetSdkVersion set to API level 22 runs on a device with API level 29, all required permissions will be granted at installation time and the run-time permissions framework will emulate earlier device behavior.

Libraries do not have control over the app's targetSdkVersion and -- in rare cases -- may need to handle variations in platform behavior. Refer to the following pages for version-specific behavior changes:

Working around Lint issues

In rare cases, Lint may fail to interpret API usages and yield a NewApi error and require the use of @TargetApi or @SuppressLint('NewApi') annotations. Both of these annotations are strongly discouraged and may only be used temporarily. They must never be used in a stable release. Any usage of these annotation must be associated with an active bug, and the usage must be removed when the bug is resolved.

Delegating to API-specific implementations

SDK-dependent reflection

Starting in API level 28, the platform restricts which non-SDK interfaces can be accessed via reflection by apps and libraries. As a general rule, you will not be able to use reflection to access hidden APIs on devices with SDK_INT greater than Build.VERSION_CODES.P (28).

On earlier devices, reflection on hidden platform APIs is allowed only when an alternative public platform API exists in a later revision of the Android SDK. For example, the following implementation is allowed:

public AccessibilityDelegate getAccessibilityDelegate(View v) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        // Retrieve the delegate using a public API.
        return v.getAccessibilityDelegate();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // Retrieve the delegate by reflecting on a private field. If the
        // field does not exist or cannot be accessed, this will no-op.
        if (sAccessibilityDelegateField == null) {
            try {
                sAccessibilityDelegateField = View.class
                        .getDeclaredField("mAccessibilityDelegate");
                sAccessibilityDelegateField.setAccessible(true);
            } catch (Throwable t) {
                sAccessibilityDelegateCheckFailed = true;
                return null;
            }
        }
        try {
            Object o = sAccessibilityDelegateField.get(v);
            if (o instanceof View.AccessibilityDelegate) {
                return (View.AccessibilityDelegate) o;
            }
            return null;
        } catch (Throwable t) {
            sAccessibilityDelegateCheckFailed = true;
            return null;
        }
    } else {
        // There is no way to retrieve the delegate, even via reflection.
        return null;
    }

Calls to public APIs added in pre-release revisions must be gated using BuildCompat:

if (BuildCompat.isAtLeastQ()) {
   // call new API added in Q
} else if (Build.SDK_INT.VERSION >= Build.VERSION_CODES.SOME_RELEASE) {
   // make a best-effort using APIs that we expect to be available
} else {
   // no-op or best-effort given no information
}

Inter-process communication

Protocols and data structures used for IPC must support interoperability between different versions of libraries and should be treated similarly to public API.

Data structures

Do not use Parcelable for any class that may be used for IPC or otherwise exposed as public API. The data format used by Parcelable does not provide any compatibility guarantees and will result in crashes if fields are added or removed between library versions.

Do not design your own serialization mechanism or wire format for disk storage or inter-process communication. Preserving and verifying compatibility is difficult and error-prone.

If you expose a Bundle to callers that can cross processes, you should prevent apps from adding their own custom parcelables as top-level entries; if any entry in a Bundle can‘t be loaded, even if it’s not actually accessed, the receiving process is likely to crash.

Do use protocol buffers or, in some simpler cases, VersionedParcelable.

Communication protocols

Any communication prototcol, handshake, etc. must maintain compatibility consistent with SemVer guidelines. Consider how your protocol will handle addition and removal of operations or constants, compatibility-breaking changes, and other modifications without crashing either the host or client process.

Deprecation and removal

While SemVer's binary compatibility guarantees restrict the types of changes that may be made within a library revision and make it difficult to remove an API, there are many other ways to influence how developers interact with your library.

Deprecation (@deprecated)

Deprecation lets a developer know that they should stop using an API or class. All deprecations must be marked with a @Deprecated Java annotation as well as a @deprecated <migration-docs> docs annotation explaining how the developer should migrate away from the API.

Deprecation is an non-breaking API change that must occur in a major or minor release.

Soft removal (@removed)

Soft removal preserves binary compatibility while preventing source code from compiling against an API. It is a source-breaking change and not recommended.

Soft removals must do the following:

  • Mark the API as deprecated for at least one stable release prior to removal.
  • Mark the API with a @RestrictTo(LIBRARY) Java annotation as well as a @removed <reason> docs annotation explaining why the API was removed.
  • Maintain binary compatibility, as the API may still be called by existing dependent libraries.
  • Maintain behavioral compatibility and existing tests.

This is a disruptive change and should be avoided when possible.

Soft removal is a source-breaking API change that must occur in a major or minor release.

Hard removal

Hard removal entails removing the entire implementation of an API that was exposed in a public release. Prior to removal, an API must be marked as @deprecated for a full minor version (alpha->beta->rc->stable), prior to being hard removed.

This is a disruptive change and should be avoided when possible.

Hard removal is a binary-breaking API change that must occur in a major release.

For entire artifacts

We do not typically deprecate or remove entire artifacts; however, it may be useful in cases where we want to halt development and focus elsewhere or strongly discourage developers from using a library.

Halting development, either because of staffing or prioritization issues, leaves the door open for future bug fixes or continued development. This quite simply means we stop releasing updates but retain the source in our tree.

Deprecating an artifact provides developers with a migration path and strongly encourages them -- through Lint warnings -- to migrate elsewhere. This is accomplished by adding a @Deprecated and @deprecated (with migration comment) annotation pair to every class and interface in the artifact.

The fully-deprecated artifact will be released as a deprecation release -- it will ship normally with accompanying release notes indicating the reason for deprecation and migration strategy, and it will be the last version of the artifact that ships. It will ship as a new minor stable release. For example, if 1.0.0 was the last stable release, then the deprecation release will be 1.1.0. This is so Android Studio users will get a suggestion to update to a new stable version, which will contain the @deprecated annotations.

After an artifact has been released as fully-deprecated, it can be removed from the source tree.

Resources

Generally, follow the official Android guidelines for app resources. Special guidelines for library resources are noted below.

Defining new resources

Libraries may define new value and attribute resources using the standard application directory structure used by Android Gradle Plugin:

src/main/res/
  values/
    attrs.xml   Theme attributes and styleables
    dimens.xml  Dimensional values
    public.xml  Public resource definitions
    ...

However, some libraries may still be using non-standard, legacy directory structures such as res-public for their public resource declarations or a top-level res directory and accompanying custom source set in build.gradle. These libraries will eventually be migrated to follow standard guidelines.

Naming conventions

Libraries follow the Android platform's resource naming conventions, which use camelCase for attributes and underline_delimited for values. For example, R.attr.fontProviderPackage and R.dimen.material_blue_grey_900.

Attribute formats

At build time, attribute definitions are pooled globally across all libraries used in an application, which means attribute formats must be identical for a given name to avoid a conflict.

Within Jetpack, new attribute names must be globally unique. Libraries may reference existing public attributes from their dependencies. See below for more information on public attributes.

When adding a new attribute, the format should be defined once in an <attr /> element in the definitions block at the top of src/main/res/attrs.xml. Subsequent references in <declare-styleable> elements must not include a format:

src/main/res/attrs.xml

<resources>
  <attr name="fontProviderPackage" format="string" />

  <declare-styleable name="FontFamily">
      <attr name="fontProviderPackage" />
  </declare-styleable>
</resources>

Public resources

Library resources are private by default, which means developers are discouraged from referencing any defined attributes or values from XML or code; however, library resources may be declared public to make them available to developers.

Public library resources are considered API surface and are thus subject to the same API consistency and documentation requirements as Java APIs.

Libraries will typically only expose theme attributes, ex. <attr /> elements, as public API so that developers can set and retrieve the values stored in styles and themes. Exposing values -- such as <dimen /> and <string /> -- or images -- such as drawable XML and PNGs -- locks the current state of those elements as public API that cannot be changed without a major version bump. That means changing a publicly-visible icon would be considered a breaking change.

Documentation

All public resource definitions should be documented, including top-level definitions and re-uses inside <styleable> elements:

src/main/res/attrs.xml

<resources>
  <!-- String specifying the application package for a Font Provider. -->
  <attr name="fontProviderPackage" format="string" />

  <!-- Attributes that are read when parsing a <fontfamily> tag. -->
  <declare-styleable name="FontFamily">
      <!-- The package for the Font Provider to be used for the request. This is
           used to verify the identity of the provider. -->
      <attr name="fontProviderPackage" />
  </declare-styleable>
</resources>

src/main/res/colors.xml

<resources>
  <!-- Color for Material Blue-Grey 900. -->
  <color name="material_blue_grey_900">#ff263238</color>
</resources>

Public declaration

Resources are declared public by providing a separate <public /> element with a matching type:

src/main/res/public.xml

<resources>
  <public name="fontProviderPackage" type="attr" />
  <public name="material_blue_grey_900" type="color" />
</resources>

More information

See also the official Android Gradle Plugin documentation for Private Resources.

Manifest entries (AndroidManifest.xml)

Metadata tags (<meta-data>)

Developers must not add <application>-level <meta-data> tags to library manifests or advise developers to add such tags to their application manifests. Doing so may inadvertently cause denial-of-service attacks against other apps.

Assume a library adds a single item of meta-data at the application level. When an app uses the library, that meta-data will be merged into the resulting app's application entry via manifest merger.

If another app attempts to obtain a list of all activities associated with the primary app, that list will contain multiple copies of the ApplicationInfo, each of which in turn contains a copy of the library's meta-data. As a result, one <metadata> tag may become hundreds of KB on the binder call to obtain the list -- resulting in apps hitting transaction too large exceptions and crashing.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="androidx.librarypackage">
  <application>
    <meta-data
        android:name="keyName"
        android:value="@string/value" />
  </application>
</manifest>

Instead, developers may consider adding <metadata> nested inside of placeholder <service> tags.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="androidx.librarypackage">
  <application>
    <service
        android:name="androidx.librarypackage.MetadataHolderService"
        android:enabled="false"
        android:exported="false">
      <meta-data
          android:name="androidx.librarypackage.MetadataHolderService.KEY_NAME"
          android:resource="@string/value" />
    </service>
  </application>
package androidx.libraryname.featurename;

/**
 * A placeholder service to avoid adding application-level metadata. The service
 * is only used to expose metadata defined in the library's manifest. It is
 * never invoked.
 */
public final class MetadataHolderService {
  private MetadataHolderService() {}

  @Override
  public IBinder onBind(Intent intent) {
    throw new UnsupportedOperationException();
  }
}

Dependencies

Generally, Jetpack libraries should avoid dependencies that negatively impact developers without providing substantial benefit. This includes large dependencies where only a small portion is needed, dependencies that slow down build times through annotation processing or compiler overhead, and generally any dependency that negatively affects system health.

Kotlin

Kotlin is strongly recommended for new libraries; however, it's important to consider its size impact on clients. Currently, the Kotlin stdlib adds a minimum of 40kB post-optimization. It may not make sense to use Kotlin for a library that targets Java-only clients or space-constrained (ex. Android Go) clients.

Existing Java-based libraries are strongly discouraged from using Kotlin, primarily because our documentation system does not currently provide a Java-facing version of Kotlin API reference docs. Java-based libraries may migrate to Kotlin, but they must consider the docs usability and size impacts on existing Java-only and space-constrained clients.

Kotlin coroutines

Kotlin's coroutine library adds around 100kB post-shrinking. New libraries that are written in Kotlin should prefer coroutines over ListenableFuture, but existing libraries must consider the size impact on their clients. See Asynchronous work with return values for more details on using Kotlin coroutines in Jetpack libraries.

Guava

The full Guava library is very large and must not be used. Libraries that would like to depend on Guava's ListenableFuture may instead depend on the standalone com.google.guava:listenablefuture artifact. See Asynchronous work with return values for more details on using ListenableFuture in Jetpack libraries.

Java 8

NOTE All Jetpack libraries will migrate to Java 8 as soon as Android Studio 4.2 launches to stable. Until then, new dependencies on Java 8 should weigh the pros and cons as documented here.

Libraries that take a dependency on a library targeting Java 8 must also target Java 8, which will incur a ~5% build performance (as of 8/2019) hit for clients. New libraries targeting Java 8 may use Java 8 dependencies; however, existing libraries targeting Java 7 should not.

The default language level for androidx libraries is Java 8, and we encourage libraries to stay on Java 8. However, if you have a business need to target Java 7, you can specify Java 7 in your build.gradle as follows:

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
}

More API guidelines

Annotations

Annotation processors

Annotation processors should opt-in to incremental annotation processing to avoid triggering a full recompilation on every client source code change. See Gradle's Incremental annotation processing documentation for information on how to opt-in.

Experimental @RequiresOptIn APIs

Jetpack libraries may choose to annotate API surfaces as unstable using either Kotlin‘s @RequiresOptIn annotation for APIs written in Kotlin or Jetpack’s @RequiresOptIn annotation for APIs written in Java.

In both cases, API surfaces marked as experimental are considered alpha and will be excluded from API compatibility guarantees. Due to the lack of compatibility guarantees, stable libraries must never call experimental APIs exposed by other libraries outside of their same-version group and may not use the @OptIn annotation except in the following cases:

  • A library within a same-version group may call an experimental API exposed by another library within its same-version group. In this case, API compatibility guarantees are covered under the same-version group policies and the library may use the @OptIn annotation to prevent propagation of the experimental property. Library owners must exercise care to ensure that post-alpha APIs backed by experimental APIs actually meet the release criteria for post-alpha APIs.
  • An alpha library may use experimental APIs from outside its same-version group. These usages must be removed when the library moves to beta.

NOTE JetBrains's own usage of @RequiresOptIn in Kotlin language libraries varies and may indicate binary instability, functional instability, or simply that an API is really difficult to use. Jetpack libraries should treat instances of @RequiresOptIn in JetBrains libraries as indicating binary instability and avoid using them outside of alpha; however, teams are welcome to obtain written assurance from JetBrains regarding binary stability of specific APIs. @RequiresOptIn APIs that are guaranteed to remain binary compatible may be used in beta, but usages must be removed when the library moves to rc.

How to mark an API surface as experimental

All libraries using @RequiresOptIn annotations must depend on the androidx.annotation:annotation-experimental artifact regardless of whether they are using the androidx or Kotlin annotation. This artifact provides Lint enforcement of experimental usage restrictions for Kotlin callers as well as Java (which the Kotlin annotation doesn‘t handle on its own, since it’s a Kotlin compiler feature). Libraries may include the dependency as api-type to make @OptIn available to Java clients; however, this will also unnecessarily expose the @RequiresOptIn annotation.

dependencies {
    implementation(project(":annotation:annotation-experimental"))
}

See Kotlin‘s opt-in requirements documentation for general usage information. If you are writing experimental Java APIs, you will use the Jetpack @RequiresOptIn annotation rather than the Kotlin compiler’s annotation.

How to transition an API out of experimental

When an API surface is ready to transition out of experimental, the annotation may only be removed during an alpha pre-release stage since removing the experimental marker from an API is equivalent to adding the API to the current API surface.

When transitioning an entire feature surface out of experimental, you should remove the associated annotations.

When making any change to the experimental API surface, you must run ./gradlew updateApi prior to uploading your change.

Restricted APIs

Jetpack's library tooling supports hiding Java-visible (ex. public and protected) APIs from developers using a combination of the @hide docs annotation and @RestrictTo source annotation. These annotations must be paired together when used, and are validated as part of presubmit checks for Java code (Kotlin not yet supported by Checkstyle).

The effects of hiding an API are as follows:

  • The API will not appear in documentation
  • Android Studio will warn the developer not to use the API

Hiding an API does not provide strong guarantees about usage:

  • There are no runtime restrictions on calling hidden APIs
  • Android Studio will not warn if hidden APIs are called using reflection
  • Hidden APIs will still show in Android Studio's auto-complete

When to use @hide

Generally, avoid using @hide. The @hide annotation indicates that developers should not call an API that is technically public from a Java visibility perspective. Hiding APIs is often a sign of a poorly-abstracted API surface, and priority should be given to creating public, maintainable APIs and using Java visibility modifiers.

Do not use @hide to bypass API tracking and review for production APIs; instead, rely on API+1 and API Council review to ensure APIs are reviewed on a timely basis.

Do not use @hide for implementation detail APIs that are used between libraries and could reasonably be made public.

Do use @hide paired with @RestrictTo(LIBRARY) for implementation detail APIs used within a single library (but prefer Java language private or default visibility).

RestrictTo.Scope and inter- versus intra-library API surfaces

To maintain binary compatibility between different versions of libraries, restricted API surfaces that are used between libraries (inter-library APIs) must follow the same Semantic Versioning rules as public APIs. Inter-library APIs should be annotated with the @RestrictTo(LIBRARY_GROUP) source annotation.

Restricted API surfaces used within a single library (intra-library APIs), on the other hand, may be added or removed without any compatibility considerations. It is safe to assume that developers never call these APIs, even though it is technically feasible. Intra-library APIs should be annotated with the @RestrictTo(LIBRARY) source annotation.

The following table shows the visibility of a hypothetical API within Maven coordinate androidx.concurrent:concurrent when annotated with a variety of scopes:

Constructors

View constructors

The four-arg View constructor -- View(Context, AttributeSet, int, int) -- was added in SDK 21 and allows a developer to pass in an explicit default style resource rather than relying on a theme attribute to resolve the default style resource. Because this API was added in SDK 21, care must be taken to ensure that it is not called through any < SDK 21 code path.

Views may implement a four-arg constructor in one of the following ways:

  1. Do not implement.
  2. Implement and annotate with @RequiresApi(21). This means the three-arg constructor must not call into the four-arg constructor.

Asynchronous work

With return values

Traditionally, asynchronous work on Android that results in an output value would use a callback; however, better alternatives exist for libraries.

Kotlin libraries should prefer coroutines and suspend functions, but please refer to the guidance on allowable dependencies before adding a new dependency on coroutines.

Java libraries should prefer ListenableFuture and the CallbackToFutureAdapter implementation provided by the androidx.concurrent:concurrent-futures library.

Libraries must not use java.util.concurrent.CompletableFuture, as it has a large API surface that permits arbitrary mutation of the future's value and has error-prone defaults.

See the Dependencies section for more information on using Kotlin coroutines and Guava in your library.

Avoid synchronized methods

Whenever multiple threads are interacting with shared (mutable) references those reads and writes must be synchronized in some way. However synchronized blocks make your code thread-safe at the expense of concurrent execution. Any time execution enters a synchronized block or method any other thread trying to enter a synchronized block on the same object has to wait; even if in practice the operations are unrelated (e.g. they interact with different fields). This can dramatically reduce the benefit of trying to write multi-threaded code in the first place.

Locking with synchronized is a heavyweight form of ensuring ordering between threads, and there are a number of common APIs and patterns that you can use that are more lightweight, depending on your use case:

  • Compute a value once and make it available to all threads
  • Update Set and Map data structures across threads
  • Allow a group of threads to process a stream of data concurrently
  • Provide instances of a non-thread-safe type to multiple threads
  • Update a value from multiple threads atomically
  • Maintain granular control of your concurrency invariants

Kotlin

Data classes

Kotlin data classes provide a convenient way to define simple container objects, where Kotlin will generate equals() and hashCode() for you. However, they are not designed to preserve API/binary compatibility when members are added. This is due to other methods which are generated for you - destructuring declarations, and copying.

Example data class as tracked by metalava:

Because members are exposed as numbered components for destructuring, you can only safely add members at the end of the member list. As copy is generated with every member name in order as well, you'll also have to manually re-implement any old copy variants as items are added. If these constraints are acceptable, data classes may still be useful to you.

As a result, Kotlin data classes are strongly discouraged in library APIs. Instead, follow best-practices for Java data classes including implementing equals, hashCode, and toString.

See Jake Wharton's article on Public API challenges in Kotlin for more details.

Extension and top-level functions

If your Kotlin file contains any sybmols outside of class-like types (extension/top-level functions, properties, etc), the file must be annotated with @JvmName. This ensures unanticipated use-cases from Java callers don't get stuck using BlahKt files.

Example:

package androidx.example

fun String.foo() = // ...
@file:JvmName("StringUtils")

package androidx.example

fun String.foo() = // ...

NOTE This guideline may be ignored for libraries that only work in Kotlin (think Compose).

Testing Guidelines

Do not Mock, AndroidX

Android Lint Guidelines

Suppression vs Baselines

Lint sometimes flags false positives, even though it is safe to ignore these errors (for example WeakerAccess warnings when you are avoiding synthetic access). There may also be lint failures when your library is in the middle of a beta / rc / stable release, and cannot make the breaking changes needed to fix the root cause. There are two ways of ignoring lint errors:

  1. Suppression - using @SuppressLint (for Java) or @Suppress annotations to ignore the warning per call site, per method, or per file. Note @SuppressLint - Requires Android dependency.
  2. Baselines - allowlisting errors in a lint-baseline.xml file at the root of the project directory.

Where possible, you should use a suppression annotation at the call site. This helps ensure that you are only suppressing the exact failure, and this also keeps the failure visible so it can be fixed later on. Only use a baseline if you are in a Java library without Android dependencies, or when enabling a new lint check, and it is prohibitively expensive / not possible to fix the errors generated by enabling this lint check.

To update a lint baseline (lint-baseline.xml) after you have fixed issues, add -PupdateLintBaseline to the end of your lint command. This will delete and then regenerate the baseline file.

./gradlew core:lintDebug -PupdateLintBaseline

Metalava API Lint

As well as Android Lint, which runs on all source code, Metalava will also run checks on the public API surface of each library. Similar to with Android Lint, there can sometimes be false positives / intended deviations from the API guidelines that Metalava will lint your API surface against. When this happens, you can suppress Metalava API lint issues using @SuppressLint (for Java) or @Suppress annotations. In cases where it is not possible, update Metalava's baseline with the updateApiLintBaseline task.

./gradlew core:updateApiLintBaseline

This will create/amend the api_lint.ignore file that lives in a library's api directory.

Build Output Guidelines

In order to more easily identify the root cause of build failures, we want to keep the amount of output generated by a successful build to a minimum. Consequently, we track build output similarly to the way in which we track Lint warnings.

Invoking build output validation

You can add -Pandroidx.validateNoUnrecognizedMessages to any other AndroidX gradlew command to enable validation of build output. For example:

/gradlew -Pandroidx.validateNoUnrecognizedMessages :help

Exempting new build output messages

Please avoid exempting new build output and instead fix or suppress the warnings themselves, because that will take effect not only on the build server but also in Android Studio, and will also run more quickly.

If you cannot prevent the message from being generating and must exempt the message anyway, follow the instructions in the error:

$ ./gradlew -Pandroidx.validateNoUnrecognizedMessages :help

Error: build_log_simplifier.py found 15 new messages found in /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.

Please fix or suppress these new messages in the tool that generates them.
If you cannot, then you can exempt them by doing:

  1. cp /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.ignore /usr/local/google/workspace/aosp-androidx-git/frameworks/support/development/build_log_simplifier/messages.ignore
  2. modify the new lines to be appropriately generalized

Each line in this exemptions file is a regular expressing matching one or more lines of output to be exempted. You may want to make these expressions as specific as possible to ensure that the addition of new, similar messages will also be detected (for example, discovering an existing warning in a new source file).

Behavior changes

Changes that affect API documentation

Do not make behavior changes that require altering API documentation in a way that would break existing clients, even if such changes are technically binary compatible. For example, changing the meaning of a method's return value to return true rather than false in a given state would be considered a breaking change. Because this change is binary-compatible, it will not be caught by tooling and is effectively invisible to clients.

Instead, add new methods and deprecate the existing ones if necessary, noting behavior changes in the deprecation message.

High-risk behavior changes

Behavior changes that conform to documented API contracts but are highly complex and difficult to comprehensively test are considered high-risk and should be implemented using behavior flags. These changes may be flagged on initially, but the original behaviors must be preserved until the library enters release candidate stage and the behavior changes have been appropriately verified by integration testing against public pre-release revisions.

It may be necessary to soft-revert a high-risk behavior change with only 24-hour notice, which should be achievable by flipping the behavior flag to off.

[example code pending]

Avoid adding multiple high-risk changes during a feature cycle, as verifying the interaction of multiple feature flags leads to unnecessary complexity and exposes clients to high risk even when a single change is flagged off. Instead, wait until one high-risk change has landed in RC before moving on to the next.

Testing

Relevant tests should be run for the behavior change in both the on and off flagged states to prevent regressions.

Sample code in Kotlin modules

Background

Public API can (and should!) have small corresponding code snippets that demonstrate functionality and usage of a particular API. These are often exposed inline in the documentation for the function / class - this causes consistency and correctness issues as this code is not compiled against, and the underlying implementation can easily change.

KDoc (JavaDoc for Kotlin) supports a @sample tag, which allows referencing the body of a function from documentation. This means that code samples can be just written as a normal function, compiled and linted against, and reused from other modules such as tests! This allows for some guarantees on the correctness of a sample, and ensuring that it is always kept up to date.

Enforcement

There are still some visibility issues here - it can be hard to tell if a function is a sample, and is used from public documentation - so as a result we have lint checks to ensure sample correctness.

Primarily, there are three requirements when using sample links:

  1. All functions linked to from a @sample KDoc tag must be annotated with @Sampled
  2. All sample functions annotated with @Sampled must be linked to from a @sample KDoc tag
  3. All sample functions must live inside a separate samples library submodule - see the section on module configuration below for more information.

This enforces visibility guarantees, and make it easier to know that a sample is a sample. This also prevents orphaned samples that aren't used, and remain unmaintained and outdated.

Sample usage

The follow demonstrates how to reference sample functions from public API. It is also recommended to reuse these samples in unit tests / integration tests / test apps / library demos where possible.

Public API:

/*
 * Fancy prints the given [string]
 *
 * @sample androidx.printer.samples.fancySample
 */
fun fancyPrint(str: String) ...

Sample function:

package androidx.printer.samples

import androidx.printer.fancyPrint

@Sampled
fun fancySample() {
   fancyPrint("Fancy!")
}

Generated documentation visible on d.android.com*

fun fancyPrint(str: String)

Fancy prints the given [string]

<code>
 import androidx.printer.fancyPrint

 fancyPrint("Fancy!")
<code>

*still some improvements to be made to DAC side, such as syntax highlighting

Module configuration

The following module setups should be used for sample functions, and are enforced by lint:

Group-level samples

For library groups with strongly related samples that want to share code.

Gradle project name: :foo-library:samples

foo-library/
  foo-module/
  bar-module/
  samples/

Per-module samples

For library groups with complex, relatively independent sub-libraries

Gradle project name: :foo-library:foo-module:samples

foo-library/
  foo-module/
    samples/