Evolution of Android platform APIs

Policies around what types of changes may be made to existing Android APIs and how those changes should be implemented to maximize compatibility with existing apps and codebases.

Binary-breaking changes

Binary-breaking changes are not allowed in finalized public API and will generally raise errors when running make update-api. There may, however, be edge cases that are not caught by Metalava’s API check. When in doubt, refer to the Eclipse Foundation’s Evolving Java-based APIs guide for a detailed explanation of what types of API changes are compatible in Java. Binary-breaking changes in non-public (ex. system) APIs should follow the deprecate/replace cycle.

Source-breaking changes

Source-breaking changes are discouraged even if they are not binary-breaking. One example of a binary-compatible but source-breaking change is adding a generic to an existing class, which is binary-compatible but may introduce compilation errors due to inheritance or ambiguous references. Source-breaking changes will not raise errors when running make update-api, so care must be taken to understand the impact of changes to existing API signatures.

In some cases, source-breaking changes are necessary to improve the developer experience or code correctness. For example, adding nullability annotations to Java sources improves interopability with Kotlin code and reduces the likelihood of errors, but often requires changes -- sometimes significant changes -- to source code.

Changes to private APIs (@SystemApi, @TestApi)

APIs annotated with @TestApi may be changed at any time.

APIs annotated with @SystemApi must be preserved for three years. Removal or refactoring of a system API must occur on the following schedule:

  • API y - Added
  • API y+1 - Deprecation
    • Mark the code as @Deprecated
    • Add replacements, and link to the replacement in the javadoc for the deprecated code using the @deprecated tag.
    • Mid-development-cycle, file bugs against internal users telling them API is going away, giving them a chance to ensure replacement APIs are adequate.
  • API y+2 - Soft removal
    • Mark code as @removed
    • Optionally, throw or no-op for apps that target the current sdk level for the release
  • API y+3 - Hard removal
    • Code is completely removed from source tree

Deprecation

Deprecation is considered an API change and may occur in a major (e.g. letter) release. Use the @Deprecated source annotation and @deprecated <summary> docs annotation together when deprecating APIs. Your summary must include a migration strategy, which may link to a replacement API or explain why the API should not be used.

/**
 * Simple version of ...
 *
 * @deprecated Use the {@link androidx.fragment.app.DialogFragment}
 *             class with {@link androidx.fragment.app.FragmentManager}
 *             instead.
 */
@Deprecated
public final void showDialog(int id)

APIs defined in XML and exposed in Java, including attributes and styleable properties exposed in the android.R class, must also be deprecated with a summary.

<!-- Attribute whether the accessibility service ...
     {@deprecated Not used by the framework}
 -->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />

When is it appropriate to deprecate an API?

Deprecations are most useful for discouraging adoption of an API in new code.

We also require that APIs are marked as @deprecated before they are @removed, but this does not provide strong motivation for developers to migrate away from an API they are already using.

Before deprecating an API, consider the impact on developers. The effects of deprecating an API include:

  • javac will emit a warning during compilation
    • Deprecation warnings cannot be suppressed globally or baselined, so developers using -Werror will need to individually fix or suppress every usage of a deprecated API before they can update their compile SDK version
    • Deprecation warnings on imports of deprecated classes cannot be suppressed, so developers will need to inline the fully-qualified classname for every usage of a deprecated class before they can update their compile SDK version
  • Documentation on d.android.com will show a deprecation notice
  • IDEs like Android Studio will show a warning at the API usage site
  • IDEs may down-rank or hide the API from auto-complete

As a result, deprecating an API may discourage the developers who are the most concerned about code health -- those using -Werror -- from adopting new SDKs. At the other end of the spectrum, developers who are not concerned about warnings in their existing code are likely to ignore deprecations altogether.

Both of these cases are made worse when an SDK introduces a large number of deprecations.

For this reason, we recommend deprecating APIs only in cases where:

  1. The API will be @removed in a future release
  2. Usage of the API leads to incorrect or undefined behavior that cannot be fixed without breaking compatibility

When an API is deprecated and replaced with a new API, we strongly recommend that a corresponding compatibility API be added to a Jetpack library like androidx.core to simplify supporting both old and new devices.

We do not recommend deprecating APIs that are working as intended and will continue to work in future releases.

/**
 * ...
 * @deprecated Use {@link #doThing(int, Bundle)} instead.
 */
@Deprecated
public void doThing(int action) {
  ...
}

public void doThing(int action, @Nullable Bundle extras) {
  ...
}
/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

Changes to deprecated APIs

The behavior of deprecated APIs must be maintained, which means test implementations must remain the same and tests must continue to pass after the API has been deprecated. If the API does not have tests, tests should be added.

Deprecated API surfaces should not be expanded in future releases. Lint correctness annotations (ex. @Nullable) may be added to an existing deprecated API, but new APIs should not be added to deprecated classes or interfaces.

New APIs should not be added as deprecated. APIs that were added and subsequently deprecated within a pre-release cycle -- thus would initially enter the public API surface as deprecated -- should be removed before API finalization.

Soft removal

Soft removal is a source-breaking change and should be avoided in public APIs unless explicitly approved by API Council. For approval, email the Android API Council mailing list. For system APIs, soft removals must be preceded by deprecation for the duration of a major release. Remove all docs references to the APIs and use the `@removed

The behavior of soft-removed APIs may be maintained as-is but more importantly must be preserved such that existing callers will not crash when calling the API. In some cases, that may mean preserving behavior.

Test coverage must be maintained, but the content of the tests may need to change to accomodate for behavioral changes. Tests must still ensure that existing callers do not crash at run time.

/**
 * Ringer volume. This is ...
 *
 * @removed Not functional since API 2.
 */
public static final String VOLUME_RING = ...

At a technical level, the API is removed from the SDK stub JAR and compile-time classpath but still exists on the run-time classpath -- similar to @hide APIs.

From an app developer perspective, the API no longer appears in auto-complete and source code that references the API will no longer compile when the compileSdk is equal to or later than the SDK at which the API was removed; however, source code will continue to compile successfully against earlier SDKs and binaries that reference the API will continue to work.

Certain categories of API must not be soft removed.

Abstract methods

Abstract methods on classes that may be extended by developers must not be soft removed. Doing so will make it impossible for developers to successfully extend the class across all SDK levels.

In rare cases where it was never and will never be possible for developers to extend a class, abstract methods may still be soft removed.

Hard removal

Hard removal is a binary-breaking change and should never occur in public APIs. For system APIs, hard removals must be preceded by soft removal for the duration of a major release. Remove the entire implementation when hard-removing APIs.

Tests for hard-removed APIs must be removed since they will no longer compile otherwise.

Discouraging {.numbered}

The @Discouraged annotation is used to indicate that an API is not recommended in most (>95%) cases. Discouraged APIs differ from deprecated APIs in that there exists a narrow critical use case that prevents deprecation. When marking an API as discouraged, an explanation and an alternative solution must be provided.

@Discouraged(message = "Use of this function is discouraged because resource
                        reflection makes it harder to perform build
                        optimizations and compile-time verification of code. It
                        is much more efficient to retrieve resources by
                        identifier (e.g. `R.foo.bar`) than by name (e.g.
                        `getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

New APIs should not be added as discouraged. Currently, only the Performance team can discourage an API.

Changing behavior of existing APIs

In some cases it can be desirable to change the implementation behavior of an existing API. For example, in Android 7.0 we improved DropBoxManager to clearly communicate when developers tried posting events that were too large to send across Binder.

However, to ensure that existing apps aren‘t surprised by these behavior changes, we strongly recommend preserving a safe behavior for older applications. We’ve historically guarded these behavior changes based on the ApplicationInfo.targetSdkVersion of the app, but we‘ve recently migrated to require using the App Compatibility Framework. Here’s an example of how to implement a behavior change using this new framework:

import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;

public class MyClass {
  @ChangeId
  // This means the change will be enabled for target SDK R and higher.
  @EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
  // Use a bug number as the value, provide extra detail in the bug.
  // FOO_NOW_DOES_X will be the change name, and 123456789 the change id.
  static final long FOO_NOW_DOES_X = 123456789L;

  public void doFoo() {
    if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
      // do the new thing
    } else {
      // do the old thing
    }
  }
}

Using this App Compatibility Framework design enables developers to temporarily disable specific behavior changes during preview and beta releases as part of debugging their apps, instead of forcing them to adjust to dozens of behavior changes simultaneously.

Forward compatibility

Forward compatibility is a design characteristic that allows a system to accept input intended for a later version of itself. In the case of API design -- especially platform APIs -- special attention must be paid to the initial design as well as future changes since developers expect to write code once, test it once, and have it run everywhere without issue.

The most common forward compatibility issues in Android are caused by:

  • Adding new constants to a set (e.g. @IntDef or enum) previously assumed to be complete, e.g. where switch has a default that throws an exception
  • Adding support for a feature that is not captured directly in the API surface, e.g. support for assigning ColorStateList-type resources in XML where previously only <color> resources were supported
  • Loosening restrictions on run-time checks, e.g. removing a requireNotNull() check that was present on older versions

In all of these cases, developers will only find out that something is wrong at run time. Worse, they may only find out as a result of crash reports from older devices in the field.

Additionally, these cases are all technically valid API changes. They do not break binary or source compatibility and API lint will not catch any of these issues.

As a result, API designers must pay careful attention when modifying existing classes. Ask the question, “Is this change going to cause code that's written and tested only against the latest version of the platform to fail on older versions?”