Consignes concernant les API Android

Cette page est destinée à servir de guide aux développeurs pour comprendre les principes généraux que le Conseil des API applique lors des examens d'API.

En plus de suivre ces consignes lors de l'écriture d'API, les développeurs doivent exécuter l'outil API Lint, qui encode un grand nombre de ces règles dans des vérifications qu'il exécute sur les API.

Considérez-le comme le guide des règles respectées par cet outil Lint, ainsi que des conseils généraux sur les règles qui ne peuvent pas être codifiées dans cet outil avec une grande précision.

Outil API Lint

API Lint est intégré à l'outil d'analyse statique Metalava et s'exécute automatiquement lors de la validation dans CI. Vous pouvez l'exécuter manuellement à partir d'un checkout de plate-forme local à l'aide de m checkapi ou d'un checkout AndroidX local à l'aide de ./gradlew :path:to:project:checkApi.

Règles relatives aux API

La plate-forme Android et de nombreuses bibliothèques Jetpack existaient avant la création de cet ensemble de consignes. Les règles énoncées plus loin sur cette page évoluent constamment pour répondre aux besoins de l'écosystème Android.

Par conséquent, il est possible que certaines API existantes ne respectent pas les consignes. Dans d'autres cas, il peut être préférable pour les développeurs d'applications qu'une nouvelle API reste cohérente avec les API existantes plutôt que de respecter strictement les consignes.

Faites preuve de discernement et contactez le Conseil des API si vous avez des questions difficiles à résoudre concernant une API ou si des consignes doivent être mises à jour.

Principes de base des API

Cette catégorie concerne les aspects fondamentaux d'une API Android.

Toutes les API doivent être implémentées

Quelle que soit l'audience d'une API (par exemple, publique ou @SystemApi), toutes les surfaces d'API doivent être implémentées lorsqu'elles sont fusionnées ou exposées en tant qu'API. Ne fusionnez pas les stubs d'API avec l'implémentation, qui sera effectuée ultérieurement.

Les surfaces d'API sans implémentation posent plusieurs problèmes :

  • Il n'est pas garanti qu'une surface appropriée ou complète ait été exposée. Tant qu'une API n'est pas testée ni utilisée par des clients, il est impossible de vérifier qu'un client dispose des API appropriées pour pouvoir utiliser la fonctionnalité.
  • Les API sans implémentation ne peuvent pas être testées dans les versions Preview développeur.
  • Les API sans implémentation ne peuvent pas être testées dans CTS.

Toutes les API doivent être testées

Cela correspond aux exigences CTS de la plate-forme, aux règles AndroidX et, de manière générale, à l'idée que les API doivent être implémentées.

Tester les surfaces d'API permet de garantir que la surface d'API est utilisable et que nous avons traité les cas d'utilisation attendus. Il ne suffit pas de tester l'existence de l'API, mais aussi son comportement.

Toute modification ajoutant une API doit inclure les tests correspondants dans le même CL ou sujet Gerrit.

Les API doivent également être testables. Vous devriez être en mesure de répondre à la question "Comment un développeur d'applications va-t-il tester le code qui utilise votre API ?".

Toutes les API doivent être documentées

La documentation est un élément clé de l'utilisabilité des API. Bien que la syntaxe d'une surface d'API puisse sembler évidente, les nouveaux clients ne comprendront pas la sémantique, le comportement ni le contexte de l'API.

Toutes les API générées doivent respecter les consignes.

Les API générées par des outils doivent respecter les mêmes consignes que le code écrit à la main.

Outils déconseillés pour générer des API :

  • AutoValue : ne respecte pas les consignes de différentes manières. Par exemple, il n'existe aucun moyen d'implémenter des classes de valeurs finales ni des compilateurs finaux avec le fonctionnement d'AutoValue.

Style de code

Cette catégorie concerne le style de code général que les développeurs doivent utiliser, en particulier lorsqu'ils écrivent des API publiques.

Suivez les conventions de codage standards, sauf indication contraire.

Les conventions de codage Android sont documentées pour les contributeurs externes ici :

https://source.android.com/source/code-style.html

En général, nous avons tendance à suivre les conventions de programmation Java et Kotlin standards.

Les acronymes ne doivent pas être mis en majuscules dans les noms de méthodes

Par exemple, le nom de la méthode doit être runCtsTests et non runCTSTests.

Les noms ne doivent pas se terminer par "Impl"

Cela expose des détails d'implémentation. Évitez cela.

Classes

Cette section décrit les règles concernant les classes, les interfaces et l'héritage.

Hériter de nouvelles classes publiques à partir de la classe de base appropriée

L'héritage expose dans votre sous-classe des éléments d'API qui ne sont pas forcément appropriés. Par exemple, une nouvelle sous-classe publique de FrameLayout ressemble à FrameLayout, plus les nouveaux comportements et éléments d'API. Si cette API héritée ne convient pas à votre cas d'utilisation, héritez d'une classe plus haut dans l'arborescence, par exemple ViewGroup ou View.

Si vous êtes tenté de remplacer les méthodes de la classe de base pour générer UnsupportedOperationException, réfléchissez à la classe de base que vous utilisez.

Utiliser les classes de collections de base

Que vous preniez une collection comme argument ou que vous la renvoyiez comme valeur, préférez toujours la classe de base à l'implémentation spécifique (par exemple, renvoyez List<Foo> plutôt que ArrayList<Foo>).

Utilisez une classe de base qui exprime les contraintes appropriées pour l'API. Par exemple, utilisez List pour une API dont la collection doit être ordonnée et Set pour une API dont la collection doit être constituée d'éléments uniques.

En Kotlin, préférez les collections immuables. Pour en savoir plus, consultez Modifiabilité des collections.

Classes abstraites et interfaces

Java 8 ajoute la compatibilité avec les méthodes d'interface par défaut, ce qui permet aux concepteurs d'API d'ajouter des méthodes aux interfaces tout en conservant la compatibilité binaire. Le code de plate-forme et toutes les bibliothèques Jetpack doivent cibler Java 8 ou version ultérieure.

Dans les cas où l'implémentation par défaut est sans état, les concepteurs d'API doivent préférer les interfaces aux classes abstraites. Autrement dit, les méthodes d'interface par défaut peuvent être implémentées en tant qu'appels à d'autres méthodes d'interface.

Dans les cas où un constructeur ou un état interne sont requis par l'implémentation par défaut, des classes abstraites doivent être utilisées.

Dans les deux cas, les concepteurs d'API peuvent choisir de laisser une seule méthode abstraite pour simplifier l'utilisation en tant que lambda :

public interface AnimationEndCallback {
  // Always called, must be implemented.
  public void onFinished(Animation anim);
  // Optional callbacks.
  public default void onStopped(Animation anim) { }
  public default void onCanceled(Animation anim) { }
}

Les noms de classe doivent refléter ce qu'ils étendent.

Par exemple, les classes qui étendent Service doivent être nommées FooService pour plus de clarté :

public class IntentHelper extends Service {}
public class IntentService extends Service {}

Suffixes génériques

Évitez d'utiliser des suffixes de nom de classe génériques tels que Helper et Util pour les collections de méthodes utilitaires. Placez plutôt les méthodes directement dans les classes associées ou dans des fonctions d'extension Kotlin.

Dans les cas où les méthodes font le lien entre plusieurs classes, donnez à la classe conteneur un nom explicite qui explique ce qu'elle fait.

Dans de très rares cas, l'utilisation du suffixe Helper peut être appropriée :

  • Utilisé pour la composition du comportement par défaut
  • Peut impliquer la délégation du comportement existant à de nouvelles classes
  • Peut nécessiter un état persistant
  • Implique généralement View

Par exemple, si les info-bulles de rétroportage nécessitent de conserver l'état associé à un View et d'appeler plusieurs méthodes sur le View pour installer le rétroportage, TooltipHelper serait un nom de classe acceptable.

N'exposez pas directement le code généré par IDL en tant qu'API publiques.

Conservez le code généré par IDL comme détails d'implémentation. Cela inclut protobuf, les sockets, FlatBuffers ou toute autre surface d'API non Java et non NDK. Toutefois, la plupart des IDL dans Android sont en AIDL. Cette page se concentre donc sur AIDL.

Les classes AIDL générées ne répondent pas aux exigences du guide de style de l'API (par exemple, elles ne peuvent pas utiliser la surcharge). De plus, l'outil AIDL n'est pas explicitement conçu pour maintenir la compatibilité de l'API du langage. Vous ne pouvez donc pas les intégrer dans une API publique.

Ajoutez plutôt une couche d'API publique au-dessus de l'interface AIDL, même s'il s'agit initialement d'un wrapper superficiel.

Interfaces Binder

Si l'interface Binder est un détail d'implémentation, elle peut être modifiée librement à l'avenir, la couche publique permettant de maintenir la rétrocompatibilité requise. Par exemple, vous devrez peut-être ajouter de nouveaux arguments aux appels internes ou optimiser le trafic IPC en utilisant le traitement par lot ou le streaming, en utilisant la mémoire partagée, etc. Aucune de ces opérations n'est possible si votre interface AIDL est également l'API publique.

Par exemple, n'exposez pas FooService directement en tant qu'API publique :

// BAD: Public API generated from IFooService.aidl
public class IFooService {
   public void doFoo(String foo);
}

En revanche, encapsulez l'interface Binder dans un gestionnaire ou une autre classe :

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo);
}

public IFooManager {
   public void doFoo(String foo) {
      mFooService.doFoo(foo);
   }
}

Si un nouvel argument est nécessaire pour cet appel, l'interface interne peut être minimale et des surcharges pratiques peuvent être ajoutées à l'API publique. Vous pouvez utiliser le calque d'encapsulation pour gérer d'autres problèmes de rétrocompatibilité à mesure que l'implémentation évolue :

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo, int flags);
}

public IFooManager {
   public void doFoo(String foo) {
      if (mAppTargetSdkLevel < 26) {
         useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
         mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
      } else {
         mFooService.doFoo(foo, 0);
      }
   }

   public void doFoo(String foo, int flags) {
      mFooService.doFoo(foo, flags);
   }
}

Pour les interfaces Binder qui ne font pas partie de la plate-forme Android (par exemple, une interface de service exportée par les services Google Play pour que les applications l'utilisent), l'exigence d'une interface IPC stable, publiée et versionnée signifie qu'il est beaucoup plus difficile de faire évoluer l'interface elle-même. Toutefois, il est toujours utile d'avoir une couche wrapper autour de celle-ci, pour correspondre aux autres consignes de l'API et pour faciliter l'utilisation de la même API publique pour une nouvelle version de l'interface IPC, si cela s'avère nécessaire.

Ne pas utiliser d'objets Binder bruts dans l'API publique

Un objet Binder n'a aucun sens en soi et ne doit donc pas être utilisé dans une API publique. Un cas d'utilisation courant consiste à utiliser un Binder ou un IBinder comme jeton, car il possède une sémantique d'identité. Au lieu d'utiliser un objet Binder brut, utilisez plutôt une classe de jeton wrapper.

public final class IdentifiableObject {
  public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
  /**
   * @hide
   */
  public Binder getRawValue() {...}

  /**
   * @hide
   */
  public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}

public final class IdentifiableObject {
  public IdentifiableObjectToken getToken() {...}
}

Les classes de gestionnaire doivent être définitives

Les classes de gestionnaire doivent être déclarées comme final. Les classes de gestionnaire communiquent avec les services système et constituent le point d'interaction unique. Comme aucune personnalisation n'est nécessaire, déclarez-le comme final.

Ne pas utiliser CompletableFuture ni Future

java.util.concurrent.CompletableFuture dispose d'une grande surface d'API qui permet une mutation arbitraire de la valeur future et comporte des valeurs par défaut sujettes aux erreurs.

À l'inverse, java.util.concurrent.Future ne dispose pas d'écoute non bloquante, ce qui le rend difficile à utiliser avec du code asynchrone.

Dans le code de plate-forme et les API de bibliothèque de bas niveau utilisées par Kotlin et Java, préférez une combinaison d'un rappel d'achèvement, Executor, et si l'API est compatible avec l'annulation CancellationSignal.

public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
    Executor callbackExecutor,
    android.os.OutcomeReceiver<FooResult, Throwable> callback);

Si vous ciblez Kotlin, préférez les fonctions suspend.

suspend fun asyncLoadFoo(): Foo

Dans les bibliothèques d'intégration spécifiques à Java, vous pouvez utiliser ListenableFuture de Guava.

public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();

Ne pas utiliser "Optional"

Bien que Optional puisse présenter des avantages dans certaines surfaces d'API, il est incompatible avec la surface d'API Android existante. @Nullable et @NonNull fournissent une assistance pour l'outillage de sécurité null, et Kotlin applique les contrats de possibilité de valeur nulle au niveau du compilateur, ce qui rend Optional inutile.

Pour les primitives facultatives, utilisez les méthodes has et get associées. Si la valeur n'est pas définie (has renvoie false), la méthode get doit générer une IllegalStateException.

public boolean hasAzimuth() { ... }
public int getAzimuth() {
  if (!hasAzimuth()) {
    throw new IllegalStateException("azimuth is not set");
  }
  return azimuth;
}

Utiliser des constructeurs privés pour les classes non instanciables

Les classes qui ne peuvent être créées que par des Builder, les classes qui ne contiennent que des constantes ou des méthodes statiques, ou les classes non instanciables doivent inclure au moins un constructeur privé pour empêcher l'instanciation à l'aide du constructeur sans argument par défaut.

public final class Log {
  // Not instantiable.
  private Log() {}
}

Singletons

Les singletons sont déconseillés, car ils présentent les inconvénients suivants en termes de tests :

  1. La construction est gérée par la classe, ce qui empêche l'utilisation de faux
  2. Les tests ne peuvent pas être hermétiques en raison de la nature statique d'un singleton.
  3. Pour contourner ces problèmes, les développeurs doivent connaître les détails internes du singleton ou créer un wrapper autour de celui-ci.

Préférez le modèle instance unique, qui s'appuie sur une classe de base abstraite pour résoudre ces problèmes.

Instance unique

Les classes à instance unique utilisent une classe de base abstraite avec un constructeur private ou internal et fournissent une méthode getInstance() statique pour obtenir une instance. La méthode getInstance() doit renvoyer le même objet lors des appels suivants.

L'objet renvoyé par getInstance() doit être une implémentation privée de la classe de base abstraite.

class Singleton private constructor(...) {
  companion object {
    private val _instance: Singleton by lazy { Singleton(...) }

    fun getInstance(): Singleton {
      return _instance
    }
  }
}
abstract class SingleInstance private constructor(...) {
  companion object {
    private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
    fun getInstance(): SingleInstance {
      return _instance
    }
  }
}

L'instance unique diffère du singleton en ce que les développeurs peuvent créer une version factice de SingleInstance et utiliser leur propre framework d'injection de dépendances pour gérer l'implémentation sans avoir à créer de wrapper. La bibliothèque peut également fournir son propre faux dans un artefact -testing.

Les classes qui libèrent des ressources doivent implémenter AutoCloseable

Les classes qui libèrent des ressources via close, release, destroy ou des méthodes similaires doivent implémenter java.lang.AutoCloseable pour permettre aux développeurs de nettoyer automatiquement ces ressources lorsqu'ils utilisent un bloc try-with-resources.

Évitez d'introduire de nouvelles sous-classes View dans android.*.

N'introduisez pas de nouvelles classes qui héritent directement ou indirectement de android.view.View dans l'API publique de la plate-forme (c'est-à-dire dans android.*).

Le kit d'outils d'UI d'Android est désormais Compose-first. Les nouvelles fonctionnalités d'UI exposées par la plate-forme doivent être exposées en tant qu'API de niveau inférieur pouvant être utilisées pour implémenter des composants d'UI basés sur Jetpack Compose et éventuellement sur des vues pour les développeurs dans les bibliothèques Jetpack. Proposer ces composants dans des bibliothèques permet d'implémenter des versions antérieures lorsque les fonctionnalités de la plate-forme ne sont pas disponibles.

Champs

Ces règles concernent les champs publics des classes.

Ne pas exposer les champs bruts

Les classes Java ne doivent pas exposer directement les champs. Les champs doivent être privés et accessibles uniquement à l'aide de getters et de setters publics, qu'ils soient finaux ou non.

Les exceptions rares incluent les structures de données de base pour lesquelles il n'est pas nécessaire d'améliorer le comportement de spécification ou de récupération d'un champ. Dans ce cas, les champs doivent être nommés en utilisant des conventions d'attribution de noms de variables standards, par exemple Point.x et Point.y.

Les classes Kotlin peuvent exposer des propriétés.

Les champs exposés doivent être marqués comme finaux

Il est fortement déconseillé d'utiliser des champs bruts (@see N'exposez pas les champs bruts). Toutefois, dans le cas rare où un champ est exposé en tant que champ public, marquez-le final.

Les champs internes ne doivent pas être exposés

Ne référencez pas les noms de champs internes dans l'API publique.

public int mFlags;

Utiliser "Public" au lieu de "Protégé"

@see Utiliser "public" au lieu de "protected"

Constantes

Il s'agit de règles concernant les constantes publiques.

Les constantes d'indicateur ne doivent pas chevaucher les valeurs int ou long

Le terme flags (indicateurs) implique des bits qui peuvent être combinés en une valeur d'union. Si ce n'est pas le cas, n'appelez pas la variable ni la constante flag.

public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;

Pour en savoir plus sur la définition des constantes d'indicateurs publics, consultez @IntDef pour les indicateurs de masque de bits.

Les constantes statiques finales doivent utiliser une convention de dénomination en majuscules et séparées par des tirets bas.

Tous les mots de la constante doivent être en majuscules et les mots multiples doivent être séparés par _. Exemple :

public static final int fooThing = 5
public static final int FOO_THING = 5

Utiliser des préfixes standards pour les constantes

De nombreuses constantes utilisées dans Android concernent des éléments standards, tels que des indicateurs, des clés et des actions. Ces constantes doivent comporter des préfixes standards pour être plus facilement identifiables.

Par exemple, les extras d'intent doivent commencer par EXTRA_. Les actions d'intent doivent commencer par ACTION_. Les constantes utilisées avec Context.bindService() doivent commencer par BIND_.

Noms et portées des constantes clés

Les valeurs constantes de chaîne doivent être cohérentes avec le nom de la constante elle-même et doivent généralement être limitées au package ou au domaine. Exemple :

public static final String FOO_THING = "foo"

n'est pas nommé de manière cohérente ni correctement défini. Voici quelques exemples :

public static final String FOO_THING = "android.fooservice.FOO_THING"

Les préfixes android dans les constantes de chaîne à portée limitée sont réservés au projet Android Open Source.

Les actions et les extras d'intent, ainsi que les entrées de Bundle, doivent être associés à un espace de noms à l'aide du nom du package dans lequel ils sont définis.

package android.foo.bar {
  public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
  public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}

Utiliser "Public" au lieu de "Protégé"

@see Utiliser "public" au lieu de "protected"

Utiliser des préfixes cohérents

Toutes les constantes associées doivent commencer par le même préfixe. Par exemple, pour un ensemble de constantes à utiliser avec les valeurs d'indicateur :

public static final int SOME_VALUE = 0x01;

public static final int SOME_OTHER_VALUE = 0x10;

public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;

public static final int FLAG_SOME_OTHER_VALUE = 0x10;

public static final int FLAG_SOME_THIRD_VALUE = 0x100;

@see Utiliser des préfixes standards pour les constantes

Utiliser des noms de ressources cohérents

Les identifiants, attributs et valeurs publics doivent être nommés à l'aide de la convention de dénomination camelCase, par exemple @id/accessibilityActionPageUp ou @attr/textAppearance, comme les champs publics en Java.

Dans certains cas, un identifiant ou un attribut public inclut un préfixe commun séparé par un trait de soulignement :

  • Valeurs de configuration de la plate-forme telles que @string/config_recentsComponentName dans config.xml
  • Attributs de vue spécifiques à la mise en page, tels que @attr/layout_marginStart dans attrs.xml

Les thèmes et styles publics doivent suivre la convention de dénomination hiérarchique PascalCase, par exemple @style/Theme.Material.Light.DarkActionBar ou @style/Widget.Material.SearchView.ActionBar, semblable aux classes imbriquées en Java.

Les ressources de mise en page et de dessinables ne doivent pas être exposées en tant qu'API publiques. Toutefois, s'ils doivent être exposés, les mises en page et les drawables publics doivent être nommés à l'aide de la convention de dénomination under_score, par exemple layout/simple_list_item_1.xml ou drawable/title_bar_tall.xml.

Rendez dynamiques les constantes susceptibles de changer

Le compilateur peut insérer des valeurs constantes, de sorte que le maintien des valeurs est considéré comme faisant partie du contrat d'API. Si la valeur d'une constante MIN_FOO ou MAX_FOO est susceptible de changer à l'avenir, envisagez de les transformer en méthodes dynamiques.

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

Tenir compte de la compatibilité ascendante pour les rappels

Les constantes définies dans les futures versions de l'API ne sont pas connues des applications qui ciblent des API plus anciennes. Pour cette raison, les constantes fournies aux applications doivent tenir compte de la version de l'API cible de l'application et mapper les constantes plus récentes sur une valeur cohérente. Prenons l'exemple suivant :

Source SDK hypothétique :

// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;

Application hypothétique avec targetSdkVersion="22" :

if (result == STATUS_FAILURE) {
  // Oh no!
} else {
  // Success!
}

Dans ce cas, l'application a été conçue dans les limites du niveau d'API 22 et a fait l'hypothèse (plus ou moins) raisonnable qu'il n'y avait que deux états possibles. Toutefois, si l'application reçoit le STATUS_FAILURE_RETRY nouvellement ajouté, elle l'interprète comme un succès.

Les méthodes qui renvoient des constantes peuvent gérer ces cas de manière sécurisée en limitant leur sortie pour qu'elle corresponde au niveau d'API ciblé par l'application :

private int mapResultForTargetSdk(Context context, int result) {
  int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
  if (targetSdkVersion < 26) {
    if (result == STATUS_FAILURE_ABORT) {
      return STATUS_FAILURE;
    }
    if (targetSdkVersion < 23) {
      if (result == STATUS_FAILURE_RETRY) {
        return STATUS_FAILURE;
      }
    }
  }
  return result;
}

Les développeurs ne peuvent pas anticiper si une liste de constantes peut changer à l'avenir. Si vous définissez une API avec une constante UNKNOWN ou UNSPECIFIED qui ressemble à un caractère générique, les développeurs supposent que les constantes publiées au moment où ils ont écrit leur application sont exhaustives. Si vous ne souhaitez pas définir cette attente, réfléchissez à nouveau à l'opportunité d'utiliser une constante générique pour votre API.

De plus, les bibliothèques ne peuvent pas spécifier leur propre fichier targetSdkVersion distinct de l'application, et la gestion des modifications de comportement de targetSdkVersion à partir du code de la bibliothèque est complexe et sujette aux erreurs.

Constante entière ou de chaîne

Utilisez des constantes entières et @IntDef si l'espace de noms pour les valeurs n'est pas extensible en dehors de votre package. Utilisez des constantes de chaîne si l'espace de noms est partagé ou peut être étendu par du code en dehors de votre package.

Classes de données

Les classes de données représentent un ensemble de propriétés immuables et fournissent un ensemble de fonctions utilitaires petit et bien défini pour interagir avec ces données.

N'utilisez pas data class dans les API Kotlin publiques, car le compilateur Kotlin ne garantit pas la compatibilité binaire ni celle de l'API de langage pour le code généré. Implémentez plutôt manuellement les fonctions requises.

Instanciation

En Java, les classes de données doivent fournir un constructeur lorsqu'il y a peu de propriétés ou utiliser le modèle Builder lorsqu'il y a de nombreuses propriétés.

En Kotlin, les classes de données doivent fournir un constructeur avec des arguments par défaut, quel que soit le nombre de propriétés. Les classes de données définies en Kotlin peuvent également bénéficier de la fourniture d'un compilateur lors du ciblage de clients Java.

Modification et copie

Si les données doivent être modifiées, fournissez une classe Builder avec un constructeur de copie (Java) ou une fonction membre copy() (Kotlin) qui renvoie un nouvel objet.

Lorsque vous fournissez une fonction copy() en Kotlin, les arguments doivent correspondre au constructeur de la classe et les valeurs par défaut doivent être renseignées à l'aide des valeurs actuelles de l'objet :

class Typography(
  val labelMedium: TextStyle = TypographyTokens.LabelMedium,
  val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
    fun copy(
      labelMedium: TextStyle = this.labelMedium,
      labelSmall: TextStyle = this.labelSmall
    ): Typography = Typography(
      labelMedium = labelMedium,
      labelSmall = labelSmall
    )
}

Comportements supplémentaires

Les classes de données doivent implémenter equals() et hashCode(), et chaque propriété doit être prise en compte dans les implémentations de ces méthodes.

Les classes de données peuvent implémenter toString() avec un format recommandé correspondant à l'implémentation de la classe de données Kotlin, par exemple User(var1=Alex, var2=42).

Méthodes

Il s'agit de règles concernant divers aspects spécifiques des méthodes, des paramètres, des noms de méthodes, des types de retour et des spécificateurs d'accès.

Durée

Ces règles couvrent la façon dont les concepts temporels tels que les dates et les durées doivent être exprimés dans les API.

Privilégiez les types java.time.* dans la mesure du possible.

java.time.Duration, java.time.Instant et de nombreux autres types java.time.* sont disponibles sur toutes les versions de la plate-forme grâce au desugaring. Il est préférable de les utiliser pour exprimer le temps dans les paramètres ou les valeurs de retour de l'API.

Privilégiez l'exposition des variantes d'une API qui acceptent ou renvoient java.time.Duration ou java.time.Instant, et omettez les variantes primitives avec le même cas d'utilisation, sauf si le domaine de l'API est un domaine où l'allocation d'objets dans les modèles d'utilisation prévus aurait un impact prohibitif sur les performances.

Les méthodes exprimant des durées doivent être nommées "duration"

Si une valeur temporelle exprime la durée concernée, nommez le paramètre "duration" (durée) et non "time" (heure).

ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);

Exceptions :

"timeout" convient lorsque la durée s'applique spécifiquement à une valeur de délai d'inactivité.

"time" avec un type java.time.Instant convient lorsqu'il s'agit d'un moment précis, et non d'une durée.

Les méthodes exprimant des durées ou du temps sous forme de primitive doivent être nommées avec leur unité de temps et utiliser le type long.

Les méthodes acceptant ou renvoyant des durées en tant que primitive doivent ajouter les unités de temps associées (telles que Millis, Nanos, Seconds) au nom de la méthode pour réserver le nom non décoré à l'utilisation avec java.time.Duration. Consultez Heure.

Les méthodes doivent également être annotées de manière appropriée avec leur unité et leur base temporelle :

  • @CurrentTimeMillisLong : la valeur est un code temporel non négatif mesuré en nombre de millisecondes depuis le 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong : la valeur est un code temporel non négatif mesuré en nombre de secondes depuis le 1er janvier 1970 à 00:00:00 UTC.
  • @DurationMillisLong : la valeur est une durée non négative en millisecondes.
  • @ElapsedRealtimeLong : la valeur est un code temporel non négatif dans la base temporelle SystemClock.elapsedRealtime().
  • @UptimeMillisLong : la valeur est un code temporel non négatif dans la base temporelle SystemClock.uptimeMillis().

Les paramètres ou valeurs de retour temporels primitifs doivent utiliser long, et non int.

ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);

Les méthodes exprimant des unités de temps doivent préférer les abréviations non abrégées pour les noms d'unités.

public void setIntervalNs(long intervalNs);

public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);

public void setTimeoutMicros(long timeoutMicros);

Annoter les arguments de longue durée

La plate-forme inclut plusieurs annotations pour fournir une typographie plus forte pour les unités de temps de type long :

  • @CurrentTimeMillisLong : la valeur est un code temporel non négatif mesuré en nombre de millisecondes depuis 1970-01-01T00:00:00Z, donc dans la base de temps System.currentTimeMillis().
  • @CurrentTimeSecondsLong : la valeur est un code temporel non négatif mesuré en nombre de secondes depuis 1970-01-01T00:00:00Z.
  • @DurationMillisLong : la valeur est une durée non négative en millisecondes.
  • @ElapsedRealtimeLong : la valeur est un code temporel non négatif dans la base temporelle SystemClock#elapsedRealtime().
  • @UptimeMillisLong : la valeur est un code temporel non négatif dans la base temporelle SystemClock#uptimeMillis().

Unités de mesure

Pour toutes les méthodes exprimant une unité de mesure autre que le temps, préférez les préfixes d'unité SI en CamelCase.

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

Placer les paramètres facultatifs à la fin des surcharges

Si vous avez des surcharges d'une méthode avec des paramètres facultatifs, conservez ces paramètres à la fin et gardez un ordre cohérent avec les autres paramètres :

public int doFoo(boolean flag);

public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);

public int doFoo(boolean flag, int id);

Lorsque vous ajoutez des surcharges pour les arguments facultatifs, le comportement des méthodes plus simples doit être exactement le même que si des arguments par défaut avaient été fournis aux méthodes plus élaborées.

Corollaire : Ne surchargez pas les méthodes, sauf pour ajouter des arguments facultatifs ou pour accepter différents types d'arguments si la méthode est polymorphe. Si la méthode surchargée fait quelque chose de fondamentalement différent, donnez-lui un nouveau nom.

Les méthodes avec des paramètres par défaut doivent être annotées avec @JvmOverloads (Kotlin uniquement).

Les méthodes et les constructeurs avec des paramètres par défaut doivent être annotés avec @JvmOverloads pour maintenir la compatibilité binaire.

Pour en savoir plus, consultez la section Surcharges de fonction pour les valeurs par défaut dans le guide d'interopérabilité Kotlin-Java officiel.

class Greeting @JvmOverloads constructor(
  loudness: Int = 5
) {
  @JvmOverloads
  fun sayHello(prefix: String = "Dr.", name: String) = // ...
}

Ne pas supprimer les valeurs de paramètre par défaut (Kotlin uniquement)

Si une méthode a été publiée avec un paramètre ayant une valeur par défaut, la suppression de cette valeur par défaut constitue une modification incompatible avec la source.

Les paramètres de méthode les plus distinctifs et les plus identifiants doivent être placés en premier.

Si vous avez une méthode avec plusieurs paramètres, placez les plus pertinents en premier. Les paramètres qui spécifient des indicateurs et d'autres options sont moins importants que ceux qui décrivent l'objet sur lequel l'action est effectuée. Si un rappel de fin existe, placez-le en dernier.

public void openFile(int flags, String name);

public void openFileAsync(OnFileOpenedListener listener, String name, int flags);

public void setFlags(int mask, int flags);
public void openFile(String name, int flags);

public void openFileAsync(String name, int flags, OnFileOpenedListener listener);

public void setFlags(int flags, int mask);

Voir aussi : Placer les paramètres facultatifs à la fin des surcharges

Compilateurs

Le modèle Builder est recommandé pour créer des objets Java complexes. Il est couramment utilisé dans Android dans les cas suivants :

  • Les propriétés de l'objet résultant doivent être immuables.
  • Il existe un grand nombre de propriétés requises, par exemple de nombreux arguments de constructeur.
  • Il existe une relation complexe entre les propriétés au moment de la construction (une étape de validation est par exemple requise). Notez que ce niveau de complexité indique souvent des problèmes d'usabilité de l'API.

Déterminez si vous avez besoin d'un générateur. Les compilateurs sont utiles dans une surface d'API s'ils sont utilisés pour :

  • Configurer seulement quelques paramètres de création facultatifs parmi un grand nombre
  • Configurer de nombreux paramètres de création facultatifs ou obligatoires différents, parfois de types similaires ou identiques, où les sites d'appel pourraient autrement devenir difficiles à lire ou sujets à des erreurs d'écriture
  • Configurez la création d'un objet de manière incrémentielle, où plusieurs éléments de code de configuration différents peuvent chacun effectuer des appels sur le générateur en tant que détails d'implémentation.
  • Autoriser un type à se développer en ajoutant des paramètres de création facultatifs supplémentaires dans les futures versions de l'API

Si vous avez un type avec trois paramètres requis ou moins et aucun paramètre facultatif, vous pouvez presque toujours ignorer un compilateur et utiliser un constructeur simple.

Les classes provenant de Kotlin doivent préférer les constructeurs annotés @JvmOverloads avec des arguments par défaut aux compilateurs, mais peuvent choisir d'améliorer la facilité d'utilisation pour les clients Java en fournissant également des compilateurs dans les cas décrits précédemment.

class Tone @JvmOverloads constructor(
  val duration: Long = 1000,
  val frequency: Int = 2600,
  val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
  class Builder {
    // ...
  }
}

Les classes de compilateur doivent renvoyer le compilateur

Les classes Builder doivent permettre le chaînage de méthodes en renvoyant l'objet Builder (tel que this) à partir de chaque méthode, à l'exception de build(). Les objets créés supplémentaires doivent être transmis en tant qu'arguments. Ne renvoyez pas le compilateur d'un autre objet. Exemple :

public static class Builder {
  public void setDuration(long);
  public void setFrequency(int);
  public DtmfConfigBuilder addDtmfConfig();
  public Tone build();
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}

Dans les rares cas où une classe de compilateur de base doit prendre en charge l'extension, utilisez un type de retour générique :

public abstract class Builder<T extends Builder<T>> {
  abstract T setValue(int);
}

public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
  T setValue(int);
  T setTypeSpecificValue(long);
}

Les classes Builder doivent être créées via un constructeur.

Pour créer des compilateurs de manière cohérente sur la surface de l'API Android, tous les compilateurs doivent être créés à l'aide d'un constructeur et non d'une méthode de création statique. Pour les API basées sur Kotlin, le Builder doit être public, même si les utilisateurs de Kotlin sont censés s'appuyer implicitement sur le compilateur via un mécanisme de création de style de méthode/DSL de fabrique. Les bibliothèques ne doivent pas utiliser @PublishedApi internal pour masquer sélectivement le constructeur de classe Builder des clients Kotlin.

public class Tone {
  public static Builder builder();
  public static class Builder {
  }
}
public class Tone {
  public static class Builder {
    public Builder();
  }
}

Tous les arguments des constructeurs de compilateur doivent être obligatoires (comme @NonNull).

Les arguments facultatifs, par exemple @Nullable, doivent être déplacés vers les méthodes setter. Le constructeur du compilateur doit générer une NullPointerException (envisagez d'utiliser Objects.requireNonNull) si des arguments requis ne sont pas spécifiés.

Les classes Builder doivent être des classes internes statiques finales de leurs types intégrés.

Pour une organisation logique au sein d'un package, les classes de compilateur doivent généralement être exposées en tant que classes internes finales de leurs types intégrés, par exemple Tone.Builder plutôt que ToneBuilder.

Les builders peuvent inclure un constructeur pour créer une instance à partir d'une instance existante.

Les compilateurs peuvent inclure un constructeur de copie pour créer une instance de compilateur à partir d'un compilateur ou d'un objet compilé existants. Ils ne doivent pas fournir d'autres méthodes pour créer des instances de compilateur à partir de compilateurs ou d'objets de compilation existants.

public class Tone {
  public static class Builder {
    public Builder clone();
  }

  public Builder toBuilder();
}
public class Tone {
  public static class Builder {
    public Builder(Builder original);
    public Builder(Tone original);
  }
}
Les setters de compilateur doivent accepter les arguments @Nullable si le compilateur possède un constructeur de copie.

La réinitialisation est essentielle si une nouvelle instance d'un générateur peut être créée à partir d'une instance existante. Si aucun constructeur de copie n'est disponible, le compilateur peut avoir des arguments @Nullable ou @NonNullable.

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
Les setters de compilateur peuvent accepter des arguments @Nullable pour les propriétés facultatives.

Il est souvent plus simple d'utiliser une valeur pouvant être nulle pour les entrées de second degré, en particulier en Kotlin, qui utilise des arguments par défaut au lieu de constructeurs et de surcharges.

De plus, les setters @Nullable seront associés à leurs getters, qui doivent être @Nullable pour les propriétés facultatives.

Value createValue(@Nullable OptionalValue optionalValue) {
  Value.Builder builder = new Value.Builder();
  if (optionalValue != null) {
    builder.setOptionalValue(optionalValue);
  }
  return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
  return new Value.Builder()
    .setOptionalValue(optionalValue);
    .build();
}

// Or in other cases:

Value createValue() {
  return new Value.Builder()
    .setOptionalValue(condition ? new OptionalValue() : null);
    .build();
}

Utilisation courante en Kotlin :

fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .apply { optionalValue?.let { setOptionalValue(it) } }
    .build()
fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .setOptionalValue(optionalValue)
    .build()

La valeur par défaut (si le setter n'est pas appelé) et la signification de null doivent être correctement documentées dans le setter et le getter.

/**
 * ...
 *
 * <p>Defaults to {@code null}, which means the optional value won't be used.
 */

Des setters de compilateur peuvent être fournis pour les propriétés mutables où des setters sont disponibles sur la classe compilée.

Si votre classe comporte des propriétés mutables et a besoin d'une classe Builder, demandez-vous d'abord si votre classe doit vraiment avoir des propriétés mutables.

Ensuite, si vous êtes certain d'avoir besoin de propriétés mutables, déterminez lequel des scénarios suivants correspond le mieux à votre cas d'utilisation :

  1. L'objet créé doit être immédiatement utilisable. Par conséquent, des setters doivent être fournis pour toutes les propriétés pertinentes, qu'elles soient mutables ou immuables.

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. Certains appels supplémentaires peuvent être nécessaires avant que l'objet créé ne soit utile. Par conséquent, les setters ne doivent pas être fournis pour les propriétés mutables.

    Value v = new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .build();
    v.setUsefulMutableProperty(usefulValue)
    Result r = v.performSomeAction();
    Key k = callSomeMethod(r);
    map.put(k, v);
    

Ne mélangez pas les deux scénarios.

Value v = new Value.Builder(requiredValue)
    .setImmutableProperty(immutableValue)
    .setUsefulMutableProperty(usefulValue)
    .build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);

Les builders ne doivent pas avoir de getters

Le getter doit se trouver sur l'objet créé, et non sur le compilateur.

Les setters de compilateur doivent avoir des getters correspondants sur la classe compilée.

public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }

  public long getDuration();
  public int getFrequency();
  public @NonNull List<DtmfConfig> getDtmfConfigs();
}

Nommage des méthodes de création

Les noms des méthodes de compilateur doivent utiliser le style setFoo(), addFoo() ou clearFoo().

Les classes Builder doivent déclarer une méthode build()

Les classes Builder doivent déclarer une méthode build() qui renvoie une instance de l'objet construit.

Les méthodes build() du compilateur doivent renvoyer des objets @NonNull

La méthode build() d'un compilateur est censée renvoyer une instance non nulle de l'objet construit. Si l'objet ne peut pas être créé en raison de paramètres non valides, la validation peut être différée à la méthode de compilation et une IllegalStateException doit être générée.

Ne pas exposer les verrous internes

Les méthodes de l'API publique ne doivent pas utiliser le mot clé synchronized. Ce mot clé entraîne l'utilisation de votre objet ou classe comme verrou. Comme il est exposé à d'autres, vous pouvez rencontrer des effets secondaires inattendus si d'autres codes en dehors de votre classe commencent à l'utiliser à des fins de verrouillage.

Effectuez plutôt tout verrouillage requis sur un objet interne et privé.

public synchronized void doThing() { ... }
private final Object mThingLock = new Object();

public void doThing() {
  synchronized (mThingLock) {
    ...
  }
}

Les méthodes de style Accessor doivent suivre les consignes relatives aux propriétés Kotlin

Lorsqu'elles sont consultées à partir de sources Kotlin, les méthodes de style accesseur (celles qui utilisent les préfixes get, set ou is) seront également disponibles en tant que propriétés Kotlin. Par exemple, int getField() défini en Java est disponible en Kotlin en tant que propriété val field: Int.

Pour cette raison, et pour répondre aux attentes des développeurs concernant le comportement des méthodes d'accesseur, les méthodes utilisant des préfixes de méthode d'accesseur doivent se comporter de la même manière que les champs Java. Évitez d'utiliser des préfixes de style accesseur dans les cas suivants :

  • La méthode a des effets secondaires. Préférez un nom de méthode plus descriptif.
  • La méthode implique un travail coûteux en termes de calcul. Préférez compute.
  • La méthode implique un blocage ou un travail de longue durée pour renvoyer une valeur, telle que l'IPC ou d'autres E/S. Préférez fetch.
  • La méthode bloque le thread jusqu'à ce qu'elle puisse renvoyer une valeur. Il est préférable d'utiliser await.
  • La méthode renvoie une nouvelle instance d'objet à chaque appel. Il est préférable d'utiliser create.
  • La méthode peut ne pas renvoyer de valeur. Préférez request.

Notez que l'exécution d'un travail coûteux en termes de calcul une seule fois et la mise en cache de la valeur pour les appels ultérieurs sont toujours considérées comme un travail coûteux en termes de calcul. Le saccade n'est pas amorti sur les frames.

Utiliser le préfixe "is" pour les méthodes d'accesseur booléennes

Il s'agit de la convention d'attribution de noms standard pour les méthodes et les champs booléens en Java. En général, les noms de méthodes et de variables booléennes doivent être rédigés sous forme de questions auxquelles la valeur renvoyée répond.

Les méthodes d'accesseur booléen Java doivent suivre un schéma de dénomination set/is et les champs doivent préférer is, comme dans l'exemple suivant :

// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();

// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();

final boolean isAvailable;

L'utilisation de set/is pour les méthodes d'accesseur Java ou de is pour les champs Java leur permettra d'être utilisées comme propriétés à partir de Kotlin :

obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return

Les propriétés et les méthodes d'accesseur doivent généralement utiliser des noms positifs, par exemple Enabled plutôt que Disabled. L'utilisation d'une terminologie négative inverse la signification de true et false, et rend le comportement plus difficile à comprendre.

// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);

Dans les cas où le booléen décrit l'inclusion ou la propriété d'une propriété, vous pouvez utiliser has plutôt que is. Toutefois, cela ne fonctionnera pas avec la syntaxe de propriété Kotlin :

// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();

Voici d'autres préfixes qui peuvent être plus adaptés : peut et doit :

// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();

// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();

Les méthodes qui activent ou désactivent des comportements ou des fonctionnalités peuvent utiliser le préfixe is et le suffixe Enabled :

// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()

De même, les méthodes qui indiquent la dépendance à d'autres comportements ou fonctionnalités peuvent utiliser le préfixe is et le suffixe Supported ou Required :

// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()

En général, les noms de méthodes doivent être écrits sous forme de questions auxquelles la valeur renvoyée répond.

Méthodes de propriété Kotlin

Pour une propriété de classe var foo: Foo, Kotlin génère des méthodes get/set en utilisant une règle cohérente : ajoutez get et mettez en majuscule le premier caractère pour le getter, et ajoutez set et mettez en majuscule le premier caractère pour le setter. La déclaration de propriété générera des méthodes nommées public Foo getFoo() et public void setFoo(Foo foo), respectivement.

Si la propriété est de type Boolean, une règle supplémentaire s'applique à la génération de noms : si le nom de la propriété commence par is, get n'est pas ajouté au nom de la méthode getter. Le nom de la propriété lui-même est utilisé comme getter. Par conséquent, préférez nommer les propriétés Boolean avec un préfixe is afin de suivre les consignes de dénomination :

var isVisible: Boolean

Si votre propriété fait partie des exceptions mentionnées ci-dessus et commence par un préfixe approprié, utilisez l'annotation @get:JvmName sur la propriété pour spécifier manuellement le nom approprié :

@get:JvmName("hasTransientState")
var hasTransientState: Boolean

@get:JvmName("canRecord")
var canRecord: Boolean

@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean

Accesseurs de masque de bits

Consultez Utiliser @IntDef pour les indicateurs de masque de bits pour obtenir des consignes sur l'API concernant la définition des indicateurs de masque de bits.

Setters

Deux méthodes de définition doivent être fournies : l'une qui accepte une chaîne de bits complète et remplace tous les indicateurs existants, et l'autre qui accepte un masque de bits personnalisé pour plus de flexibilité.

/**
 * Sets the state of all scroll indicators.
 * <p>
 * See {@link #setScrollIndicators(int, int)} for usage information.
 *
 * @param indicators a bitmask of indicators that should be enabled, or
 *                   {@code 0} to disable all indicators
 * @see #setScrollIndicators(int, int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators);

/**
 * Sets the state of the scroll indicators specified by the mask. To change
 * all scroll indicators at once, see {@link #setScrollIndicators(int)}.
 * <p>
 * When a scroll indicator is enabled, it will be displayed if the view
 * can scroll in the direction of the indicator.
 * <p>
 * Multiple indicator types may be enabled or disabled by passing the
 * logical OR of the specified types. If multiple types are specified, they
 * will all be set to the same enabled state.
 * <p>
 * For example, to enable the top scroll indicator:
 * {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
 * <p>
 * To disable the top scroll indicator:
 * {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
 *
 * @param indicators a bitmask of values to set; may be a single flag,
 *                   the logical OR of multiple flags, or 0 to clear
 * @param mask a bitmask indicating which indicator flags to modify
 * @see #setScrollIndicators(int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);

Getters

Un getter doit être fourni pour obtenir le masque de bits complet.

/**
 * Returns a bitmask representing the enabled scroll indicators.
 * <p>
 * For example, if the top and left scroll indicators are enabled and all
 * other indicators are disabled, the return value will be
 * {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
 * <p>
 * To check whether the bottom scroll indicator is enabled, use the value
 * of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
 *
 * @return a bitmask representing the enabled scroll indicators
 */
@ScrollIndicators
public int getScrollIndicators();

Utiliser "Public" au lieu de "Protégé"

Toujours préférer public à protected dans l'API publique. L'accès protégé finit par être pénible à long terme, car les implémenteurs doivent remplacer les accesseurs publics dans les cas où un accès externe par défaut aurait été tout aussi bien.

N'oubliez pas que la visibilité protected n'empêche pas les développeurs d'appeler une API. Elle ne fait que rendre la chose légèrement plus désagréable.

Implémenter ni equals() ni hashCode(), ou les deux

Si vous en remplacez un, vous devez remplacer l'autre.

Implémenter toString() pour les classes de données

Il est recommandé aux classes de données de remplacer toString() pour aider les développeurs à déboguer leur code.

Indiquez si la sortie concerne le comportement du programme ou le débogage.

Décidez si vous souhaitez que le comportement du programme repose sur votre implémentation ou non. Par exemple, UUID.toString() et File.toString() documentent leur format spécifique pour les programmes. Si vous n'exposez des informations que pour le débogage, comme Intent, impliquez l'héritage des documents de la superclasse.

N'incluez pas d'informations supplémentaires

Toutes les informations disponibles sur toString() doivent également être disponibles via l'API publique de l'objet. Sinon, vous encouragez les développeurs à analyser et à s'appuyer sur votre résultat toString(), ce qui empêchera les modifications futures. Il est recommandé d'implémenter toString() en utilisant uniquement l'API publique de l'objet.

Décourager la dépendance à la sortie de débogage

Bien qu'il soit impossible d'empêcher les développeurs de dépendre de la sortie de débogage, y compris le System.identityHashCode de votre objet dans sa sortie toString(), il est très peu probable que deux objets différents aient une sortie toString() égale.

@Override
public String toString() {
  return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}

Cela peut dissuader efficacement les développeurs d'écrire des assertions de test telles que assertThat(a.toString()).isEqualTo(b.toString()) sur vos objets.

Utiliser createFoo lors du renvoi d'objets nouvellement créés

Utilisez le préfixe create, et non get ni new, pour les méthodes qui créeront des valeurs de retour, par exemple en construisant de nouveaux objets.

Lorsque la méthode crée un objet à renvoyer, indiquez-le clairement dans le nom de la méthode.

public FooThing getFooThing() {
  return new FooThing();
}
public FooThing createFooThing() {
  return new FooThing();
}

Les méthodes acceptant les objets File doivent également accepter les flux

Les emplacements de stockage de données sur Android ne sont pas toujours des fichiers sur disque. Par exemple, le contenu transmis au-delà des limites de l'utilisateur est représenté par content:// Uri. Pour permettre le traitement de diverses sources de données, les API qui acceptent les objets File doivent également accepter les objets InputStream, OutputStream ou les deux.

public void setDataSource(File file)
public void setDataSource(InputStream stream)

Prendre et renvoyer des primitives brutes au lieu de versions boxed

Si vous devez communiquer des valeurs manquantes ou nulles, envisagez d'utiliser -1, Integer.MAX_VALUE ou Integer.MIN_VALUE.

public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)

Éviter les équivalents de classe des types primitifs permet d'éviter la surcharge de mémoire de ces classes, l'accès aux valeurs par méthode et, plus important encore, l'autoboxing qui résulte de la conversion entre les types primitifs et les types d'objet. Éviter ces comportements permet d'économiser de la mémoire et des allocations temporaires qui peuvent entraîner des collectes de déchets coûteuses et plus fréquentes.

Utiliser des annotations pour clarifier les valeurs de paramètre et de retour valides

Des annotations de développeur ont été ajoutées pour clarifier les valeurs autorisées dans différentes situations. Les outils peuvent ainsi aider plus facilement les développeurs lorsqu'ils fournissent des valeurs incorrectes (par exemple, en transmettant un int arbitraire lorsque le framework nécessite l'un des ensembles spécifiques de valeurs constantes). Utilisez toutes les annotations suivantes, le cas échéant :

Possibilité de valeur nulle

Des annotations de possibilité de valeur nulle explicites sont requises pour les API Java, mais le concept de possibilité de valeur nulle fait partie du langage Kotlin. Les annotations de possibilité de valeur nulle ne doivent donc jamais être utilisées dans les API Kotlin.

@Nullable : indique qu'une valeur de retour, un paramètre ou un champ donné peuvent être nuls :

@Nullable
public String getName()

public void setName(@Nullable String name)

@NonNull : indique qu'une valeur de retour, un paramètre ou un champ donné ne peuvent pas être nuls. Le marquage des éléments comme @Nullable est relativement nouveau sur Android. Par conséquent, la plupart des méthodes d'API d'Android ne sont pas documentées de manière cohérente. Nous avons donc trois états : "inconnu, @Nullable, @NonNull". C'est pourquoi @NonNull fait partie des consignes relatives à l'API :

@NonNull
public String getName()

public void setName(@NonNull String name)

Pour la documentation de la plate-forme Android, l'annotation des paramètres de votre méthode générera automatiquement une documentation sous la forme "Cette valeur peut être nulle", sauf si "null" est explicitement utilisé ailleurs dans la documentation des paramètres.

Méthodes existantes "pas vraiment annulables" : les méthodes existantes dans l'API sans annotation @Nullable déclarée peuvent être annotées @Nullable si la méthode peut renvoyer null dans des circonstances spécifiques et évidentes (telles que findViewById()). Des méthodes @NotNull requireFoo() associées qui génèrent IllegalArgumentException doivent être ajoutées pour les développeurs qui ne souhaitent pas effectuer de vérification de la valeur nulle.

Méthodes d'interface : les nouvelles API doivent ajouter l'annotation appropriée lors de l'implémentation des méthodes d'interface, comme Parcelable.writeToParcel() (c'est-à-dire que cette méthode dans la classe d'implémentation doit être writeToParcel(@NonNull Parcel, int), et non writeToParcel(Parcel, int)). Toutefois, les API existantes qui ne comportent pas d'annotations n'ont pas besoin d'être "corrigées".

Application de la possibilité de valeur nulle

En Java, les méthodes sont recommandées pour effectuer la validation des entrées pour les paramètres @NonNull à l'aide de Objects.requireNonNull() et générer une NullPointerException lorsque les paramètres sont nuls. Cette opération est effectuée automatiquement en Kotlin.

Ressources

Identifiants de ressources : les paramètres entiers qui désignent des ID pour des ressources spécifiques doivent être annotés avec la définition de type de ressource appropriée. Il existe une annotation pour chaque type de ressource, comme @StringRes, @ColorRes et @AnimRes, en plus de l'annotation générique @AnyRes. Exemple :

public void setTitle(@StringRes int resId)

@IntDef pour les ensembles de constantes

Constantes magiques : les paramètres String et int qui sont censés recevoir l'une des valeurs possibles d'un ensemble fini désignées par des constantes publiques doivent être annotés de manière appropriée avec @StringDef ou @IntDef. Ces annotations vous permettent de créer une annotation que vous pouvez utiliser comme un typedef pour les paramètres autorisés. Exemple :

/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
  NAVIGATION_MODE_STANDARD,
  NAVIGATION_MODE_LIST,
  NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}

public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);

Il est recommandé d'utiliser des méthodes pour vérifier la validité des paramètres annotés et générer une IllegalArgumentException si le paramètre ne fait pas partie de @IntDef.

@IntDef pour les indicateurs de masque de bits

L'annotation peut également spécifier que les constantes sont des indicateurs et peuvent être combinées avec & et I :

/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
  FLAG_USE_LOGO,
  FLAG_SHOW_HOME,
  FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

@StringDef pour les ensembles de constantes de chaîne

Il existe également l'annotation @StringDef, qui est exactement comme @IntDef dans la section précédente, mais pour les constantes String. Vous pouvez inclure plusieurs valeurs de "prefix" qui sont utilisées pour émettre automatiquement la documentation de toutes les valeurs.

@SdkConstant pour les constantes du SDK

@SdkConstant Annotez les champs publics lorsqu'ils correspondent à l'une des valeurs SdkConstant suivantes : ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE.

@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";

Fournir une nullité compatible pour les substitutions

Pour la compatibilité des API, la possibilité de valeur nulle des remplacements doit être compatible avec la possibilité de valeur nulle actuelle du parent. Le tableau suivant présente les attentes en termes de compatibilité. En d'autres termes, les remplacements ne doivent pas être moins restrictifs que l'élément qu'ils remplacent.

Saisie Parent Enfant
Type de retour Non annoté Non annoté ou non nul
Type de retour Nullable Acceptant ou non les valeurs nulles
Type de retour Nonnull Nonnull
Argument amusant Non annoté Non annoté ou pouvant être nul
Argument amusant Nullable Nullable
Argument amusant Nonnull Acceptant ou non les valeurs nulles

Privilégiez les arguments non nullables (tels que @NonNull) dans la mesure du possible.

Lorsque les méthodes sont surchargées, il est préférable que tous les arguments ne soient pas nuls.

public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }

Cette règle s'applique également aux setters de propriété surchargés. L'argument principal ne doit pas être nul et l'effacement de la propriété doit être implémenté en tant que méthode distincte. Cela évite les appels "absurdes" où le développeur doit définir des paramètres de fin même s'ils ne sont pas requis.

public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)

// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()

Privilégiez les types de retour non nullables (tels que @NonNull) pour les conteneurs.

Pour les types de conteneurs tels que Bundle ou Collection, renvoyez un conteneur vide et immuable, le cas échéant. Dans les cas où null serait utilisé pour distinguer la disponibilité d'un conteneur, envisagez de fournir une méthode booléenne distincte.

@NonNull
public Bundle getExtras() { ... }

Les annotations de nullabilité pour les paires get et set doivent correspondre

Les paires de méthodes get et set pour une même propriété logique doivent toujours être cohérentes dans leurs annotations de nullabilité. Si vous ne respectez pas cette consigne, la syntaxe de propriété de Kotlin sera ignorée. L'ajout d'annotations de nullabilité différentes aux méthodes de propriété existantes constitue donc une modification incompatible avec la source pour les utilisateurs Kotlin.

@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }

Valeur renvoyée en cas d'échec ou d'erreur

Toutes les API doivent permettre aux applications de réagir aux erreurs. Le renvoi de false, -1, null ou d'autres valeurs génériques de type "un problème est survenu" ne fournit pas aux développeurs suffisamment d'informations sur l'échec pour définir les attentes des utilisateurs ou suivre précisément la fiabilité de leur application sur le terrain. Lorsque vous concevez une API, imaginez que vous créez une application. Si vous rencontrez une erreur, l'API vous fournit-elle suffisamment d'informations pour la présenter à l'utilisateur ou réagir de manière appropriée ?

  1. Il est tout à fait acceptable (et même recommandé) d'inclure des informations détaillées dans un message d'exception, mais les développeurs ne devraient pas avoir à l'analyser pour gérer l'erreur de manière appropriée. Les codes d'erreur détaillés ou d'autres informations doivent être exposés en tant que méthodes.
  2. Assurez-vous que l'option de gestion des erreurs choisie vous permet d'introduire de nouveaux types d'erreurs à l'avenir. Pour @IntDef, cela signifie inclure une valeur OTHER ou UNKNOWN. Lorsque vous renvoyez un nouveau code, vous pouvez vérifier le targetSdkVersion de l'appelant pour éviter de renvoyer un code d'erreur que l'application ne connaît pas. Pour les exceptions, utilisez une superclasse commune que vos exceptions implémentent. Ainsi, tout code qui gère ce type d'exception pourra également détecter et gérer les sous-types.
  3. Il doit être difficile, voire impossible, pour un développeur d'ignorer une erreur par inadvertance. Si votre erreur est communiquée en renvoyant une valeur, annotez votre méthode avec @CheckResult.

Il est préférable de générer une ? extends RuntimeException lorsqu'une condition d'échec ou d'erreur est atteinte en raison d'une erreur du développeur, par exemple en ignorant les contraintes sur les paramètres d'entrée ou en ne vérifiant pas l'état observable.

Les méthodes de définition ou d'action (par exemple, perform) peuvent renvoyer un code d'état entier si l'action peut échouer en raison d'un état mis à jour de manière asynchrone ou de conditions indépendantes de la volonté du développeur.

Les codes d'état doivent être définis dans la classe conteneur en tant que champs public static final, préfixés par ERROR_ et énumérés dans une annotation @hide @IntDef.

Les noms de méthodes doivent toujours commencer par le verbe, et non par le sujet.

Le nom de la méthode doit toujours commencer par le verbe (par exemple, get, create, reload, etc.), et non par l'objet sur lequel vous agissez.

public void tableReload() {
  mTable.reload();
}
public void reloadTable() {
  mTable.reload();
}

Préférez les types Collection aux tableaux comme type de retour ou de paramètre

Les interfaces de collection à typage générique offrent plusieurs avantages par rapport aux tableaux, y compris des contrats d'API plus stricts concernant l'unicité et l'ordre, la prise en charge des génériques et un certain nombre de méthodes pratiques pour les développeurs.

Exception pour les primitives

Si les éléments sont des primitives, préférez les tableaux pour éviter le coût de l'auto-boxing. Voir Prendre et renvoyer des primitives brutes au lieu de versions encadrées

Exception pour le code sensible aux performances

Dans certains cas, lorsque l'API est utilisée dans du code sensible aux performances (comme les API graphiques ou d'autres API de mesure/mise en page/dessin), il est acceptable d'utiliser des tableaux au lieu de collections afin de réduire les allocations et le churn de mémoire.

Exception pour Kotlin

Les tableaux Kotlin sont invariants et le langage Kotlin fournit de nombreuses API utilitaires autour des tableaux. Les tableaux sont donc équivalents à List et Collection pour les API Kotlin destinées à être accessibles depuis Kotlin.

Préférer les collections @NonNull

Privilégiez toujours @NonNull pour les objets de collection. Lorsque vous renvoyez une collection vide, utilisez la méthode Collections.empty appropriée pour renvoyer un objet de collection immuable, correctement typé et à faible coût.

Lorsque les annotations de type sont acceptées, préférez toujours @NonNull pour les éléments de collection.

Vous devez également préférer @NonNull lorsque vous utilisez des tableaux au lieu de collections (voir l'élément précédent). Si l'allocation d'objets est un problème, créez une constante et transmettez-la. Après tout, un tableau vide est immuable. Exemple :

private static final int[] EMPTY_USER_IDS = new int[0];

@NonNull
public int[] getUserIds() {
  int [] userIds = mService.getUserIds();
  return userIds != null ? userIds : EMPTY_USER_IDS;
}

Mutabilité de la collection

Les API Kotlin doivent préférer les types de retour en lecture seule (et non Mutable) pour les collections par défaut sauf si le contrat d'API exige spécifiquement un type de retour mutable.

Toutefois, les API Java doivent préférer les types de retour mutables par défaut, car l'implémentation de la plate-forme Android des API Java ne fournit pas encore d'implémentation pratique des collections immuables. Les types de retour Collections.empty font exception à cette règle, car ils sont immuables. Dans les cas où la mutabilité pourrait être exploitée par les clients (intentionnellement ou par erreur) pour enfreindre le modèle d'utilisation prévu de l'API, les API Java doivent sérieusement envisager de renvoyer une copie superficielle de la collection.

@Nullable
public PermissionInfo[] getGrantedPermissions() {
  return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
  if (mPermissions == null) {
    return Collections.emptySet();
  }
  return new ArraySet<>(mPermissions);
}

Types de retour explicitement modifiables

Idéalement, les API qui renvoient des collections ne doivent pas modifier l'objet de collection renvoyé après l'avoir renvoyé. Si la collection renvoyée doit être modifiée ou réutilisée d'une manière ou d'une autre (par exemple, une vue adaptée d'un ensemble de données mutable), le comportement précis de when (quand) le contenu peut changer doit être explicitement documenté ou suivre les conventions de nommage établies de l'API.

/**
 * Returns a view of this object as a list of [Item]s.
 */
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)

La convention Kotlin .asFoo() est décrite ci-dessous et permet à la collection renvoyée par .asList() de changer si la collection d'origine change.

Mutabilité des objets de type de données renvoyés

Comme pour les API qui renvoient des collections, les API qui renvoient des objets de type de données ne doivent idéalement pas modifier les propriétés de l'objet renvoyé après l'avoir renvoyé.

val tempResult = DataContainer()

fun add(other: DataContainer): DataContainer {
  tempResult.innerValue = innerValue + other.innerValue
  return tempResult
}
fun add(other: DataContainer): DataContainer {
  return DataContainer(innerValue + other.innerValue)
}

Dans de très rares cas, certains codes sensibles aux performances peuvent bénéficier du pooling ou de la réutilisation d'objets. N'écrivez pas votre propre structure de données de pool d'objets et n'exposez pas les objets réutilisés dans les API publiques. Dans les deux cas, soyez extrêmement prudent quant à la gestion des accès simultanés.

Utilisation du type de paramètre vararg

Les API Kotlin et Java sont encouragées à utiliser vararg dans les cas où le développeur serait susceptible de créer un tableau au niveau de l'appel dans le seul but de transmettre plusieurs paramètres associés du même type.

public void setFeatures(Feature[] features) { ... }

// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }

// Developer code
setFeatures(Features.A, Features.B, Features.C);

Copies défensives

Les implémentations Java et Kotlin des paramètres vararg sont compilées dans le même bytecode soutenu par un tableau et peuvent donc être appelées à partir du code Java avec un tableau mutable. Les concepteurs d'API sont vivement encouragés à créer une copie superficielle défensive du paramètre de tableau dans les cas où il sera conservé dans un champ ou une classe interne anonyme.

public void setValues(SomeObject... values) {
   this.values = Arrays.copyOf(values, values.length);
}

Notez que la création d'une copie défensive ne protège pas contre les modifications simultanées entre l'appel de méthode initial et la création de la copie, ni contre la mutation des objets contenus dans le tableau.

Fournir une sémantique correcte avec des paramètres ou des types renvoyés de type collection

List<Foo> est l'option par défaut, mais vous pouvez envisager d'autres types pour fournir des informations supplémentaires :

  • Utilisez Set<Foo> si votre API est indifférente à l'ordre des éléments et qu'elle n'autorise pas les doublons ou que les doublons n'ont pas de sens.

  • Collection<Foo>, si votre API est indifférente à l'ordre et autorise les doublons.

Fonctions de conversion Kotlin

Kotlin utilise fréquemment .toFoo() et .asFoo() pour obtenir un objet d'un type différent à partir d'un objet existant, où Foo est le nom du type renvoyé de la conversion. Cela correspond au JDK Object.toString() que vous connaissez. Kotlin va plus loin en l'utilisant pour les conversions primitives telles que 25.toFloat().

La distinction entre les conversions nommées .toFoo() et .asFoo() est importante :

Utiliser .toFoo() lors de la création d'un objet indépendant

Comme .toString(), une conversion "to" renvoie un nouvel objet indépendant. Si l'objet d'origine est modifié ultérieurement, le nouvel objet ne reflétera pas ces modifications. De même, si l'objet new est modifié ultérieurement, l'objet old ne reflétera pas ces modifications.

fun Foo.toBundle(): Bundle = Bundle().apply {
    putInt(FOO_VALUE_KEY, value)
}

Utiliser .asFoo() lors de la création d'un wrapper dépendant, d'un objet décoré ou d'un cast

En Kotlin, le casting est effectué à l'aide du mot clé as. Il reflète un changement d'interface, mais pas d'identité. Lorsqu'il est utilisé comme préfixe dans une fonction d'extension, .asFoo() décore le récepteur. Une mutation dans l'objet récepteur d'origine se reflétera dans l'objet renvoyé par asFoo(). Une mutation dans le nouvel objet Foo peut se refléter dans l'objet d'origine.

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

Les fonctions de conversion doivent être écrites en tant que fonctions d'extension.

Écrire des fonctions de conversion en dehors des définitions de classe du récepteur et du résultat réduit le couplage entre les types. Une conversion idéale ne nécessite qu'un accès à l'API publique de l'objet d'origine. Cela prouve par l'exemple qu'un développeur peut également écrire des conversions analogues à ses propres types préférés.

Générer les exceptions spécifiques appropriées

Les méthodes ne doivent pas générer d'exceptions génériques telles que java.lang.Exception ou java.lang.Throwable. Une exception spécifique appropriée doit être utilisée à la place, comme java.lang.NullPointerException, pour permettre aux développeurs de gérer les exceptions sans être trop généraux.

Les erreurs qui ne sont pas liées aux arguments fournis directement à la méthode appelée publiquement doivent générer java.lang.IllegalStateException au lieu de java.lang.IllegalArgumentException ou java.lang.NullPointerException.

Écouteurs et rappels

Voici les règles concernant les classes et les méthodes utilisées pour les mécanismes d'écouteur et de rappel.

Les noms de classes de rappel doivent être au singulier

Utilisez MyObjectCallback à la place de MyObjectCallbacks.

Les noms des méthodes de rappel doivent être au format on.

onFooEvent signifie que FooEvent est en cours et que le rappel doit agir en conséquence.

Le temps du verbe (passé ou présent) doit décrire le comportement temporel.

Les méthodes de rappel concernant les événements doivent être nommées pour indiquer si l'événement s'est déjà produit ou est en cours.

Par exemple, si la méthode est appelée après une action de clic :

public void onClicked()

Toutefois, si la méthode est responsable de l'exécution de l'action de clic :

public boolean onClick()

Enregistrement du rappel

Lorsqu'un écouteur ou un rappel peuvent être ajoutés ou supprimés d'un objet, les méthodes associées doivent être nommées add et remove ou register et unregister. Respectez la convention existante utilisée par la classe ou par d'autres classes du même package. En l'absence d'un tel précédent, préférez "Ajouter" et "Supprimer".

Les méthodes impliquant l'enregistrement ou la désinscription des rappels doivent spécifier le nom complet du type de rappel.

public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);

Éviter les getters pour les rappels

N'ajoutez pas de méthodes getFooCallback(). Il s'agit d'une échappatoire tentante pour les cas où les développeurs peuvent vouloir enchaîner un rappel existant avec leur propre remplacement, mais elle est fragile et rend l'état actuel difficile à comprendre pour les développeurs de composants. Par exemple,

  • Le développeur A appelle setFooCallback(a)
  • Le développeur B appelle setFooCallback(new B(getFooCallback()))
  • Le développeur A souhaite supprimer son rappel a, mais n'a aucun moyen de le faire sans connaître le type de B, et B ayant été conçu pour permettre de telles modifications de son rappel encapsulé.

Accepter l'exécuteur pour contrôler la distribution des rappels

Lors de l'enregistrement de rappels sans attentes de threading explicites (presque partout en dehors de la boîte à outils UI), il est fortement recommandé d'inclure un paramètre Executor dans l'enregistrement pour permettre au développeur de spécifier le thread sur lequel les rappels seront appelés.

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

Par exception à nos consignes habituelles concernant les paramètres facultatifs, il est acceptable de fournir une surcharge omettant le Executor même s'il ne s'agit pas du dernier argument de la liste des paramètres. Si Executor n'est pas fourni, le rappel doit être appelé sur le thread principal à l'aide de Looper.getMainLooper(). Cela doit être documenté sur la méthode surchargée associée.

/**
 * ...
 * Note that the callback will be executed on the main thread using
 * {@link Looper.getMainLooper()}. To specify the execution thread, use
 * {@link registerFooCallback(Executor, FooCallback)}.
 * ...
 */
public void registerFooCallback(
    @NonNull FooCallback callback)

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

Points à vérifier lors de l'implémentation de Executor : notez que l'exécuteur suivant est valide.

public class SynchronousExecutor implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

Cela signifie que lors de l'implémentation d'API qui prennent cette forme, l'implémentation de votre objet Binder entrant côté processus d'application doit appeler Binder.clearCallingIdentity() avant d'appeler le rappel de l'application sur le Executor fourni par l'application. De cette façon, tout code d'application qui utilise l'identité Binder (comme Binder.getCallingUid()) pour les vérifications d'autorisation attribue correctement le code en cours d'exécution à l'application et non au processus système qui appelle l'application. Si les utilisateurs de votre API souhaitent obtenir des informations sur l'UID ou le PID de l'appelant, cela doit faire partie explicitement de la surface de votre API, plutôt qu'implicitement en fonction de l'endroit où le Executor qu'ils ont fourni s'est exécuté.

Votre API doit accepter la spécification d'un Executor. Dans les cas où les performances sont critiques, les applications peuvent avoir besoin d'exécuter du code immédiatement ou de manière synchrone avec les commentaires de votre API. Pour ce faire, vous devez accepter un Executor. La création défensive d'un HandlerThread supplémentaire ou similaire à un trampoline à partir de la défaite ce cas d'utilisation souhaitable.

Si une application doit exécuter du code coûteux dans son propre processus, laissez-la faire. Les solutions de contournement que les développeurs d'applications trouveront pour surmonter vos restrictions seront beaucoup plus difficiles à prendre en charge à long terme.

Exception pour un seul rappel : lorsque la nature des événements signalés nécessite de n'accepter qu'une seule instance de rappel, utilisez le style suivant :

public void setFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

public void clearFooCallback()

Utiliser Executor au lieu de Handler

L'Handler d'Android était utilisé comme norme pour rediriger l'exécution du rappel vers un thread Looper spécifique dans le passé. Cette norme a été modifiée pour privilégier Executor, car la plupart des développeurs d'applications gèrent leurs propres pools de threads, ce qui fait du thread principal ou de l'UI le seul thread Looper disponible pour l'application. Utilisez Executor pour donner aux développeurs le contrôle dont ils ont besoin pour réutiliser leurs contextes d'exécution existants/préférés.

Les bibliothèques de concurrence modernes telles que kotlinx.coroutines ou RxJava fournissent leurs propres mécanismes de planification qui effectuent leur propre dispatch si nécessaire. Il est donc important de fournir la possibilité d'utiliser un exécuteur direct (tel que Runnable::run) pour éviter la latence due aux doubles sauts de thread. Par exemple, un saut pour publier dans un thread Looper à l'aide d'un Handler, suivi d'un autre saut à partir du framework de simultanéité de l'application.

Les exceptions à cette consigne sont rares. Voici quelques cas de figure courants :

Je dois utiliser un Looper, car j'ai besoin d'un Looper pour epoll pour l'événement. Cette demande d'exception est accordée, car les avantages de Executor ne peuvent pas être réalisés dans cette situation.

Je ne veux pas que le code de l'application bloque mon thread lors de la publication de l'événement. Cette demande d'exception n'est généralement pas accordée pour le code qui s'exécute dans un processus d'application. Les applications qui ne respectent pas cette règle ne se font du tort qu'à elles-mêmes, sans impacter l'état général du système. Les applications qui le font correctement ou qui utilisent un framework de concurrence commun ne devraient pas subir de pénalités de latence supplémentaires.

Handler est cohérent au niveau local avec d'autres API similaires de la même classe. Cette demande d'exception est accordée au cas par cas. La préférence est que les surcharges basées sur Executor soient ajoutées, en migrant les implémentations Handler pour utiliser la nouvelle implémentation Executor. (myHandler::post est un Executor valide.) En fonction de la taille de la classe, du nombre de méthodes Handler existantes et de la probabilité que les développeurs aient besoin d'utiliser les méthodes Handler existantes en même temps que la nouvelle méthode, une exception peut être accordée pour ajouter une nouvelle méthode basée sur Handler.

Symétrie dans l'enregistrement

Si vous pouvez ajouter ou enregistrer un élément, vous devez également pouvoir le supprimer ou l'annuler. La méthode

registerThing(Thing)

doit avoir un

unregisterThing(Thing)

Fournir un identifiant de requête

S'il est raisonnable pour un développeur de réutiliser un rappel, fournissez un objet d'identifiant pour associer le rappel à la requête.

class RequestParameters {
  public int getId() { ... }
}

class RequestExecutor {
  public void executeRequest(
    RequestParameters parameters,
    Consumer<RequestParameters> onRequestCompletedListener) { ... }
}

Objets de rappel à méthodes multiples

Les rappels à méthodes multiples doivent privilégier interface et utiliser les méthodes default lors de l'ajout à des interfaces déjà publiées. Auparavant, ce guide recommandait abstract class en raison de l'absence de méthodes default dans Java 7.

public interface MostlyOptionalCallback {
  void onImportantAction();
  default void onOptionalInformation() {
    // Empty stub, this method is optional.
  }
}

Utiliser android.os.OutcomeReceiver pour modéliser un appel de fonction non bloquant

OutcomeReceiver<R,E> renvoie une valeur de résultat R en cas de succès ou E : Throwable dans le cas contraire, comme le ferait un simple appel de méthode. Utilisez OutcomeReceiver comme type de rappel lorsque vous convertissez une méthode bloquante qui renvoie un résultat ou lève une exception en méthode asynchrone non bloquante :

interface FooType {
  // Before:
  public FooResult requestFoo(FooRequest request);

  // After:
  public void requestFooAsync(FooRequest request, Executor executor,
      OutcomeReceiver<FooResult, Throwable> callback);
}

Les méthodes Async converties de cette manière renvoient toujours void. Tout résultat que requestFoo renverrait est plutôt signalé au OutcomeReceiver.onResult du paramètre callback de requestFooAsync en l'appelant sur le executor fourni. Toute exception que requestFoo générerait est signalée à la méthode OutcomeReceiver.onError de la même manière.

L'utilisation de OutcomeReceiver pour signaler les résultats des méthodes asynchrones fournit également un wrapper Kotlin suspend fun pour les méthodes asynchrones utilisant l'extension Continuation.asOutcomeReceiver de androidx.core:core-ktx :

suspend fun FooType.requestFoo(request: FooRequest): FooResult =
  suspendCancellableCoroutine { continuation ->
    requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
  }

Les extensions comme celle-ci permettent aux clients Kotlin d'appeler des méthodes asynchrones non bloquantes avec la simplicité d'un appel de fonction simple sans bloquer le thread d'appel. Ces extensions 1-1 pour les API de plate-forme peuvent être proposées dans l'artefact androidx.core:core-ktx dans Jetpack lorsqu'elles sont combinées avec des vérifications et des considérations de compatibilité de version standard. Pour en savoir plus, consultez la documentation sur asOutcomeReceiver, qui fournit des informations, des considérations sur l'annulation et des exemples.

Les méthodes asynchrones qui ne correspondent pas à la sémantique d'une méthode renvoyant un résultat ou générant une exception lorsque leur travail est terminé ne doivent pas utiliser OutcomeReceiver comme type de rappel. Envisagez plutôt l'une des autres options listées dans la section suivante.

Privilégier les interfaces fonctionnelles plutôt que de créer de nouveaux types SAM (Single Abstract Method)

Le niveau d'API 24 a ajouté les types java.util.function.* (documentation de référence), qui offrent des interfaces SAM génériques telles que Consumer<T>, qui peuvent être utilisées comme lambdas de rappel. Dans de nombreux cas, la création de nouvelles interfaces SAM n'apporte que peu de valeur en termes de sécurité des types ou de communication de l'intention, tout en élargissant inutilement la surface de l'API Android.

Envisagez d'utiliser ces interfaces génériques plutôt que d'en créer de nouvelles :

Emplacement des paramètres SAM

Les paramètres SAM doivent être placés en dernier pour permettre une utilisation idiomatique à partir de Kotlin, même si la méthode est surchargée avec des paramètres supplémentaires.

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Docs

Il s'agit de règles concernant la documentation publique (Javadoc) des API.

Toutes les API publiques doivent être documentées

Toutes les API publiques doivent disposer d'une documentation suffisante pour expliquer comment un développeur peut les utiliser. Supposons que le développeur a trouvé la méthode à l'aide de la saisie semi-automatique ou en parcourant la documentation de référence de l'API, et qu'il dispose d'un minimum de contexte à partir de la surface de l'API adjacente (par exemple, la même classe).

Méthodes

Les paramètres de méthode et les valeurs renvoyées doivent être documentés à l'aide des annotations de documentation @param et @return, respectivement. Mettez en forme le corps Javadoc comme s'il était précédé de "Cette méthode...".

Dans les cas où une méthode ne prend aucun paramètre, ne présente aucune considération particulière et renvoie ce que le nom de la méthode indique, vous pouvez omettre @return et rédiger une documentation semblable à celle-ci :

/**
 * Returns the priority of the thread.
 */
@IntRange(from = 1, to = 10)
public int getPriority() { ... }

Les documents doivent contenir des liens vers d'autres documents pour les constantes, les méthodes et d'autres éléments associés. Utilisez des tags Javadoc (par exemple, @see et {@link foo}), et pas seulement des mots en texte brut.

Pour l'exemple de source suivant :

public static final int FOO = 0;
public static final int BAR = 1;

N'utilisez pas de texte brut ni de police de code :

/**
 * Sets value to one of FOO or <code>BAR</code>.
 *
 * @param value the value being set, one of FOO or BAR
 */
public void setValue(int value) { ... }

Utilisez plutôt des liens :

/**
 * Sets value to one of {@link #FOO} or {@link #BAR}.
 *
 * @param value the value being set
 */
public void setValue(@ValueType int value) { ... }

Notez que l'utilisation d'une annotation IntDef telle que @ValueType sur un paramètre génère automatiquement une documentation spécifiant les types autorisés. Pour en savoir plus sur IntDef, consultez les conseils sur les annotations.

Exécuter la cible update-api ou docs lors de l'ajout de Javadoc

Cette règle est particulièrement importante lorsque vous ajoutez des balises @link ou @see. Assurez-vous que le résultat correspond à vos attentes. Les erreurs dans Javadoc sont souvent dues à des liens incorrects. La vérification est effectuée par la cible Make update-api ou docs, mais la cible docs peut être plus rapide si vous ne modifiez que Javadoc et que vous n'avez pas besoin d'exécuter la cible update-api.

Utilisez {@code foo} pour distinguer les valeurs Java.

Encapsulez les valeurs Java telles que true, false et null avec {@code...} pour les distinguer du texte de la documentation.

Lorsque vous rédigez de la documentation dans des sources Kotlin, vous pouvez encadrer le code avec des accents graves, comme vous le feriez pour Markdown.

Les résumés @param et @return doivent être des fragments de phrase uniques.

Les résumés des paramètres et des valeurs renvoyées doivent commencer par une lettre minuscule et ne contenir qu'un seul fragment de phrase. Si vous avez des informations supplémentaires qui dépassent une seule phrase, déplacez-les vers le corps du Javadoc de la méthode :

/**
 * @param e The element to be appended to the list. This must not be
 *       null. If the list contains no entries, this element will
 *       be added at the beginning.
 * @return This method returns true on success.
 */

Doit être remplacé par :

/**
 * @param e element to be appended to this list, must be non-{@code null}
 * @return {@code true} on success, {@code false} otherwise
 */

Les annotations Docs nécessitent des explications

Documentez pourquoi les annotations @hide et @removed sont masquées dans l'API publique. Incluez des instructions sur la façon de remplacer les éléments d'API marqués par l'annotation @deprecated.

Utiliser @throws pour documenter les exceptions

Si une méthode génère une exception vérifiée, par exemple IOException, documentez l'exception avec @throws. Pour les API provenant de Kotlin et destinées à être utilisées par des clients Java, annotez les fonctions avec @Throws.

Si une méthode génère une exception non vérifiée indiquant une erreur évitable, par exemple IllegalArgumentException ou IllegalStateException, documentez l'exception en expliquant pourquoi elle est générée. L'exception générée doit également indiquer pourquoi elle a été générée.

Certains cas d'exception non vérifiée sont considérés comme implicites et n'ont pas besoin d'être documentés, comme NullPointerException ou IllegalArgumentException où un argument ne correspond pas à une annotation @IntDef ou similaire qui intègre le contrat d'API dans la signature de la méthode :

/**
 * ...
 * @throws IOException If it cannot find the schema for {@code toVersion}
 * @throws IllegalStateException If the schema validation fails
 */
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
    boolean validateDroppedTables, Migration... migrations) throws IOException {
  // ...
  if (!dbPath.exists()) {
    throw new IllegalStateException("Cannot find the database file for " + name
        + ". Before calling runMigrations, you must first create the database "
        + "using createDatabase.");
  }
  // ...

Ou en Kotlin :

/**
 * ...
 * @throws IOException If something goes wrong reading the file, such as a bad
 *                     database header or missing permissions
 */
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
  // ...
  val read = input.read(buffer)
    if (read != 4) {
      throw IOException("Bad database header, unable to read 4 bytes at " +
          "offset 60")
    }
  }
  // ...

Si la méthode appelle du code asynchrone susceptible de générer des exceptions, réfléchissez à la manière dont le développeur peut les découvrir et y répondre. En règle générale, cela implique de transférer l'exception à un rappel et de documenter les exceptions générées sur la méthode qui les reçoit. Les exceptions asynchrones ne doivent pas être documentées avec @throws, sauf si elles sont réellement relancées à partir de la méthode annotée.

Terminez la première phrase des documents par un point.

L'outil Doclava analyse les documents de manière simpliste, en mettant fin au document de synopsis (la première phrase, utilisée dans la description rapide en haut des documents de classe) dès qu'il voit un point (.) suivi d'un espace. Cela pose deux problèmes :

  • Si un document court ne se termine pas par un point et que ce membre a hérité de documents qui sont détectés par l'outil, le synopsis reprend également ces documents hérités. Par exemple, consultez actionBarTabStyle dans la documentation R.attr, qui contient la description de la dimension ajoutée au synopsis.
  • Évitez "par exemple" dans la première phrase pour la même raison, car Doclava met fin à la documentation du synopsis après "g.". Par exemple, consultez TEXT_ALIGNMENT_CENTER dans View.java. Notez que Metalava corrige automatiquement cette erreur en insérant une espace insécable après le point. Toutefois, évitez de commettre cette erreur.

Mettre en forme des documents pour qu'ils soient affichés en HTML

Javadoc est rendu au format HTML. Mettez donc en forme ces documents en conséquence :

  • Les sauts de ligne doivent utiliser une balise <p> explicite. N'ajoutez pas de balise fermante </p>.

  • N'utilisez pas le code ASCII pour afficher des listes ou des tableaux.

  • Les listes doivent utiliser <ul> ou <ol> pour les listes non ordonnées et ordonnées, respectivement. Chaque élément doit commencer par une balise <li>, mais n'a pas besoin de balise de fermeture </li>. Une balise fermante </ul> ou </ol> est requise après le dernier élément.

  • Les tableaux doivent utiliser <table>, <tr> pour les lignes, <th> pour les en-têtes et <td> pour les cellules. Tous les tags de tableau doivent être accompagnés d'un tag de fermeture correspondant. Vous pouvez utiliser class="deprecated" sur n'importe quelle balise pour indiquer qu'elle est obsolète.

  • Pour créer une police de code intégré, utilisez {@code foo}.

  • Pour créer des blocs de code, utilisez <pre>.

  • Tout le texte à l'intérieur d'un bloc <pre> est analysé par le navigateur. Soyez donc prudent avec les crochets <>. Vous pouvez les échapper avec les entités HTML &lt; et &gt;.

  • Vous pouvez également laisser des crochets bruts <> dans votre extrait de code si vous entourez les sections concernées avec {@code foo}. Exemple :

    <pre>{@code <manifest>}</pre>
    

Suivre le guide de style de la documentation de référence de l'API

Pour assurer la cohérence du style des résumés de classe, des descriptions de méthodes, des descriptions de paramètres et d'autres éléments, suivez les recommandations des consignes officielles du langage Java sur la page How to Write Doc Comments for the Javadoc Tool (Comment rédiger des commentaires de documentation pour l'outil Javadoc).

Règles spécifiques au framework Android

Ces règles concernent les API, les modèles et les structures de données spécifiques aux API et aux comportements intégrés au framework Android (par exemple, Bundle ou Parcelable).

Les générateurs d'intent doivent utiliser le modèle create*Intent()

Les créateurs d'intents doivent utiliser des méthodes nommées createFooIntent().

Utiliser Bundle au lieu de créer des structures de données à usage général

Évitez de créer des structures de données à usage général pour représenter des mappages arbitraires de clés vers des valeurs typées. Utilisez plutôt Bundle.

Cela se produit généralement lors de l'écriture d'API de plate-forme qui servent de canaux de communication entre les applications et services non liés à la plate-forme, où la plate-forme ne lit pas les données envoyées sur le canal et où le contrat d'API peut être partiellement défini en dehors de la plate-forme (par exemple, dans une bibliothèque Jetpack).

Dans les cas où la plate-forme lit les données, évitez d'utiliser Bundle et préférez une classe de données fortement typée.

Les implémentations Parcelable doivent comporter un champ CREATOR public

Le gonflement Parcelable est exposé via CREATOR, et non via des constructeurs bruts. Si une classe implémente Parcelable, son champ CREATOR doit également être une API publique et le constructeur de classe prenant un argument Parcel doit être privé.

Utiliser CharSequence pour les chaînes d'interface utilisateur

Lorsqu'une chaîne est présentée dans une interface utilisateur, utilisez CharSequence pour autoriser les instances Spannable.

Si ce n'est qu'une clé ou un autre libellé ou valeur qui n'est pas visible par les utilisateurs, String convient.

Éviter d'utiliser des énumérations

IntDef doit être utilisé à la place des énumérations dans toutes les API de plate-forme et doit être fortement envisagé dans les API de bibliothèque non groupées. N'utilisez les énumérations que si vous êtes certain que de nouvelles valeurs ne seront pas ajoutées.

Avantages de IntDef :

  • Permet d'ajouter des valeurs au fil du temps
    • Les instructions when Kotlin peuvent échouer lors de l'exécution si elles ne sont plus exhaustives en raison d'une valeur d'énumération ajoutée dans la plate-forme.
  • Aucune classe ni aucun objet utilisés lors de l'exécution, uniquement des primitives
    • Bien que R8 ou la minification puissent éviter ce coût pour les API de bibliothèque non groupées, cette optimisation ne peut pas affecter les classes d'API de plate-forme.

Avantages de l'énumération

  • Fonctionnalité idiomatique du langage Java, Kotlin
  • Active l'utilisation exhaustive des instructions switch when
    • Remarque : Les valeurs ne doivent pas changer au fil du temps. Consultez la liste précédente.
  • Nommage clair et facile à trouver
  • Permet la validation au moment de la compilation
    • Par exemple, une instruction when en Kotlin qui renvoie une valeur
  • Il s'agit d'une classe fonctionnelle qui peut implémenter des interfaces, disposer d'assistants statiques, exposer des méthodes de membre ou d'extension, et exposer des champs.

Suivre la hiérarchie des couches de packages Android

La hiérarchie des packages android.* présente un ordre implicite, dans lequel les packages de niveau inférieur ne peuvent pas dépendre des packages de niveau supérieur.

Évitez de mentionner Google, d'autres entreprises et leurs produits.

La plate-forme Android est un projet Open Source qui vise à être neutre vis-à-vis des fournisseurs. L'API doit être générique et utilisable de la même manière par les intégrateurs système ou les applications disposant des autorisations requises.

Les implémentations Parcelable doivent être définitives

Les classes Parcelable définies par la plate-forme sont toujours chargées à partir de framework.jar. Il est donc incorrect qu'une application tente de remplacer une implémentation Parcelable.

Si l'application d'envoi étend un Parcelable, l'application de réception ne disposera pas de l'implémentation personnalisée de l'expéditeur pour le décompresser. Remarque sur la rétrocompatibilité : si votre classe n'était pas finale par le passé, mais ne disposait pas d'un constructeur accessible au public, vous pouvez toujours la marquer comme final.

Les méthodes appelant le processus système doivent relancer RemoteException en tant que RuntimeException

RemoteException est généralement générée par AIDL interne et indique que le processus système est arrêté ou que l'application tente d'envoyer trop de données. Dans les deux cas, l'API publique doit relancer l'exception RuntimeException pour empêcher les applications de conserver les décisions de sécurité ou de règles.

Si vous savez que l'autre côté d'un appel Binder est le processus système, ce code standard est la bonne pratique :

try {
    ...
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

Générer des exceptions spécifiques pour les modifications apportées à l'API

Le comportement des API publiques peut changer selon les niveaux d'API et entraîner des plantages d'applications (par exemple, pour appliquer de nouvelles règles de sécurité).

Lorsque l'API doit générer une exception pour une requête qui était auparavant valide, générez une nouvelle exception spécifique au lieu d'une exception générique. Par exemple, ExportedFlagRequired au lieu de SecurityException (et ExportedFlagRequired peut étendre SecurityException).

Cela aidera les développeurs d'applications et les outils à détecter les changements de comportement des API.

Implémenter un constructeur de copie au lieu d'un clone

L'utilisation de la méthode Java clone() est fortement déconseillée en raison du manque de contrats d'API fournis par la classe Object et des difficultés inhérentes à l'extension des classes qui utilisent clone(). Utilisez plutôt un constructeur de copie qui accepte un objet du même type.

/**
 * Constructs a shallow copy of {@code other}.
 */
public Foo(Foo other)

Les classes qui s'appuient sur un Builder pour la construction doivent envisager d'ajouter un constructeur de copie Builder pour permettre les modifications de la copie.

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

Utiliser ParcelFileDescriptor plutôt que FileDescriptor

L'objet java.io.FileDescriptor a une définition de propriété médiocre, ce qui peut entraîner des bugs obscurs d'utilisation après fermeture. À la place, les API doivent renvoyer ou accepter des instances ParcelFileDescriptor. Si nécessaire, l'ancien code peut effectuer des conversions entre PFD et FD à l'aide de dup() ou getFileDescriptor().

Éviter d'utiliser des valeurs numériques impaires

Évitez d'utiliser directement les valeurs short ou byte, car elles limitent souvent la façon dont vous pourrez faire évoluer l'API à l'avenir.

Évitez d'utiliser BitSet

java.util.BitSet est idéal pour l'implémentation, mais pas pour l'API publique. Il est mutable, nécessite une allocation pour les appels de méthode à haute fréquence et ne fournit pas de signification sémantique pour ce que chaque bit représente.

Pour les scénarios à hautes performances, utilisez un int ou un long avec @IntDef. Pour les scénarios de faible performance, envisagez un Set<EnumType>. Pour les données binaires brutes, utilisez byte[].

Préférez android.net.Uri

android.net.Uri est l'encapsulation recommandée pour les URI dans les API Android.

Évitez java.net.URI, car il est trop strict dans l'analyse des URI, et n'utilisez jamais java.net.URL, car sa définition de l'égalité est gravement défectueuse.

Masquer les annotations marquées comme @IntDef, @LongDef ou @StringDef

Les annotations marquées comme @IntDef, @LongDef ou @StringDef désignent un ensemble de constantes valides pouvant être transmises à une API. Toutefois, lorsqu'elles sont exportées en tant qu'API, le compilateur insère les constantes et seules les valeurs (désormais inutiles) restent dans le stub d'API de l'annotation (pour la plate-forme) ou le fichier JAR (pour les bibliothèques).

Par conséquent, les utilisations de ces annotations doivent être marquées avec l'annotation de documentation @hide dans la plate-forme ou l'annotation de code @RestrictTo.Scope.LIBRARY) dans les bibliothèques. Dans les deux cas, ils doivent être marqués @Retention(RetentionPolicy.SOURCE) pour ne pas apparaître dans les stubs d'API ni dans les fichiers JAR.

@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

Lors de la création du SDK de plate-forme et des AAR de bibliothèque, un outil extrait les annotations et les regroupe séparément des sources compilées. Android Studio lit ce format groupé et applique les définitions de type.

Ne pas ajouter de nouvelles clés de fournisseur de paramètres

N'exposez pas de nouvelles clés à partir de Settings.Global, Settings.System ou Settings.Secure.

À la place, ajoutez une API Java getter et setter appropriée dans une classe pertinente, qui est généralement une classe "manager". Ajoutez un mécanisme d'écouteur ou une diffusion pour informer les clients des modifications si nécessaire.

Les paramètres SettingsProvider présentent plusieurs problèmes par rapport aux getters/setters :

  • Aucune sécurité de type.
  • Il n'existe pas de méthode unifiée pour fournir une valeur par défaut.
  • Il n'existe aucun moyen de personnaliser les autorisations.
    • Par exemple, il n'est pas possible de protéger votre paramètre avec une autorisation personnalisée.
  • Aucun moyen approprié d'ajouter correctement une logique personnalisée.
    • Par exemple, il n'est pas possible de modifier la valeur du paramètre A en fonction de la valeur du paramètre B.

Exemple : Settings.Secure.LOCATION_MODE existe depuis longtemps, mais l'équipe chargée de la localisation l'a abandonné au profit d'une véritable API Java LocationManager.isLocationEnabled() et de la diffusion MODE_CHANGED_ACTION, qui ont donné à l'équipe beaucoup plus de flexibilité. La sémantique des API est désormais beaucoup plus claire.

Ne pas étendre Activity et AsyncTask

AsyncTask est un détail d'implémentation. À la place, exposez un écouteur ou, dans androidx, une API ListenableFuture.

Il est impossible de composer des sous-classes Activity. Étendue de l'activité pour votre fonctionnalité, ce qui la rend incompatible avec d'autres fonctionnalités qui exigent la même chose des utilisateurs. Utilisez plutôt la composition à l'aide d'outils tels que LifecycleObserver.

Utiliser getUser() du contexte

Les classes liées à un Context, comme tout ce qui est renvoyé par Context.getSystemService(), doivent utiliser l'utilisateur lié au Context au lieu d'exposer des membres qui ciblent des utilisateurs spécifiques.

class FooManager {
  Context mContext;

  void fooBar() {
    mIFooBar.fooBarForUser(mContext.getUser());
  }
}
class FooManager {
  Context mContext;

  Foobar getFoobar() {
    // Bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBarForUser(Process.myUserHandle());
  }

  Foobar getFoobar() {
    // Also bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBar();
  }

  Foobar getFoobarForUser(UserHandle user) {
    mIFooBar.fooBarForUser(user);
  }
}

Exception : Une méthode peut accepter un argument utilisateur si elle accepte des valeurs qui ne représentent pas un seul utilisateur, comme UserHandle.ALL.

Utiliser UserHandle au lieu d'entiers simples

Il est préférable d'utiliser UserHandle pour assurer la sécurité des types et éviter de confondre les ID utilisateur avec les UID.

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

Lorsque cela est inévitable, un int représentant un ID utilisateur doit être annoté avec @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

Préférer les écouteurs ou les rappels aux intents de diffusion

Les intents de diffusion sont très puissants, mais ils ont entraîné des comportements émergents qui peuvent avoir un impact négatif sur l'état du système. Par conséquent, les nouveaux intents de diffusion doivent être ajoutés avec discernement.

Voici quelques préoccupations spécifiques qui nous incitent à déconseiller l'introduction de nouvelles intentions de diffusion :

  • Lorsque vous envoyez des diffusions sans l'indicateur FLAG_RECEIVER_REGISTERED_ONLY, elles forcent le démarrage de toutes les applications qui ne sont pas déjà en cours d'exécution. Bien que cela puisse parfois être un résultat souhaité, cela peut entraîner une ruée de dizaines d'applications, ce qui a un impact négatif sur l'état du système. Nous vous recommandons d'utiliser d'autres stratégies, telles que JobScheduler, pour mieux coordonner le moment où différentes conditions préalables sont remplies.

  • Lorsque vous envoyez des diffusions, vous avez peu de possibilités de filtrer ou d'ajuster le contenu diffusé aux applications. Il est alors difficile, voire impossible, de répondre aux futurs problèmes de confidentialité ou d'introduire des changements de comportement en fonction du SDK cible de l'application réceptrice.

  • Étant donné que les files d'attente de diffusion sont une ressource partagée, elles peuvent être surchargées et ne pas permettre la diffusion de votre événement en temps voulu. Nous avons observé plusieurs files d'attente de diffusion en direct dont la latence de bout en bout est de 10 minutes ou plus.

Pour ces raisons, nous encourageons les nouvelles fonctionnalités à envisager d'utiliser des écouteurs ou des rappels, ou d'autres outils tels que JobScheduler au lieu d'intents de diffusion.

Dans les cas où les intents de diffusion restent la conception idéale, voici quelques bonnes pratiques à prendre en compte :

  • Si possible, utilisez Intent.FLAG_RECEIVER_REGISTERED_ONLY pour limiter votre diffusion aux applications déjà en cours d'exécution. Par exemple, ACTION_SCREEN_ON utilise cette conception pour éviter de réveiller les applications.
  • Si possible, utilisez Intent.setPackage() ou Intent.setComponent() pour cibler la diffusion sur une application spécifique qui vous intéresse. Par exemple, ACTION_MEDIA_BUTTON utilise cette conception pour se concentrer sur les commandes de lecture de l'application actuelle.
  • Si possible, définissez votre diffusion comme <protected-broadcast> pour empêcher les applications malveillantes d'usurper l'identité de l'OS.

Intents dans les services pour les développeurs liés au système

Les services destinés à être étendus par le développeur et liés par le système, par exemple les services abstraits tels que NotificationListenerService, peuvent répondre à une action Intent du système. Ces services doivent répondre aux critères suivants :

  1. Définissez une constante de chaîne SERVICE_INTERFACE sur la classe contenant le nom de classe complet du service. Cette constante doit être annotée avec @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION).
  2. Document sur la classe à laquelle un développeur doit ajouter un <intent-filter> à son AndroidManifest.xml pour recevoir des intents de la plate-forme.
  3. Nous vous recommandons vivement d'ajouter une autorisation au niveau du système pour empêcher les applications malveillantes d'envoyer des Intent aux services pour les développeurs.

Interopérabilité Kotlin-Java

Pour obtenir la liste complète des consignes, consultez le guide d'interopérabilité Kotlin-Java officiel d'Android. Certaines consignes ont été copiées dans ce guide pour améliorer la visibilité.

Visibilité de l'API

Certaines API Kotlin, comme suspend fun, ne sont pas destinées à être utilisées par les développeurs Java. Cependant, n'essayez pas de contrôler la visibilité spécifique à la langue à l'aide de @JvmSynthetic, car cela a des effets secondaires sur la façon dont l'API est présentée dans les débogueurs, ce qui rend le débogage plus difficile.

Pour obtenir des conseils spécifiques, consultez le guide d'interopérabilité Kotlin-Java ou le guide Async.

Objets compagnons

Kotlin utilise companion object pour exposer les membres statiques. Dans certains cas, ces éléments s'affichent à partir de Java sur une classe interne nommée Companion plutôt que sur la classe conteneur. Il est normal que les classes Companion apparaissent comme des classes vides dans les fichiers texte de l'API.

Pour maximiser la compatibilité avec Java, annotez les champs non constants des objets compagnons avec @JvmField et les fonctions publiques avec @JvmStatic pour les exposer directement sur la classe conteneur.

companion object {
  @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
  @JvmStatic fun fromPointF(pointf: PointF) {
    /* ... */
  }
}

Évolution des API de la plate-forme Android

Cette section décrit les règles concernant les types de modifications que vous pouvez apporter aux API Android existantes et la manière dont vous devez implémenter ces modifications pour maximiser la compatibilité avec les applications et les bases de code existantes.

Modifications destructives au niveau du binaire

Évitez les modifications destructives binaires dans les surfaces d'API publiques finalisées. Ces types de modifications génèrent généralement des erreurs lors de l'exécution de make update-api, mais il peut exister des cas extrêmes que la vérification de l'API de Metalava ne détecte pas. En cas de doute, consultez le guide Evolving Java-based APIs de l'Eclipse Foundation pour obtenir une explication détaillée des types de modifications d'API compatibles en Java. Les modifications destructives binaires apportées aux API masquées (par exemple, système) doivent suivre le cycle obsolescence/remplacement.

Modifications destructives de la source

Nous déconseillons les modifications destructives de la compatibilité source, même si elles ne sont pas destructives de la compatibilité binaire. Un exemple de modification compatible avec le binaire, mais qui casse la source, consiste à ajouter un générique à une classe existante. Cette modification est compatible avec le binaire, mais peut entraîner des erreurs de compilation en raison de l'héritage ou de références ambiguës. Les modifications incompatibles avec la source ne génèrent pas d'erreurs lors de l'exécution de make update-api. Vous devez donc veiller à comprendre l'impact des modifications apportées aux signatures d'API existantes.

Dans certains cas, des modifications incompatibles avec la source deviennent nécessaires pour améliorer l'expérience des développeurs ou l'exactitude du code. Par exemple, l'ajout d'annotations de possibilité de valeur nulle aux sources Java améliore l'interopérabilité avec le code Kotlin et réduit le risque d'erreurs, mais nécessite souvent des modifications (parfois importantes) du code source.

Modifications apportées aux API privées

Vous pouvez modifier les API annotées avec @TestApi à tout moment.

Vous devez conserver les API annotées avec @SystemApi pendant trois ans. Vous devez supprimer ou refactoriser une API système selon le calendrier suivant :

  • API y : ajoutée
  • API y+1 : arrêt
    • Marquez le code avec @Deprecated.
    • Ajoutez des remplacements et créez un lien vers le remplacement dans le Javadoc du code obsolète à l'aide de l'annotation @deprecated.
    • Pendant le cycle de développement, signalez les bugs aux utilisateurs internes en leur indiquant que l'API est en cours d'abandon. Cela permet de valider l'adéquation des API de remplacement.
  • API y+2 : suppression réversible
    • Marquez le code avec @removed.
    • Éventuellement, générez une exception ou une opération sans effet pour les applications qui ciblent le niveau de SDK actuel pour la version.
  • API y+3 : suppression définitive
    • Supprimez complètement le code de l'arborescence source.

Obsolescence

Nous considérons la suppression comme une modification de l'API, qui peut se produire dans une version majeure (par exemple, une version alphabétique). Utilisez les annotations de source @Deprecated et de documentation @deprecated <summary> ensemble lorsque vous déconseillez des API. Votre résumé doit inclure une stratégie de migration. Cette stratégie peut renvoyer vers une API de remplacement ou expliquer pourquoi vous ne devez pas utiliser l'API :

/**
 * 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)

Vous devez également rendre obsolètes les API définies dans XML et exposées dans Java, y compris les attributs et les propriétés stylables exposés dans la classe android.R, avec un récapitulatif :

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

Quand abandonner une API

Les obsolescences sont particulièrement utiles pour décourager l'adoption d'une API dans un nouveau code.

Nous vous demandons également de marquer les API comme @deprecated avant qu'elles ne soient @removed, mais cela n'incite pas fortement les développeurs à migrer depuis une API qu'ils utilisent déjà.

Avant d'abandonner une API, réfléchissez à l'impact sur les développeurs. Voici les conséquences de l'abandon d'une API :

  • javac émet un avertissement lors de la compilation.
    • Les avertissements de suppression ne peuvent pas être supprimés globalement ni servir de référence. Les développeurs qui utilisent -Werror doivent donc corriger ou supprimer chaque utilisation d'une API obsolète avant de pouvoir mettre à jour la version du SDK de compilation.
    • Les avertissements d'obsolescence concernant les importations de classes obsolètes ne peuvent pas être supprimés. Par conséquent, les développeurs doivent insérer le nom de classe complet pour chaque utilisation d'une classe obsolète avant de pouvoir mettre à jour leur version du SDK de compilation.
  • La documentation sur d.android.com affiche un avis d'abandon.
  • Les IDE tels qu'Android Studio affichent un avertissement sur le site d'utilisation de l'API.
  • Les IDE peuvent rétrograder ou masquer l'API dans la saisie semi-automatique.

Par conséquent, la suppression d'une API peut dissuader les développeurs les plus soucieux de la santé du code (ceux qui utilisent -Werror) d'adopter de nouveaux SDK. Les développeurs qui ne se soucient pas des avertissements dans leur code existant sont susceptibles d'ignorer complètement les obsolescences.

Un SDK qui introduit un grand nombre d'obsolescences aggrave ces deux cas.

C'est pourquoi nous vous recommandons de n'abandonner les API que dans les cas suivants :

  • Nous prévoyons de @remove l'API dans une prochaine version.
  • L'utilisation de l'API entraîne un comportement incorrect ou indéfini que nous ne pouvons pas corriger sans rompre la compatibilité.

Lorsque vous abandonnez une API et la remplacez par une nouvelle, nous vous recommandons vivement d'ajouter une API de compatibilité correspondante à une bibliothèque Jetpack telle que androidx.core pour simplifier la prise en charge des anciens et des nouveaux appareils.

Nous ne vous recommandons pas d'abandonner les API qui fonctionnent comme prévu dans les versions actuelles et futures :

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

public void doThing(int action, @Nullable Bundle extras) {
  ...
}

L'abandon est approprié dans les cas où les API ne peuvent plus maintenir leurs comportements documentés :

/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

Modifications apportées aux API obsolètes

Vous devez maintenir le comportement des API obsolètes. Cela signifie que les implémentations de test doivent rester les mêmes et que les tests doivent continuer à réussir après l'abandon de l'API. Si l'API ne comporte pas de tests, vous devez en ajouter.

Ne développez pas les surfaces d'API obsolètes dans les prochaines versions. Vous pouvez ajouter des annotations de correction lint (par exemple, @Nullable) à une API obsolète existante, mais vous ne devez pas ajouter de nouvelles API.

N'ajoutez pas de nouvelles API comme obsolètes. Si des API ont été ajoutées et ensuite obsolètes au cours d'un cycle de préversion (et entreraient donc initialement dans la surface de l'API publique en tant qu'obsolètes), vous devez les supprimer avant de finaliser l'API.

Suppression logicielle

La suppression logicielle est une modification destructive de la source. Vous devez l'éviter dans les API publiques, sauf si le Conseil des API l'approuve explicitement. Pour les API système, vous devez les rendre obsolètes pendant la durée d'une version majeure avant de les supprimer de manière progressive. Supprimez toutes les références aux API dans la documentation et utilisez l'annotation @removed <summary> lorsque vous supprimez les API de manière temporaire. Votre résumé doit inclure la raison de la suppression et peut inclure une stratégie de migration, comme nous l'avons expliqué dans Obsolescence.

Le comportement des API supprimées de manière réversible peut être conservé tel quel, mais il doit surtout être préservé de sorte que les appelants existants ne plantent pas lorsqu'ils appellent l'API. Dans certains cas, cela peut signifier la préservation du comportement.

La couverture des tests doit être maintenue, mais le contenu des tests peut avoir besoin d'être modifié pour tenir compte des changements de comportement. Les tests doivent toujours valider que les appelants existants ne plantent pas au moment de l'exécution. Vous pouvez conserver le comportement des API supprimées de manière réversible tel quel, mais surtout, vous devez le préserver de sorte que les appelants existants ne plantent pas lorsqu'ils appellent l'API. Dans certains cas, cela peut signifier préserver le comportement.

Vous devez maintenir la couverture des tests, mais le contenu des tests peut avoir besoin d'être modifié pour s'adapter aux changements de comportement. Les tests doivent toujours valider que les appelants existants ne plantent pas au moment de l'exécution.

D'un point de vue technique, nous supprimons l'API du fichier JAR du stub SDK et du classpath de compilation à l'aide de l'annotation Javadoc @remove, mais elle existe toujours dans le classpath d'exécution, comme les API @hide :

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

Du point de vue d'un développeur d'applications, l'API n'apparaît plus dans la saisie semi-automatique et le code source qui fait référence à l'API ne se compilera pas lorsque compileSdk sera égal ou supérieur au SDK dans lequel l'API a été supprimée. Toutefois, le code source continue de se compiler correctement par rapport aux SDK antérieurs, et les binaires qui font référence à l'API continuent de fonctionner.

Certaines catégories d'API ne doivent pas être supprimées de manière réversible. Vous ne devez pas supprimer temporairement certaines catégories d'API.

Méthodes abstraites

Vous ne devez pas supprimer de manière réversible les méthodes abstraites des classes que les développeurs peuvent étendre. Cela empêche les développeurs d'étendre correctement la classe à tous les niveaux du SDK.

Dans les rares cas où il n'a jamais été et ne sera jamais possible pour les développeurs d'étendre une classe, vous pouvez toujours supprimer en douceur les méthodes abstraites.

Suppression définitive

La suppression définitive est une modification qui casse la compatibilité binaire et ne doit jamais se produire dans les API publiques.

Annotation déconseillée

Nous utilisons l'annotation @Discouraged pour indiquer que nous ne recommandons pas une API dans la plupart des cas (plus de 95 %). Les API déconseillées diffèrent des API obsolètes en ce qu'il existe un cas d'utilisation critique étroit qui empêche l'obsolescence. Lorsque vous marquez une API comme déconseillée, vous devez fournir une explication et une solution alternative :

@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 (such as `R.foo.bar`) than by name (such as
                        `getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

Vous ne devez pas ajouter de nouvelles API déconseillées.

Modifications apportées au comportement des API existantes

Dans certains cas, vous pouvez modifier le comportement d'implémentation d'une API existante. Par exemple, dans Android 7.0, nous avons amélioré DropBoxManager pour indiquer clairement aux développeurs lorsqu'ils tentaient de publier des événements trop volumineux pour être envoyés sur Binder.

Toutefois, pour éviter de causer des problèmes aux applications existantes, nous vous recommandons vivement de conserver un comportement sûr pour les anciennes applications. Historiquement, nous avons protégé ces changements de comportement en fonction du ApplicationInfo.targetSdkVersion de l'application, mais nous avons récemment migré pour exiger l'utilisation du framework de compatibilité des applications. Voici un exemple d'implémentation d'un changement de comportement à l'aide de ce nouveau 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
    }
  }
}

L'utilisation de cette conception du framework de compatibilité des applications permet aux développeurs de désactiver temporairement des modifications de comportement spécifiques lors des versions Preview et bêta dans le cadre du débogage de leurs applications, au lieu de les obliger à s'adapter à des dizaines de modifications de comportement simultanément.

Compatibilité ascendante

La compatibilité ascendante est une caractéristique de conception qui permet à un système d'accepter des entrées destinées à une version ultérieure de lui-même. Dans le cas de la conception d'API, vous devez accorder une attention particulière à la conception initiale ainsi qu'aux modifications futures, car les développeurs s'attendent à écrire du code une seule fois, à le tester une seule fois et à ce qu'il s'exécute partout sans problème.

Voici les causes les plus courantes des problèmes de compatibilité ascendante dans Android :

  • Ajout de nouvelles constantes à un ensemble (tel que @IntDef ou enum) précédemment supposé complet (par exemple, lorsque switch comporte un default qui génère une exception).
  • Ajout de la prise en charge d'une fonctionnalité qui n'est pas directement capturée dans la surface de l'API (par exemple, la prise en charge de l'attribution de ressources de type ColorStateList dans XML alors que seules les ressources <color> étaient auparavant prises en charge).
  • Assouplissement des restrictions sur les vérifications d'exécution, par exemple en supprimant une vérification requireNotNull() qui était présente dans les versions antérieures.

Dans tous ces cas, les développeurs ne découvrent qu'à l'exécution qu'il y a un problème. Pire encore, ils pourraient le découvrir à la suite de rapports d'erreur provenant d'anciens appareils sur le terrain.

De plus, ces cas sont tous des modifications d'API techniquement valides. Elles ne rompent pas la compatibilité binaire ni la compatibilité du code source, et le lint de l'API ne détecte aucun de ces problèmes.

Par conséquent, les concepteurs d'API doivent faire très attention lorsqu'ils modifient des classes existantes. Posez-vous la question suivante : "Cette modification va-t-elle entraîner l'échec du code écrit et testé uniquement par rapport à la dernière version de la plate-forme sur les versions antérieures ?"

Schémas XML

Si un schéma XML sert d'interface stable entre les composants, ce schéma doit être spécifié explicitement et doit évoluer de manière rétrocompatible, comme les autres API Android. Par exemple, la structure des éléments et des attributs XML doit être conservée de la même manière que les méthodes et les variables sont conservées sur d'autres surfaces d'API Android.

Abandon du format XML

Si vous souhaitez abandonner un élément ou un attribut XML, vous pouvez ajouter le marqueur xs:annotation, mais vous devez continuer à prendre en charge les fichiers XML existants en suivant le cycle de vie d'évolution @SystemApi habituel.

<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

Les types d'éléments doivent être conservés

Les schémas acceptent les éléments sequence, choice et all en tant qu'éléments enfants de l'élément complexType. Toutefois, ces éléments enfants diffèrent par le nombre et l'ordre de leurs éléments enfants. La modification d'un type existant serait donc un changement incompatible.

Si vous souhaitez modifier un type existant, la bonne pratique consiste à rendre l'ancien type obsolète et à introduire un nouveau type pour le remplacer.

<!-- Original "sequence" value -->
<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

<!-- New "choice" value -->
<xs:element name="fooChoice">
    <xs:complexType>
        <xs:choice>
            <xs:element name="name" type="xs:string"/>
        </xs:choice>
    </xs:complexType>
</xs:element>

Schémas spécifiques à la ligne principale

Mainline est un projet qui permet de mettre à jour individuellement les sous-systèmes ("modules Mainline") de l'OS Android, plutôt que de mettre à jour l'intégralité de l'image système.

Les modules Mainline doivent être "dégroupés" de la plate-forme principale, ce qui signifie que toutes les interactions entre chaque module et le reste du monde doivent être effectuées à l'aide d'API formelles (publiques ou système).

Les modules mainline doivent suivre certains modèles de conception. Cette section les décrit.

Le modèle <Module>FrameworkInitializer

Si un module mainline doit exposer des classes @SystemService (par exemple, JobScheduler), utilisez le format suivant :

  • Exposez une classe <YourModule>FrameworkInitializer à partir de votre module. Cette classe doit se trouver dans $BOOTCLASSPATH. Exemple : StatsFrameworkInitializer

  • Marquez-le avec @SystemApi(client = MODULE_LIBRARIES).

  • Ajoutez-y une méthode public static void registerServiceWrappers().

  • Utilisez SystemServiceRegistry.registerContextAwareService() pour enregistrer une classe de gestionnaire de services lorsqu'elle a besoin d'une référence à un Context.

  • Utilisez SystemServiceRegistry.registerStaticService() pour enregistrer une classe de gestionnaire de services lorsqu'elle n'a pas besoin de référence à un Context.

  • Appelez la méthode registerServiceWrappers() à partir de l'initialiseur statique de SystemServiceRegistry.

Le modèle <Module>ServiceManager

Normalement, pour enregistrer des objets binder de service système ou obtenir des références à ceux-ci, il faut utiliser ServiceManager, mais les modules mainline ne peuvent pas l'utiliser, car il est masqué. Cette classe est masquée, car les modules principaux ne sont pas censés enregistrer ni faire référence aux objets binder de service système exposés par la plate-forme statique ou par d'autres modules.

Les modules Mainline peuvent utiliser le modèle suivant pour pouvoir enregistrer et obtenir des références aux services Binder implémentés dans le module.

  • Créez une classe <YourModule>ServiceManager en suivant la conception de TelephonyServiceManager.

  • Exposez la classe en tant que @SystemApi. Si vous n'avez besoin d'y accéder qu'à partir des classes $BOOTCLASSPATH ou des classes de serveur système, vous pouvez utiliser @SystemApi(client = MODULE_LIBRARIES). Sinon, @SystemApi(client = PRIVILEGED_APPS) fonctionnera.

  • Ce cours se compose des éléments suivants :

    • Constructeur masqué, de sorte que seul le code de plate-forme statique peut l'instancier.
    • Méthodes getter publiques qui renvoient une instance ServiceRegisterer pour un nom spécifique. Si vous avez un objet binder, vous avez besoin d'une méthode getter. Si vous en avez deux, vous avez besoin de deux getters.
    • Dans ActivityThread.initializeMainlineModules(), instanciez cette classe et transmettez-la à une méthode statique exposée par votre module. Normalement, vous ajoutez une API @SystemApi(client = MODULE_LIBRARIES) statique dans votre classe FrameworkInitializer qui la prend.

Ce modèle empêcherait d'autres modules principaux d'accéder à ces API, car il n'existe aucun moyen pour les autres modules d'obtenir une instance de <YourModule>ServiceManager, même si les API get() et register() leur sont visibles.

Voici comment la téléphonie obtient une référence au service de téléphonie : lien de recherche de code.

Si votre application implémente un objet binder de service en code natif, vous utilisez les API natives AServiceManager. Ces API correspondent aux API Java ServiceManager, mais les API natives sont directement exposées aux modules principaux. N'utilisez pas ces méthodes pour enregistrer ou faire référence à des objets Binder qui n'appartiennent pas à votre module. Si vous exposez un objet Binder à partir du code natif, votre <YourModule>ServiceManager.ServiceRegisterer n'a pas besoin de méthode register().

Définitions des autorisations dans les modules Mainline

Les modules Mainline contenant des APK peuvent définir des autorisations (personnalisées) dans leur APK AndroidManifest.xml de la même manière qu'un APK normal.

Si l'autorisation définie n'est utilisée qu'en interne dans un module, son nom doit être préfixé avec le nom du package APK, par exemple :

<permission
    android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
    android:protectionLevel="signature" />

Si l'autorisation définie doit être fournie dans le cadre d'une API de plate-forme pouvant être mise à jour à d'autres applications, son nom doit être précédé de "android.permission.". (comme toute autorisation de plate-forme statique) plus le nom du package du module, pour indiquer qu'il s'agit d'une API de plate-forme provenant d'un module tout en évitant tout conflit de nommage, par exemple :

<permission
    android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
    android:label="@string/active_calories_burned_read_content_description"
    android:protectionLevel="dangerous"
    android:permissionGroup="android.permission-group.HEALTH" />

Le module peut ensuite exposer ce nom d'autorisation en tant que constante d'API dans sa surface d'API, par exemple HealthPermissions.READ_ACTIVE_CALORIES_BURNED.