Cette page est destinée à servir de guide aux développeurs pour comprendre les principes généraux que le Conseil des API applique lors des examens d'API.
En plus de suivre ces consignes lors de l'écriture d'API, les développeurs doivent exécuter l'outil API Lint, qui encode un grand nombre de ces règles dans des vérifications qu'il exécute sur les API.
Considérez-le comme le guide des règles respectées par cet outil Lint, ainsi que des conseils généraux sur les règles qui ne peuvent pas être codifiées dans cet outil avec une grande précision.
Outil API Lint
API Lint est intégré à l'outil d'analyse statique Metalava et s'exécute automatiquement lors de la validation dans CI. Vous pouvez l'exécuter manuellement à partir d'un checkout de plate-forme local à l'aide de m
checkapi
ou d'un checkout AndroidX local à l'aide de ./gradlew :path:to:project:checkApi
.
Règles relatives aux API
La plate-forme Android et de nombreuses bibliothèques Jetpack existaient avant la création de cet ensemble de consignes. Les règles énoncées plus loin sur cette page évoluent constamment pour répondre aux besoins de l'écosystème Android.
Par conséquent, il est possible que certaines API existantes ne respectent pas les consignes. Dans d'autres cas, il peut être préférable pour les développeurs d'applications qu'une nouvelle API reste cohérente avec les API existantes plutôt que de respecter strictement les consignes.
Faites preuve de discernement et contactez le Conseil des API si vous avez des questions difficiles à résoudre concernant une API ou si des consignes doivent être mises à jour.
Principes de base des API
Cette catégorie concerne les aspects fondamentaux d'une API Android.
Toutes les API doivent être implémentées
Quelle que soit l'audience d'une API (par exemple, publique ou @SystemApi
), toutes les surfaces d'API doivent être implémentées lorsqu'elles sont fusionnées ou exposées en tant qu'API. Ne fusionnez pas les stubs d'API avec l'implémentation, qui sera effectuée ultérieurement.
Les surfaces d'API sans implémentation posent plusieurs problèmes :
- Il n'est pas garanti qu'une surface appropriée ou complète ait été exposée. Tant qu'une API n'est pas testée ni utilisée par des clients, il est impossible de vérifier qu'un client dispose des API appropriées pour pouvoir utiliser la fonctionnalité.
- Les API sans implémentation ne peuvent pas être testées dans les versions Preview développeur.
- Les API sans implémentation ne peuvent pas être testées dans CTS.
Toutes les API doivent être testées
Cela correspond aux exigences CTS de la plate-forme, aux règles AndroidX et, de manière générale, à l'idée que les API doivent être implémentées.
Tester les surfaces d'API permet de garantir que la surface d'API est utilisable et que nous avons traité les cas d'utilisation attendus. Il ne suffit pas de tester l'existence de l'API, mais aussi son comportement.
Toute modification ajoutant une API doit inclure les tests correspondants dans le même CL ou sujet Gerrit.
Les API doivent également être testables. Vous devriez être en mesure de répondre à la question "Comment un développeur d'applications va-t-il tester le code qui utilise votre API ?".
Toutes les API doivent être documentées
La documentation est un élément clé de l'utilisabilité des API. Bien que la syntaxe d'une surface d'API puisse sembler évidente, les nouveaux clients ne comprendront pas la sémantique, le comportement ni le contexte de l'API.
Toutes les API générées doivent respecter les consignes.
Les API générées par des outils doivent respecter les mêmes consignes que le code écrit à la main.
Outils déconseillés pour générer des API :
AutoValue
: ne respecte pas les consignes de différentes manières. Par exemple, il n'existe aucun moyen d'implémenter des classes de valeurs finales ni des compilateurs finaux avec le fonctionnement d'AutoValue.
Style de code
Cette catégorie concerne le style de code général que les développeurs doivent utiliser, en particulier lorsqu'ils écrivent des API publiques.
Suivez les conventions de codage standards, sauf indication contraire.
Les conventions de codage Android sont documentées pour les contributeurs externes ici :
https://source.android.com/source/code-style.html
En général, nous avons tendance à suivre les conventions de programmation Java et Kotlin standards.
Les acronymes ne doivent pas être mis en majuscules dans les noms de méthodes
Par exemple, le nom de la méthode doit être runCtsTests
et non runCTSTests
.
Les noms ne doivent pas se terminer par "Impl"
Cela expose des détails d'implémentation. Évitez cela.
Classes
Cette section décrit les règles concernant les classes, les interfaces et l'héritage.
Hériter de nouvelles classes publiques à partir de la classe de base appropriée
L'héritage expose dans votre sous-classe des éléments d'API qui ne sont pas forcément appropriés.
Par exemple, une nouvelle sous-classe publique de FrameLayout
ressemble à FrameLayout
, plus les nouveaux comportements et éléments d'API. Si cette API héritée ne convient pas à votre cas d'utilisation, héritez d'une classe plus haut dans l'arborescence, par exemple ViewGroup
ou View
.
Si vous êtes tenté de remplacer les méthodes de la classe de base pour générer UnsupportedOperationException
, réfléchissez à la classe de base que vous utilisez.
Utiliser les classes de collections de base
Que vous preniez une collection comme argument ou que vous la renvoyiez comme valeur, préférez toujours la classe de base à l'implémentation spécifique (par exemple, renvoyez List<Foo>
plutôt que ArrayList<Foo>
).
Utilisez une classe de base qui exprime les contraintes appropriées pour l'API. Par exemple, utilisez List
pour une API dont la collection doit être ordonnée et Set
pour une API dont la collection doit être constituée d'éléments uniques.
En Kotlin, préférez les collections immuables. Pour en savoir plus, consultez Modifiabilité des collections.
Classes abstraites et interfaces
Java 8 ajoute la compatibilité avec les méthodes d'interface par défaut, ce qui permet aux concepteurs d'API d'ajouter des méthodes aux interfaces tout en conservant la compatibilité binaire. Le code de plate-forme et toutes les bibliothèques Jetpack doivent cibler Java 8 ou version ultérieure.
Dans les cas où l'implémentation par défaut est sans état, les concepteurs d'API doivent préférer les interfaces aux classes abstraites. Autrement dit, les méthodes d'interface par défaut peuvent être implémentées en tant qu'appels à d'autres méthodes d'interface.
Dans les cas où un constructeur ou un état interne sont requis par l'implémentation par défaut, des classes abstraites doivent être utilisées.
Dans les deux cas, les concepteurs d'API peuvent choisir de laisser une seule méthode abstraite pour simplifier l'utilisation en tant que lambda :
public interface AnimationEndCallback {
// Always called, must be implemented.
public void onFinished(Animation anim);
// Optional callbacks.
public default void onStopped(Animation anim) { }
public default void onCanceled(Animation anim) { }
}
Les noms de classe doivent refléter ce qu'ils étendent.
Par exemple, les classes qui étendent Service
doivent être nommées FooService
pour plus de clarté :
public class IntentHelper extends Service {}
public class IntentService extends Service {}
Suffixes génériques
Évitez d'utiliser des suffixes de nom de classe génériques tels que Helper
et Util
pour les collections de méthodes utilitaires. Placez plutôt les méthodes directement dans les classes associées ou dans des fonctions d'extension Kotlin.
Dans les cas où les méthodes font le lien entre plusieurs classes, donnez à la classe conteneur un nom explicite qui explique ce qu'elle fait.
Dans de très rares cas, l'utilisation du suffixe Helper
peut être appropriée :
- Utilisé pour la composition du comportement par défaut
- Peut impliquer la délégation du comportement existant à de nouvelles classes
- Peut nécessiter un état persistant
- Implique généralement
View
Par exemple, si les info-bulles de rétroportage nécessitent de conserver l'état associé à un View
et d'appeler plusieurs méthodes sur le View
pour installer le rétroportage, TooltipHelper
serait un nom de classe acceptable.
N'exposez pas directement le code généré par IDL en tant qu'API publiques.
Conservez le code généré par IDL comme détails d'implémentation. Cela inclut protobuf, les sockets, FlatBuffers ou toute autre surface d'API non Java et non NDK. Toutefois, la plupart des IDL dans Android sont en AIDL. Cette page se concentre donc sur AIDL.
Les classes AIDL générées ne répondent pas aux exigences du guide de style de l'API (par exemple, elles ne peuvent pas utiliser la surcharge). De plus, l'outil AIDL n'est pas explicitement conçu pour maintenir la compatibilité de l'API du langage. Vous ne pouvez donc pas les intégrer dans une API publique.
Ajoutez plutôt une couche d'API publique au-dessus de l'interface AIDL, même s'il s'agit initialement d'un wrapper superficiel.
Interfaces Binder
Si l'interface Binder
est un détail d'implémentation, elle peut être modifiée librement à l'avenir, la couche publique permettant de maintenir la rétrocompatibilité requise. Par exemple, vous devrez peut-être ajouter de nouveaux arguments aux appels internes ou optimiser le trafic IPC en utilisant le traitement par lot ou le streaming, en utilisant la mémoire partagée, etc. Aucune de ces opérations n'est possible si votre interface AIDL est également l'API publique.
Par exemple, n'exposez pas FooService
directement en tant qu'API publique :
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
En revanche, encapsulez l'interface Binder
dans un gestionnaire ou une autre classe :
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
Si un nouvel argument est nécessaire pour cet appel, l'interface interne peut être minimale et des surcharges pratiques peuvent être ajoutées à l'API publique. Vous pouvez utiliser le calque d'encapsulation pour gérer d'autres problèmes de rétrocompatibilité à mesure que l'implémentation évolue :
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo, int flags);
}
public IFooManager {
public void doFoo(String foo) {
if (mAppTargetSdkLevel < 26) {
useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
} else {
mFooService.doFoo(foo, 0);
}
}
public void doFoo(String foo, int flags) {
mFooService.doFoo(foo, flags);
}
}
Pour les interfaces Binder
qui ne font pas partie de la plate-forme Android (par exemple, une interface de service exportée par les services Google Play pour que les applications l'utilisent), l'exigence d'une interface IPC stable, publiée et versionnée signifie qu'il est beaucoup plus difficile de faire évoluer l'interface elle-même. Toutefois, il est toujours utile d'avoir une couche wrapper autour de celle-ci, pour correspondre aux autres consignes de l'API et pour faciliter l'utilisation de la même API publique pour une nouvelle version de l'interface IPC, si cela s'avère nécessaire.
Ne pas utiliser d'objets Binder bruts dans l'API publique
Un objet Binder
n'a aucun sens en soi et ne doit donc pas être utilisé dans une API publique. Un cas d'utilisation courant consiste à utiliser un Binder
ou un IBinder
comme jeton, car il possède une sémantique d'identité. Au lieu d'utiliser un objet Binder
brut, utilisez plutôt une classe de jeton wrapper.
public final class IdentifiableObject {
public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
/**
* @hide
*/
public Binder getRawValue() {...}
/**
* @hide
*/
public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}
public final class IdentifiableObject {
public IdentifiableObjectToken getToken() {...}
}
Les classes de gestionnaire doivent être définitives
Les classes de gestionnaire doivent être déclarées comme final
. Les classes de gestionnaire communiquent avec les services système et constituent le point d'interaction unique. Comme aucune personnalisation n'est nécessaire, déclarez-le comme final
.
Ne pas utiliser CompletableFuture ni Future
java.util.concurrent.CompletableFuture
dispose d'une grande surface d'API qui permet une mutation arbitraire de la valeur future et comporte des valeurs par défaut sujettes aux erreurs.
À l'inverse, java.util.concurrent.Future
ne dispose pas d'écoute non bloquante, ce qui le rend difficile à utiliser avec du code asynchrone.
Dans le code de plate-forme et les API de bibliothèque de bas niveau utilisées par Kotlin et Java, préférez une combinaison d'un rappel d'achèvement, Executor
, et si l'API est compatible avec l'annulation CancellationSignal
.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
Si vous ciblez Kotlin, préférez les fonctions suspend
.
suspend fun asyncLoadFoo(): Foo
Dans les bibliothèques d'intégration spécifiques à Java, vous pouvez utiliser ListenableFuture
de Guava.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
Ne pas utiliser "Optional"
Bien que Optional
puisse présenter des avantages dans certaines surfaces d'API, il est incompatible avec la surface d'API Android existante. @Nullable
et @NonNull
fournissent une assistance pour l'outillage de sécurité null
, et Kotlin applique les contrats de possibilité de valeur nulle au niveau du compilateur, ce qui rend Optional
inutile.
Pour les primitives facultatives, utilisez les méthodes has
et get
associées. Si la valeur n'est pas définie (has
renvoie false
), la méthode get
doit générer une IllegalStateException
.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
Utiliser des constructeurs privés pour les classes non instanciables
Les classes qui ne peuvent être créées que par des Builder
, les classes qui ne contiennent que des constantes ou des méthodes statiques, ou les classes non instanciables doivent inclure au moins un constructeur privé pour empêcher l'instanciation à l'aide du constructeur sans argument par défaut.
public final class Log {
// Not instantiable.
private Log() {}
}
Singletons
Les singletons sont déconseillés, car ils présentent les inconvénients suivants en termes de tests :
- La construction est gérée par la classe, ce qui empêche l'utilisation de faux
- Les tests ne peuvent pas être hermétiques en raison de la nature statique d'un singleton.
- Pour contourner ces problèmes, les développeurs doivent connaître les détails internes du singleton ou créer un wrapper autour de celui-ci.
Préférez le modèle instance unique, qui s'appuie sur une classe de base abstraite pour résoudre ces problèmes.
Instance unique
Les classes à instance unique utilisent une classe de base abstraite avec un constructeur private
ou internal
et fournissent une méthode getInstance()
statique pour obtenir une instance. La méthode getInstance()
doit renvoyer le même objet lors des appels suivants.
L'objet renvoyé par getInstance()
doit être une implémentation privée de la classe de base abstraite.
class Singleton private constructor(...) {
companion object {
private val _instance: Singleton by lazy { Singleton(...) }
fun getInstance(): Singleton {
return _instance
}
}
}
abstract class SingleInstance private constructor(...) {
companion object {
private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
fun getInstance(): SingleInstance {
return _instance
}
}
}
L'instance unique diffère du singleton en ce que les développeurs peuvent créer une version factice de SingleInstance
et utiliser leur propre framework d'injection de dépendances pour gérer l'implémentation sans avoir à créer de wrapper. La bibliothèque peut également fournir son propre faux dans un artefact -testing
.
Les classes qui libèrent des ressources doivent implémenter AutoCloseable
Les classes qui libèrent des ressources via close
, release
, destroy
ou des méthodes similaires doivent implémenter java.lang.AutoCloseable
pour permettre aux développeurs de nettoyer automatiquement ces ressources lorsqu'ils utilisent un bloc try-with-resources
.
Évitez d'introduire de nouvelles sous-classes View dans android.*.
N'introduisez pas de nouvelles classes qui héritent directement ou indirectement de android.view.View
dans l'API publique de la plate-forme (c'est-à-dire dans android.*
).
Le kit d'outils d'UI d'Android est désormais Compose-first. Les nouvelles fonctionnalités d'UI exposées par la plate-forme doivent être exposées en tant qu'API de niveau inférieur pouvant être utilisées pour implémenter des composants d'UI basés sur Jetpack Compose et éventuellement sur des vues pour les développeurs dans les bibliothèques Jetpack. Proposer ces composants dans des bibliothèques permet d'implémenter des versions antérieures lorsque les fonctionnalités de la plate-forme ne sont pas disponibles.
Champs
Ces règles concernent les champs publics des classes.
Ne pas exposer les champs bruts
Les classes Java ne doivent pas exposer directement les champs. Les champs doivent être privés et accessibles uniquement à l'aide de getters et de setters publics, qu'ils soient finaux ou non.
Les exceptions rares incluent les structures de données de base pour lesquelles il n'est pas nécessaire d'améliorer le comportement de spécification ou de récupération d'un champ. Dans ce cas, les champs doivent être nommés en utilisant des conventions d'attribution de noms de variables standards, par exemple Point.x
et Point.y
.
Les classes Kotlin peuvent exposer des propriétés.
Les champs exposés doivent être marqués comme finaux
Il est fortement déconseillé d'utiliser des champs bruts (@see N'exposez pas les champs bruts). Toutefois, dans le cas rare où un champ est exposé en tant que champ public, marquez-le final
.
Les champs internes ne doivent pas être exposés
Ne référencez pas les noms de champs internes dans l'API publique.
public int mFlags;
Utiliser "Public" au lieu de "Protégé"
@see Utiliser "public" au lieu de "protected"
Constantes
Il s'agit de règles concernant les constantes publiques.
Les constantes d'indicateur ne doivent pas chevaucher les valeurs int ou long
Le terme flags (indicateurs) implique des bits qui peuvent être combinés en une valeur d'union. Si ce n'est pas le cas, n'appelez pas la variable ni la constante flag
.
public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;
Pour en savoir plus sur la définition des constantes d'indicateurs publics, consultez @IntDef
pour les indicateurs de masque de bits.
Les constantes statiques finales doivent utiliser une convention de dénomination en majuscules et séparées par des tirets bas.
Tous les mots de la constante doivent être en majuscules et les mots multiples doivent être séparés par _
. Exemple :
public static final int fooThing = 5
public static final int FOO_THING = 5
Utiliser des préfixes standards pour les constantes
De nombreuses constantes utilisées dans Android concernent des éléments standards, tels que des indicateurs, des clés et des actions. Ces constantes doivent comporter des préfixes standards pour être plus facilement identifiables.
Par exemple, les extras d'intent doivent commencer par EXTRA_
. Les actions d'intent doivent commencer par ACTION_
. Les constantes utilisées avec Context.bindService()
doivent commencer par BIND_
.
Noms et portées des constantes clés
Les valeurs constantes de chaîne doivent être cohérentes avec le nom de la constante elle-même et doivent généralement être limitées au package ou au domaine. Exemple :
public static final String FOO_THING = "foo"
n'est pas nommé de manière cohérente ni correctement défini. Voici quelques exemples :
public static final String FOO_THING = "android.fooservice.FOO_THING"
Les préfixes android
dans les constantes de chaîne à portée limitée sont réservés au projet Android Open Source.
Les actions et les extras d'intent, ainsi que les entrées de Bundle, doivent être associés à un espace de noms à l'aide du nom du package dans lequel ils sont définis.
package android.foo.bar {
public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}
Utiliser "Public" au lieu de "Protégé"
@see Utiliser "public" au lieu de "protected"
Utiliser des préfixes cohérents
Toutes les constantes associées doivent commencer par le même préfixe. Par exemple, pour un ensemble de constantes à utiliser avec les valeurs d'indicateur :
public static final int SOME_VALUE = 0x01;
public static final int SOME_OTHER_VALUE = 0x10;
public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;
public static final int FLAG_SOME_OTHER_VALUE = 0x10;
public static final int FLAG_SOME_THIRD_VALUE = 0x100;
@see Utiliser des préfixes standards pour les constantes
Utiliser des noms de ressources cohérents
Les identifiants, attributs et valeurs publics doivent être nommés à l'aide de la convention de dénomination camelCase, par exemple @id/accessibilityActionPageUp
ou @attr/textAppearance
, comme les champs publics en Java.
Dans certains cas, un identifiant ou un attribut public inclut un préfixe commun séparé par un trait de soulignement :
- Valeurs de configuration de la plate-forme telles que
@string/config_recentsComponentName
dans config.xml - Attributs de vue spécifiques à la mise en page, tels que
@attr/layout_marginStart
dans attrs.xml
Les thèmes et styles publics doivent suivre la convention de dénomination hiérarchique PascalCase, par exemple @style/Theme.Material.Light.DarkActionBar
ou @style/Widget.Material.SearchView.ActionBar
, semblable aux classes imbriquées en Java.
Les ressources de mise en page et de dessinables ne doivent pas être exposées en tant qu'API publiques. Toutefois, s'ils doivent être exposés, les mises en page et les drawables publics doivent être nommés à l'aide de la convention de dénomination under_score, par exemple layout/simple_list_item_1.xml
ou drawable/title_bar_tall.xml
.
Rendez dynamiques les constantes susceptibles de changer
Le compilateur peut insérer des valeurs constantes, de sorte que le maintien des valeurs est considéré comme faisant partie du contrat d'API. Si la valeur d'une constante MIN_FOO
ou MAX_FOO
est susceptible de changer à l'avenir, envisagez de les transformer en méthodes dynamiques.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
Tenir compte de la compatibilité ascendante pour les rappels
Les constantes définies dans les futures versions de l'API ne sont pas connues des applications qui ciblent des API plus anciennes. Pour cette raison, les constantes fournies aux applications doivent tenir compte de la version de l'API cible de l'application et mapper les constantes plus récentes sur une valeur cohérente. Prenons l'exemple suivant :
Source SDK hypothétique :
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
Application hypothétique avec targetSdkVersion="22"
:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
Dans ce cas, l'application a été conçue dans les limites du niveau d'API 22 et a fait l'hypothèse (plus ou moins) raisonnable qu'il n'y avait que deux états possibles. Toutefois, si l'application reçoit le STATUS_FAILURE_RETRY
nouvellement ajouté, elle l'interprète comme un succès.
Les méthodes qui renvoient des constantes peuvent gérer ces cas de manière sécurisée en limitant leur sortie pour qu'elle corresponde au niveau d'API ciblé par l'application :
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
Les développeurs ne peuvent pas anticiper si une liste de constantes peut changer à l'avenir. Si vous définissez une API avec une constante UNKNOWN
ou UNSPECIFIED
qui ressemble à un caractère générique, les développeurs supposent que les constantes publiées au moment où ils ont écrit leur application sont exhaustives. Si vous ne souhaitez pas définir cette attente, réfléchissez à nouveau à l'opportunité d'utiliser une constante générique pour votre API.
De plus, les bibliothèques ne peuvent pas spécifier leur propre fichier targetSdkVersion
distinct de l'application, et la gestion des modifications de comportement de targetSdkVersion
à partir du code de la bibliothèque est complexe et sujette aux erreurs.
Constante entière ou de chaîne
Utilisez des constantes entières et @IntDef
si l'espace de noms pour les valeurs n'est pas extensible en dehors de votre package. Utilisez des constantes de chaîne si l'espace de noms est partagé ou peut être étendu par du code en dehors de votre package.
Classes de données
Les classes de données représentent un ensemble de propriétés immuables et fournissent un ensemble de fonctions utilitaires petit et bien défini pour interagir avec ces données.
N'utilisez pas data class
dans les API Kotlin publiques, car le compilateur Kotlin ne garantit pas la compatibilité binaire ni celle de l'API de langage pour le code généré. Implémentez plutôt manuellement les fonctions requises.
Instanciation
En Java, les classes de données doivent fournir un constructeur lorsqu'il y a peu de propriétés ou utiliser le modèle Builder
lorsqu'il y a de nombreuses propriétés.
En Kotlin, les classes de données doivent fournir un constructeur avec des arguments par défaut, quel que soit le nombre de propriétés. Les classes de données définies en Kotlin peuvent également bénéficier de la fourniture d'un compilateur lors du ciblage de clients Java.
Modification et copie
Si les données doivent être modifiées, fournissez une classe Builder
avec un constructeur de copie (Java) ou une fonction membre copy()
(Kotlin) qui renvoie un nouvel objet.
Lorsque vous fournissez une fonction copy()
en Kotlin, les arguments doivent correspondre au constructeur de la classe et les valeurs par défaut doivent être renseignées à l'aide des valeurs actuelles de l'objet :
class Typography(
val labelMedium: TextStyle = TypographyTokens.LabelMedium,
val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
fun copy(
labelMedium: TextStyle = this.labelMedium,
labelSmall: TextStyle = this.labelSmall
): Typography = Typography(
labelMedium = labelMedium,
labelSmall = labelSmall
)
}
Comportements supplémentaires
Les classes de données doivent implémenter equals()
et hashCode()
, et chaque propriété doit être prise en compte dans les implémentations de ces méthodes.
Les classes de données peuvent implémenter toString()
avec un format recommandé correspondant à l'implémentation de la classe de données Kotlin, par exemple User(var1=Alex, var2=42)
.
Méthodes
Il s'agit de règles concernant divers aspects spécifiques des méthodes, des paramètres, des noms de méthodes, des types de retour et des spécificateurs d'accès.
Durée
Ces règles couvrent la façon dont les concepts temporels tels que les dates et les durées doivent être exprimés dans les API.
Privilégiez les types java.time.* dans la mesure du possible.
java.time.Duration
, java.time.Instant
et de nombreux autres types java.time.*
sont disponibles sur toutes les versions de la plate-forme grâce au desugaring. Il est préférable de les utiliser pour exprimer le temps dans les paramètres ou les valeurs de retour de l'API.
Privilégiez l'exposition des variantes d'une API qui acceptent ou renvoient java.time.Duration
ou java.time.Instant
, et omettez les variantes primitives avec le même cas d'utilisation, sauf si le domaine de l'API est un domaine où l'allocation d'objets dans les modèles d'utilisation prévus aurait un impact prohibitif sur les performances.
Les méthodes exprimant des durées doivent être nommées "duration"
Si une valeur temporelle exprime la durée concernée, nommez le paramètre "duration" (durée) et non "time" (heure).
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
Exceptions :
"timeout" convient lorsque la durée s'applique spécifiquement à une valeur de délai d'inactivité.
"time" avec un type java.time.Instant
convient lorsqu'il s'agit d'un moment précis, et non d'une durée.
Les méthodes exprimant des durées ou du temps sous forme de primitive doivent être nommées avec leur unité de temps et utiliser le type long.
Les méthodes acceptant ou renvoyant des durées en tant que primitive doivent ajouter les unités de temps associées (telles que Millis
, Nanos
, Seconds
) au nom de la méthode pour réserver le nom non décoré à l'utilisation avec java.time.Duration
. Consultez Heure.
Les méthodes doivent également être annotées de manière appropriée avec leur unité et leur base temporelle :
@CurrentTimeMillisLong
: la valeur est un code temporel non négatif mesuré en nombre de millisecondes depuis le 1970-01-01T00:00:00Z.@CurrentTimeSecondsLong
: la valeur est un code temporel non négatif mesuré en nombre de secondes depuis le 1er janvier 1970 à 00:00:00 UTC.@DurationMillisLong
: la valeur est une durée non négative en millisecondes.@ElapsedRealtimeLong
: la valeur est un code temporel non négatif dans la base temporelleSystemClock.elapsedRealtime()
.@UptimeMillisLong
: la valeur est un code temporel non négatif dans la base temporelleSystemClock.uptimeMillis()
.
Les paramètres ou valeurs de retour temporels primitifs doivent utiliser long
, et non int
.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
Les méthodes exprimant des unités de temps doivent préférer les abréviations non abrégées pour les noms d'unités.
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
Annoter les arguments de longue durée
La plate-forme inclut plusieurs annotations pour fournir une typographie plus forte pour les unités de temps de type long
:
@CurrentTimeMillisLong
: la valeur est un code temporel non négatif mesuré en nombre de millisecondes depuis1970-01-01T00:00:00Z
, donc dans la base de tempsSystem.currentTimeMillis()
.@CurrentTimeSecondsLong
: la valeur est un code temporel non négatif mesuré en nombre de secondes depuis1970-01-01T00:00:00Z
.@DurationMillisLong
: la valeur est une durée non négative en millisecondes.@ElapsedRealtimeLong
: la valeur est un code temporel non négatif dans la base temporelleSystemClock#elapsedRealtime()
.@UptimeMillisLong
: la valeur est un code temporel non négatif dans la base temporelleSystemClock#uptimeMillis()
.
Unités de mesure
Pour toutes les méthodes exprimant une unité de mesure autre que le temps, préférez les préfixes d'unité SI en CamelCase.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
Placer les paramètres facultatifs à la fin des surcharges
Si vous avez des surcharges d'une méthode avec des paramètres facultatifs, conservez ces paramètres à la fin et gardez un ordre cohérent avec les autres paramètres :
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
Lorsque vous ajoutez des surcharges pour les arguments facultatifs, le comportement des méthodes plus simples doit être exactement le même que si des arguments par défaut avaient été fournis aux méthodes plus élaborées.
Corollaire : Ne surchargez pas les méthodes, sauf pour ajouter des arguments facultatifs ou pour accepter différents types d'arguments si la méthode est polymorphe. Si la méthode surchargée fait quelque chose de fondamentalement différent, donnez-lui un nouveau nom.
Les méthodes avec des paramètres par défaut doivent être annotées avec @JvmOverloads (Kotlin uniquement).
Les méthodes et les constructeurs avec des paramètres par défaut doivent être annotés avec @JvmOverloads
pour maintenir la compatibilité binaire.
Pour en savoir plus, consultez la section Surcharges de fonction pour les valeurs par défaut dans le guide d'interopérabilité Kotlin-Java officiel.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
Ne pas supprimer les valeurs de paramètre par défaut (Kotlin uniquement)
Si une méthode a été publiée avec un paramètre ayant une valeur par défaut, la suppression de cette valeur par défaut constitue une modification incompatible avec la source.
Les paramètres de méthode les plus distinctifs et les plus identifiants doivent être placés en premier.
Si vous avez une méthode avec plusieurs paramètres, placez les plus pertinents en premier. Les paramètres qui spécifient des indicateurs et d'autres options sont moins importants que ceux qui décrivent l'objet sur lequel l'action est effectuée. Si un rappel de fin existe, placez-le en dernier.
public void openFile(int flags, String name);
public void openFileAsync(OnFileOpenedListener listener, String name, int flags);
public void setFlags(int mask, int flags);
public void openFile(String name, int flags);
public void openFileAsync(String name, int flags, OnFileOpenedListener listener);
public void setFlags(int flags, int mask);
Voir aussi : Placer les paramètres facultatifs à la fin des surcharges
Compilateurs
Le modèle Builder est recommandé pour créer des objets Java complexes. Il est couramment utilisé dans Android dans les cas suivants :
- Les propriétés de l'objet résultant doivent être immuables.
- Il existe un grand nombre de propriétés requises, par exemple de nombreux arguments de constructeur.
- Il existe une relation complexe entre les propriétés au moment de la construction (une étape de validation est par exemple requise). Notez que ce niveau de complexité indique souvent des problèmes d'usabilité de l'API.
Déterminez si vous avez besoin d'un générateur. Les compilateurs sont utiles dans une surface d'API s'ils sont utilisés pour :
- Configurer seulement quelques paramètres de création facultatifs parmi un grand nombre
- Configurer de nombreux paramètres de création facultatifs ou obligatoires différents, parfois de types similaires ou identiques, où les sites d'appel pourraient autrement devenir difficiles à lire ou sujets à des erreurs d'écriture
- Configurez la création d'un objet de manière incrémentielle, où plusieurs éléments de code de configuration différents peuvent chacun effectuer des appels sur le générateur en tant que détails d'implémentation.
- Autoriser un type à se développer en ajoutant des paramètres de création facultatifs supplémentaires dans les futures versions de l'API
Si vous avez un type avec trois paramètres requis ou moins et aucun paramètre facultatif, vous pouvez presque toujours ignorer un compilateur et utiliser un constructeur simple.
Les classes provenant de Kotlin doivent préférer les constructeurs annotés @JvmOverloads
avec des arguments par défaut aux compilateurs, mais peuvent choisir d'améliorer la facilité d'utilisation pour les clients Java en fournissant également des compilateurs dans les cas décrits précédemment.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
Les classes de compilateur doivent renvoyer le compilateur
Les classes Builder doivent permettre le chaînage de méthodes en renvoyant l'objet Builder (tel que this
) à partir de chaque méthode, à l'exception de build()
. Les objets créés supplémentaires doivent être transmis en tant qu'arguments. Ne renvoyez pas le compilateur d'un autre objet.
Exemple :
public static class Builder {
public void setDuration(long);
public void setFrequency(int);
public DtmfConfigBuilder addDtmfConfig();
public Tone build();
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
Dans les rares cas où une classe de compilateur de base doit prendre en charge l'extension, utilisez un type de retour générique :
public abstract class Builder<T extends Builder<T>> {
abstract T setValue(int);
}
public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
T setValue(int);
T setTypeSpecificValue(long);
}
Les classes Builder doivent être créées via un constructeur.
Pour créer des compilateurs de manière cohérente sur la surface de l'API Android, tous les compilateurs doivent être créés à l'aide d'un constructeur et non d'une méthode de création statique. Pour les API basées sur Kotlin, le Builder
doit être public, même si les utilisateurs de Kotlin sont censés s'appuyer implicitement sur le compilateur via un mécanisme de création de style de méthode/DSL de fabrique. Les bibliothèques ne doivent pas utiliser @PublishedApi internal
pour masquer sélectivement le constructeur de classe Builder
des clients Kotlin.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
Tous les arguments des constructeurs de compilateur doivent être obligatoires (comme @NonNull).
Les arguments facultatifs, par exemple @Nullable
, doivent être déplacés vers les méthodes setter.
Le constructeur du compilateur doit générer une NullPointerException
(envisagez d'utiliser Objects.requireNonNull
) si des arguments requis ne sont pas spécifiés.
Les classes Builder doivent être des classes internes statiques finales de leurs types intégrés.
Pour une organisation logique au sein d'un package, les classes de compilateur doivent généralement être exposées en tant que classes internes finales de leurs types intégrés, par exemple Tone.Builder
plutôt que ToneBuilder
.
Les builders peuvent inclure un constructeur pour créer une instance à partir d'une instance existante.
Les compilateurs peuvent inclure un constructeur de copie pour créer une instance de compilateur à partir d'un compilateur ou d'un objet compilé existants. Ils ne doivent pas fournir d'autres méthodes pour créer des instances de compilateur à partir de compilateurs ou d'objets de compilation existants.
public class Tone {
public static class Builder {
public Builder clone();
}
public Builder toBuilder();
}
public class Tone {
public static class Builder {
public Builder(Builder original);
public Builder(Tone original);
}
}
Les setters de compilateur doivent accepter les arguments @Nullable si le compilateur possède un constructeur de copie.
La réinitialisation est essentielle si une nouvelle instance d'un générateur peut être créée à partir d'une instance existante. Si aucun constructeur de copie n'est disponible, le compilateur peut avoir des arguments @Nullable
ou @NonNullable
.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
Les setters de compilateur peuvent accepter des arguments @Nullable pour les propriétés facultatives.
Il est souvent plus simple d'utiliser une valeur pouvant être nulle pour les entrées de second degré, en particulier en Kotlin, qui utilise des arguments par défaut au lieu de constructeurs et de surcharges.
De plus, les setters @Nullable
seront associés à leurs getters, qui doivent être @Nullable
pour les propriétés facultatives.
Value createValue(@Nullable OptionalValue optionalValue) {
Value.Builder builder = new Value.Builder();
if (optionalValue != null) {
builder.setOptionalValue(optionalValue);
}
return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
return new Value.Builder()
.setOptionalValue(optionalValue);
.build();
}
// Or in other cases:
Value createValue() {
return new Value.Builder()
.setOptionalValue(condition ? new OptionalValue() : null);
.build();
}
Utilisation courante en Kotlin :
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
La valeur par défaut (si le setter n'est pas appelé) et la signification de null
doivent être correctement documentées dans le setter et le getter.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
Des setters de compilateur peuvent être fournis pour les propriétés mutables où des setters sont disponibles sur la classe compilée.
Si votre classe comporte des propriétés mutables et a besoin d'une classe Builder
, demandez-vous d'abord si votre classe doit vraiment avoir des propriétés mutables.
Ensuite, si vous êtes certain d'avoir besoin de propriétés mutables, déterminez lequel des scénarios suivants correspond le mieux à votre cas d'utilisation :
L'objet créé doit être immédiatement utilisable. Par conséquent, des setters doivent être fournis pour toutes les propriétés pertinentes, qu'elles soient mutables ou immuables.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
Certains appels supplémentaires peuvent être nécessaires avant que l'objet créé ne soit utile. Par conséquent, les setters ne doivent pas être fournis pour les propriétés mutables.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
Ne mélangez pas les deux scénarios.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
Les builders ne doivent pas avoir de getters
Le getter doit se trouver sur l'objet créé, et non sur le compilateur.
Les setters de compilateur doivent avoir des getters correspondants sur la classe compilée.
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
public long getDuration();
public int getFrequency();
public @NonNull List<DtmfConfig> getDtmfConfigs();
}
Nommage des méthodes de création
Les noms des méthodes de compilateur doivent utiliser le style setFoo()
, addFoo()
ou clearFoo()
.
Les classes Builder doivent déclarer une méthode build()
Les classes Builder doivent déclarer une méthode build()
qui renvoie une instance de l'objet construit.
Les méthodes build() du compilateur doivent renvoyer des objets @NonNull
La méthode build()
d'un compilateur est censée renvoyer une instance non nulle de l'objet construit. Si l'objet ne peut pas être créé en raison de paramètres non valides, la validation peut être différée à la méthode de compilation et une IllegalStateException
doit être générée.
Ne pas exposer les verrous internes
Les méthodes de l'API publique ne doivent pas utiliser le mot clé synchronized
. Ce mot clé entraîne l'utilisation de votre objet ou classe comme verrou. Comme il est exposé à d'autres, vous pouvez rencontrer des effets secondaires inattendus si d'autres codes en dehors de votre classe commencent à l'utiliser à des fins de verrouillage.
Effectuez plutôt tout verrouillage requis sur un objet interne et privé.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
Les méthodes de style Accessor doivent suivre les consignes relatives aux propriétés Kotlin
Lorsqu'elles sont consultées à partir de sources Kotlin, les méthodes de style accesseur (celles qui utilisent les préfixes get
, set
ou is
) seront également disponibles en tant que propriétés Kotlin.
Par exemple, int getField()
défini en Java est disponible en Kotlin en tant que propriété val field: Int
.
Pour cette raison, et pour répondre aux attentes des développeurs concernant le comportement des méthodes d'accesseur, les méthodes utilisant des préfixes de méthode d'accesseur doivent se comporter de la même manière que les champs Java. Évitez d'utiliser des préfixes de style accesseur dans les cas suivants :
- La méthode a des effets secondaires. Préférez un nom de méthode plus descriptif.
- La méthode implique un travail coûteux en termes de calcul. Préférez
compute
. - La méthode implique un blocage ou un travail de longue durée pour renvoyer une valeur, telle que l'IPC ou d'autres E/S. Préférez
fetch
. - La méthode bloque le thread jusqu'à ce qu'elle puisse renvoyer une valeur. Il est préférable d'utiliser
await
. - La méthode renvoie une nouvelle instance d'objet à chaque appel. Il est préférable d'utiliser
create
. - La méthode peut ne pas renvoyer de valeur. Préférez
request
.
Notez que l'exécution d'un travail coûteux en termes de calcul une seule fois et la mise en cache de la valeur pour les appels ultérieurs sont toujours considérées comme un travail coûteux en termes de calcul. Le saccade n'est pas amorti sur les frames.
Utiliser le préfixe "is" pour les méthodes d'accesseur booléennes
Il s'agit de la convention d'attribution de noms standard pour les méthodes et les champs booléens en Java. En général, les noms de méthodes et de variables booléennes doivent être rédigés sous forme de questions auxquelles la valeur renvoyée répond.
Les méthodes d'accesseur booléen Java doivent suivre un schéma de dénomination set
/is
et les champs doivent préférer is
, comme dans l'exemple suivant :
// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();
// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();
final boolean isAvailable;
L'utilisation de set
/is
pour les méthodes d'accesseur Java ou de is
pour les champs Java leur permettra d'être utilisées comme propriétés à partir de Kotlin :
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
Les propriétés et les méthodes d'accesseur doivent généralement utiliser des noms positifs, par exemple Enabled
plutôt que Disabled
. L'utilisation d'une terminologie négative inverse la signification de true
et false
, et rend le comportement plus difficile à comprendre.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
Dans les cas où le booléen décrit l'inclusion ou la propriété d'une propriété, vous pouvez utiliser has plutôt que is. Toutefois, cela ne fonctionnera pas avec la syntaxe de propriété Kotlin :
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
Voici d'autres préfixes qui peuvent être plus adaptés : peut et doit :
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
Les méthodes qui activent ou désactivent des comportements ou des fonctionnalités peuvent utiliser le préfixe is et le suffixe Enabled :
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
De même, les méthodes qui indiquent la dépendance à d'autres comportements ou fonctionnalités peuvent utiliser le préfixe is et le suffixe Supported ou Required :
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
En général, les noms de méthodes doivent être écrits sous forme de questions auxquelles la valeur renvoyée répond.
Méthodes de propriété Kotlin
Pour une propriété de classe var foo: Foo
, Kotlin génère des méthodes get
/set
en utilisant une règle cohérente : ajoutez get
et mettez en majuscule le premier caractère pour le getter, et ajoutez set
et mettez en majuscule le premier caractère pour le setter. La déclaration de propriété générera des méthodes nommées public Foo getFoo()
et public void setFoo(Foo foo)
, respectivement.
Si la propriété est de type Boolean
, une règle supplémentaire s'applique à la génération de noms : si le nom de la propriété commence par is
, get
n'est pas ajouté au nom de la méthode getter. Le nom de la propriété lui-même est utilisé comme getter.
Par conséquent, préférez nommer les propriétés Boolean
avec un préfixe is
afin de suivre les consignes de dénomination :
var isVisible: Boolean
Si votre propriété fait partie des exceptions mentionnées ci-dessus et commence par un préfixe approprié, utilisez l'annotation @get:JvmName
sur la propriété pour spécifier manuellement le nom approprié :
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
Accesseurs de masque de bits
Consultez Utiliser @IntDef
pour les indicateurs de masque de bits pour obtenir des consignes sur l'API concernant la définition des indicateurs de masque de bits.
Setters
Deux méthodes de définition doivent être fournies : l'une qui accepte une chaîne de bits complète et remplace tous les indicateurs existants, et l'autre qui accepte un masque de bits personnalisé pour plus de flexibilité.
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Getters
Un getter doit être fourni pour obtenir le masque de bits complet.
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
Utiliser "Public" au lieu de "Protégé"
Toujours préférer public
à protected
dans l'API publique. L'accès protégé finit par être pénible à long terme, car les implémenteurs doivent remplacer les accesseurs publics dans les cas où un accès externe par défaut aurait été tout aussi bien.
N'oubliez pas que la visibilité protected
n'empêche pas les développeurs d'appeler une API. Elle ne fait que rendre la chose légèrement plus désagréable.
Implémenter ni equals() ni hashCode(), ou les deux
Si vous en remplacez un, vous devez remplacer l'autre.
Implémenter toString() pour les classes de données
Il est recommandé aux classes de données de remplacer toString()
pour aider les développeurs à déboguer leur code.
Indiquez si la sortie concerne le comportement du programme ou le débogage.
Décidez si vous souhaitez que le comportement du programme repose sur votre implémentation ou non. Par exemple, UUID.toString() et File.toString() documentent leur format spécifique pour les programmes. Si vous n'exposez des informations que pour le débogage, comme Intent, impliquez l'héritage des documents de la superclasse.
N'incluez pas d'informations supplémentaires
Toutes les informations disponibles sur toString()
doivent également être disponibles via l'API publique de l'objet. Sinon, vous encouragez les développeurs à analyser et à s'appuyer sur votre résultat toString()
, ce qui empêchera les modifications futures. Il est recommandé d'implémenter toString()
en utilisant uniquement l'API publique de l'objet.
Décourager la dépendance à la sortie de débogage
Bien qu'il soit impossible d'empêcher les développeurs de dépendre de la sortie de débogage, y compris le System.identityHashCode
de votre objet dans sa sortie toString()
, il est très peu probable que deux objets différents aient une sortie toString()
égale.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
Cela peut dissuader efficacement les développeurs d'écrire des assertions de test telles que assertThat(a.toString()).isEqualTo(b.toString())
sur vos objets.
Utiliser createFoo lors du renvoi d'objets nouvellement créés
Utilisez le préfixe create
, et non get
ni new
, pour les méthodes qui créeront des valeurs de retour, par exemple en construisant de nouveaux objets.
Lorsque la méthode crée un objet à renvoyer, indiquez-le clairement dans le nom de la méthode.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
Les méthodes acceptant les objets File doivent également accepter les flux
Les emplacements de stockage de données sur Android ne sont pas toujours des fichiers sur disque. Par exemple, le contenu transmis au-delà des limites de l'utilisateur est représenté par content://
Uri
. Pour permettre le traitement de diverses sources de données, les API qui acceptent les objets File
doivent également accepter les objets InputStream
, OutputStream
ou les deux.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
Prendre et renvoyer des primitives brutes au lieu de versions boxed
Si vous devez communiquer des valeurs manquantes ou nulles, envisagez d'utiliser -1
, Integer.MAX_VALUE
ou Integer.MIN_VALUE
.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
Éviter les équivalents de classe des types primitifs permet d'éviter la surcharge de mémoire de ces classes, l'accès aux valeurs par méthode et, plus important encore, l'autoboxing qui résulte de la conversion entre les types primitifs et les types d'objet. Éviter ces comportements permet d'économiser de la mémoire et des allocations temporaires qui peuvent entraîner des collectes de déchets coûteuses et plus fréquentes.
Utiliser des annotations pour clarifier les valeurs de paramètre et de retour valides
Des annotations de développeur ont été ajoutées pour clarifier les valeurs autorisées dans différentes situations. Les outils peuvent ainsi aider plus facilement les développeurs lorsqu'ils fournissent des valeurs incorrectes (par exemple, en transmettant un int
arbitraire lorsque le framework nécessite l'un des ensembles spécifiques de valeurs constantes). Utilisez toutes les annotations suivantes, le cas échéant :
Possibilité de valeur nulle
Des annotations de possibilité de valeur nulle explicites sont requises pour les API Java, mais le concept de possibilité de valeur nulle fait partie du langage Kotlin. Les annotations de possibilité de valeur nulle ne doivent donc jamais être utilisées dans les API Kotlin.
@Nullable
: indique qu'une valeur de retour, un paramètre ou un champ donné peuvent être nuls :
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: indique qu'une valeur de retour, un paramètre ou un champ donné ne peuvent pas être nuls. Le marquage des éléments comme @Nullable
est relativement nouveau sur Android. Par conséquent, la plupart des méthodes d'API d'Android ne sont pas documentées de manière cohérente. Nous avons donc trois états : "inconnu, @Nullable
, @NonNull
". C'est pourquoi @NonNull
fait partie des consignes relatives à l'API :
@NonNull
public String getName()
public void setName(@NonNull String name)
Pour la documentation de la plate-forme Android, l'annotation des paramètres de votre méthode générera automatiquement une documentation sous la forme "Cette valeur peut être nulle", sauf si "null" est explicitement utilisé ailleurs dans la documentation des paramètres.
Méthodes existantes "pas vraiment annulables" : les méthodes existantes dans l'API sans annotation @Nullable
déclarée peuvent être annotées @Nullable
si la méthode peut renvoyer null
dans des circonstances spécifiques et évidentes (telles que findViewById()
). Des méthodes @NotNull requireFoo()
associées qui génèrent IllegalArgumentException
doivent être ajoutées pour les développeurs qui ne souhaitent pas effectuer de vérification de la valeur nulle.
Méthodes d'interface : les nouvelles API doivent ajouter l'annotation appropriée lors de l'implémentation des méthodes d'interface, comme Parcelable.writeToParcel()
(c'est-à-dire que cette méthode dans la classe d'implémentation doit être writeToParcel(@NonNull Parcel,
int)
, et non writeToParcel(Parcel, int)
). Toutefois, les API existantes qui ne comportent pas d'annotations n'ont pas besoin d'être "corrigées".
Application de la possibilité de valeur nulle
En Java, les méthodes sont recommandées pour effectuer la validation des entrées pour les paramètres @NonNull
à l'aide de Objects.requireNonNull()
et générer une NullPointerException
lorsque les paramètres sont nuls. Cette opération est effectuée automatiquement en Kotlin.
Ressources
Identifiants de ressources : les paramètres entiers qui désignent des ID pour des ressources spécifiques doivent être annotés avec la définition de type de ressource appropriée.
Il existe une annotation pour chaque type de ressource, comme @StringRes
, @ColorRes
et @AnimRes
, en plus de l'annotation générique @AnyRes
. Exemple :
public void setTitle(@StringRes int resId)
@IntDef pour les ensembles de constantes
Constantes magiques : les paramètres String
et int
qui sont censés recevoir l'une des valeurs possibles d'un ensemble fini désignées par des constantes publiques doivent être annotés de manière appropriée avec @StringDef
ou @IntDef
. Ces annotations vous permettent de créer une annotation que vous pouvez utiliser comme un typedef pour les paramètres autorisés. Exemple :
/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
NAVIGATION_MODE_STANDARD,
NAVIGATION_MODE_LIST,
NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);
Il est recommandé d'utiliser des méthodes pour vérifier la validité des paramètres annotés et générer une IllegalArgumentException
si le paramètre ne fait pas partie de @IntDef
.
@IntDef pour les indicateurs de masque de bits
L'annotation peut également spécifier que les constantes sont des indicateurs et peuvent être combinées avec & et I :
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_USE_LOGO,
FLAG_SHOW_HOME,
FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}
@StringDef pour les ensembles de constantes de chaîne
Il existe également l'annotation @StringDef
, qui est exactement comme @IntDef
dans la section précédente, mais pour les constantes String
. Vous pouvez inclure plusieurs valeurs de "prefix" qui sont utilisées pour émettre automatiquement la documentation de toutes les valeurs.
@SdkConstant pour les constantes du SDK
@SdkConstant Annotez les champs publics lorsqu'ils correspondent à l'une des valeurs SdkConstant
suivantes : ACTIVITY_INTENT_ACTION
, BROADCAST_INTENT_ACTION
, SERVICE_ACTION
, INTENT_CATEGORY
, FEATURE
.
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";
Fournir une nullité compatible pour les substitutions
Pour la compatibilité des API, la possibilité de valeur nulle des remplacements doit être compatible avec la possibilité de valeur nulle actuelle du parent. Le tableau suivant présente les attentes en termes de compatibilité. En d'autres termes, les remplacements ne doivent pas être moins restrictifs que l'élément qu'ils remplacent.
Saisie | Parent | Enfant |
---|---|---|
Type de retour | Non annoté | Non annoté ou non nul |
Type de retour | Nullable | Acceptant ou non les valeurs nulles |
Type de retour | Nonnull | Nonnull |
Argument amusant | Non annoté | Non annoté ou pouvant être nul |
Argument amusant | Nullable | Nullable |
Argument amusant | Nonnull | Acceptant ou non les valeurs nulles |
Privilégiez les arguments non nullables (tels que @NonNull) dans la mesure du possible.
Lorsque les méthodes sont surchargées, il est préférable que tous les arguments ne soient pas nuls.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
Cette règle s'applique également aux setters de propriété surchargés. L'argument principal ne doit pas être nul et l'effacement de la propriété doit être implémenté en tant que méthode distincte. Cela évite les appels "absurdes" où le développeur doit définir des paramètres de fin même s'ils ne sont pas requis.
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)
// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()
Privilégiez les types de retour non nullables (tels que @NonNull) pour les conteneurs.
Pour les types de conteneurs tels que Bundle
ou Collection
, renvoyez un conteneur vide et immuable, le cas échéant. Dans les cas où null
serait utilisé pour distinguer la disponibilité d'un conteneur, envisagez de fournir une méthode booléenne distincte.
@NonNull
public Bundle getExtras() { ... }
Les annotations de nullabilité pour les paires get et set doivent correspondre
Les paires de méthodes get et set pour une même propriété logique doivent toujours être cohérentes dans leurs annotations de nullabilité. Si vous ne respectez pas cette consigne, la syntaxe de propriété de Kotlin sera ignorée. L'ajout d'annotations de nullabilité différentes aux méthodes de propriété existantes constitue donc une modification incompatible avec la source pour les utilisateurs Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Valeur renvoyée en cas d'échec ou d'erreur
Toutes les API doivent permettre aux applications de réagir aux erreurs. Le renvoi de false
, -1
, null
ou d'autres valeurs génériques de type "un problème est survenu" ne fournit pas aux développeurs suffisamment d'informations sur l'échec pour définir les attentes des utilisateurs ou suivre précisément la fiabilité de leur application sur le terrain. Lorsque vous concevez une API, imaginez que vous créez une application. Si vous rencontrez une erreur, l'API vous fournit-elle suffisamment d'informations pour la présenter à l'utilisateur ou réagir de manière appropriée ?
- Il est tout à fait acceptable (et même recommandé) d'inclure des informations détaillées dans un message d'exception, mais les développeurs ne devraient pas avoir à l'analyser pour gérer l'erreur de manière appropriée. Les codes d'erreur détaillés ou d'autres informations doivent être exposés en tant que méthodes.
- Assurez-vous que l'option de gestion des erreurs choisie vous permet d'introduire de nouveaux types d'erreurs à l'avenir. Pour
@IntDef
, cela signifie inclure une valeurOTHER
ouUNKNOWN
. Lorsque vous renvoyez un nouveau code, vous pouvez vérifier letargetSdkVersion
de l'appelant pour éviter de renvoyer un code d'erreur que l'application ne connaît pas. Pour les exceptions, utilisez une superclasse commune que vos exceptions implémentent. Ainsi, tout code qui gère ce type d'exception pourra également détecter et gérer les sous-types. - Il doit être difficile, voire impossible, pour un développeur d'ignorer une erreur par inadvertance. Si votre erreur est communiquée en renvoyant une valeur, annotez votre méthode avec
@CheckResult
.
Il est préférable de générer une ? extends RuntimeException
lorsqu'une condition d'échec ou d'erreur est atteinte en raison d'une erreur du développeur, par exemple en ignorant les contraintes sur les paramètres d'entrée ou en ne vérifiant pas l'état observable.
Les méthodes de définition ou d'action (par exemple, perform
) peuvent renvoyer un code d'état entier si l'action peut échouer en raison d'un état mis à jour de manière asynchrone ou de conditions indépendantes de la volonté du développeur.
Les codes d'état doivent être définis dans la classe conteneur en tant que champs public static final
, préfixés par ERROR_
et énumérés dans une annotation @hide
@IntDef
.
Les noms de méthodes doivent toujours commencer par le verbe, et non par le sujet.
Le nom de la méthode doit toujours commencer par le verbe (par exemple, get
, create
, reload
, etc.), et non par l'objet sur lequel vous agissez.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Préférez les types Collection aux tableaux comme type de retour ou de paramètre
Les interfaces de collection à typage générique offrent plusieurs avantages par rapport aux tableaux, y compris des contrats d'API plus stricts concernant l'unicité et l'ordre, la prise en charge des génériques et un certain nombre de méthodes pratiques pour les développeurs.
Exception pour les primitives
Si les éléments sont des primitives, préférez les tableaux pour éviter le coût de l'auto-boxing. Voir Prendre et renvoyer des primitives brutes au lieu de versions encadrées
Exception pour le code sensible aux performances
Dans certains cas, lorsque l'API est utilisée dans du code sensible aux performances (comme les API graphiques ou d'autres API de mesure/mise en page/dessin), il est acceptable d'utiliser des tableaux au lieu de collections afin de réduire les allocations et le churn de mémoire.
Exception pour Kotlin
Les tableaux Kotlin sont invariants et le langage Kotlin fournit de nombreuses API utilitaires autour des tableaux. Les tableaux sont donc équivalents à List
et Collection
pour les API Kotlin destinées à être accessibles depuis Kotlin.
Préférer les collections @NonNull
Privilégiez toujours @NonNull
pour les objets de collection. Lorsque vous renvoyez une collection vide, utilisez la méthode Collections.empty
appropriée pour renvoyer un objet de collection immuable, correctement typé et à faible coût.
Lorsque les annotations de type sont acceptées, préférez toujours @NonNull
pour les éléments de collection.
Vous devez également préférer @NonNull
lorsque vous utilisez des tableaux au lieu de collections (voir l'élément précédent). Si l'allocation d'objets est un problème, créez une constante et transmettez-la. Après tout, un tableau vide est immuable. Exemple :
private static final int[] EMPTY_USER_IDS = new int[0];
@NonNull
public int[] getUserIds() {
int [] userIds = mService.getUserIds();
return userIds != null ? userIds : EMPTY_USER_IDS;
}
Mutabilité de la collection
Les API Kotlin doivent préférer les types de retour en lecture seule (et non Mutable
) pour les collections par défaut sauf si le contrat d'API exige spécifiquement un type de retour mutable.
Toutefois, les API Java doivent préférer les types de retour mutables par défaut, car l'implémentation de la plate-forme Android des API Java ne fournit pas encore d'implémentation pratique des collections immuables. Les types de retour Collections.empty
font exception à cette règle, car ils sont immuables. Dans les cas où la mutabilité pourrait être exploitée par les clients (intentionnellement ou par erreur) pour enfreindre le modèle d'utilisation prévu de l'API, les API Java doivent sérieusement envisager de renvoyer une copie superficielle de la collection.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
Types de retour explicitement modifiables
Idéalement, les API qui renvoient des collections ne doivent pas modifier l'objet de collection renvoyé après l'avoir renvoyé. Si la collection renvoyée doit être modifiée ou réutilisée d'une manière ou d'une autre (par exemple, une vue adaptée d'un ensemble de données mutable), le comportement précis de when (quand) le contenu peut changer doit être explicitement documenté ou suivre les conventions de nommage établies de l'API.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
La convention Kotlin .asFoo()
est décrite ci-dessous et permet à la collection renvoyée par .asList()
de changer si la collection d'origine change.
Mutabilité des objets de type de données renvoyés
Comme pour les API qui renvoient des collections, les API qui renvoient des objets de type de données ne doivent idéalement pas modifier les propriétés de l'objet renvoyé après l'avoir renvoyé.
val tempResult = DataContainer()
fun add(other: DataContainer): DataContainer {
tempResult.innerValue = innerValue + other.innerValue
return tempResult
}
fun add(other: DataContainer): DataContainer {
return DataContainer(innerValue + other.innerValue)
}
Dans de très rares cas, certains codes sensibles aux performances peuvent bénéficier du pooling ou de la réutilisation d'objets. N'écrivez pas votre propre structure de données de pool d'objets et n'exposez pas les objets réutilisés dans les API publiques. Dans les deux cas, soyez extrêmement prudent quant à la gestion des accès simultanés.
Utilisation du type de paramètre vararg
Les API Kotlin et Java sont encouragées à utiliser vararg
dans les cas où le développeur serait susceptible de créer un tableau au niveau de l'appel dans le seul but de transmettre plusieurs paramètres associés du même type.
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
Copies défensives
Les implémentations Java et Kotlin des paramètres vararg
sont compilées dans le même bytecode soutenu par un tableau et peuvent donc être appelées à partir du code Java avec un tableau mutable. Les concepteurs d'API sont vivement encouragés à créer une copie superficielle défensive du paramètre de tableau dans les cas où il sera conservé dans un champ ou une classe interne anonyme.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
Notez que la création d'une copie défensive ne protège pas contre les modifications simultanées entre l'appel de méthode initial et la création de la copie, ni contre la mutation des objets contenus dans le tableau.
Fournir une sémantique correcte avec des paramètres ou des types renvoyés de type collection
List<Foo>
est l'option par défaut, mais vous pouvez envisager d'autres types pour fournir des informations supplémentaires :
Utilisez
Set<Foo>
si votre API est indifférente à l'ordre des éléments et qu'elle n'autorise pas les doublons ou que les doublons n'ont pas de sens.Collection<Foo>,
si votre API est indifférente à l'ordre et autorise les doublons.
Fonctions de conversion Kotlin
Kotlin utilise fréquemment .toFoo()
et .asFoo()
pour obtenir un objet d'un type différent à partir d'un objet existant, où Foo
est le nom du type renvoyé de la conversion. Cela correspond au JDK Object.toString()
que vous connaissez. Kotlin va plus loin en l'utilisant pour les conversions primitives telles que 25.toFloat()
.
La distinction entre les conversions nommées .toFoo()
et .asFoo()
est importante :
Utiliser .toFoo() lors de la création d'un objet indépendant
Comme .toString()
, une conversion "to" renvoie un nouvel objet indépendant. Si l'objet d'origine est modifié ultérieurement, le nouvel objet ne reflétera pas ces modifications.
De même, si l'objet new est modifié ultérieurement, l'objet old ne reflétera pas ces modifications.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
Utiliser .asFoo() lors de la création d'un wrapper dépendant, d'un objet décoré ou d'un cast
En Kotlin, le casting est effectué à l'aide du mot clé as
. Il reflète un changement d'interface, mais pas d'identité. Lorsqu'il est utilisé comme préfixe dans une fonction d'extension, .asFoo()
décore le récepteur. Une mutation dans l'objet récepteur d'origine se reflétera dans l'objet renvoyé par asFoo()
.
Une mutation dans le nouvel objet Foo
peut se refléter dans l'objet d'origine.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
Les fonctions de conversion doivent être écrites en tant que fonctions d'extension.
Écrire des fonctions de conversion en dehors des définitions de classe du récepteur et du résultat réduit le couplage entre les types. Une conversion idéale ne nécessite qu'un accès à l'API publique de l'objet d'origine. Cela prouve par l'exemple qu'un développeur peut également écrire des conversions analogues à ses propres types préférés.
Générer les exceptions spécifiques appropriées
Les méthodes ne doivent pas générer d'exceptions génériques telles que java.lang.Exception
ou java.lang.Throwable
. Une exception spécifique appropriée doit être utilisée à la place, comme java.lang.NullPointerException
, pour permettre aux développeurs de gérer les exceptions sans être trop généraux.
Les erreurs qui ne sont pas liées aux arguments fournis directement à la méthode appelée publiquement doivent générer java.lang.IllegalStateException
au lieu de java.lang.IllegalArgumentException
ou java.lang.NullPointerException
.
Écouteurs et rappels
Voici les règles concernant les classes et les méthodes utilisées pour les mécanismes d'écouteur et de rappel.
Les noms de classes de rappel doivent être au singulier
Utilisez MyObjectCallback
à la place de MyObjectCallbacks
.
Les noms des méthodes de rappel doivent être au format on .
onFooEvent
signifie que FooEvent
est en cours et que le rappel doit agir en conséquence.
Le temps du verbe (passé ou présent) doit décrire le comportement temporel.
Les méthodes de rappel concernant les événements doivent être nommées pour indiquer si l'événement s'est déjà produit ou est en cours.
Par exemple, si la méthode est appelée après une action de clic :
public void onClicked()
Toutefois, si la méthode est responsable de l'exécution de l'action de clic :
public boolean onClick()
Enregistrement du rappel
Lorsqu'un écouteur ou un rappel peuvent être ajoutés ou supprimés d'un objet, les méthodes associées doivent être nommées add et remove ou register et unregister. Respectez la convention existante utilisée par la classe ou par d'autres classes du même package. En l'absence d'un tel précédent, préférez "Ajouter" et "Supprimer".
Les méthodes impliquant l'enregistrement ou la désinscription des rappels doivent spécifier le nom complet du type de rappel.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
Éviter les getters pour les rappels
N'ajoutez pas de méthodes getFooCallback()
. Il s'agit d'une échappatoire tentante pour les cas où les développeurs peuvent vouloir enchaîner un rappel existant avec leur propre remplacement, mais elle est fragile et rend l'état actuel difficile à comprendre pour les développeurs de composants. Par exemple,
- Le développeur A appelle
setFooCallback(a)
- Le développeur B appelle
setFooCallback(new B(getFooCallback()))
- Le développeur A souhaite supprimer son rappel
a
, mais n'a aucun moyen de le faire sans connaître le type deB
, etB
ayant été conçu pour permettre de telles modifications de son rappel encapsulé.
Accepter l'exécuteur pour contrôler la distribution des rappels
Lors de l'enregistrement de rappels sans attentes de threading explicites (presque partout en dehors de la boîte à outils UI), il est fortement recommandé d'inclure un paramètre Executor
dans l'enregistrement pour permettre au développeur de spécifier le thread sur lequel les rappels seront appelés.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Par exception à nos consignes habituelles concernant les paramètres facultatifs, il est acceptable de fournir une surcharge omettant le Executor
même s'il ne s'agit pas du dernier argument de la liste des paramètres. Si Executor
n'est pas fourni, le rappel doit être appelé sur le thread principal à l'aide de Looper.getMainLooper()
. Cela doit être documenté sur la méthode surchargée associée.
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Points à vérifier lors de l'implémentation de Executor
: notez que l'exécuteur suivant est valide.
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Cela signifie que lors de l'implémentation d'API qui prennent cette forme, l'implémentation de votre objet Binder entrant côté processus d'application doit appeler Binder.clearCallingIdentity()
avant d'appeler le rappel de l'application sur le Executor
fourni par l'application. De cette façon, tout code d'application qui utilise l'identité Binder (comme Binder.getCallingUid()
) pour les vérifications d'autorisation attribue correctement le code en cours d'exécution à l'application et non au processus système qui appelle l'application. Si les utilisateurs de votre API souhaitent obtenir des informations sur l'UID ou le PID de l'appelant, cela doit faire partie explicitement de la surface de votre API, plutôt qu'implicitement en fonction de l'endroit où le Executor
qu'ils ont fourni s'est exécuté.
Votre API doit accepter la spécification d'un Executor
. Dans les cas où les performances sont critiques, les applications peuvent avoir besoin d'exécuter du code immédiatement ou de manière synchrone avec les commentaires de votre API. Pour ce faire, vous devez accepter un Executor
.
La création défensive d'un HandlerThread
supplémentaire ou similaire à un trampoline à partir de la défaite ce cas d'utilisation souhaitable.
Si une application doit exécuter du code coûteux dans son propre processus, laissez-la faire. Les solutions de contournement que les développeurs d'applications trouveront pour surmonter vos restrictions seront beaucoup plus difficiles à prendre en charge à long terme.
Exception pour un seul rappel : lorsque la nature des événements signalés nécessite de n'accepter qu'une seule instance de rappel, utilisez le style suivant :
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Utiliser Executor au lieu de Handler
L'Handler
d'Android était utilisé comme norme pour rediriger l'exécution du rappel vers un thread Looper
spécifique dans le passé. Cette norme a été modifiée pour privilégier Executor
, car la plupart des développeurs d'applications gèrent leurs propres pools de threads, ce qui fait du thread principal ou de l'UI le seul thread Looper
disponible pour l'application. Utilisez Executor
pour donner aux développeurs le contrôle dont ils ont besoin pour réutiliser leurs contextes d'exécution existants/préférés.
Les bibliothèques de concurrence modernes telles que kotlinx.coroutines ou RxJava fournissent leurs propres mécanismes de planification qui effectuent leur propre dispatch si nécessaire. Il est donc important de fournir la possibilité d'utiliser un exécuteur direct (tel que Runnable::run
) pour éviter la latence due aux doubles sauts de thread. Par exemple, un saut pour publier dans un thread Looper
à l'aide d'un Handler
, suivi d'un autre saut à partir du framework de simultanéité de l'application.
Les exceptions à cette consigne sont rares. Voici quelques cas de figure courants :
Je dois utiliser un Looper
, car j'ai besoin d'un Looper
pour epoll
pour l'événement.
Cette demande d'exception est accordée, car les avantages de Executor
ne peuvent pas être réalisés dans cette situation.
Je ne veux pas que le code de l'application bloque mon thread lors de la publication de l'événement. Cette demande d'exception n'est généralement pas accordée pour le code qui s'exécute dans un processus d'application. Les applications qui ne respectent pas cette règle ne se font du tort qu'à elles-mêmes, sans impacter l'état général du système. Les applications qui le font correctement ou qui utilisent un framework de concurrence commun ne devraient pas subir de pénalités de latence supplémentaires.
Handler
est cohérent au niveau local avec d'autres API similaires de la même classe.
Cette demande d'exception est accordée au cas par cas. La préférence est que les surcharges basées sur Executor
soient ajoutées, en migrant les implémentations Handler
pour utiliser la nouvelle implémentation Executor
. (myHandler::post
est un Executor
valide.) En fonction de la taille de la classe, du nombre de méthodes Handler
existantes et de la probabilité que les développeurs aient besoin d'utiliser les méthodes Handler
existantes en même temps que la nouvelle méthode, une exception peut être accordée pour ajouter une nouvelle méthode basée sur Handler
.
Symétrie dans l'enregistrement
Si vous pouvez ajouter ou enregistrer un élément, vous devez également pouvoir le supprimer ou l'annuler. La méthode
registerThing(Thing)
doit avoir un
unregisterThing(Thing)
Fournir un identifiant de requête
S'il est raisonnable pour un développeur de réutiliser un rappel, fournissez un objet d'identifiant pour associer le rappel à la requête.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
Objets de rappel à méthodes multiples
Les rappels à méthodes multiples doivent privilégier interface
et utiliser les méthodes default
lors de l'ajout à des interfaces déjà publiées. Auparavant, ce guide recommandait abstract class
en raison de l'absence de méthodes default
dans Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
Utiliser android.os.OutcomeReceiver pour modéliser un appel de fonction non bloquant
OutcomeReceiver<R,E>
renvoie une valeur de résultat R
en cas de succès ou E : Throwable
dans le cas contraire, comme le ferait un simple appel de méthode. Utilisez OutcomeReceiver
comme type de rappel lorsque vous convertissez une méthode bloquante qui renvoie un résultat ou lève une exception en méthode asynchrone non bloquante :
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
Les méthodes Async converties de cette manière renvoient toujours void
. Tout résultat que requestFoo
renverrait est plutôt signalé au OutcomeReceiver.onResult
du paramètre callback
de requestFooAsync
en l'appelant sur le executor
fourni.
Toute exception que requestFoo
générerait est signalée à la méthode OutcomeReceiver.onError
de la même manière.
L'utilisation de OutcomeReceiver
pour signaler les résultats des méthodes asynchrones fournit également un wrapper Kotlin suspend fun
pour les méthodes asynchrones utilisant l'extension Continuation.asOutcomeReceiver
de androidx.core:core-ktx
:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
Les extensions comme celle-ci permettent aux clients Kotlin d'appeler des méthodes asynchrones non bloquantes avec la simplicité d'un appel de fonction simple sans bloquer le thread d'appel. Ces extensions 1-1 pour les API de plate-forme peuvent être proposées dans l'artefact androidx.core:core-ktx
dans Jetpack lorsqu'elles sont combinées avec des vérifications et des considérations de compatibilité de version standard. Pour en savoir plus, consultez la documentation sur asOutcomeReceiver, qui fournit des informations, des considérations sur l'annulation et des exemples.
Les méthodes asynchrones qui ne correspondent pas à la sémantique d'une méthode renvoyant un résultat ou générant une exception lorsque leur travail est terminé ne doivent pas utiliser OutcomeReceiver
comme type de rappel. Envisagez plutôt l'une des autres options listées dans la section suivante.
Privilégier les interfaces fonctionnelles plutôt que de créer de nouveaux types SAM (Single Abstract Method)
Le niveau d'API 24 a ajouté les types java.util.function.*
(documentation de référence), qui offrent des interfaces SAM génériques telles que Consumer<T>
, qui peuvent être utilisées comme lambdas de rappel. Dans de nombreux cas, la création de nouvelles interfaces SAM n'apporte que peu de valeur en termes de sécurité des types ou de communication de l'intention, tout en élargissant inutilement la surface de l'API Android.
Envisagez d'utiliser ces interfaces génériques plutôt que d'en créer de nouvelles :
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- De nombreux autres sont disponibles dans la documentation de référence.
Emplacement des paramètres SAM
Les paramètres SAM doivent être placés en dernier pour permettre une utilisation idiomatique à partir de Kotlin, même si la méthode est surchargée avec des paramètres supplémentaires.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
Docs
Il s'agit de règles concernant la documentation publique (Javadoc) des API.
Toutes les API publiques doivent être documentées
Toutes les API publiques doivent disposer d'une documentation suffisante pour expliquer comment un développeur peut les utiliser. Supposons que le développeur a trouvé la méthode à l'aide de la saisie semi-automatique ou en parcourant la documentation de référence de l'API, et qu'il dispose d'un minimum de contexte à partir de la surface de l'API adjacente (par exemple, la même classe).
Méthodes
Les paramètres de méthode et les valeurs renvoyées doivent être documentés à l'aide des annotations de documentation @param
et @return
, respectivement. Mettez en forme le corps Javadoc comme s'il était précédé de "Cette méthode...".
Dans les cas où une méthode ne prend aucun paramètre, ne présente aucune considération particulière et renvoie ce que le nom de la méthode indique, vous pouvez omettre @return
et rédiger une documentation semblable à celle-ci :
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Toujours utiliser des liens dans Javadoc
Les documents doivent contenir des liens vers d'autres documents pour les constantes, les méthodes et d'autres éléments associés. Utilisez des tags Javadoc (par exemple, @see
et {@link foo}
), et pas seulement des mots en texte brut.
Pour l'exemple de source suivant :
public static final int FOO = 0;
public static final int BAR = 1;
N'utilisez pas de texte brut ni de police de code :
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
Utilisez plutôt des liens :
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
Notez que l'utilisation d'une annotation IntDef
telle que @ValueType
sur un paramètre génère automatiquement une documentation spécifiant les types autorisés. Pour en savoir plus sur IntDef
, consultez les conseils sur les annotations.
Exécuter la cible update-api ou docs lors de l'ajout de Javadoc
Cette règle est particulièrement importante lorsque vous ajoutez des balises @link
ou @see
. Assurez-vous que le résultat correspond à vos attentes. Les erreurs dans Javadoc sont souvent dues à des liens incorrects. La vérification est effectuée par la cible Make update-api
ou docs
, mais la cible docs
peut être plus rapide si vous ne modifiez que Javadoc et que vous n'avez pas besoin d'exécuter la cible update-api
.
Utilisez {@code foo} pour distinguer les valeurs Java.
Encapsulez les valeurs Java telles que true
, false
et null
avec {@code...}
pour les distinguer du texte de la documentation.
Lorsque vous rédigez de la documentation dans des sources Kotlin, vous pouvez encadrer le code avec des accents graves, comme vous le feriez pour Markdown.
Les résumés @param et @return doivent être des fragments de phrase uniques.
Les résumés des paramètres et des valeurs renvoyées doivent commencer par une lettre minuscule et ne contenir qu'un seul fragment de phrase. Si vous avez des informations supplémentaires qui dépassent une seule phrase, déplacez-les vers le corps du Javadoc de la méthode :
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
Doit être remplacé par :
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Les annotations Docs nécessitent des explications
Documentez pourquoi les annotations @hide
et @removed
sont masquées dans l'API publique.
Incluez des instructions sur la façon de remplacer les éléments d'API marqués par l'annotation @deprecated
.
Utiliser @throws pour documenter les exceptions
Si une méthode génère une exception vérifiée, par exemple IOException
, documentez l'exception avec @throws
. Pour les API provenant de Kotlin et destinées à être utilisées par des clients Java, annotez les fonctions avec @Throws
.
Si une méthode génère une exception non vérifiée indiquant une erreur évitable, par exemple IllegalArgumentException
ou IllegalStateException
, documentez l'exception en expliquant pourquoi elle est générée. L'exception générée doit également indiquer pourquoi elle a été générée.
Certains cas d'exception non vérifiée sont considérés comme implicites et n'ont pas besoin d'être documentés, comme NullPointerException
ou IllegalArgumentException
où un argument ne correspond pas à une annotation @IntDef
ou similaire qui intègre le contrat d'API dans la signature de la méthode :
/**
* ...
* @throws IOException If it cannot find the schema for {@code toVersion}
* @throws IllegalStateException If the schema validation fails
*/
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
boolean validateDroppedTables, Migration... migrations) throws IOException {
// ...
if (!dbPath.exists()) {
throw new IllegalStateException("Cannot find the database file for " + name
+ ". Before calling runMigrations, you must first create the database "
+ "using createDatabase.");
}
// ...
Ou en Kotlin :
/**
* ...
* @throws IOException If something goes wrong reading the file, such as a bad
* database header or missing permissions
*/
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
// ...
val read = input.read(buffer)
if (read != 4) {
throw IOException("Bad database header, unable to read 4 bytes at " +
"offset 60")
}
}
// ...
Si la méthode appelle du code asynchrone susceptible de générer des exceptions, réfléchissez à la manière dont le développeur peut les découvrir et y répondre. En règle générale, cela implique de transférer l'exception à un rappel et de documenter les exceptions générées sur la méthode qui les reçoit. Les exceptions asynchrones ne doivent pas être documentées avec @throws
, sauf si elles sont réellement relancées à partir de la méthode annotée.
Terminez la première phrase des documents par un point.
L'outil Doclava analyse les documents de manière simpliste, en mettant fin au document de synopsis (la première phrase, utilisée dans la description rapide en haut des documents de classe) dès qu'il voit un point (.) suivi d'un espace. Cela pose deux problèmes :
- Si un document court ne se termine pas par un point et que ce membre a hérité de documents qui sont détectés par l'outil, le synopsis reprend également ces documents hérités. Par exemple, consultez
actionBarTabStyle
dans la documentationR.attr
, qui contient la description de la dimension ajoutée au synopsis. - Évitez "par exemple" dans la première phrase pour la même raison, car Doclava met fin à la documentation du synopsis après "g.". Par exemple, consultez
TEXT_ALIGNMENT_CENTER
dansView.java
. Notez que Metalava corrige automatiquement cette erreur en insérant une espace insécable après le point. Toutefois, évitez de commettre cette erreur.
Mettre en forme des documents pour qu'ils soient affichés en HTML
Javadoc est rendu au format HTML. Mettez donc en forme ces documents en conséquence :
Les sauts de ligne doivent utiliser une balise
<p>
explicite. N'ajoutez pas de balise fermante</p>
.N'utilisez pas le code ASCII pour afficher des listes ou des tableaux.
Les listes doivent utiliser
<ul>
ou<ol>
pour les listes non ordonnées et ordonnées, respectivement. Chaque élément doit commencer par une balise<li>
, mais n'a pas besoin de balise de fermeture</li>
. Une balise fermante</ul>
ou</ol>
est requise après le dernier élément.Les tableaux doivent utiliser
<table>
,<tr>
pour les lignes,<th>
pour les en-têtes et<td>
pour les cellules. Tous les tags de tableau doivent être accompagnés d'un tag de fermeture correspondant. Vous pouvez utiliserclass="deprecated"
sur n'importe quelle balise pour indiquer qu'elle est obsolète.Pour créer une police de code intégré, utilisez
{@code foo}
.Pour créer des blocs de code, utilisez
<pre>
.Tout le texte à l'intérieur d'un bloc
<pre>
est analysé par le navigateur. Soyez donc prudent avec les crochets<>
. Vous pouvez les échapper avec les entités HTML<
et>
.Vous pouvez également laisser des crochets bruts
<>
dans votre extrait de code si vous entourez les sections concernées avec{@code foo}
. Exemple :<pre>{@code <manifest>}</pre>
Suivre le guide de style de la documentation de référence de l'API
Pour assurer la cohérence du style des résumés de classe, des descriptions de méthodes, des descriptions de paramètres et d'autres éléments, suivez les recommandations des consignes officielles du langage Java sur la page How to Write Doc Comments for the Javadoc Tool (Comment rédiger des commentaires de documentation pour l'outil Javadoc).
Règles spécifiques au framework Android
Ces règles concernent les API, les modèles et les structures de données spécifiques aux API et aux comportements intégrés au framework Android (par exemple, Bundle
ou Parcelable
).
Les générateurs d'intent doivent utiliser le modèle create*Intent()
Les créateurs d'intents doivent utiliser des méthodes nommées createFooIntent()
.
Utiliser Bundle au lieu de créer des structures de données à usage général
Évitez de créer des structures de données à usage général pour représenter des mappages arbitraires de clés vers des valeurs typées. Utilisez plutôt Bundle
.
Cela se produit généralement lors de l'écriture d'API de plate-forme qui servent de canaux de communication entre les applications et services non liés à la plate-forme, où la plate-forme ne lit pas les données envoyées sur le canal et où le contrat d'API peut être partiellement défini en dehors de la plate-forme (par exemple, dans une bibliothèque Jetpack).
Dans les cas où la plate-forme lit les données, évitez d'utiliser Bundle
et préférez une classe de données fortement typée.
Les implémentations Parcelable doivent comporter un champ CREATOR public
Le gonflement Parcelable est exposé via CREATOR
, et non via des constructeurs bruts. Si une classe implémente Parcelable
, son champ CREATOR
doit également être une API publique et le constructeur de classe prenant un argument Parcel
doit être privé.
Utiliser CharSequence pour les chaînes d'interface utilisateur
Lorsqu'une chaîne est présentée dans une interface utilisateur, utilisez CharSequence
pour autoriser les instances Spannable
.
Si ce n'est qu'une clé ou un autre libellé ou valeur qui n'est pas visible par les utilisateurs, String
convient.
Éviter d'utiliser des énumérations
IntDef
doit être utilisé à la place des énumérations dans toutes les API de plate-forme et doit être fortement envisagé dans les API de bibliothèque non groupées. N'utilisez les énumérations que si vous êtes certain que de nouvelles valeurs ne seront pas ajoutées.
Avantages de IntDef
:
- Permet d'ajouter des valeurs au fil du temps
- Les instructions
when
Kotlin peuvent échouer lors de l'exécution si elles ne sont plus exhaustives en raison d'une valeur d'énumération ajoutée dans la plate-forme.
- Les instructions
- Aucune classe ni aucun objet utilisés lors de l'exécution, uniquement des primitives
- Bien que R8 ou la minification puissent éviter ce coût pour les API de bibliothèque non groupées, cette optimisation ne peut pas affecter les classes d'API de plate-forme.
Avantages de l'énumération
- Fonctionnalité idiomatique du langage Java, Kotlin
- Active l'utilisation exhaustive des instructions switch
when
- Remarque : Les valeurs ne doivent pas changer au fil du temps. Consultez la liste précédente.
- Nommage clair et facile à trouver
- Permet la validation au moment de la compilation
- Par exemple, une instruction
when
en Kotlin qui renvoie une valeur
- Par exemple, une instruction
- Il s'agit d'une classe fonctionnelle qui peut implémenter des interfaces, disposer d'assistants statiques, exposer des méthodes de membre ou d'extension, et exposer des champs.
Suivre la hiérarchie des couches de packages Android
La hiérarchie des packages android.*
présente un ordre implicite, dans lequel les packages de niveau inférieur ne peuvent pas dépendre des packages de niveau supérieur.
Évitez de mentionner Google, d'autres entreprises et leurs produits.
La plate-forme Android est un projet Open Source qui vise à être neutre vis-à-vis des fournisseurs. L'API doit être générique et utilisable de la même manière par les intégrateurs système ou les applications disposant des autorisations requises.
Les implémentations Parcelable doivent être définitives
Les classes Parcelable définies par la plate-forme sont toujours chargées à partir de framework.jar
. Il est donc incorrect qu'une application tente de remplacer une implémentation Parcelable
.
Si l'application d'envoi étend un Parcelable
, l'application de réception ne disposera pas de l'implémentation personnalisée de l'expéditeur pour le décompresser. Remarque sur la rétrocompatibilité : si votre classe n'était pas finale par le passé, mais ne disposait pas d'un constructeur accessible au public, vous pouvez toujours la marquer comme final
.
Les méthodes appelant le processus système doivent relancer RemoteException en tant que RuntimeException
RemoteException
est généralement générée par AIDL interne et indique que le processus système est arrêté ou que l'application tente d'envoyer trop de données. Dans les deux cas, l'API publique doit relancer l'exception RuntimeException
pour empêcher les applications de conserver les décisions de sécurité ou de règles.
Si vous savez que l'autre côté d'un appel Binder
est le processus système, ce code standard est la bonne pratique :
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Générer des exceptions spécifiques pour les modifications apportées à l'API
Le comportement des API publiques peut changer selon les niveaux d'API et entraîner des plantages d'applications (par exemple, pour appliquer de nouvelles règles de sécurité).
Lorsque l'API doit générer une exception pour une requête qui était auparavant valide, générez une nouvelle exception spécifique au lieu d'une exception générique. Par exemple, ExportedFlagRequired
au lieu de SecurityException
(et ExportedFlagRequired
peut étendre SecurityException
).
Cela aidera les développeurs d'applications et les outils à détecter les changements de comportement des API.
Implémenter un constructeur de copie au lieu d'un clone
L'utilisation de la méthode Java clone()
est fortement déconseillée en raison du manque de contrats d'API fournis par la classe Object
et des difficultés inhérentes à l'extension des classes qui utilisent clone()
. Utilisez plutôt un constructeur de copie qui accepte un objet du même type.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
Les classes qui s'appuient sur un Builder pour la construction doivent envisager d'ajouter un constructeur de copie Builder pour permettre les modifications de la copie.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Utiliser ParcelFileDescriptor plutôt que FileDescriptor
L'objet java.io.FileDescriptor
a une définition de propriété médiocre, ce qui peut entraîner des bugs obscurs d'utilisation après fermeture. À la place, les API doivent renvoyer ou accepter des instances ParcelFileDescriptor
. Si nécessaire, l'ancien code peut effectuer des conversions entre PFD et FD à l'aide de dup() ou getFileDescriptor().
Éviter d'utiliser des valeurs numériques impaires
Évitez d'utiliser directement les valeurs short
ou byte
, car elles limitent souvent la façon dont vous pourrez faire évoluer l'API à l'avenir.
Évitez d'utiliser BitSet
java.util.BitSet
est idéal pour l'implémentation, mais pas pour l'API publique. Il est mutable, nécessite une allocation pour les appels de méthode à haute fréquence et ne fournit pas de signification sémantique pour ce que chaque bit représente.
Pour les scénarios à hautes performances, utilisez un int
ou un long
avec @IntDef
. Pour les scénarios de faible performance, envisagez un Set<EnumType>
. Pour les données binaires brutes, utilisez byte[]
.
Préférez android.net.Uri
android.net.Uri
est l'encapsulation recommandée pour les URI dans les API Android.
Évitez java.net.URI
, car il est trop strict dans l'analyse des URI, et n'utilisez jamais java.net.URL
, car sa définition de l'égalité est gravement défectueuse.
Masquer les annotations marquées comme @IntDef, @LongDef ou @StringDef
Les annotations marquées comme @IntDef
, @LongDef
ou @StringDef
désignent un ensemble de constantes valides pouvant être transmises à une API. Toutefois, lorsqu'elles sont exportées en tant qu'API, le compilateur insère les constantes et seules les valeurs (désormais inutiles) restent dans le stub d'API de l'annotation (pour la plate-forme) ou le fichier JAR (pour les bibliothèques).
Par conséquent, les utilisations de ces annotations doivent être marquées avec l'annotation de documentation @hide
dans la plate-forme ou l'annotation de code @RestrictTo.Scope.LIBRARY)
dans les bibliothèques. Dans les deux cas, ils doivent être marqués @Retention(RetentionPolicy.SOURCE)
pour ne pas apparaître dans les stubs d'API ni dans les fichiers JAR.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
Lors de la création du SDK de plate-forme et des AAR de bibliothèque, un outil extrait les annotations et les regroupe séparément des sources compilées. Android Studio lit ce format groupé et applique les définitions de type.
Ne pas ajouter de nouvelles clés de fournisseur de paramètres
N'exposez pas de nouvelles clés à partir de Settings.Global
, Settings.System
ou Settings.Secure
.
À la place, ajoutez une API Java getter et setter appropriée dans une classe pertinente, qui est généralement une classe "manager". Ajoutez un mécanisme d'écouteur ou une diffusion pour informer les clients des modifications si nécessaire.
Les paramètres SettingsProvider
présentent plusieurs problèmes par rapport aux getters/setters :
- Aucune sécurité de type.
- Il n'existe pas de méthode unifiée pour fournir une valeur par défaut.
- Il n'existe aucun moyen de personnaliser les autorisations.
- Par exemple, il n'est pas possible de protéger votre paramètre avec une autorisation personnalisée.
- Aucun moyen approprié d'ajouter correctement une logique personnalisée.
- Par exemple, il n'est pas possible de modifier la valeur du paramètre A en fonction de la valeur du paramètre B.
Exemple :
Settings.Secure.LOCATION_MODE
existe depuis longtemps, mais l'équipe chargée de la localisation l'a abandonné au profit d'une véritable API Java
LocationManager.isLocationEnabled()
et de la diffusion
MODE_CHANGED_ACTION
, qui ont donné à l'équipe beaucoup plus de flexibilité. La sémantique des API est désormais beaucoup plus claire.
Ne pas étendre Activity et AsyncTask
AsyncTask
est un détail d'implémentation. À la place, exposez un écouteur ou, dans androidx, une API ListenableFuture
.
Il est impossible de composer des sous-classes Activity
. Étendue de l'activité pour votre fonctionnalité, ce qui la rend incompatible avec d'autres fonctionnalités qui exigent la même chose des utilisateurs. Utilisez plutôt la composition à l'aide d'outils tels que LifecycleObserver.
Utiliser getUser() du contexte
Les classes liées à un Context
, comme tout ce qui est renvoyé par Context.getSystemService()
, doivent utiliser l'utilisateur lié au Context
au lieu d'exposer des membres qui ciblent des utilisateurs spécifiques.
class FooManager {
Context mContext;
void fooBar() {
mIFooBar.fooBarForUser(mContext.getUser());
}
}
class FooManager {
Context mContext;
Foobar getFoobar() {
// Bad: doesn't appy mContext.getUserId().
mIFooBar.fooBarForUser(Process.myUserHandle());
}
Foobar getFoobar() {
// Also bad: doesn't appy mContext.getUserId().
mIFooBar.fooBar();
}
Foobar getFoobarForUser(UserHandle user) {
mIFooBar.fooBarForUser(user);
}
}
Exception : Une méthode peut accepter un argument utilisateur si elle accepte des valeurs qui ne représentent pas un seul utilisateur, comme UserHandle.ALL
.
Utiliser UserHandle au lieu d'entiers simples
Il est préférable d'utiliser UserHandle
pour assurer la sécurité des types et éviter de confondre les ID utilisateur avec les UID.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
Lorsque cela est inévitable, un int
représentant un ID utilisateur doit être annoté avec @UserIdInt
.
Foobar getFoobarForUser(@UserIdInt int user);
Préférer les écouteurs ou les rappels aux intents de diffusion
Les intents de diffusion sont très puissants, mais ils ont entraîné des comportements émergents qui peuvent avoir un impact négatif sur l'état du système. Par conséquent, les nouveaux intents de diffusion doivent être ajoutés avec discernement.
Voici quelques préoccupations spécifiques qui nous incitent à déconseiller l'introduction de nouvelles intentions de diffusion :
Lorsque vous envoyez des diffusions sans l'indicateur
FLAG_RECEIVER_REGISTERED_ONLY
, elles forcent le démarrage de toutes les applications qui ne sont pas déjà en cours d'exécution. Bien que cela puisse parfois être un résultat souhaité, cela peut entraîner une ruée de dizaines d'applications, ce qui a un impact négatif sur l'état du système. Nous vous recommandons d'utiliser d'autres stratégies, telles queJobScheduler
, pour mieux coordonner le moment où différentes conditions préalables sont remplies.Lorsque vous envoyez des diffusions, vous avez peu de possibilités de filtrer ou d'ajuster le contenu diffusé aux applications. Il est alors difficile, voire impossible, de répondre aux futurs problèmes de confidentialité ou d'introduire des changements de comportement en fonction du SDK cible de l'application réceptrice.
Étant donné que les files d'attente de diffusion sont une ressource partagée, elles peuvent être surchargées et ne pas permettre la diffusion de votre événement en temps voulu. Nous avons observé plusieurs files d'attente de diffusion en direct dont la latence de bout en bout est de 10 minutes ou plus.
Pour ces raisons, nous encourageons les nouvelles fonctionnalités à envisager d'utiliser des écouteurs ou des rappels, ou d'autres outils tels que JobScheduler
au lieu d'intents de diffusion.
Dans les cas où les intents de diffusion restent la conception idéale, voici quelques bonnes pratiques à prendre en compte :
- Si possible, utilisez
Intent.FLAG_RECEIVER_REGISTERED_ONLY
pour limiter votre diffusion aux applications déjà en cours d'exécution. Par exemple,ACTION_SCREEN_ON
utilise cette conception pour éviter de réveiller les applications. - Si possible, utilisez
Intent.setPackage()
ouIntent.setComponent()
pour cibler la diffusion sur une application spécifique qui vous intéresse. Par exemple,ACTION_MEDIA_BUTTON
utilise cette conception pour se concentrer sur les commandes de lecture de l'application actuelle. - Si possible, définissez votre diffusion comme
<protected-broadcast>
pour empêcher les applications malveillantes d'usurper l'identité de l'OS.
Intents dans les services pour les développeurs liés au système
Les services destinés à être étendus par le développeur et liés par le système, par exemple les services abstraits tels que NotificationListenerService
, peuvent répondre à une action Intent
du système. Ces services doivent répondre aux critères suivants :
- 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)
. - Document sur la classe à laquelle un développeur doit ajouter un
<intent-filter>
à sonAndroidManifest.xml
pour recevoir des intents de la plate-forme. - Nous vous recommandons vivement d'ajouter une autorisation au niveau du système pour empêcher les applications malveillantes d'envoyer des
Intent
aux services pour les développeurs.
Interopérabilité Kotlin-Java
Pour obtenir la liste complète des consignes, consultez le guide d'interopérabilité Kotlin-Java officiel d'Android. Certaines consignes ont été copiées dans ce guide pour améliorer la visibilité.
Visibilité de l'API
Certaines API Kotlin, comme suspend fun
, ne sont pas destinées à être utilisées par les développeurs Java. Cependant, n'essayez pas de contrôler la visibilité spécifique à la langue à l'aide de @JvmSynthetic
, car cela a des effets secondaires sur la façon dont l'API est présentée dans les débogueurs, ce qui rend le débogage plus difficile.
Pour obtenir des conseils spécifiques, consultez le guide d'interopérabilité Kotlin-Java ou le guide Async.
Objets compagnons
Kotlin utilise companion object
pour exposer les membres statiques. Dans certains cas, ces éléments s'affichent à partir de Java sur une classe interne nommée Companion
plutôt que sur la classe conteneur. Il est normal que les classes Companion
apparaissent comme des classes vides dans les fichiers texte de l'API.
Pour maximiser la compatibilité avec Java, annotez les champs non constants des objets compagnons avec @JvmField
et les fonctions publiques avec @JvmStatic
pour les exposer directement sur la classe conteneur.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Évolution des API de la plate-forme Android
Cette section décrit les règles concernant les types de modifications que vous pouvez apporter aux API Android existantes et la manière dont vous devez implémenter ces modifications pour maximiser la compatibilité avec les applications et les bases de code existantes.
Modifications destructives au niveau du binaire
Évitez les modifications destructives binaires dans les surfaces d'API publiques finalisées. Ces types de modifications génèrent généralement des erreurs lors de l'exécution de make update-api
, mais il peut exister des cas extrêmes que la vérification de l'API de Metalava ne détecte pas. En cas de doute, consultez le guide Evolving Java-based APIs de l'Eclipse Foundation pour obtenir une explication détaillée des types de modifications d'API compatibles en Java. Les modifications destructives binaires apportées aux API masquées (par exemple, système) doivent suivre le cycle obsolescence/remplacement.
Modifications destructives de la source
Nous déconseillons les modifications destructives de la compatibilité source, même si elles ne sont pas destructives de la compatibilité binaire. Un exemple de modification compatible avec le binaire, mais qui casse la source, consiste à ajouter un générique à une classe existante. Cette modification est compatible avec le binaire, mais peut entraîner des erreurs de compilation en raison de l'héritage ou de références ambiguës.
Les modifications incompatibles avec la source ne génèrent pas d'erreurs lors de l'exécution de make update-api
. Vous devez donc veiller à comprendre l'impact des modifications apportées aux signatures d'API existantes.
Dans certains cas, des modifications incompatibles avec la source deviennent nécessaires pour améliorer l'expérience des développeurs ou l'exactitude du code. Par exemple, l'ajout d'annotations de possibilité de valeur nulle aux sources Java améliore l'interopérabilité avec le code Kotlin et réduit le risque d'erreurs, mais nécessite souvent des modifications (parfois importantes) du code source.
Modifications apportées aux API privées
Vous pouvez modifier les API annotées avec @TestApi
à tout moment.
Vous devez conserver les API annotées avec @SystemApi
pendant trois ans. Vous devez supprimer ou refactoriser une API système selon le calendrier suivant :
- API y : ajoutée
- API y+1 : arrêt
- Marquez le code avec
@Deprecated
. - Ajoutez des remplacements et créez un lien vers le remplacement dans le Javadoc du code obsolète à l'aide de l'annotation
@deprecated
. - Pendant le cycle de développement, signalez les bugs aux utilisateurs internes en leur indiquant que l'API est en cours d'abandon. Cela permet de valider l'adéquation des API de remplacement.
- Marquez le code avec
- API y+2 : suppression réversible
- Marquez le code avec
@removed
. - Éventuellement, générez une exception ou une opération sans effet pour les applications qui ciblent le niveau de SDK actuel pour la version.
- Marquez le code avec
- API y+3 : suppression définitive
- Supprimez complètement le code de l'arborescence source.
Obsolescence
Nous considérons la suppression comme une modification de l'API, qui peut se produire dans une version majeure (par exemple, une version alphabétique). Utilisez les annotations de source @Deprecated
et de documentation @deprecated
<summary>
ensemble lorsque vous déconseillez des API. Votre résumé doit inclure une stratégie de migration. Cette stratégie peut renvoyer vers une API de remplacement ou expliquer pourquoi vous ne devez pas utiliser l'API :
/**
* Simple version of ...
*
* @deprecated Use the {@link androidx.fragment.app.DialogFragment}
* class with {@link androidx.fragment.app.FragmentManager}
* instead.
*/
@Deprecated
public final void showDialog(int id)
Vous devez également rendre obsolètes les API définies dans XML et exposées dans Java, y compris les attributs et les propriétés stylables exposés dans la classe android.R
, avec un récapitulatif :
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
Quand abandonner une API
Les obsolescences sont particulièrement utiles pour décourager l'adoption d'une API dans un nouveau code.
Nous vous demandons également de marquer les API comme @deprecated
avant qu'elles ne soient @removed
, mais cela n'incite pas fortement les développeurs à migrer depuis une API qu'ils utilisent déjà.
Avant d'abandonner une API, réfléchissez à l'impact sur les développeurs. Voici les conséquences de l'abandon d'une API :
javac
émet un avertissement lors de la compilation.- Les avertissements de suppression ne peuvent pas être supprimés globalement ni servir de référence. Les développeurs qui utilisent
-Werror
doivent donc corriger ou supprimer chaque utilisation d'une API obsolète avant de pouvoir mettre à jour la version du SDK de compilation. - Les avertissements d'obsolescence concernant les importations de classes obsolètes ne peuvent pas être supprimés. Par conséquent, les développeurs doivent insérer le nom de classe complet pour chaque utilisation d'une classe obsolète avant de pouvoir mettre à jour leur version du SDK de compilation.
- Les avertissements de suppression ne peuvent pas être supprimés globalement ni servir de référence. Les développeurs qui utilisent
- La documentation sur
d.android.com
affiche un avis d'abandon. - Les IDE tels qu'Android Studio affichent un avertissement sur le site d'utilisation de l'API.
- Les IDE peuvent rétrograder ou masquer l'API dans la saisie semi-automatique.
Par conséquent, la suppression d'une API peut dissuader les développeurs les plus soucieux de la santé du code (ceux qui utilisent -Werror
) d'adopter de nouveaux SDK.
Les développeurs qui ne se soucient pas des avertissements dans leur code existant sont susceptibles d'ignorer complètement les obsolescences.
Un SDK qui introduit un grand nombre d'obsolescences aggrave ces deux cas.
C'est pourquoi nous vous recommandons de n'abandonner les API que dans les cas suivants :
- Nous prévoyons de
@remove
l'API dans une prochaine version. - L'utilisation de l'API entraîne un comportement incorrect ou indéfini que nous ne pouvons pas corriger sans rompre la compatibilité.
Lorsque vous abandonnez une API et la remplacez par une nouvelle, nous vous recommandons vivement d'ajouter une API de compatibilité correspondante à une bibliothèque Jetpack telle que androidx.core
pour simplifier la prise en charge des anciens et des nouveaux appareils.
Nous ne vous recommandons pas d'abandonner les API qui fonctionnent comme prévu dans les versions actuelles et futures :
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
L'abandon est approprié dans les cas où les API ne peuvent plus maintenir leurs comportements documentés :
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Modifications apportées aux API obsolètes
Vous devez maintenir le comportement des API obsolètes. Cela signifie que les implémentations de test doivent rester les mêmes et que les tests doivent continuer à réussir après l'abandon de l'API. Si l'API ne comporte pas de tests, vous devez en ajouter.
Ne développez pas les surfaces d'API obsolètes dans les prochaines versions. Vous pouvez ajouter des annotations de correction lint (par exemple, @Nullable
) à une API obsolète existante, mais vous ne devez pas ajouter de nouvelles API.
N'ajoutez pas de nouvelles API comme obsolètes. Si des API ont été ajoutées et ensuite obsolètes au cours d'un cycle de préversion (et entreraient donc initialement dans la surface de l'API publique en tant qu'obsolètes), vous devez les supprimer avant de finaliser l'API.
Suppression logicielle
La suppression logicielle est une modification destructive de la source. Vous devez l'éviter dans les API publiques, sauf si le Conseil des API l'approuve explicitement.
Pour les API système, vous devez les rendre obsolètes pendant la durée d'une version majeure avant de les supprimer de manière progressive. Supprimez toutes les références aux API dans la documentation et utilisez l'annotation @removed <summary>
lorsque vous supprimez les API de manière temporaire. Votre résumé doit inclure la raison de la suppression et peut inclure une stratégie de migration, comme nous l'avons expliqué dans Obsolescence.
Le comportement des API supprimées de manière réversible peut être conservé tel quel, mais il doit surtout être préservé de sorte que les appelants existants ne plantent pas lorsqu'ils appellent l'API. Dans certains cas, cela peut signifier la préservation du comportement.
La couverture des tests doit être maintenue, mais le contenu des tests peut avoir besoin d'être modifié pour tenir compte des changements de comportement. Les tests doivent toujours valider que les appelants existants ne plantent pas au moment de l'exécution. Vous pouvez conserver le comportement des API supprimées de manière réversible tel quel, mais surtout, vous devez le préserver de sorte que les appelants existants ne plantent pas lorsqu'ils appellent l'API. Dans certains cas, cela peut signifier préserver le comportement.
Vous devez maintenir la couverture des tests, mais le contenu des tests peut avoir besoin d'être modifié pour s'adapter aux changements de comportement. Les tests doivent toujours valider que les appelants existants ne plantent pas au moment de l'exécution.
D'un point de vue technique, nous supprimons l'API du fichier JAR du stub SDK et du classpath de compilation à l'aide de l'annotation Javadoc @remove
, mais elle existe toujours dans le classpath d'exécution, comme les API @hide
:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
Du point de vue d'un développeur d'applications, l'API n'apparaît plus dans la saisie semi-automatique et le code source qui fait référence à l'API ne se compilera pas lorsque compileSdk
sera égal ou supérieur au SDK dans lequel l'API a été supprimée. Toutefois, le code source continue de se compiler correctement par rapport aux SDK antérieurs, et les binaires qui font référence à l'API continuent de fonctionner.
Certaines catégories d'API ne doivent pas être supprimées de manière réversible. Vous ne devez pas supprimer temporairement certaines catégories d'API.
Méthodes abstraites
Vous ne devez pas supprimer de manière réversible les méthodes abstraites des classes que les développeurs peuvent étendre. Cela empêche les développeurs d'étendre correctement la classe à tous les niveaux du SDK.
Dans les rares cas où il n'a jamais été et ne sera jamais possible pour les développeurs d'étendre une classe, vous pouvez toujours supprimer en douceur les méthodes abstraites.
Suppression définitive
La suppression définitive est une modification qui casse la compatibilité binaire et ne doit jamais se produire dans les API publiques.
Annotation déconseillée
Nous utilisons l'annotation @Discouraged
pour indiquer que nous ne recommandons pas une API dans la plupart des cas (plus de 95 %). Les API déconseillées diffèrent des API obsolètes en ce qu'il existe un cas d'utilisation critique étroit qui empêche l'obsolescence. Lorsque vous marquez une API comme déconseillée, vous devez fournir une explication et une solution alternative :
@Discouraged(message = "Use of this function is discouraged because resource
reflection makes it harder to perform build
optimizations and compile-time verification of code. It
is much more efficient to retrieve resources by
identifier (such as `R.foo.bar`) than by name (such as
`getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
Vous ne devez pas ajouter de nouvelles API déconseillées.
Modifications apportées au comportement des API existantes
Dans certains cas, vous pouvez modifier le comportement d'implémentation d'une API existante. Par exemple, dans Android 7.0, nous avons amélioré DropBoxManager
pour indiquer clairement aux développeurs lorsqu'ils tentaient de publier des événements trop volumineux pour être envoyés sur Binder
.
Toutefois, pour éviter de causer des problèmes aux applications existantes, nous vous recommandons vivement de conserver un comportement sûr pour les anciennes applications. Historiquement, nous avons protégé ces changements de comportement en fonction du ApplicationInfo.targetSdkVersion
de l'application, mais nous avons récemment migré pour exiger l'utilisation du framework de compatibilité des applications. Voici un exemple d'implémentation d'un changement de comportement à l'aide de ce nouveau framework :
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
L'utilisation de cette conception du framework de compatibilité des applications permet aux développeurs de désactiver temporairement des modifications de comportement spécifiques lors des versions Preview et bêta dans le cadre du débogage de leurs applications, au lieu de les obliger à s'adapter à des dizaines de modifications de comportement simultanément.
Compatibilité ascendante
La compatibilité ascendante est une caractéristique de conception qui permet à un système d'accepter des entrées destinées à une version ultérieure de lui-même. Dans le cas de la conception d'API, vous devez accorder une attention particulière à la conception initiale ainsi qu'aux modifications futures, car les développeurs s'attendent à écrire du code une seule fois, à le tester une seule fois et à ce qu'il s'exécute partout sans problème.
Voici les causes les plus courantes des problèmes de compatibilité ascendante dans Android :
- Ajout de nouvelles constantes à un ensemble (tel que
@IntDef
ouenum
) précédemment supposé complet (par exemple, lorsqueswitch
comporte undefault
qui génère une exception). - Ajout de la prise en charge d'une fonctionnalité qui n'est pas directement capturée dans la surface de l'API (par exemple, la prise en charge de l'attribution de ressources de type
ColorStateList
dans XML alors que seules les ressources<color>
étaient auparavant prises en charge). - Assouplissement des restrictions sur les vérifications d'exécution, par exemple en supprimant une vérification
requireNotNull()
qui était présente dans les versions antérieures.
Dans tous ces cas, les développeurs ne découvrent qu'à l'exécution qu'il y a un problème. Pire encore, ils pourraient le découvrir à la suite de rapports d'erreur provenant d'anciens appareils sur le terrain.
De plus, ces cas sont tous des modifications d'API techniquement valides. Elles ne rompent pas la compatibilité binaire ni la compatibilité du code source, et le lint de l'API ne détecte aucun de ces problèmes.
Par conséquent, les concepteurs d'API doivent faire très attention lorsqu'ils modifient des classes existantes. Posez-vous la question suivante : "Cette modification va-t-elle entraîner l'échec du code écrit et testé uniquement par rapport à la dernière version de la plate-forme sur les versions antérieures ?"
Schémas XML
Si un schéma XML sert d'interface stable entre les composants, ce schéma doit être spécifié explicitement et doit évoluer de manière rétrocompatible, comme les autres API Android. Par exemple, la structure des éléments et des attributs XML doit être conservée de la même manière que les méthodes et les variables sont conservées sur d'autres surfaces d'API Android.
Abandon du format XML
Si vous souhaitez abandonner un élément ou un attribut XML, vous pouvez ajouter le marqueur xs:annotation
, mais vous devez continuer à prendre en charge les fichiers XML existants en suivant le cycle de vie d'évolution @SystemApi
habituel.
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
Les types d'éléments doivent être conservés
Les schémas acceptent les éléments sequence
, choice
et all
en tant qu'éléments enfants de l'élément complexType
. Toutefois, ces éléments enfants diffèrent par le nombre et l'ordre de leurs éléments enfants. La modification d'un type existant serait donc un changement incompatible.
Si vous souhaitez modifier un type existant, la bonne pratique consiste à rendre l'ancien type obsolète et à introduire un nouveau type pour le remplacer.
<!-- Original "sequence" value -->
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- New "choice" value -->
<xs:element name="fooChoice">
<xs:complexType>
<xs:choice>
<xs:element name="name" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
Schémas spécifiques à la ligne principale
Mainline est un projet qui permet de mettre à jour individuellement les sous-systèmes ("modules Mainline") de l'OS Android, plutôt que de mettre à jour l'intégralité de l'image système.
Les modules Mainline doivent être "dégroupés" de la plate-forme principale, ce qui signifie que toutes les interactions entre chaque module et le reste du monde doivent être effectuées à l'aide d'API formelles (publiques ou système).
Les modules mainline doivent suivre certains modèles de conception. Cette section les décrit.
Le modèle <Module>FrameworkInitializer
Si un module mainline doit exposer des classes @SystemService
(par exemple, JobScheduler
), utilisez le format suivant :
Exposez une classe
<YourModule>FrameworkInitializer
à partir de votre module. Cette classe doit se trouver dans$BOOTCLASSPATH
. Exemple : StatsFrameworkInitializerMarquez-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 à unContext
.Utilisez
SystemServiceRegistry.registerStaticService()
pour enregistrer une classe de gestionnaire de services lorsqu'elle n'a pas besoin de référence à unContext
.Appelez la méthode
registerServiceWrappers()
à partir de l'initialiseur statique deSystemServiceRegistry
.
Le modèle <Module>ServiceManager
Normalement, pour enregistrer des objets binder de service système ou obtenir des références à ceux-ci, il faut utiliser ServiceManager
, mais les modules mainline ne peuvent pas l'utiliser, car il est masqué. Cette classe est masquée, car les modules principaux ne sont pas censés enregistrer ni faire référence aux objets binder de service système exposés par la plate-forme statique ou par d'autres modules.
Les modules Mainline peuvent utiliser le modèle suivant pour pouvoir enregistrer et obtenir des références aux services Binder implémentés dans le module.
Créez une classe
<YourModule>ServiceManager
en suivant la conception de TelephonyServiceManager.Exposez la classe en tant que
@SystemApi
. Si vous n'avez besoin d'y accéder qu'à partir des classes$BOOTCLASSPATH
ou des classes de serveur système, vous pouvez utiliser@SystemApi(client = MODULE_LIBRARIES)
. Sinon,@SystemApi(client = PRIVILEGED_APPS)
fonctionnera.Ce cours se compose des éléments suivants :
- Constructeur masqué, de sorte que seul le code de plate-forme statique peut l'instancier.
- Méthodes getter publiques qui renvoient une instance
ServiceRegisterer
pour un nom spécifique. Si vous avez un objet binder, vous avez besoin d'une méthode getter. Si vous en avez deux, vous avez besoin de deux getters. - Dans
ActivityThread.initializeMainlineModules()
, instanciez cette classe et transmettez-la à une méthode statique exposée par votre module. Normalement, vous ajoutez une API@SystemApi(client = MODULE_LIBRARIES)
statique dans votre classeFrameworkInitializer
qui la prend.
Ce modèle empêcherait d'autres modules principaux d'accéder à ces API, car il n'existe aucun moyen pour les autres modules d'obtenir une instance de <YourModule>ServiceManager
, même si les API get()
et register()
leur sont visibles.
Voici comment la téléphonie obtient une référence au service de téléphonie : lien de recherche de code.
Si votre application implémente un objet binder de service en code natif, vous utilisez les API natives AServiceManager
.
Ces API correspondent aux API Java ServiceManager
, mais les API natives sont directement exposées aux modules principaux. N'utilisez pas ces méthodes pour enregistrer ou faire référence à des objets Binder qui n'appartiennent pas à votre module. Si vous exposez un objet Binder à partir du code natif, votre <YourModule>ServiceManager.ServiceRegisterer
n'a pas besoin de méthode register()
.
Définitions des autorisations dans les modules Mainline
Les modules Mainline contenant des APK peuvent définir des autorisations (personnalisées) dans leur APK AndroidManifest.xml
de la même manière qu'un APK normal.
Si l'autorisation définie n'est utilisée qu'en interne dans un module, son nom doit être préfixé avec le nom du package APK, par exemple :
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
Si l'autorisation définie doit être fournie dans le cadre d'une API de plate-forme pouvant être mise à jour à d'autres applications, son nom doit être précédé de "android.permission.". (comme toute autorisation de plate-forme statique) plus le nom du package du module, pour indiquer qu'il s'agit d'une API de plate-forme provenant d'un module tout en évitant tout conflit de nommage, par exemple :
<permission
android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
android:label="@string/active_calories_burned_read_content_description"
android:protectionLevel="dangerous"
android:permissionGroup="android.permission-group.HEALTH" />
Le module peut ensuite exposer ce nom d'autorisation en tant que constante d'API dans sa surface d'API, par exemple HealthPermissions.READ_ACTIVE_CALORIES_BURNED
.