Consignes concernant les API Android

Cette page est destinée à aider les développeurs à comprendre les principes généraux que le Conseil des API applique lors des examens des 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 de nombreuses de ces règles dans les vérifications qu'il effectue sur les API.

Il s'agit d'un guide des règles respectées par cet outil lint, ainsi que de 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 l'intégration continue. Vous pouvez l'exécuter manuellement à partir d'un vérificateur de plate-forme local à l'aide de m checkapi ou d'un vérificateur AndroidX local à l'aide de ./gradlew :path:to:project:checkApi.

Règles des API

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

Par conséquent, il est possible que certaines API existantes ne respectent pas ces consignes. Dans d'autres cas, une nouvelle API peut offrir une meilleure expérience utilisateur aux développeurs d'applications si elle reste cohérente avec les API existantes plutôt que de respecter strictement les consignes.

Faites preuve de bon sens et contactez le Conseil des API si des questions difficiles concernant une API doivent être résolues ou si des consignes doivent être mises à jour.

Principes de base des API

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

Toutes les API doivent être implémentées

Indépendamment de 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 bouchons d'API avec une implémentation à venir.

Les surfaces d'API sans implémentation présentent 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 ou utilisée par les clients, il n'est pas possible 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 est conforme 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 garantit que la surface de l'API est utilisable et que nous avons répondu aux cas d'utilisation attendus. Tester l'existence n'est pas suffisant. Le comportement de l'API elle-même doit être testé.

Une modification qui ajoute une nouvelle API doit inclure les tests correspondants dans le même sujet CL ou Gerrit.

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

Toutes les API doivent être documentées

La documentation est un élément clé de l'usabilité 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 derrière 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'est pas possible d'implémenter des classes de valeurs finales ni de 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 lors de l'écriture d'API publiques.

Respecter les conventions de codage standards, sauf indication contraire

Vous trouverez ici les conventions de codage Android pour les contributeurs externes:

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

De manière générale, nous avons tendance à suivre les conventions de codage Java et Kotlin standards.

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

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 les détails de l'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 des éléments d'API dans votre sous-classe qui peuvent ne pas être appropriés. Par exemple, une nouvelle sous-classe publique de FrameLayout se présente comme FrameLayout, avec les nouveaux comportements et éléments d'API. Si cette API héritée n'est pas adaptée à 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 une exception UnsupportedOperationException, réexaminez la classe de base que vous utilisez.

Utiliser les classes de collections de base

Que vous utilisiez une collection en tant qu'argument ou que vous la renvoyiez en tant que valeur, privilégiez 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 composée d'éléments uniques.

En Kotlin, privilégiez les collections immuables. Pour en savoir plus, consultez la section Modificabilité des collections.

Classes abstraites et interfaces

Java 8 prend en charge 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 la plate-forme et toutes les bibliothèques Jetpack doivent cibler Java 8 ou version ultérieure.

Lorsque 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 est 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.

Lorsque des méthodes établissent un pont entre plusieurs classes, attribuez à la classe contenant un nom explicite qui explique son rôle.

Dans des cas très limités, 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 le rétroportage des info-bulles nécessite de conserver l'état associé à un View et d'appeler plusieurs méthodes sur le View pour installer le rétroportage, TooltipHelper est un nom de classe acceptable.

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

Conservez le code généré par l'IDL en tant que 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 d'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) et l'outil AIDL n'est pas conçu explicitement pour maintenir la compatibilité des API de 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 de liaison

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 à l'aide de la mise en lot ou du streaming, à l'aide de la mémoire partagée ou d'un autre type de mémoire. Aucune de ces opérations ne peut être effectuée si votre interface AIDL est également l'API publique.

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

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

Encapsulez plutôt 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 plus tard, l'interface interne peut être minimale et des surcharges pratiques peuvent être ajoutées à l'API publique. Vous pouvez utiliser la couche de wrapper 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 les applications), l'exigence d'une interface IPC stable, publiée et versionnée signifie qu'il est beaucoup plus difficile d'évoluer l'interface elle-même. Toutefois, il est toujours utile d'avoir une couche de wrapper autour de celle-ci, pour correspondre aux autres consignes relatives aux API et pour faciliter l'utilisation de la même API publique pour une nouvelle version de l'interface IPC, si cela devient nécessaire.

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

Un objet Binder n'a aucune signification 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 gestion doivent être définitives

Les classes de gestionnaire doivent être déclarées comme final. Les classes de gestionnaires communiquent avec les services système et constituent le point d'interaction unique. Aucune personnalisation n'est requise. Déclarez-la comme final.

Ne pas utiliser CompletableFuture ou Future

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

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

Dans le code de la plate-forme et les API de bibliothèque de bas niveau utilisées à la fois par Kotlin et Java, privilégiez une combinaison d'un rappel de fin, Executor, et si l'API prend en charge l'annulation, CancellationSignal.

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

Si vous ciblez Kotlin, privilégiez 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 "Facultatif"

Bien que Optional puisse présenter des avantages dans certaines surfaces d'API, il n'est pas cohérent avec la surface d'API Android existante. @Nullable et @NonNull fournissent une assistance d'outils pour la sécurité de 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 exception 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 ne contenant 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 Singleton sont déconseillés, car ils présentent les inconvénients suivants en termes de test:

  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.

Privilégiez 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 fausse version de SingleInstance et utiliser leur propre framework d'injection de dépendances pour gérer l'implémentation sans avoir à créer de wrapper, ou la bibliothèque peut fournir sa propre simulation 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'UI d'Android est désormais axé sur Compose. 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 Jetpack Compose et éventuellement des composants d'UI basés sur des vues pour les développeurs dans les bibliothèques Jetpack. L'offre de ces composants dans des bibliothèques offre des possibilités d'implémentations rétroportées 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, que ces champs soient finaux ou non.

Les exceptions rares incluent les structures de données de base où 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 à l'aide de 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 définitifs

Il est fortement déconseillé d'utiliser des champs bruts (@voir Ne pas exposer de champs bruts). Toutefois, dans les rares cas 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

Les indicateurs impliquent des bits pouvant être combinés dans une valeur d'union. Dans le cas contraire, n'appelez pas la variable ou 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 de constantes d'indicateurs publics, consultez @IntDef pour les indicateurs de masque de bits.

Les constantes finales statiques doivent utiliser une convention d'attribution de noms en majuscules, séparées par des tirets bas.

Tous les mots de la constante doivent être en majuscules et les différents mots 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

La plupart des constantes utilisées dans Android sont destinées à des éléments standards, tels que les indicateurs, les clés et les actions. Ces constantes doivent comporter des préfixes standards pour les identifier plus facilement comme telles.

Par exemple, les éléments supplémentaires 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 des 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"

ne sont pas nommés de manière cohérente ni n'ont pas de portée appropriée. À la place, réfléchissez aux points suivants:

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

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

Les actions et les extras d'intent, ainsi que les entrées de bundle, doivent être définis dans 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

Les constantes associées doivent toutes commencer par le même préfixe. Par exemple, pour un ensemble de constantes à utiliser avec des 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 d'attribution de noms 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 respecter la convention d'attribution de noms 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 drawable ne doivent pas être exposées en tant qu'API publiques. Toutefois, si elles doivent être exposées, les mises en page et les éléments drawable publics doivent être nommés à l'aide de la convention de dénomination sous_barre, par exemple layout/simple_list_item_1.xml ou drawable/title_bar_tall.xml.

Lorsque des constantes peuvent changer, faites-les évoluer de manière dynamique

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

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

Tenir compte de la rétrocompatibilité des rappels

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

Source de 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 une hypothèse (quelque peu) raisonnable selon laquelle 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 de manière sécurisée des cas comme celui-ci 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 prévoir si une liste de constantes peut changer à l'avenir. Si vous définissez une API avec une constante UNKNOWN ou UNSPECIFIED qui ressemble à une valeur générique, les développeurs supposent que les constantes publiées au moment de la rédaction de leur application sont exhaustives. Si vous ne souhaitez pas définir cette attente, réfléchissez à nouveau à l'opportunité d'utiliser une constante fourre-tout pour votre API.

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

Constante d'entier ou de chaîne

Utilisez des constantes entières et @IntDef si l'espace de noms des 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 petit ensemble de fonctions utilitaires 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é de l'API de langage ni la compatibilité binaire pour le code généré. À la place, implémentez 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 en a beaucoup.

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. Il peut également être utile de fournir un compilateur lorsque vous ciblez des 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 à la fois 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 de Kotlin, par exemple User(var1=Alex, var2=42).

Méthodes

Il s'agit de règles concernant diverses spécificités des méthodes, concernant les paramètres, les noms de méthode, les types de retour et les spécificateurs d'accès.

Durée

Ces règles couvrent la façon dont les concepts temporels tels que les dates et la durée 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 via le désucrage et doivent être privilégiés pour exprimer le temps dans les paramètres d'API ou les valeurs de retour.

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 tel que 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 impliquée, nommez le paramètre "durée", et non "temps".

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

Exceptions:

"timeout" est approprié lorsque la durée s'applique spécifiquement à une valeur de délai avant expiration.

"time" avec un type java.time.Instant est approprié lorsque vous faites référence à un moment spécifique, et non à une durée.

Les méthodes exprimant des durées ou du temps en tant que primitive doivent être nommées avec leur unité de temps et utiliser long

Les méthodes acceptant ou renvoyant des durées en tant que primitive doivent ajouter un suffixe au nom de la méthode avec les unités de temps associées (par exemple, Millis, Nanos, Seconds) pour réserver le nom non décoré à l'utilisation avec java.time.Duration. Consultez la section 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 millisecondes depuis le 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong: la valeur est un code temporel non négatif mesuré en secondes depuis le 1970-01-01T00:00:00Z.
  • @DurationMillisLong: la valeur correspond à 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 temporels primitifs ou les valeurs de retour 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 des arguments de temps longs

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

  • @CurrentTimeMillisLong: la valeur est un code temporel non négatif mesuré en tant que nombre de millisecondes depuis 1970-01-01T00:00:00Z, donc dans la base temporelle 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 correspond à 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, privilégiez les préfixes d'unités 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 respectez l'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 des 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, attribuez-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 par défaut des paramètres (Kotlin uniquement)

Si une méthode a été fournie avec un paramètre avec une valeur par défaut, la suppression de la valeur par défaut est une modification incompatible avec la source.

Les paramètres de méthode les plus distinctifs et les plus identifiants doivent être les premiers.

Si une méthode comporte 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 une action est effectuée. S'il existe un rappel de fin, 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);

Consultez également 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
  • Un grand nombre de propriétés sont obligatoires, par exemple de nombreux arguments de constructeur.
  • Il existe une relation complexe entre les propriétés au moment de la création. Par exemple, une étape de validation est 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 de site Web. Les générateurs sont utiles dans une surface d'API s'ils sont utilisés pour:

  • Configurer seulement quelques-uns d'un ensemble potentiellement important de paramètres de création facultatifs
  • Configurer de nombreux paramètres de création facultatifs ou obligatoires, parfois de types similaires ou correspondants, dans lesquels les sites d'appel pourraient autrement devenir difficiles à lire ou à écrire
  • 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 au générateur en tant que détails d'implémentation.
  • Autoriser la croissance d'un type en ajoutant des paramètres de création facultatifs supplémentaires dans les futures versions de l'API

Si vous disposez d'un type avec trois paramètres obligatoires ou moins et sans paramètres facultatifs, vous pouvez presque toujours ignorer un compilateur et utiliser un constructeur simple.

Les classes provenant de Kotlin doivent privilégier les constructeurs annotés @JvmOverloads avec des arguments par défaut aux constructeurs, mais peuvent choisir d'améliorer l'usabilité pour les clients Java en fournissant également des constructeurs 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 de constructeur doivent activer l'enchaînement de méthodes en renvoyant l'objet Builder (tel que this) à partir de chaque méthode, sauf 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 de 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 de compilateur doivent être créées via un constructeur

Pour assurer une création de compilateur cohérente via la surface de l'API Android, tous les compilateurs doivent être créés via un constructeur et non une méthode de création statique. Pour les API basées sur Kotlin, le Builder doit être public, même si les utilisateurs Kotlin sont censés s'appuyer implicitement sur le générateur via un mécanisme de création de type méthode de fabrique/DSL. Les bibliothèques ne doivent pas utiliser @PublishedApi internal pour masquer sélectivement le constructeur de la classe Builder aux 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 (par exemple, @NonNull)

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

Les classes de compilateur doivent être des classes internes statiques finales de leurs types de compilation

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

Les compilateurs 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é existant. Elles 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 du générateur doivent accepter des arguments @Nullable si le générateur dispose d'un constructeur de copie

La réinitialisation est essentielle si une nouvelle instance d'un compilateur 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 du compilateur peuvent accepter des arguments @Nullable pour les propriétés facultatives

Il est souvent plus simple d'utiliser une valeur nullable pour une entrée de deuxième degré, en particulier en Kotlin, qui utilise des arguments par défaut au lieu de constructeurs et de surcharges.

De plus, les setters @Nullable les mettront en correspondance avec 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 à la fois 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 modifiables lorsque des setters sont disponibles dans la classe compilée.

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

Ensuite, si vous êtes certain d'avoir besoin de propriétés modifiables, décidez lequel des scénarios suivants convient le mieux à votre cas d'utilisation prévu:

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

    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éé puisse être utile. Par conséquent, les setters ne doivent pas être fournis pour les propriétés modifiables.

    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 compilateurs ne doivent pas avoir de getters

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

Les setters du générateur doivent avoir des getters correspondants dans la classe généré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();
}

Nom des méthodes du générateur

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

Les classes de compilation doivent déclarer une méthode build().

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

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

La méthode build() d'un constructeur doit 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 exception 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é permet d'utiliser votre objet ou votre classe comme verrou. Comme il est exposé à d'autres, vous pouvez rencontrer des effets secondaires inattendus si un autre code en dehors de votre classe commence à l'utiliser à des fins de verrouillage.

Effectuez plutôt le 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 accesseur doivent respecter 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) sont é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 généralement aux attentes des développeurs concernant le comportement des méthodes d'accès, les méthodes utilisant des préfixes de méthode d'accès doivent se comporter de manière similaire aux champs Java. Évitez d'utiliser des préfixes de style accesseur dans les cas suivants:

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

Notez que l'exécution d'une tâche gourmande en calcul une seule fois et la mise en cache de la valeur pour les appels suivants est toujours considérée comme une tâche gourmande en calcul. Le saccade n'est pas amorti sur les images.

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 règle générale, les noms de méthode et de variable booléens doivent être écrits sous forme de questions auxquelles la valeur renvoyée répond.

Les méthodes d'accès booléennes Java doivent suivre un schéma de dénomination set/is, et les champs doivent préférer is, comme dans:

// 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'accès Java ou de is pour les champs Java vous permet de les utiliser 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'accès doivent généralement utiliser un nom positif, par exemple Enabled plutôt que Disabled. L'utilisation d'une terminologie négative inverse le sens de true et false, ce qui rend le raisonnement sur le comportement plus difficile.

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

Lorsque la valeur booléenne décrit l'inclusion ou la propriété d'une propriété, vous pouvez utiliser has plutôt que is. Toutefois, cela ne fonctionne 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 plus approprié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 (Compatible) ou Required (Obligatoire) :

// "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 règle générale, les noms de méthode 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 à l'aide d'une règle cohérente: ajoutez get au début et mettez le premier caractère en majuscule pour le getter, et ajoutez set au début et mettez le premier caractère en majuscule pour le setter. La déclaration de propriété génère 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 lors de la génération du nom: 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, nommez de préférence les propriétés Boolean avec un préfixe is pour respecter les consignes d'attribution de noms:

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 la définition des indicateurs de masque de bits dans l'API.

Setters

Deux méthodes de setter doivent être fournies: l'une qui prend une chaîne de bits complète et écrase tous les indicateurs existants, et l'autre qui prend un masque de bits personnalisé pour permettre 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é"

Privilégiez toujours public à protected dans l'API publique. L'accès protégé finit par être pénible à long terme, car les implémentateurs doivent remplacer pour fournir des accesseurs publics dans les cas où l'accès externe par défaut aurait été tout aussi efficace.

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

Ne pas implémenter equals() ni hashCode()

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

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

Les classes de données sont encouragées à remplacer toString() pour aider les développeurs à déboguer leur code.

Indiquez si la sortie est destinée au comportement du programme ou au 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 à utiliser par les programmes. Si vous exposez des informations uniquement à des fins de débogage, comme Intent, impliquez l'héritage des documents de la super-classe.

N'incluez pas d'informations supplémentaires

Toutes les informations disponibles dans toString() doivent également être disponibles via l'API publique de l'objet. Sinon, vous encouragez les développeurs à analyser et à s'appuyer sur votre sortie toString(), ce qui empêchera toute modification future. Il est recommandé d'implémenter toString() en n'utilisant que l'API publique de l'objet.

Décourager l'utilisation de 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, l'inclusion de la System.identityHashCode de votre objet dans sa sortie toString() rend 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 retour d'objets nouvellement créés

Utilisez le préfixe create, et non get ou new, pour les méthodes qui créeront des valeurs de retour, par exemple en créant 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 des objets File doivent également accepter des flux

Les emplacements de stockage des données sur Android ne sont pas toujours des fichiers sur disque. Par exemple, le contenu transmis au-delà des limites utilisateur est représenté par des Uri content://. Pour permettre le traitement de diverses sources de données, les API qui acceptent des objets File doivent également accepter 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 en boîte

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 évite les coûts de mémoire de ces classes, l'accès des méthodes aux valeurs et, plus important encore, l'auto-coffretisation qui résulte du casting entre les types primitifs et les types d'objets. Éviter ces comportements permet d'économiser de la mémoire et des allocations temporaires qui peuvent entraîner des récupérations de mémoire coûteuses et plus fréquentes.

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

Des annotations pour les développeurs ont été ajoutées pour clarifier les valeurs autorisées dans diverses situations. Cela permet aux outils d'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 d'un ensemble spécifique de valeurs constantes). Utilisez toutes les annotations suivantes lorsque cela est approprié:

Possibilité de valeur nulle

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

@Nullable:indique qu'une valeur de retour, un paramètre ou un champ donnés 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és ne peuvent pas être nuls. Le marquage d'é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 un état ternaire "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 les documents de la plate-forme Android, l'ajout d'annotations aux paramètres de votre méthode génère automatiquement une documentation sous la forme "Cette valeur peut être nulle", sauf si "null" est utilisé explicitement ailleurs dans la documentation du paramètre.

Méthodes existantes "pas vraiment nullables":les méthodes existantes de 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 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)). 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, il est recommandé d'utiliser des méthodes pour effectuer la validation des entrées pour les paramètres @NonNull à l'aide de Objects.requireNonNull() et de générer une exception 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. Une annotation est associée à chaque type de ressource, comme @StringRes, @ColorRes et @AnimRes, en plus de l'élément 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 destinés à recevoir l'une d'un ensemble fini de valeurs possibles 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, qui fonctionne 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 la @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 générer automatiquement la documentation pour toutes les valeurs.

@SdkConstant pour les constantes du SDK

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

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

Fournir une nullabilité compatible pour les forçages

Pour la compatibilité des API, la possibilité de nullité des forçages doit être compatible avec la possibilité de nullité actuelle du parent. Le tableau suivant présente les attentes en termes de compatibilité. En clair, les forçages ne doivent être aussi restrictifs que l'élément qu'ils remplacent, voire plus.

Saisie Parent Enfant
Type de retour Non annotées Non annoté ou non nul
Type de retour Nullable Valeurs non nulles ou nulles
Type de retour Nonnull Nonnull
Argument amusant Non annotées Non annoté ou nullable
Argument amusant Nullable Nullable
Argument amusant Nonnull Valeurs non nulles ou nulles

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

Lorsque des 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 sétteurs 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 obligatoires.

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égier 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 est 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 être cohérentes

Les paires de méthodes "get" et "set" d'une même propriété logique doivent toujours être cohérentes dans leurs annotations de nullabilité. Le non-respect de cette consigne annulera la syntaxe des propriétés de Kotlin. L'ajout d'annotations de non-nullabilité en désaccord aux méthodes de propriété existantes constitue donc un changement de source pour les utilisateurs de 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 retour de false, -1, null ou d'autres valeurs génériques indiquant "un problème est survenu" n'informe pas suffisamment le développeur sur l'échec de la définition des attentes des utilisateurs ni sur le suivi précis de la fiabilité de son 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 acceptable (et recommandé) d'inclure des informations détaillées dans un message d'exception, mais les développeurs ne doivent pas avoir à l'analyser pour gérer correctement l'erreur. Les codes d'erreur de type "verbose" ou d'autres informations doivent être exposés en tant que méthodes.
  2. Assurez-vous que l'option de gestion des erreurs que vous avez choisie vous permet de présenter 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, de sorte que tout code qui gère ce type détecte et gère également les sous-types.
  3. Il doit être difficile ou impossible pour un développeur d'ignorer accidentellement une erreur. Si votre erreur est communiquée en renvoyant une valeur, annotez votre méthode avec @CheckResult.

Préférez générer une exception ? 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 setter ou d'action (par exemple, perform) peuvent renvoyer un code d'état entier si l'action peut échouer en raison d'un état ou de conditions mis à jour de manière asynchrone hors du contrôle du développeur.

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

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

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

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

Privilégier les types de collection aux tableaux comme type de retour ou de paramètre

Les interfaces de collection typées génériquement présentent 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 primitifs, préférez les tableaux afin d'éviter le coût de la mise en boîte automatique. Consultez Prendre et renvoyer des primitives brutes au lieu de versions en boîte.

Exception pour le code sensible aux performances

Dans certains cas, lorsque l'API est utilisée dans du code sensible aux performances (comme les 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 les fluctuations de mémoire.

Exception pour Kotlin

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

Privilégier 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, à faible coût et correctement typé.

Lorsque les annotations de type sont acceptées, privilégiez toujours @NonNull pour les éléments de collection.

Vous devez également privilégier @NonNull lorsque vous utilisez des tableaux au lieu de collections (voir l'élément précédent). Si l'allocation d'objets pose 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 privilégier les types de retour en lecture seule (et non Mutable) pour les collections par défaut sauf si le contrat de l'API exige spécifiquement un type de retour modifiable.

Toutefois, les API Java doivent privilégier les types de retour modifiables par défaut, car l'implémentation des API Java sur la plate-forme Android ne fournit pas encore une implémentation pratique des collections immuables. Les types de retour Collections.empty, qui sont immuables, font exception à cette règle. Dans les cas où la mutabilité pourrait être exploitée par les clients (à dessein ou par erreur) pour enfreindre le modèle d'utilisation prévu de l'API, les API Java doivent 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 le renvoi. Si la collection renvoyée doit changer ou être réutilisée d'une manière ou d'une autre (par exemple, une vue adaptée d'un ensemble de données modifiable), le comportement précis de lorsque le contenu peut changer doit être documenté explicitement ou suivre les conventions de nommage d'API établies.

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

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

Comme 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 le renvoi.

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 des cas extrêmement limités, 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 d'objets réutilisés dans des API publiques. Dans les deux cas, soyez extrêmement prudent lorsque vous gérez l'accès simultané.

Utilisation du type de paramètre vararg

Les API Kotlin et Java sont encouragées à utiliser vararg dans les cas où le développeur est susceptible de créer un tableau au site d'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 se compilent au même bytecode basé sur un tableau et peuvent donc être appelées à partir de code Java avec un tableau modifiable. Les concepteurs d'API sont fortement encouragés à créer une copie superficielle de protection 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 de protection ne fournit aucune protection 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 les paramètres de type de collection ou les types renvoyés

List<Foo> est l'option par défaut, mais envisagez d'autres types pour ajouter du sens:

  • 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 aucune signification.

  • 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 d'un objet existant, où Foo est le nom du type de retour de la conversion. Cela est cohérent avec le JDK Object.toString() familier. 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 "à" 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 nouveau est modifié ultérieurement, l'objet ancien 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

Le casting en Kotlin est effectué à l'aide du mot clé as. Il reflète un changement d'interface, mais pas de l'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 sera reflétée 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 la 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 exemple qu'un développeur peut également écrire des conversions analogues à ses propres types préférés.

Générer des 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. À la place, une exception spécifique appropriée doit être utilisée, comme java.lang.NullPointerException, pour permettre aux développeurs de gérer les exceptions sans être trop larges.

Les erreurs qui n'ont aucun rapport avec les 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 classe 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 se produit et que le rappel doit agir en réponse.

Le passé et le présent doivent 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 chargée d'effectuer l'action de clic:

public boolean onClick()

Enregistrement du rappel

Lorsqu'un écouteur ou un rappel peut être ajouté ou supprimé d'un objet, les méthodes associées doivent être nommées add et remove ou register et unregister. Soyez cohérent avec la convention existante utilisée par la classe ou par d'autres classes du même package. En l'absence de précédent, privilégiez l'ajout et la suppression.

Les méthodes impliquant l'enregistrement ou le désenregistrement de 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 dans les cas où les développeurs peuvent vouloir enchaîner un rappel existant avec leur propre remplacement, mais il est fragile et rend l'état actuel difficile à raisonner 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 et n'a aucun moyen de le faire sans connaître le type de B, et B ayant été créé pour permettre de telles modifications de son rappel encapsulé.

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

Lorsque vous enregistrez des rappels qui n'ont pas d'attentes de thread explicites (presque partout en dehors de la boîte à outils d'interface utilisateur), nous vous recommandons vivement d'inclure un paramètre Executor lors de 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 en omettant Executor, même si ce n'est pas le 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(), et cela doit être documenté dans 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)

Pièges d'implémentation de Executor:Notez que ce qui suit est un exécuteur 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 l'objet de liaison entrant côté processus de l'application doit appeler Binder.clearCallingIdentity() avant d'invoquer le rappel de l'application sur le Executor fourni par l'application. De cette manière, tout code d'application qui utilise l'identité du liaisonneur (comme Binder.getCallingUid()) pour les vérifications d'autorisation attribue correctement le code exécuté à l'application et non au processus système qui appelle l'application. Si les utilisateurs de votre API souhaitent obtenir les informations UID ou PID de l'appelant, cela doit être une partie explicite de la surface de votre API, plutôt qu'implicite en fonction de l'emplacement où le Executor qu'ils ont fourni a été exécuté.

Votre API doit être compatible avec la spécification d'un Executor. Dans les cas critiques en termes de performances, les applications peuvent devoir exécuter du code immédiatement ou de manière synchrone avec les commentaires de votre API. L'acceptation d'un Executor permet cela. La création défensive d'un HandlerThread supplémentaire ou d'un trampoline similaire à partir de ce cas d'utilisation souhaitable est contre-productive.

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

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

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

public void clearFooCallback()

Utiliser un exécuteur au lieu d'un gestionnaire

Auparavant, Handler d'Android était utilisé comme norme pour rediriger l'exécution du rappel vers un thread Looper spécifique. 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 distribution si nécessaire. Il est donc important de pouvoir 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 sur 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 exemples de demandes d'exception courantes:

Je dois utiliser un Looper, car j'ai besoin d'un Looper pour epoll 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 souhaite pas que le code de l'application bloque la publication de l'événement par mon thread. Cette demande d'exception n'est généralement pas accordée pour le code exécuté dans un processus d'application. Les applications qui ne respectent pas cette règle ne font que s'enliser, sans affecter l'état général du système. Les applications qui font les choses correctement ou qui utilisent un framework de simultanéité commun ne devraient pas être pénalisées par des latences supplémentaires.

Handler est cohérent localement avec les autres API similaires de la même classe. Cette demande d'exception est accordée selon la situation. Il est préférable d'ajouter des surcharges basées sur Executor, en migrant les implémentations Handler pour qu'elles utilisent 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 existantes basées sur Handler en plus de 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

S'il existe un moyen d'ajouter ou d'enregistrer quelque chose, il devrait également exister un moyen de le supprimer/de l'annuler. La méthode

registerThing(Thing)

doit être associé à

unregisterThing(Thing)

Indiquer un identifiant de requête

S'il est raisonnable qu'un développeur réutilise 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 à plusieurs méthodes

Les rappels à plusieurs méthodes doivent privilégier interface et utiliser les méthodes default lors de l'ajout à des interfaces publiées précédemment. Auparavant, cette consigne 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 lors de la modélisation d'un appel de fonction non bloquant

OutcomeReceiver<R,E> indique une valeur de résultat R en cas de réussite ou E : Throwable dans le cas contraire, ce qui est identique à ce qu'un appel de méthode simple peut faire. Utilisez OutcomeReceiver comme type de rappel lors de la conversion d'une méthode bloquante qui renvoie un résultat ou lève une exception vers une 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 asynchrones 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 lève est signalée à la méthode OutcomeReceiver.onError de la même manière.

L'utilisation de OutcomeReceiver pour générer des rapports sur les résultats des méthodes asynchrones permet également de créer un wrapper Kotlin suspend fun pour les méthodes asynchrones à l'aide de l'extension Continuation.asOutcomeReceiver de androidx.core:core-ktx:

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

Des extensions comme celle-ci permettent aux clients Kotlin d'appeler des méthodes asynchrones non bloquantes avec la commodité d'un appel de fonction simple, sans bloquer le thread d'appel. Ces extensions individuelles pour les API de la plate-forme peuvent être proposées dans l'artefact androidx.core:core-ktx de Jetpack, en plus des vérifications et des considérations de compatibilité de version standards. Pour en savoir plus, consulter la documentation sur asOutcomeReceiver, ainsi que les considérations concernant l'annulation et les 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 son 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 des types de méthode abstraite unique (SAM)

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 d'interfaces SAM n'a que peu d'intérêt en termes de sécurité de type ou de communication d'intent, tout en étendant inutilement la surface de l'API Android.

Pensez à utiliser ces interfaces génériques plutôt que d'en créer:

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 de paramètres supplémentaires.

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Docs

Il s'agit de règles concernant les documents publics (Javadoc) des API.

Toutes les API publiques doivent être documentées

Toutes les API publiques doivent être accompagnées de documentation suffisante pour expliquer comment un développeur utiliserait l'API. Supposons que le développeur ait 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'une quantité minimale de contexte provenant 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 d'annotations de documentation @param et @return, respectivement. Formatez le corps du document Javadoc comme s'il était précédé de "Cette méthode...".

Dans les cas où une méthode n'accepte aucun paramètre, ne nécessite aucune considération particulière et renvoie ce que le nom de la méthode indique, vous pouvez omettre @return et écrire des documents similaires à:

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

Les documents doivent comporter des liens vers d'autres documents pour les constantes, méthodes et autres éléments associés. Utilisez des balises Javadoc (par exemple, @see et {@link foo}), et non uniquement 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 police de texte brut ou 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 consignes 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, et assurez-vous que la sortie est conforme aux attentes. La sortie ERROR dans Javadoc est souvent due à de mauvais liens. La cible de compilation update-api ou docs effectue cette vérification, 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.

Utiliser {@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 encapsuler le code avec des guillemets inversés, comme vous le feriez pour Markdown.

Les résumés @param et @return doivent être un fragment de phrase unique

Les résumés des paramètres et des valeurs de retour doivent commencer par une lettre minuscule et ne contenir qu'un seul fragment de phrase. Si vous disposez d'informations supplémentaires qui s'étendent au-delà d'une seule phrase, déplacez-les dans le corps de la méthode Javadoc:

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

Expliquez pourquoi les annotations @hide et @removed sont masquées dans l'API publique. Incluez des instructions pour remplacer les éléments de l'API marqués avec 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 Kotlin 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, tels que NullPointerException ou IllegalArgumentException, où un argument ne correspond pas à une @IntDef ou à une annotation similaire qui intègre le contrat de l'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 façon dont le développeur découvre et répond à ces exceptions. En règle générale, cela implique de transférer l'exception vers 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 renvoyées à partir de la méthode annotée.

Terminer la première phrase d'un document par un point

L'outil Doclava analyse les documents de manière simple, en terminant le document de synopsis (première phrase, utilisée dans la description rapide en haut des documents de cours) dès qu'il voit un point (.) suivi d'un espace. Cela entraîne deux problèmes:

  • Si un court document ne se termine pas par un point et que ce membre a des documents hérités détectés par l'outil, le synopsis détecte é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 termine les documents de synthèse après "g.". Par exemple, consultez TEXT_ALIGNMENT_CENTER dans View.java. Notez que Metalava corrige automatiquement cette erreur en insérant un espace non fractionnable après le point. Toutefois, ne faites pas cette erreur en premier lieu.

Mettre en forme des documents pour les afficher au format HTML

Javadoc est affiché au format HTML. Formatez donc ces documents en conséquence:

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

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

  • Les listes doivent utiliser <ul> ou <ol> pour les listes non triées et triées, respectivement. Chaque élément doit commencer par une balise <li>, mais n'a pas besoin d'une balise </li> de fermeture. Une balise </ul> ou </ol> fermante 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 table nécessitent des balises de fermeture correspondantes. Vous pouvez utiliser class="deprecated" sur n'importe quelle balise pour indiquer son abandon.

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

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

  • Tout le texte d'un bloc <pre> est analysé par le navigateur. Faites donc attention aux crochets <>. Vous pouvez les échapper à l'aide des entités HTML &lt; et &gt;.

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

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

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

Pour assurer la cohérence du style pour les résumés de classe, les descriptions de méthode, les descriptions de paramètres et d'autres éléments, suivez les recommandations des consignes officielles sur le langage Java dans How to Write Doc Comments for the Javadoc Tool (Écrire 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 outils de création d'intents doivent utiliser le modèle create*Intent()

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

Utiliser un 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 de clés arbitraires 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 les services non liés à la plate-forme, où la plate-forme ne lit pas les données envoyées via le canal et que 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 de Parcelable doivent comporter un champ CREATOR public

L'inflation parcellable est exposée 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.

S'il ne s'agit que d'une clé, d'un autre libellé ou d'une autre 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 la plate-forme et doit être fortement envisagé dans les API de bibliothèque non groupées. N'utilisez des énumérations que lorsque vous êtes certain qu'aucune nouvelle valeur ne sera ajoutée.

Avantages deIntDef:

  • Permet d'ajouter des valeurs au fil du temps
    • Les instructions when Kotlin peuvent échouer au moment 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 objet utilisés au moment 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 d'un énumérateur

  • Fonctionnalité idiomatique du langage Java, Kotlin
  • Active le switch exhaustif et l'utilisation de l'instruction when
    • Remarque : les valeurs ne doivent pas changer au fil du temps. Voir la liste précédente.
  • Nom clair et facile à trouver
  • Active 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 pouvant implémenter des interfaces, disposer d'assistants statiques, exposer des méthodes de membre ou d'extension, et exposer des champs.

Respecter la hiérarchie de superposition des packages Android

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

Évitez de faire référence à Google, à d'autres entreprises et à leurs produits.

La plate-forme Android est un projet Open Source et vise à être neutre du point de vue du fournisseur. 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 finales

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

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

Les méthodes qui appellent le processus système doivent renvoyer RemoteException en tant que RuntimeException

RemoteException est généralement généré par AIDL interne et indique que le processus système a expiré ou que l'application tente d'envoyer trop de données. Dans les deux cas, l'API publique doit être renvoyée en tant que RuntimeException pour empêcher les applications de conserver les décisions de sécurité ou de stratégie.

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

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

Générer des exceptions spécifiques pour les modifications apportées aux API

Les comportements des API publiques peuvent changer d'un niveau d'API à l'autre et entraîner des plantages d'application (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 plutôt qu'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 modifications de comportement des API.

Implémenter le constructeur de copie au lieu du clone

L'utilisation de la méthode Java clone() est fortement déconseillée en raison de l'absence 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 prend un objet du même type.

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

Les classes qui s'appuient sur un constructeur pour la création doivent envisager d'ajouter un constructeur de copie de constructeur pour permettre de modifier 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 au lieu de FileDescriptor

La propriété de l'objet java.io.FileDescriptor est mal définie, ce qui peut entraîner des bugs obscurs d'utilisation après fermeture. À la place, les API doivent renvoyer ou accepter des instances ParcelFileDescriptor. Le code ancien peut convertir entre PFD et FD si nécessaire à l'aide de dup() ou getFileDescriptor().

Éviter d'utiliser des valeurs numériques de taille impaire

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

Éviter d'utiliser BitSet

java.util.BitSet est idéal pour l'implémentation, mais pas pour l'API publique. Il est modifiable, 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 à faible performance, envisagez un Set<EnumType>. Pour les données binaires brutes, utilisez byte[].

Privilégier android.net.Uri

android.net.Uri est l'encapsulation privilégié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 très 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 bouchon 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, elles doivent être marquées @Retention(RetentionPolicy.SOURCE) pour éviter qu'elles n'apparaissent dans les bouchons d'API ou 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 compilation du SDK de la plate-forme et des AAR de la 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 clés de fournisseur de paramètres

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

Ajoutez plutôt une API Java getter et setter appropriée dans une classe appropriée, 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 un certain nombre de problèmes par rapport aux getters/setters:

  • Aucune sûreté du typage.
  • Il n'existe pas de méthode unifiée pour fournir une valeur par défaut.
  • Il n'existe aucun moyen approprié de personnaliser les autorisations.
    • Par exemple, il n'est pas possible de protéger votre paramètre avec une autorisation personnalisée.
  • Il n'existe aucun moyen approprié d'ajouter 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 position l'a abandonnée pour une API Java appropriée LocationManager.isLocationEnabled() et la diffusion MODE_CHANGED_ACTION, ce qui a donné à l'équipe beaucoup plus de flexibilité, et la sémantique des API est beaucoup plus claire maintenant.

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.

Les sous-classes Activity sont impossibles à composer. Étendre l'activité pour votre fonctionnalité la rend incompatible avec d'autres fonctionnalités qui exigent des utilisateurs de faire de même. Utilisez plutôt la composition à l'aide d'outils tels que LifecycleObserver.

Utiliser getUser() du contexte

Les classes liées à un Context, comme tout élément renvoyé à partir de Context.getSystemService(), doivent utiliser l'utilisateur lié à 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'ints simples

UserHandle est préférable 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);

Dans les cas inévitables, un int représentant un ID utilisateur doit être annoté avec @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

Privilégier 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 judicieusement.

Voici quelques préoccupations spécifiques qui nous dissuadent d'introduire de nouveaux intents de diffusion:

  • Lorsque vous envoyez des diffusions sans l'indicateur FLAG_RECEIVER_REGISTERED_ONLY, elles démarrent de force 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 l'écroulement 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 les différentes conditions préalables.

  • Lorsque vous envoyez des diffusions, vous avez peu de possibilités de filtrer ou d'ajuster le contenu diffusé dans les applications. Il est donc difficile, voire impossible, de répondre à de futurs problèmes de confidentialité ou d'apporter des modifications 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 diffuser votre événement dans les délais. Nous avons observé plusieurs files d'attente de diffusion dans la nature qui ont une latence de bout en bout 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 la gestion actuelle des commandes de lecture par l'application.
  • Si possible, définissez votre diffusion en tant que <protected-broadcast> pour empêcher les applications malveillantes de se faire passer pour l'OS.

Intents dans les services de développement 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 qu'un développeur doit ajouter à son AndroidManifest.xml pour recevoir des intents de la plate-forme.<intent-filter>
  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 de développement.

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 les suspend fun, ne sont pas destinées à être utilisées par les développeurs Java. Toutefois, n'essayez pas de contrôler la visibilité spécifique au langage à 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, ils s'affichent à partir de Java sur une classe interne nommée Companion plutôt que sur la classe contenante. Les classes Companion peuvent s'afficher comme des classes vides dans les fichiers texte de l'API. Cela fonctionne comme prévu.

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 dans la classe contenante.

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 codebases existants.

Modifications destructives pour les binaires

Évitez les modifications destructives des 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 y avoir des cas particuliers que la vérification de l'API de Metalava ne détecte pas. En cas de doute, consultez le guide Évolution des API basées sur Java de la Fondation Eclipse pour obtenir une explication détaillée des types de modifications d'API compatibles avec Java. Les modifications destructives binaires dans les API masquées (par exemple, système) doivent suivre le cycle obsolescence/remplacement.

Modifications destructives pour 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 brise la source, est l'ajout d'un générique à une classe existante, qui 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 qui cassent 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 de rupture de source deviennent nécessaires pour améliorer l'expérience du développeur 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 la probabilité 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 : abandon
    • Marquez le code avec @Deprecated.
    • Ajoutez des remplacements et créez un lien vers le remplacement dans la documentation Javadoc pour le code obsolète à l'aide de l'annotation de documentation @deprecated.
    • Au cours du cycle de développement, signalez les bugs aux utilisateurs internes en leur indiquant que l'API est en cours d'abandon. Cela permet de vérifier que les API de remplacement sont adaptées.
  • API y+2 : Suppression réversible
    • Marquez le code avec @removed.
    • Éventuellement, générer 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 forcée
    • Supprimez complètement le code de l'arborescence source.

Abandon

Nous considérons l'abandon comme un changement d'API. Il peut se produire dans une version majeure (comme une lettre). Utilisez l'annotation de source @Deprecated et l'annotation de documentation @deprecated <summary> ensemble lorsque vous supprimez 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 abandonner les API définies en XML et exposées en Java, y compris les attributs et les propriétés stylables exposés dans la classe android.R, avec un résumé:

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

Quand abandonner une API

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

Nous vous demandons également de marquer les API comme @deprecated avant qu'elles ne deviennent @removed, mais cela ne motive pas vraiment les développeurs à abandonner 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 utilisés comme référence. Par conséquent, les développeurs qui utilisent -Werror doivent corriger ou supprimer individuellement chaque utilisation d'une API obsolète avant de pouvoir mettre à jour la version de leur SDK de compilation.
    • Les avertissements d'abandon 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 de SDK de compilation.
  • La documentation sur d.android.com affiche une notification d'abandon.
  • Les IDE tels qu'Android Studio affichent un avertissement sur le site d'utilisation de l'API.
  • Les IDE peuvent rétrograder l'API ou la masquer dans la saisie semi-automatique.

Par conséquent, l'abandon d'une API peut dissuader les développeurs les plus préoccupés par 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 ignorent probablement les abandons.

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

C'est pourquoi nous vous recommandons de ne supprimer des 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 non défini que nous ne pouvons pas corriger sans endommager la compatibilité.

Lorsque vous abandonnez une API et la remplacez par une nouvelle, nous 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 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é lorsque 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 conserver 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 avoir abandonné 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 par la suite obsolètes au cours d'un cycle de préversion (elles apparaîtront donc initialement dans la surface de l'API publique comme obsolètes), vous devez les supprimer avant de finaliser l'API.

Suppression réversible

La suppression réversible 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 abandonner l'API pendant la durée d'une version majeure avant de la supprimer de manière temporaire. Supprimez toutes les références de la documentation aux API et utilisez l'annotation de documentation @removed <summary> lorsque vous supprimez des API de manière non définitive. Votre résumé doit inclure la raison de la suppression et peut inclure une stratégie de migration, comme expliqué dans la section Obsolescence.

Le comportement des API supprimées de manière logicielle peut être conservé tel quel, mais surtout doit être préservé afin que les appelants existants ne plantent pas lorsqu'ils appellent l'API. Dans certains cas, cela peut signifier préserver le comportement.

La couverture des tests doit être maintenue, mais le contenu des tests peut devoir changer pour tenir compte des modifications 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 logicielle tel quel, mais surtout, vous devez le conserver 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 devoir changer 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.

À un niveau technique, nous supprimons l'API du fichier JAR du bouchon du SDK et du chemin d'accès au moment de la compilation à l'aide de l'annotation Javadoc @remove, mais elle existe toujours dans le chemin d'accès au moment de l'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 du développeur d'applications, l'API n'apparaît plus dans la saisie semi-automatique et le code source qui la référence ne se compile pas lorsque le compileSdk est égal ou postérieur au SDK à partir duquel l'API a été supprimée. Toutefois, le code source continue de se compiler avec succès avec les SDK et binaires précédents qui font référence à l'API.

Certaines catégories d'API ne doivent pas être supprimées de manière réversible. Vous ne devez pas supprimer de manière temporaire 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 la classe à tous les niveaux de 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 de manière douce les méthodes abstraites.

Suppression définitive

La suppression forcée est une modification qui interrompt le 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 d'API dans la plupart des cas (> 95%). Les API déconseillées diffèrent des API obsolètes en ce sens qu'il existe un cas d'utilisation critique restreint qui empêche l'obsolescence. Lorsque vous marquez une API comme déconseillée, vous devez fournir une explication et une solution de remplacement:

@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 comme 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 communiquer clairement lorsque les développeurs essayaient d'afficher des événements trop volumineux pour être envoyés via Binder.

Toutefois, pour éviter de causer des problèmes aux applications existantes, nous vous recommandons vivement de conserver un comportement sécurisé pour les anciennes applications. Nous avons toujours protégé ces modifications de comportement en fonction du ApplicationInfo.targetSdkVersion de l'application, mais nous avons récemment migré vers 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 forcer à s'adapter à des dizaines de modifications de comportement simultanément.

Compatibilité ascendante

La rétrocompatibilité 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'une API, vous devez accorder une attention particulière à la conception initiale ainsi qu'aux futures modifications, car les développeurs s'attendent à écrire du code une fois, à le tester une fois et à le faire exécuter partout sans problème.

Les éléments suivants sont à l'origine des problèmes de compatibilité ascendante les plus courants dans Android:

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

Dans tous ces cas, les développeurs ne découvrent qu'un problème existe qu'au moment de l'exécution. Pire encore, ils peuvent s'en rendre compte à la suite de rapports d'erreur provenant d'appareils plus anciens sur le terrain.

De plus, ces cas sont tous des modifications de l'API techniquement valides. Elles ne génèrent pas de problème de compatibilité binaire ou source, et le lint de l'API ne détecte aucun de ces problèmes.

Par conséquent, les concepteurs d'API doivent faire preuve de la plus grande attention lorsqu'ils modifient des classes existantes. Posez-vous la question suivante : "Cette modification entraînera-t-elle l'échec du code écrit et testé uniquement avec 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 préservée de la même manière que les méthodes et les variables sont géré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 repère xs:annotation, mais vous devez continuer à prendre en charge les fichiers XML existants en suivant le cycle de vie d'évolution @SystemApi typique.

<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, le nombre et l'ordre de ces éléments enfants diffèrent. Par conséquent, modifier un type existant serait une modification incompatible.

Si vous souhaitez modifier un type existant, nous vous recommandons de le supprimer et d'en introduire un nouveau 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>

Modèles spécifiques à la ligne principale

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

Les modules principaux 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 principaux doivent respecter certains modèles de conception. Cette section les décrit.

Modèle <Module>FrameworkInitializer

Si un module principal 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 d'une référence à un Context.

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

Modèle <Module>ServiceManager

Normalement, pour enregistrer des objets de liaison de service système ou obtenir des références à ceux-ci, vous devez utiliser ServiceManager, mais les modules principaux 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 à des objets de liaison de service système exposés par la plate-forme statique ou par d'autres modules.

Les modules principaux peuvent utiliser le modèle suivant pour pouvoir enregistrer et obtenir des références aux services de liaison 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 que depuis des classes $BOOTCLASSPATH ou des classes de serveur système, vous pouvez utiliser @SystemApi(client = MODULE_LIBRARIES). Sinon, @SystemApi(client = PRIVILEGED_APPS) convient.

  • Cette classe se compose des éléments suivants:

    • Constructeur masqué, de sorte que seul le code de plate-forme statique puisse l'instancier.
    • Méthodes getter publiques qui renvoient une instance ServiceRegisterer pour un nom spécifique. Si vous disposez d'un seul objet de liaison, vous avez besoin d'une seule méthode de 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 l'utilise.

Ce modèle empêcherait les autres modules principaux d'accéder à ces API, car les autres modules ne peuvent pas obtenir une instance de <YourModule>ServiceManager, même si les API get() et register() sont visibles par eux.

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

Si vous implémentez un objet de liaison 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 de liaison qui ne sont pas la propriété de votre module. Si vous exposez un objet de liaison à partir de la version native, votre <YourModule>ServiceManager.ServiceRegisterer n'a pas besoin d'une méthode register().

Définitions des autorisations dans les modules de ligne principale

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

Si l'autorisation définie n'est utilisée qu'en interne dans un module, son nom d'autorisation doit être précédé du 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 d'autorisation doit être précédé de "android.permission." (comme toute autorisation de plate-forme statique) ainsi que le nom du package du module, pour indiquer qu'il s'agit d'une API de plate-forme à partir d'un module tout en évitant tout conflit de dénomination, 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.