Questa pagina è pensata come guida per gli sviluppatori per comprendere i principi generali applicati dal Consiglio API nelle revisioni delle API.
Oltre a seguire queste linee guida durante la scrittura delle API, gli sviluppatori devono eseguire lo strumento API Lint, che codifica molte di queste regole nei controlli eseguiti sulle API.
Puoi considerarla una guida alle regole rispettate dallo strumento Lint, oltre a consigli generali sulle regole che non possono essere codificate in questo strumento con elevata precisione.
Strumento API Lint
API Lint
è integrato nello strumento di analisi statica Metalava e viene eseguito automaticamente
durante la convalida in CI. Puoi eseguirlo manualmente da un
controllo della piattaforma locale utilizzando m
checkapi
o da un controllo AndroidX locale utilizzando
./gradlew :path:to:project:checkApi
.
Regole API
La piattaforma Android e molte librerie Jetpack esistevano prima della creazione di questo insieme di linee guida e le norme riportate di seguito in questa pagina sono in continua evoluzione per soddisfare le esigenze dell'ecosistema Android.
Di conseguenza, alcune API esistenti potrebbero non rispettare le linee guida. In altri casi, potrebbe essere utile offrire un'esperienza utente migliore agli sviluppatori di app se una nuova API rimane coerente con le API esistenti anziché rispettare rigorosamente le linee guida.
Usa il tuo giudizio e contatta il Consiglio API se hai domande difficili su un'API che devono essere risolte o linee guida che devono essere aggiornate.
Nozioni di base sulle API
Questa categoria riguarda gli aspetti fondamentali di un'API Android.
Tutte le API devono essere implementate
Indipendentemente dal pubblico di un'API (ad esempio pubblica o @SystemApi
), tutte le sue interfacce devono essere implementate quando vengono unite o esposte come API. Non unire gli stub API con l'implementazione che verrà rilasciata in un secondo momento.
Le piattaforme API senza implementazioni presentano diversi problemi:
- Non è garantito che sia stata esposta una superficie adeguata o completa. Finché un'API non viene testata o utilizzata dai client, non è possibile verificare se un client dispone delle API appropriate per poter utilizzare la funzionalità.
- Le API senza implementazione non possono essere testate nelle Developer Preview.
- Le API senza implementazione non possono essere testate in CTS.
Tutte le API devono essere testate
Ciò è in linea con i requisiti CTS della piattaforma, con le norme di AndroidX e in generale con l'idea che le API debbano essere implementate.
Il test delle API garantisce che l'API sia utilizzabile e che abbiamo affrontato i casi d'uso previsti. Il test dell'esistenza non è sufficiente; è necessario testare il comportamento dell'API stessa.
Una modifica che aggiunge una nuova API deve includere i test corrispondenti nello stesso argomento CL o Gerrit.
Le API devono anche essere testabili. Dovresti essere in grado di rispondere alla domanda "In che modo un sviluppatore di app testerà il codice che utilizza la tua API?"
Tutte le API devono essere documentate
La documentazione è una parte fondamentale dell'usabilità dell'API. Sebbene la sintassi di un'interfaccia API possa sembrare ovvia, i nuovi client non comprenderanno la semantica, il comportamento o il contesto alla base dell'API.
Tutte le API generate devono essere conformi alle linee guida
Le API generate dagli strumenti devono seguire le stesse linee guida delle API del codice scritto a mano.
Strumenti sconsigliati per la generazione di API:
AutoValue
: viola le linee guida in vari modi, ad esempio non è possibile implementare classi di valori finali né costruttori finali con il funzionamento di AutoValue.
Stile di codice
Questa categoria riguarda lo stile di codice generale che gli sviluppatori devono utilizzare, soprattutto quando scrivono API pubbliche.
Segui le convenzioni di codifica standard, tranne dove indicato
Le convenzioni di codifica di Android sono documentate per i collaboratori esterni qui:
https://source.android.com/source/code-style.html
In generale, tendiamo a seguire le convenzioni di codifica standard di Java e Kotlin.
Gli acronimi non devono avere l'iniziale maiuscola nei nomi dei metodi
Ad esempio, il nome del metodo deve essere runCtsTests
e non runCTSTests
.
I nomi non devono terminare con Impl
Questo espone i dettagli dell'implementazione, evitalo.
Classi
Questa sezione descrive le regole relative a classi, interfacce ed ereditarietà.
Eredita nuove classi pubbliche dalla classe di base appropriata
L'eredità espone elementi dell'API nella sottoclasse che potrebbero non essere appropriati.
Ad esempio, una nuova sottoclasse pubblica di FrameLayout
ha lo stesso aspetto di FrameLayout
più i nuovi comportamenti e gli elementi dell'API. Se l'API ereditata non è appropriata per il tuo caso d'uso, eredita da una classe più in alto nell'albero, ad esempio ViewGroup
o View
.
Se hai la tentazione di eseguire l'override dei metodi della classe di base per lanciareUnsupportedOperationException
, riconsidera la classe di base che stai utilizzando.
Utilizzare le classi di raccolte di base
Che tu prenda una raccolta come argomento o la restituisca come valore, preferisci sempre la classe di base all'implementazione specifica (ad esempio, restituisci
List<Foo>
anziché ArrayList<Foo>
).
Utilizza una classe di base che esprima vincoli appropriati per l'API. Ad esempio, utilizza List
per un'API la cui raccolta deve essere ordinata e Set
per un'API la cui raccolta deve essere composta da elementi univoci.
In Kotlin, preferisci le collezioni immutabili. Per ulteriori dettagli, consulta la sezione Mutabilità delle raccolte.
Classi astratte e interfacce
Java 8 aggiunge il supporto per i metodi di interfaccia predefiniti, che consente ai progettisti di API di aggiungere metodi alle interfacce mantenendo la compatibilità binaria. Il codice della piattaforma e tutte le librerie Jetpack devono avere come target Java 8 o versioni successive.
Nei casi in cui l'implementazione predefinita sia senza stato, i progettisti di API devono scegliere le interfacce anziché le classi astratte, ovvero i metodi di interfaccia predefiniti possono essere implementati come chiamate ad altri metodi di interfaccia.
Nei casi in cui un costruttore o uno stato interno sia richiesto dall'implementazione predefinita, devono essere utilizzate classi astratte.
In entrambi i casi, i progettisti di API possono scegliere di lasciare un singolo metodo astratto per semplificarne l'utilizzo come 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) { }
}
I nomi delle classi devono riflettere ciò che estendono
Ad esempio, le classi che estendono Service
devono essere denominate FooService
per motivi di chiarezza:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
Suffissi generici
Evita di utilizzare suffissi generici per i nomi di classi come Helper
e Util
per le raccolte di metodi di utilità. Inserisci invece i metodi direttamente nelle classi associate o nelle funzioni di estensione Kotlin.
Se i metodi collegano più classi, assegna alla classe contenente un nome significativo che ne descriva la funzione.
In casi molto limitati, l'utilizzo del suffisso Helper
potrebbe essere appropriato:
- Utilizzato per la composizione del comportamento predefinito
- Potrebbe comportare la delega del comportamento esistente a nuovi gruppi
- Potrebbe essere necessario uno stato persistente
- In genere comporta
View
Ad esempio, se il backporting delle descrizioni comando richiede la persistenza dello stato associato a un View
e l'uso di diversi metodi su View
per installare il backport, TooltipHelper
sarebbe un nome di classe accettabile.
Non esporre direttamente il codice generato da IDL come API pubbliche
Mantieni il codice generato dall'IDL come dettagli di implementazione. Sono inclusi protobuf, socket, FlatBuffers o qualsiasi altra API non Java e non NDK. Tuttavia, la maggior parte degli IDL in Android è in AIDL, pertanto questa pagina si concentra su AIDL.
Le classi AIDL generate non soddisfano i requisiti della guida allo stile dell'API (ad esempio, non possono utilizzare il sovraccarico) e lo strumento AIDL non è progettato esplicitamente per mantenere la compatibilità con le API di linguaggio, pertanto non puoi incorporarle in un'API pubblica.
Aggiungi invece un livello API pubblico sopra l'interfaccia AIDL, anche se inizialmente si tratta di un wrapper superficiale.
Interfacce Binder
Se l'interfaccia Binder
è un dettaglio di implementazione, può essere modificata liberamente in futuro, con il livello pubblico che consente di mantenere la compatibilità con le versioni precedenti. Ad esempio, potresti dover aggiungere nuovi argomenti alle chiamate interne o ottimizzare il traffico IPC utilizzando il batching o lo streaming, la memoria condivisa o simili. Nessuna di queste operazioni può essere eseguita se l'interfaccia AIDL è anche l'API pubblica.
Ad esempio, non esporre FooService
direttamente come API pubblica:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
In alternativa, inserisci l'interfaccia Binder
in un gestore o in un'altra classe:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
Se in un secondo momento è necessario un nuovo argomento per questa chiamata, all'API pubblica possono essere aggiunti sovraccarichi minimi e pratici all'interfaccia interna. Puoi utilizzare il livello di wrapping per gestire altri problemi di compatibilità con le versioni precedenti man mano che l'implementazione si evolve:
/**
* @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);
}
}
Per le interfacce Binder
che non fanno parte della piattaforma Android (ad esempio un'interfaccia di servizio esportata da Google Play Services per l'utilizzo da parte delle app), il requisito di un'interfaccia IPC stabile, pubblicata e con versione significa che è molto più difficile far evolvere l'interfaccia stessa. Tuttavia, vale la pena di avere un livello di wrapper per allinearsi ad altre linee guida per le API e semplificare l'utilizzo della stessa API pubblica per una nuova versione dell'interfaccia IPC, se dovessero essere necessarie.
Non utilizzare oggetti Binder non elaborati nell'API pubblica
Un oggetto Binder
non ha alcun significato da solo e pertanto non deve essere utilizzato nell'API pubblica. Un caso d'uso comune è utilizzare un Binder
o un IBinder
come token perché ha una semantica di identità. Anziché utilizzare un oggetto Binder
non elaborato, usa una classe token 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() {...}
}
I corsi per i gestori devono essere definitivi
Le classi di gestione devono essere dichiarate come final
. Le classi di gestione comunicano con i servizi di sistema e sono il punto di interazione singolo. Non è necessaria alcuna personalizzazione, quindi dichiaralo come final
.
Non utilizzare CompletableFuture o Future
java.util.concurrent.CompletableFuture
ha una vasta API che consente la mutazione arbitraria del valore futuro e ha valori predefiniti soggetti a errori.
Al contrario, in java.util.concurrent.Future
manca l'ascolto non bloccante,
il che ne rende difficile l'utilizzo con il codice asincrono.
Nel codice di piattaforma e nelle API di librerie di basso livello utilizzate sia da Kotlin che da Java, preferisci una combinazione di un callback di completamento, Executor
, e, se l'API supporta l'annullamento, CancellationSignal
.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
Se scegli come target Kotlin, preferisci le funzioni suspend
.
suspend fun asyncLoadFoo(): Foo
Nelle librerie di integrazione specifiche per Java, puoi utilizzare ListenableFuture
di Guava.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
Non utilizzare Facoltativo
Sebbene Optional
possa avere vantaggi in alcune piattaforme API, non è coerente con l'area della piattaforma API Android esistente. @Nullable
e @NonNull
forniscono assistenza per gli strumenti per la sicurezza null
e Kotlin applica i contratti di assegnazione di valori null
a livello di compilatore, rendendo Optional
non necessario.
Per le primitive facoltative, utilizza i metodi has
e get
accoppiati. Se il valore non è impostato (has
restituisce false
), il metodo get
deve generare un IllegalStateException
.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
Utilizza i costruttori privati per le classi non inizializzabili
Le classi che possono essere create solo da Builder
, le classi contenenti solo costanti o metodi statici o classi altrimenti non instantiabili devono includere almeno un costruttore privato per impedire l'instanziazione utilizzando il costruttore predefinito senza argomenti.
public final class Log {
// Not instantiable.
private Log() {}
}
Singleton
I singleton sono sconsigliati perché presentano i seguenti svantaggi correlati ai test:
- La costruzione è gestita dalla classe, impedendo l'uso di falsi
- I test non possono essere ermetici a causa della natura statica di un singleton
- Per risolvere questi problemi, gli sviluppatori devono conoscere i dettagli interni del singleton o creare un wrapper.
Per risolvere questi problemi, preferisci il pattern singola istanza, che si basa su una classe di base astratta.
Singola istanza
Le classi con una singola istanza utilizzano una classe di base astratta con un costruttore private
o
internal
e forniscono un metodo getInstance()
statico per ottenere un'
istanza. Il metodo getInstance()
deve restituire lo stesso oggetto nelle chiamate successive.
L'oggetto restituito da getInstance()
deve essere un'implementazione privata della
classe base astratta.
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'istanza singola è diversa dall'singleton in quanto gli sviluppatori possono creare una versione falsa di SingleInstance
e utilizzare il proprio framework di Dependency Injection per gestire l'implementazione senza dover creare un wrapper oppure la libreria può fornire il proprio falso in un artefatto -testing
.
Le classi che rilasciano risorse devono implementare AutoCloseable
Le classi che rilasciano risorse tramite metodi close
, release
, destroy
o simili devono implementare java.lang.AutoCloseable
per consentire agli sviluppatori di ripulire automaticamente queste risorse quando utilizzano un blocco try-with-resources
.
Evita di introdurre nuovi sottoclassi View in android.*
Non introdurre nuove classi che ereditano direttamente o indirettamente da
android.view.View
nell'API pubblica della piattaforma (ovvero in android.*
).
Il toolkit per l'interfaccia utente di Android ora è incentrato su Compose. Le nuove funzionalità dell'interfaccia utente esposte dalla piattaforma devono essere esposte come API di livello inferiore che possono essere utilizzate per implementare Jetpack Compose e, facoltativamente, componenti dell'interfaccia utente basati su visualizzazioni per gli sviluppatori nelle librerie Jetpack. L'offerta di questi componenti nelle librerie offre opportunità per le implementazioni di backport quando le funzionalità della piattaforma non sono disponibili.
Campi
Queste regole riguardano i campi pubblici dei moduli.
Non esporre i campi non elaborati
Le classi Java non devono esporre direttamente i campi. I campi devono essere privati e accessibili solo utilizzando getter e setter pubblici, indipendentemente dal fatto che questi campi siano finali o meno.
Rare eccezioni includono strutture di dati di base in cui non è necessario migliorare il comportamento di specificazione o recupero di un campo. In questi casi, i campi devono essere denominati utilizzando convenzioni di denominazione delle variabili standard, ad esempio Point.x
e
Point.y
.
Le classi Kotlin possono esporre proprietà.
I campi esposti devono essere contrassegnati come definitivi
Sconsigliamo vivamente di utilizzare i campi non elaborati (@vedi
Non esporre i campi non elaborati). Tuttavia, nei rari casi in cui un
campo è esposto come campo pubblico, contrassegnalo come final
.
I campi interni non devono essere esposti
Non fare riferimento ai nomi dei campi interni nell'API pubblica.
public int mFlags;
Utilizza pubblico anziché protetto
@vedi Utilizzare pubblico anziché protetto
Costanti
Queste sono regole relative alle costanti pubbliche.
Le costanti di flag non devono sovrapporsi ai valori int o long
Flags implica bit che possono essere combinati in un determinato valore unione. In caso contrario, non chiamare la variabile o la costante 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;
Per ulteriori informazioni sulla definizione delle costanti di flag pubblico, consulta @IntDef
per i flag di maschera di bit.
Le costanti final statiche devono utilizzare una convenzione di denominazione in maiuscolo separata da trattini bassi
Tutte le parole nella costante devono essere in maiuscolo e più parole devono essere separate da _
. Ad esempio:
public static final int fooThing = 5
public static final int FOO_THING = 5
Utilizza prefissi standard per le costanti
Molte delle costanti utilizzate in Android sono per elementi standard, come flag, chiavi e azioni. Queste costanti devono avere prefissi standard per essere più identificabili come tali.
Ad esempio, gli extra dell'intent devono iniziare con EXTRA_
. Le azioni di intenzione devono iniziare con ACTION_
. Le costanti utilizzate con Context.bindService()
devono iniziare con BIND_
.
Nomi e ambiti delle costanti principali
I valori delle costanti di stringa devono essere coerenti con il nome della costante stessa e generalmente devono essere limitati al pacchetto o al dominio. Ad esempio:
public static final String FOO_THING = "foo"
Non sono denominati in modo coerente né hanno un ambito appropriato. Valuta invece:
public static final String FOO_THING = "android.fooservice.FOO_THING"
I prefissi android
nelle costanti di stringa con ambito sono riservati al progetto open source Android.
Le azioni e gli extra dell'intent, nonché le voci del bundle, devono avere uno spazio dei nomi utilizzando il nome del pacchetto in cui sono definiti.
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"
}
Utilizza pubblico anziché protetto
@vedi Utilizzare pubblico anziché protetto
Utilizza prefissi coerenti
Le costanti correlate devono iniziare tutte con lo stesso prefisso. Ad esempio, per un insieme di costanti da utilizzare con i valori dei flag:
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;
@vedi Utilizzare prefissi standard per le costanti
Utilizzare nomi di risorse coerenti
Gli identificatori, gli attributi e i valori pubblici devono essere denominati utilizzando la convenzione di denominazione camelCase, ad esempio @id/accessibilityActionPageUp
o
@attr/textAppearance
, in modo simile ai campi pubblici in Java.
In alcuni casi, un attributo o un identificatore pubblico include un prefisso comune distinto da un'underscore:
- Valori di configurazione della piattaforma come
@string/config_recentsComponentName
in config.xml - Attributi di visualizzazione specifici del layout, come
@attr/layout_marginStart
in attrs.xml
I temi e gli stili pubblici devono seguire la convenzione di denominazione gerarchica PascalCase, ad esempio @style/Theme.Material.Light.DarkActionBar
o
@style/Widget.Material.SearchView.ActionBar
, in modo simile alle classi nidificate in
Java.
Le risorse di layout e drawable non devono essere esposte come API pubbliche. Tuttavia, se devono essere esposti, i layout e gli elementi drawable pubblici devono essere denominati utilizzando la convenzione di denominazione sottotraccia, ad esempio layout/simple_list_item_1.xml
o drawable/title_bar_tall.xml
.
Se le costanti possono cambiare, rendile dinamiche
Il compilatore potrebbe inserire in linea i valori costanti, pertanto mantenere invariati i valori è considerato parte del contratto dell'API. Se il valore di una costante MIN_FOO
o MAX_FOO
potrebbe cambiare in futuro, valuta la possibilità di creare metodi dinamici.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
Valutare la compatibilità con le versioni future per i callback
Le costanti definite nelle versioni future dell'API non sono note alle app che hanno come target API meno recenti. Per questo motivo, le costanti inviate alle app devono tenere conto della versione dell'API target dell'app e mappare le costanti più recenti a un valore coerente. Considera il seguente scenario:
Sorgente SDK ipotetica:
// 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;
App ipotetica con targetSdkVersion="22"
:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
In questo caso, l'app è stata progettata nei limiti del livello API 22 e si è basata sull'assunto (relativamente) ragionevole che esistessero solo due possibili stati. Tuttavia, se l'app riceve il valore STATUS_FAILURE_RETRY
appena aggiunto, lo interpreta come un esito positivo.
I metodi che restituiscono costanti possono gestire in sicurezza casi come questo vincolando il loro output in modo che corrisponda al livello API scelto come target dall'app:
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;
}
Gli sviluppatori non possono prevedere se un elenco di costanti potrebbe cambiare in futuro. Se definisci un'API con una costante UNKNOWN
o UNSPECIFIED
che sembra essere generica, gli sviluppatori presumono che le costanti pubblicate quando hanno scritto la loro app siano esaustive. Se non vuoi impostare questa aspettativa,
riconsidera se una costante generica è una buona idea per la tua API.
Inoltre, le librerie non possono specificare il proprio targetSdkVersion
separatamente dall'app e gestire le modifiche del comportamento di targetSdkVersion
dal codice della libreria è complicato e soggetto a errori.
Costante intera o di stringa
Utilizza costanti intere e @IntDef
se lo spazio dei nomi per i valori non è estendibile al di fuori del pacchetto. Utilizza costanti di stringa se lo spazio dei nomi è condiviso o può essere esteso da codice esterno al pacchetto.
Classi di dati
Le classi di dati rappresentano un insieme di proprietà immutabili e forniscono un insieme piccolo e ben definito di funzioni di utilità per interagire con questi dati.
Non utilizzare data class
nelle API Kotlin pubbliche, poiché il compilatore Kotlin non garantisce la compatibilità dell'API di linguaggio o binaria per il codice generato. Implementa invece manualmente le funzioni richieste.
Creazione istanza
In Java, le classi di dati devono fornire un costruttore quando ci sono poche proprietà o utilizzare il pattern Builder
quando sono presenti molte proprietà.
In Kotlin, i Data Class devono fornire un costruttore con argomenti predefiniti, indipendentemente dal numero di proprietà. Le classi di dati definite in Kotlin possono anche beneficiare della fornitura di un generatore quando si sceglie come target i client Java.
Modifica e copia
Se i dati devono essere modificati, fornisci una classe Builder
con un costruttore di copia (Java) o una funzione membro copy()
(Kotlin) che restituisce un nuovo oggetto.
Quando fornisci una funzione copy()
in Kotlin, gli argomenti devono corrispondere al
costruttore della classe e i valori predefiniti devono essere compilati utilizzando i valori correnti dell'oggetto:
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
)
}
Comportamenti aggiuntivi
Le classi di dati devono implementare sia equals()
sia hashCode()
e ogni proprietà deve essere presa in considerazione nelle implementazioni di questi metodi.
Le classi di dati possono implementare toString()
con un formato consigliato corrispondente all'implementazione della classe di dati di Kotlin, ad esempio User(var1=Alex, var2=42)
.
Metodi
Si tratta di regole relative a varie specifiche dei metodi, dei parametri, dei nomi dei metodi, dei tipi di ritorno e degli specificatori di accesso.
Tempo
Queste regole riguardano il modo in cui i concetti di tempo come date e durata devono essere espressi nelle API.
Se possibile, preferisci i tipi java.time.*
java.time.Duration
, java.time.Instant
e molti altri tipi di java.time.*
sono disponibili su tutte le versioni della piattaforma tramite il rimuovere le zuccherose e devono essere preferiti quando si esprime il tempo nei parametri o nei valori restituiti dell'API.
Preferisci esporre solo le varianti di un'API che accettano o restituiscono java.time.Duration
o java.time.Instant
e ometti le varianti primitive con lo stesso caso d'uso, a meno che il dominio dell'API non sia uno in cui l'allocazione degli oggetti nei pattern di utilizzo previsti avrebbe un impatto proibitivo sul rendimento.
I metodi che esprimono le durate devono avere il nome durata
Se un valore di tempo esprime la durata del tempo necessario, denomina il parametro "duration", non "time".
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
Eccezioni:
"timeout" è appropriato quando la durata si applica specificamente a un valore di timeout.
"time" con un tipo java.time.Instant
è appropriato quando si fa riferimento a un
momento specifico nel tempo, non a una durata.
I metodi che esprimono durate o tempo come primitive devono essere denominati con la relativa unità di tempo e utilizzare long
I metodi che accettano o restituiscono durate come primitive devono aggiungere al nome del metodo le unità di tempo associate (ad esempio Millis
, Nanos
, Seconds
) per riservare il nome non decorato per l'utilizzo con java.time.Duration
. Vedi
Ora.
I metodi devono inoltre essere annotati in modo appropriato con l'unità di misura e la base di tempo:
@CurrentTimeMillisLong
: il valore è un timestamp non negativo misurato come numero di millisecondi dal 1970-01-01T00:00:00Z.@CurrentTimeSecondsLong
: il valore è un timestamp non negativo misurato come numero di secondi dal giorno 01/01/1970 00:00:00 UTC.@DurationMillisLong
: il valore è una durata non negativa in millisecondi.@ElapsedRealtimeLong
: il valore è un timestamp non negativo nella base di tempoSystemClock.elapsedRealtime()
.@UptimeMillisLong
: il valore è un timestamp non negativo nelSystemClock.uptimeMillis()
sistema di misura temporale.
I parametri di tempo primitivi o i valori restituiti devono utilizzare long
, non int
.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
I metodi che esprimono unità di tempo devono preferire le abbreviazioni non abbreviate per i nomi delle unità
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
Annotare gli argomenti con tempi lunghi
La piattaforma include diverse annotazioni per fornire una tipizzazione più rigorosa per le unità di misura del tempo di tipo long
:
@CurrentTimeMillisLong
: il valore è un timestamp non negativo misurato come numero di millisecondi da1970-01-01T00:00:00Z
, quindi nella base di tempoSystem.currentTimeMillis()
.@CurrentTimeSecondsLong
: il valore è un timestamp non negativo misurato come numero di secondi trascorsi da1970-01-01T00:00:00Z
.@DurationMillisLong
: il valore è una durata non negativa in millisecondi.@ElapsedRealtimeLong
: il valore è un timestamp non negativo nella base di tempoSystemClock#elapsedRealtime()
.@UptimeMillisLong
: il valore è un timestamp non negativo nella base di tempoSystemClock#uptimeMillis()
.
Unità di misura
Per tutti i metodi che esprimono un'unità di misura diversa dal tempo, preferisci i prefissi delle unità di misura SI in stile CamelCase.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
Inserisci i parametri facoltativi alla fine delle sovraccaricamenti
Se hai sovraccarichi di un metodo con parametri facoltativi, mantieni questi parametri alla fine e mantieni l'ordine coerente con gli altri parametri:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
Quando aggiungi sovraccarichi per gli argomenti facoltativi, il comportamento dei metodi più semplici deve essere esattamente lo stesso che se fossero stati forniti argomenti predefiniti ai metodi più elaborati.
Corollario: non sovraccaricare i metodi se non per aggiungere argomenti facoltativi o per accettare diversi tipi di argomenti se il metodo è polimorfico. Se il metodo sovraccaricato fa qualcosa di fondamentalmente diverso, assegnagli un nuovo nome.
I metodi con parametri predefiniti devono essere annotati con @JvmOverloads (solo Kotlin)
I metodi e i costruttori con parametri predefiniti devono essere annotati con
@JvmOverloads
per mantenere la compatibilità binaria.
Per maggiori dettagli, consulta la sezione Sovraccarichi di funzioni per i valori predefiniti nella guida ufficiale all'interoperabilità Kotlin-Java.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
Non rimuovere i valori predefiniti dei parametri (solo Kotlin)
Se un metodo è stato fornito con un parametro con un valore predefinito, la rimozione del valore predefinito è una modifica che comporta la rottura del codice sorgente.
I parametri del metodo più distintivi e che consentono l'identificazione devono essere i primi
Se hai un metodo con più parametri, metti per primi quelli più pertinenti. I parametri che specificano flag e altre opzioni sono meno importanti di quelli che descrivono l'oggetto su cui viene eseguito l'intervento. Se è presente un callback di completamento, inseriscilo per ultimo.
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);
Vedi anche: Mettere i parametri facoltativi alla fine nelle sovraccaricamenti
Costruttori
Il pattern Builder è consigliato per la creazione di oggetti Java complessi e viene spesso utilizzato in Android nei casi in cui:
- Le proprietà dell'oggetto risultante devono essere immutabili
- Esistono un gran numero di proprietà obbligatorie, ad esempio molti argomenti del costruttore
- Esiste una relazione complessa tra le proprietà al momento della costruzione, ad esempio è necessario un passaggio di verifica. Tieni presente che questo livello di complessità spesso indica problemi di usabilità dell'API.
Valuta se hai bisogno di un creator. I generatori sono utili in un'interfaccia API se vengono utilizzati per:
- Configurare solo alcuni di un insieme potenzialmente ampio di parametri di creazione facoltativi
- Configurare molti parametri di creazione facoltativi o obbligatori diversi, a volte di tipi simili o corrispondenti, in cui i siti di chiamata potrebbero altrimenti diventare difficili da leggere o essere soggetti a errori di scrittura
- Configura la creazione di un oggetto in modo incrementale, in cui diversi frammenti di codice di configurazione potrebbero eseguire chiamate al generatore come dettagli di implementazione
- Consentire la crescita di un tipo aggiungendo ulteriori parametri di creazione facoltativi nelle versioni future dell'API
Se hai un tipo con tre o meno parametri obbligatori e nessun parametro facoltativo, puoi quasi sempre saltare un generatore e utilizzare un normale costruttore.
Per i classi di origine Kotlin, è preferibile utilizzare i costruttori con annotazione @JvmOverloads
e argomenti predefiniti anziché i builder, ma è possibile scegliere di migliorare l'usabilità per i client Java fornendo anche i builder nei casi descritti in precedenza.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
Le classi di generatori devono restituire il generatore
Le classi di builder devono consentire l'accodamento dei metodi restituendo l'oggetto Builder
(ad esempio this
) da ogni metodo tranne build()
. Gli oggetti costruiti aggiuntivi devono essere passati come argomenti. Non restituire il costruttore di un altro oggetto.
Ad esempio:
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();
}
}
Nei rari casi in cui una classe di generatore di base deve supportare l'estensione, utilizza un tipo di valore restituito generico:
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);
}
Le classi di builder devono essere create tramite un costruttore
Per mantenere una creazione coerente dei builder tramite la piattaforma API Android, tutti i
builder devono essere creati tramite un costruttore e non un metodo statico del creator. Per le API basate su Kotlin, Builder
deve essere pubblico anche se gli utenti Kotlin devono fare affidamento implicitamente sul builder tramite un metodo di fabbrica/un meccanismo di creazione in stile DSL. Le librerie non devono utilizzare @PublishedApi internal
per nascondere in modo selettivo il costruttore della classe Builder
ai client Kotlin.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
Tutti gli argomenti dei costruttori del generatore devono essere obbligatori (ad esempio @NonNull)
Gli argomenti facoltativi, ad esempio @Nullable
, devono essere spostati nei metodi setter.
Il costruttore del generatore deve lanciare un NullPointerException
(consigliamo di utilizzare
Objects.requireNonNull
) se non sono specificati gli argomenti obbligatori.
Le classi di builder devono essere classi interne statiche finali dei relativi tipi costruiti
Per motivi di organizzazione logica all'interno di un pacchetto, le classi di builder devono essere tipicamente esposte come classi interne finali dei relativi tipi costruiti, ad esempio Tone.Builder
anziché ToneBuilder
.
I costruttori possono includere un costruttore per creare una nuova istanza da un'istanza esistente
I generatori possono includere un costruttore di copia per creare una nuova istanza di generatore da un generatore o un oggetto creato esistente. Non devono fornire metodi alternativi per creare istanze di builder da oggetti di build o builder esistenti.
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);
}
}
I setter del builder devono accettare argomenti @Nullable se il builder ha un costruttore di copia
Il ripristino è essenziale se è possibile creare una nuova istanza di un generatore da un'istanza esistente. Se non è disponibile un costruttore di copie, il generatore può avere argomenti @Nullable
o @NonNullable
.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
I settati del costruttore possono accettare argomenti @Nullable per le proprietà facoltative
Spesso è più semplice utilizzare un valore nullable per l'input di secondo grado, in particolare in Kotlin, che utilizza argomenti predefiniti anziché costruttori e sovraccarichi.
Inoltre, i setters @Nullable
verranno associati ai relativi getter, che devono essere @Nullable
per le proprietà facoltative.
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();
}
Utilizzo comune in Kotlin:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
Il valore predefinito (se il settatore non viene chiamato) e il significato di null
devono essere documentati correttamente sia nel settatore sia nel getter.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
I settati del generatore possono essere forniti per le proprietà mutabili se i settati sono disponibili nella classe creata
Se la tua classe ha proprietà mutabili e ha bisogno di una classe Builder
, prima chiedi
a te stesso se la tua classe dovrebbe effettivamente avere proprietà mutabili.
Se hai la certezza di aver bisogno di proprietà mutabili, decidi quale dei seguenti scenari è più adatto al tuo caso d'uso previsto:
L'oggetto creato deve essere immediatamente utilizzabile, pertanto devono essere forniti metodi setter per tutte le proprietà pertinenti, mutabili o immutabili.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
Potrebbero essere necessarie alcune chiamate aggiuntive prima che l'oggetto creato possa essere utile, pertanto i setter non devono essere forniti per le proprietà mutabili.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
Non confondere i due scenari.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
I costruttori non devono avere getter
Il getter deve trovarsi nell'oggetto costruito, non nel builder.
I setters del builder devono avere getter corrispondenti nella classe costruita
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();
}
Denominazione dei metodi del generatore
I nomi dei metodi del generatore devono utilizzare lo stile setFoo()
, addFoo()
o clearFoo()
.
Le classi di generatore devono dichiarare un metodo build()
Le classi di builder devono dichiarare un metodo build()
che restituisce un'istanza dell'oggetto costruito.
I metodi build() del builder devono restituire oggetti @NonNull
Il metodo build()
di un generatore deve restituire un'istanza non null dell'oggetto costruito. Se l'oggetto non può essere creato a causa di parametri non validi, la convalida può essere posticipata al metodo di compilazione e deve essere generato un IllegalStateException
.
Non esporre le serrature interne
I metodi nell'API pubblica non devono utilizzare la parola chiave synchronized
. Questa parola chiave fa sì che l'oggetto o la classe venga utilizzato come blocco e, poiché è esposto ad altri, potresti riscontrare effetti collaterali imprevisti se altro codice al di fuori della tua classe inizia a utilizzarlo a scopo di blocco.
Esegui invece le eventuali operazioni di blocco necessarie su un oggetto privato interno.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
I metodi con stile di accesso devono seguire le linee guida per le proprietà Kotlin
Se visualizzati dalle origini Kotlin, i metodi nello stile degli accessori, ovvero quelli che utilizzano i prefissi get
, set
o is
, saranno disponibili anche come proprietà Kotlin.
Ad esempio, int getField()
definito in Java è disponibile in Kotlin come proprietà val field: Int
.
Per questo motivo e per soddisfare in generale le aspettative degli sviluppatori in merito al comportamento dei metodi di accesso, i metodi che utilizzano i prefissi dei metodi di accesso devono comportarsi in modo simile ai campi Java. Evita di utilizzare prefissi in stile accessorio quando:
- Il metodo ha effetti collaterali: preferisci un nome del metodo più descrittivo
- Il metodo comporta un lavoro computazionalmente costoso: preferisci
compute
- Il metodo prevede un lavoro bloccante o comunque di lunga durata per restituire un valore, ad esempio IPC o altre I/O. Preferisci
fetch
- Il metodo blocca il thread finché non può restituire un valore. Preferisci
await
- Il metodo restituisce una nuova istanza di oggetto a ogni chiamata. Preferisci
create
- Il metodo potrebbe non restituire un valore. Preferisci
request
Tieni presente che l'esecuzione di un'operazione di calcolo complessa una volta e la memorizzazione nella cache del valore per le chiamate successive viene comunque considerata un'operazione di calcolo complessa. Il ritardo non viene ammortizzato tra i fotogrammi.
Utilizza il prefisso is per i metodi di accesso booleani
Questa è la convenzione di denominazione standard per i metodi e i campi booleani in Java. In genere, i nomi delle variabili e dei metodi booleani devono essere scritti come domande a cui viene data risposta dal valore restituito.
I metodi di accesso booleani Java devono seguire uno schema di denominazione set
/is
e
i campi devono preferire is
, ad esempio:
// 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'utilizzo di set
/is
per i metodi di accesso Java o di is
per i campi Java consentirà di utilizzarli come proprietà da Kotlin:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
In genere, le proprietà e i metodi di accesso devono utilizzare nomi positivi, ad esempio Enabled
anziché Disabled
. L'utilizzo di una terminologia negativa inverte il significato di true
e false
e rende più difficile ragionare sul comportamento.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
Nei casi in cui il valore booleano descriva l'inclusione o la proprietà di una proprietà, puoi utilizzare has anziché is. Tuttavia, questo non funzionerà con la sintassi della proprietà 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();
Alcuni prefissi alternativi che potrebbero essere più adatti sono can e should:
// "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();
I metodi che attivano/disattivano comportamenti o funzionalità possono utilizzare il prefisso is e il suffisso 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()
Analogamente, i metodi che indicano la dipendenza da altri comportamenti o funzionalità possono utilizzare il prefisso is e il suffisso Supported o 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()
In genere, i nomi dei metodi devono essere scritti come domande a cui viene data risposta dal valore restituito.
Metodi di proprietà Kotlin
Per una proprietà di classe var foo: Foo
, Kotlin genererà metodi get
/set
utilizzando una regola coerente: anteponi get
e metti in maiuscolo il primo carattere per il
metodo getter e anteponi set
e metti in maiuscolo il primo carattere per il metodo setter. La dichiarazione della proprietà produrrà metodi denominati rispettivamente public Foo getFoo()
e
public void setFoo(Foo foo)
.
Se la proprietà è di tipo Boolean
, viene applicata una regola aggiuntiva per la generazione del nome: se il nome della proprietà inizia con is
, non viene anteposto get
al nome del metodo getter, ma viene utilizzato il nome della proprietà stessa come getter.
Pertanto, preferisci assegnare alle proprietà Boolean
un prefisso is
per rispettare le linee guida per la denominazione:
var isVisible: Boolean
Se la tua proprietà è una delle eccezioni sopra menzionate e inizia con un prefisso appropriato, utilizza l'annotazione @get:JvmName
nella proprietà per specificare manualmente il nome appropriato:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
Accessori di maschera di bit
Consulta Utilizzare @IntDef
per i flag di maschera di bit per le linee guida sull'API relative alla definizione dei flag di maschera di bit.
Setter
Devono essere forniti due metodi setter: uno che accetta una stringa di bit completa e sovrascrive tutti i flag esistenti e un altro che accetta una maschera di bit personalizzata per consentire una maggiore flessibilità.
/**
* 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);
Getter
Deve essere fornito un getter per ottenere la maschera di bit completa.
/**
* 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();
Utilizza pubblico anziché protetto
Preferisci sempre public
a protected
nell'API pubblica. L'accesso protetto risulta essere problematico nel lungo periodo, perché gli implementatori devono eseguire l'override per fornire gli accessi pubblici nei casi in cui l'accesso esterno per impostazione predefinita sarebbe stato altrettanto valido.
Ricorda che la visibilità protected
non impedisce agli sviluppatori di chiamare un'API, ma la rende solo un po' più fastidiosa.
Non implementare né equals() né hashCode() o implementare entrambi
Se ne sostituisci una, devi sostituire anche l'altra.
Implementa toString() per le classi di dati
È consigliabile che le classi di dati sostituiscano toString()
per aiutare gli sviluppatori a eseguire il debug del loro codice.
Indica se l'output riguarda il comportamento del programma o il debug
Decidi se vuoi che il comportamento del programma si basi o meno sulla tua implementazione. Ad esempio, UUID.toString() e File.toString() documentano il loro formato specifico per l'utilizzo da parte dei programmi. Se esponi informazioni solo per il debug, ad esempio Intent, implica l'eredità dei documenti dalla superclasse.
Non includere informazioni aggiuntive
Tutte le informazioni disponibili su toString()
devono essere disponibili anche tramite
l'API pubblica dell'oggetto. In caso contrario, incoraggi gli sviluppatori a eseguire l'analisi e a fare affidamento sull'output toString()
, il che impedirà modifiche future. Una buona prassi è implementare toString()
utilizzando solo l'API pubblica dell'oggetto.
Scoraggiare il ricorso all'output di debug
Sebbene sia impossibile impedire agli sviluppatori di fare affidamento sull'output di debug, includere il System.identityHashCode
dell'oggetto nell'output toString()
farà sì che sia molto improbabile che due oggetti diversi abbiano un output toString()
uguale.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
Ciò può scoraggiare efficacemente gli sviluppatori dal scrivere asserzioni di test come
assertThat(a.toString()).isEqualTo(b.toString())
sui tuoi oggetti.
Utilizza createFoo quando restituisci oggetti appena creati
Utilizza il prefisso create
, non get
o new
, per i metodi che creano valori di ritorno, ad esempio mediante la costruzione di nuovi oggetti.
Quando il metodo crea un oggetto da restituire, indicalo chiaramente nel nome del metodo.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
I metodi che accettano oggetti File devono accettare anche stream
Le posizioni di archiviazione dei dati su Android non sono sempre file su disco. Ad esempio, i contenuti trasmessi oltre i confini degli utenti sono rappresentati come content://
Uri
. Per
abilitare l'elaborazione di varie origini dati, le API che accettano oggetti File
devono accettare anche InputStream
, OutputStream
o entrambi.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
Prendi e restituisci primitive non elaborate anziché versioni con casella
Se devi comunicare valori mancanti o null, valuta la possibilità di utilizzare -1
,
Integer.MAX_VALUE
o Integer.MIN_VALUE
.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
Evitare gli equivalenti di classe dei tipi primitivi evita il sovraccarico di memoria di queste classi, l'accesso ai valori tramite metodo e, soprattutto, l'autoboxing derivante dal passaggio tra tipi primitivi e oggetti. Evitare questi comportamenti consente di risparmiare memoria e allocazioni temporanee che possono comportare raccolte di rifiuti costose e più frequenti.
Utilizza le annotazioni per chiarire i valori validi dei parametri e di ritorno
Sono state aggiunte annotazioni per gli sviluppatori per chiarire i valori consentiti in varie situazioni. In questo modo, è più facile per gli strumenti aiutare gli sviluppatori quando forniscono valori errati (ad esempio, passando un valore int
arbitrario quando il framework richiede uno di un insieme specifico di valori costanti). Utilizza tutte le seguenti annotazioni, se opportuno:
Supporto di valori Null
Le annotazioni con supporto di valori null esplicite sono obbligatorie per le API Java, ma il concetto di supporti di valori null fa parte del linguaggio Kotlin e le annotazioni con supporto di valori null non devono mai essere utilizzate nelle API Kotlin.
@Nullable
: indica che un determinato valore restituito, parametro o campo può essere
null:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: indica che un determinato valore restituito, parametro o campo non può essere nullo. Il contrassegno @Nullable
è relativamente nuovo per Android, pertanto la maggior parte dei metodi dell'API di Android non è documentata in modo coerente. Pertanto, abbiamo un valore di stato di tipo "sconosciuto, @Nullable
, @NonNull
", motivo per cui @NonNull
fa parte delle linee guida dell'API:
@NonNull
public String getName()
public void setName(@NonNull String name)
Per la documentazione della piattaforma Android, l'annotazione dei parametri del metodo genererà automaticamente la documentazione nel formato "Questo valore può essere nullo", a meno che "null" non venga utilizzato esplicitamente altrove nella documentazione del parametro.
Metodi esistenti "non realmente null": i metodi esistenti nell'API senza un'annotazione @Nullable
dichiarata possono essere annotati @Nullable
se il metodo può restituire null
in circostanze specifiche e evidenti (ad esempio findViewById()
). Per gli sviluppatori che non vogliono eseguire il controllo di null, devono essere aggiunti metodi complementari @NotNull requireFoo()
che generano IllegalArgumentException
.
Metodi di interfaccia: le nuove API devono aggiungere l'annotazione appropriata quando implementano metodi di interfaccia, come Parcelable.writeToParcel()
(ovvero il metodo nella classe di implementazione deve essere writeToParcel(@NonNull Parcel,
int)
, non writeToParcel(Parcel, int)
); tuttavia, le API esistenti prive di annotazioni non devono essere "corrette".
Applicazione della nullità
In Java, è consigliato utilizzare metodi per eseguire la convalida dell'input per i parametri @NonNull
utilizzando
Objects.requireNonNull()
e generare un NullPointerException
quando i parametri sono null. Questa operazione viene eseguita automaticamente in Kotlin.
Risorse
Identificatori delle risorse: i parametri interi che indicano gli ID per risorse specifiche devono essere annotati con la definizione del tipo di risorsa appropriata.
Esiste un'annotazione per ogni tipo di risorsa, ad esempio @StringRes
,
@ColorRes
e @AnimRes
, oltre a @AnyRes
generico. Per
esempio:
public void setTitle(@StringRes int resId)
@IntDef per insiemi di costanti
Costanti magiche: i parametri String
e int
destinati a ricevere uno
di un insieme finito di valori possibili indicati da costanti pubbliche devono essere
annotati in modo appropriato con @StringDef
o @IntDef
. Queste annotazioni ti consentono di creare una nuova annotazione che puoi utilizzare come typedef per i parametri consentiti. Ad esempio:
/** @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);
I metodi sono consigliati per verificare la validità dei parametri annotati
e generare un IllegalArgumentException
se il parametro non fa parte del
@IntDef
@IntDef per i flag di maschera di bit
L'annotazione può anche specificare che le costanti sono flag e possono essere combinate con & e 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 per insiemi di costanti stringa
Esiste anche l'annotazione @StringDef
, che è esattamente come @IntDef
nella
sezione precedente, ma per le costanti String
. Puoi includere più valori "prefisso" che vengono utilizzati per generare automaticamente la documentazione per tutti i valori.
@SdkConstant per le costanti dell'SDK
@SdkConstant Aggiungi un'annotazione ai campi pubblici quando sono uno di questi valori SdkConstant
: 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";
Fornisci valori null compatibili per le sostituzioni
Per la compatibilità con l'API, la nullabilità delle sostituzioni deve essere compatibile con la nullabilità corrente dell'elemento principale. La tabella seguente rappresenta le aspettative di compatibilità. In parole povere, le sostituzioni devono essere solo più restrittive o quanto l'elemento sostituito.
Tipo | Genitore | Figlio |
---|---|---|
Tipo restituito | Non annotato | Non annotato o non null |
Tipo restituito | Nullable | Con valori null o non null |
Tipo restituito | Nonnull | Nonnull |
Argomento divertente | Non annotato | Non annotato o con valori null |
Argomento divertente | Nullable | Nullable |
Argomento divertente | Nonnull | Con valori null o non null |
Se possibile, preferisci gli argomenti non null (ad esempio @NonNull)
Quando i metodi sono sovraccaricati, è preferibile che tutti gli argomenti siano non null.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
Questa regola si applica anche ai settatiri di proprietà sovraccaricati. L'argomento principale deve essere diverso da null e l'eliminazione della proprietà deve essere implementata come metodo distinto. In questo modo vengono evitate chiamate "senza senso" in cui lo sviluppatore deve impostare parametri aggiuntivi anche se non sono obbligatori.
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()
Preferisci tipi di ritorno non null (ad esempio @NonNull) per i contenitori
Per tipi di contenitori come Bundle
o Collection
, restituisci un contenitore vuoto e immutabile, se applicabile. Nei casi in cui null
verrebbe utilizzato per distinguere la disponibilità di un contenitore, ti consigliamo di fornire un metodo booleano distinto.
@NonNull
public Bundle getExtras() { ... }
Le annotazioni di nullabilità per le coppie get e set devono essere conformi
Le coppie di metodi get e set per una singola proprietà logica devono sempre essere in accordo nelle loro annotazioni di nullabilità. Il mancato rispetto di questa linea guida comporterà la mancata applicazione della sintassi delle proprietà di Kotlin e l'aggiunta di annotazioni di nullità in disaccordo ai metodi delle proprietà esistenti è quindi una modifica che comporta la rottura del codice sorgente per gli utenti di Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Restituire un valore in condizioni di errore o guasto
Tutte le API devono consentire alle app di reagire agli errori. Il ritorno di false
, -1
, null
o altri valori generici di "si è verificato un problema" non dice abbastanza a uno sviluppatore circa la mancata definizione delle aspettative degli utenti o il monitoraggio accurato dell'affidabilità della sua app sul campo. Quando progetti un'API, immagina di sviluppare un'app. Se riscontri un errore, l'API ti fornisce informazioni sufficienti per presentarlo all'utente o reagire di conseguenza?
- È consentito (e consigliato) includere informazioni dettagliate in un messaggio di eccezione, ma gli sviluppatori non devono analizzarlo per gestire l'errore in modo appropriato. I codici di errore dettagliati o altre informazioni devono essere esposti come metodi.
- Assicurati che l'opzione di gestione degli errori scelta ti offra la flessibilità di
introdurne di nuovi in futuro. Per
@IntDef
, significa includere un valoreOTHER
oUNKNOWN
. Quando restituisci un nuovo codice, puoi controllare iltargetSdkVersion
dell'autore della chiamata per evitare di restituire un codice di errore sconosciuto all'app. Per le eccezioni, definisci un superclasse comune che le implementi, in modo che qualsiasi codice che gestisce quel tipo catturi e gestisca anche i sottotipi. - Per uno sviluppatore dovrebbe essere difficile o impossibile ignorare per sbaglio un errore. Se l'errore viene comunicato restituendo un valore, annota il metodo con
@CheckResult
.
È preferibile lanciare un ? extends RuntimeException
quando viene raggiunta una condizione di errore o guasto a causa di un errore dello sviluppatore, ad esempio l'ignoranza dei vincoli sui parametri di input o la mancata verifica dello stato osservabile.
I metodi di impostazione o di azione (ad esempio perform
) possono restituire un codice stato intero se l'azione potrebbe non riuscire a causa di condizioni o stati aggiornati in modo asincrono al di fuori del controllo dello sviluppatore.
I codici di stato devono essere definiti nella classe contenente come campi public static final
, con prefisso ERROR_
ed elencati in un'annotazione @hide
@IntDef
.
I nomi dei metodi devono sempre iniziare con il verbo, non con il soggetto
Il nome del metodo deve sempre iniziare con il verbo (ad esempio get
,
create
, reload
e così via), non con l'oggetto su cui stai agendo.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Preferisci i tipi di raccolta rispetto agli array come tipo di ritorno o parametro
Le interfacce di raccolta con tipi generici offrono diversi vantaggi rispetto agli array, tra cui contratti API più stringenti per unicità e ordinamento, supporto per i generici e una serie di metodi di praticità per gli sviluppatori.
Eccezione per i tipi primitivi
Se gli elementi sono primitivi, non preferire gli array per evitare il costo dell'auto-boxing. Consulta Prendere e restituire primitive non elaborate anziché versioni con casella
Eccezione per il codice sensibile alle prestazioni
In alcuni scenari, in cui l'API viene utilizzata in codice sensibile alle prestazioni (come grafica o altre API di misurazione/layout/disegno), è accettabile utilizzare array anziché raccolte per ridurre le allocazioni e il ricambio di memoria.
Eccezione per Kotlin
Gli array di Kotlin sono invariati e il linguaggio Kotlin fornisce ampie API di utilità per gli array, pertanto sono paragonabili a List
e Collection
per le API Kotlin a cui si intende accedere da Kotlin.
Preferisci le raccolte @NonNull
Preferisci sempre @NonNull
per gli oggetti della raccolta. Quando restituisci una raccolta vuota, utilizza il metodo Collections.empty
appropriato per restituire un oggetto raccolta immutabile, a basso costo e con tipo corretto.
Se le annotazioni del tipo sono supportate, preferisci sempre @NonNull
per gli elementi della raccolta.
Inoltre, dovresti preferire @NonNull
quando utilizzi array anziché raccolte (vedi
elemento precedente). Se l'allocazione degli oggetti è un problema, crea una costante e trasmettela: dopo tutto, un array vuoto è immutabile. Esempio:
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à della raccolta
Per impostazione predefinita, le API Kotlin dovrebbero preferire i tipi di ritorno di sola lettura (non Mutable
) per le raccolte a meno che il contratto dell'API non richieda specificamente un tipo di ritorno mutabile.
Tuttavia, per impostazione predefinita le API Java dovrebbero preferire i tipi di ritorno mutabili perché l'implementazione delle API Java sulla piattaforma Android non fornisce ancora un'implementazione comoda delle collezioni immutabili. L'eccezione a questa regola sono i tipi di ritornoCollections.empty
, che sono immutabili. Nei casi in cui la mutabilità possa essere sfruttata dai client, intenzionalmente o per errore, per violare il pattern di utilizzo previsto dell'API, le API Java dovrebbero prendere in seria considerazione la possibilità di restituire una copia superficiale della raccolta.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
Tipi di valori restituiti esplicitamente mutabili
Le API che restituiscono raccolte non dovrebbero idealmente modificare l'oggetto collezione restituito dopo il ritorno. Se la raccolta restituita deve essere modificata o riutilizzata in qualche modo, ad esempio una visualizzazione adattata di un set di dati mutabile, il comportamento preciso di quando i contenuti possono cambiare deve essere documentato esplicitamente o seguire le convenzioni di denominazione delle API stabilite.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
La convenzione .asFoo()
di Kotlin è descritta
di seguito e consente alla raccolta restituita da
.asList()
di cambiare se cambia la raccolta originale.
Mutabilità degli oggetti di tipo di dati restituiti
Analogamente alle API che restituiscono raccolte, le API che restituiscono oggetti di tipo di dati idealmente non dovrebbero modificare le proprietà dell'oggetto restituito dopo il ritorno.
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)
}
In casi estremamente limitati, alcuni codici sensibili alle prestazioni potrebbero trarre vantaggio dal pooling o dal riutilizzo degli oggetti. Non scrivere la tua struttura di dati del pool di oggetti e non esporre oggetti riutilizzati nelle API pubbliche. In entrambi i casi, fai molta attenzione a gestire l'accesso simultaneo.
Utilizzo del tipo di parametro vararg
È consigliabile utilizzare vararg
sia per le API Kotlin che per quelle Java nei casi in cui lo sviluppatore potrebbe creare un array nel sito di chiamata al solo scopo di passare più parametri correlati dello stesso tipo.
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);
Copie difensive
Sia le implementazioni Java che quelle Kotlin dei parametri vararg
vengono compilate nello stesso bytecode basato su array e, di conseguenza, possono essere chiamate dal codice Java con un array mutabile. I progettisti di API sono vivamente incoraggiati a creare una copia superficiale difensiva del parametro array nei casi in cui verrà eseguito il commit in un campo o in una classe interna anonima.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
Tieni presente che la creazione di una copia difensiva non fornisce alcuna protezione contro la modifica simultanea tra la chiamata del metodo iniziale e la creazione della copia, né protegge dalla mutazione degli oggetti contenuti nell'array.
Fornisci una semantica corretta con i parametri di tipo di raccolta o i tipi restituiti
List<Foo>
è l'opzione predefinita, ma prendi in considerazione altri tipi per fornire un significato aggiuntivo:
Utilizza
Set<Foo>
se la tua API è indifferente all'ordine degli elementi e non consente duplicati o se i duplicati non hanno significato.Collection<Foo>,
se la tua API è indifferente all'ordine e consente duplicati.
Funzioni di conversione di Kotlin
Kotlin utilizza spesso .toFoo()
e .asFoo()
per ottenere un oggetto di un tipo diverso da un oggetto esistente, dove Foo
è il nome del tipo di ritorno della conversione. Questo è coerente con il familiare JDKObject.toString()
. Kotlin fa un passo avanti utilizzandolo per le conversioni primitive come 25.toFloat()
.
La distinzione tra le conversioni denominate .toFoo()
e .asFoo()
è significativa:
Utilizza .toFoo() per creare un nuovo oggetto indipendente
Come .toString()
, una conversione "a" restituisce un nuovo oggetto indipendente. Se l'oggetto originale viene modificato in un secondo momento, il nuovo oggetto non rifletterà queste modifiche.
Analogamente, se l'oggetto nuovo viene modificato in un secondo momento, l'oggetto vecchio non rifletterà queste modifiche.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
Utilizza .asFoo() quando crei un wrapper dipendente, un oggetto decorato o un trasferimento
Il trasferimento in Kotlin viene eseguito utilizzando la parola chiave as
. Riflette una modifica dell'interfaccia, ma non dell'identità. Se utilizzato come prefisso in una funzione di estensione, .asFoo()
decora il ricevente. Una mutazione nell'oggetto visualizzatore originale verrà applicata all'oggetto restituito da asFoo()
.
Una mutazione nel nuovo oggetto Foo
potrebbe essere applicata all'oggetto originale.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
Le funzioni di conversione devono essere scritte come funzioni di estensione
La scrittura di funzioni di conversione al di fuori delle definizioni della classe di ricezione e del risultato riduce l'accoppiamento tra i tipi. Una conversione ideale richiede solo accesso all'API pubblica all'oggetto originale. Questo dimostra che un sviluppatore può scrivere conversioni analoghe anche per i propri tipi preferiti.
Genera eccezioni specifiche appropriate
I metodi non devono generare eccezioni generiche come java.lang.Exception
o
java.lang.Throwable
, ma deve essere utilizzata un'eccezione specifica appropriata come
java.lang.NullPointerException
per consentire agli sviluppatori di gestire le eccezioni
senza essere eccessivamente generiche.
Gli errori non correlati agli argomenti forniti direttamente al metodo invocato pubblicamente devono generare java.lang.IllegalStateException
anziché java.lang.IllegalArgumentException
o java.lang.NullPointerException
.
Listener e callback
Queste sono le regole relative alle classi e ai metodi utilizzati per i meccanismi di listener e callback.
I nomi delle classi di callback devono essere singolari
Utilizza MyObjectCallback
anziché MyObjectCallbacks
.
I nomi dei metodi di callback devono avere il formato on
onFooEvent
indica che si sta verificando FooEvent
e che il callback deve reagire di conseguenza.
Il tempo passato rispetto al tempo presente deve descrivere il comportamento di temporizzazione
I metodi di callback relativi agli eventi devono essere denominati in modo da indicare se l'evento è già avvenuto o è in corso.
Ad esempio, se il metodo viene chiamato dopo l'esecuzione di un'azione di clic:
public void onClicked()
Tuttavia, se il metodo è responsabile dell'esecuzione dell'azione di clic:
public boolean onClick()
Registrazione dei callback
Quando un ascoltatore o un callback può essere aggiunto o rimosso da un oggetto, i metodi associati devono essere denominati add e remove o register e unregister. Essere coerente con la convenzione esistente utilizzata dalla classe o da altre classi nello stesso pacchetto. Se non esiste un precedente di questo tipo, preferisci aggiungere e rimuovere.
I metodi che prevedono la registrazione o la disattivazione dei callback devono specificare il nome completo del tipo di callback.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
Evita i getter per i callback
Non aggiungere metodi getFooCallback()
. Questa è una via di fuga allettante per i casi in cui gli sviluppatori potrebbero voler collegare un callback esistente con la propria sostituzione, ma è fragile e rende difficile ragionare sullo stato attuale per gli sviluppatori di componenti. Ad esempio,
- Lo sviluppatore A chiama
setFooCallback(a)
- Lo sviluppatore B chiama
setFooCallback(new B(getFooCallback()))
- Lo sviluppatore A vuole rimuovere il proprio callback
a
e non ha modo di farlo senza conoscere il tipo diB
e senza cheB
sia stato creato per consentire simili modifiche al suo callback con wrapping.
Accetta Executor per controllare l'invio dei callback
Quando registri i callback che non hanno aspettative di threading esplicite (praticamente
ovunque al di fuori del toolkit UI), ti consigliamo vivamente di includere un
parametro Executor
durante la registrazione per consentire allo sviluppatore di specificare
il thread su cui verranno richiamati i callback.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Come eccezione alle nostre consuete
linee guida sui parametri facoltativi, è accettabile
fornire un sovraccarico omettendo Executor
anche se non è l'ultimo
argomento nell'elenco dei parametri. Se Executor
non è fornito, il callback deve essere invocato nel thread principale utilizzando Looper.getMainLooper()
e questo deve essere documentato nel metodo sovraccaricato associato.
/**
* ...
* 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)
Aspetti da considerare per l'implementazione di Executor
: tieni presente che quanto segue è un executor valido.
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Ciò significa che, quando implementi API che utilizzano questo formato, l'implementazione dell'oggetto binder in entrata lato processo dell'app deve chiamare Binder.clearCallingIdentity()
prima di invocare il callback dell'app su Executor
fornito dall'app. In questo modo, qualsiasi codice dell'app che utilizza l'identità del binder (ad esempio Binder.getCallingUid()
) per i controlli delle autorizzazioni attribuisce correttamente il codice in esecuzione all'app e non al processo di sistema che chiama l'app. Se gli utenti della tua API vogliono le informazioni UID o PID dell'utente che chiama, queste devono essere una parte esplicita della tua API, anziché implicita in base a dove è stato eseguito il Executor
fornito.
La specifica di un Executor
deve essere supportata dalla tua API. In alcuni casi critici per le prestazioni, le app potrebbero dover eseguire il codice immediatamente o in modo sincrono con il feedback dell'API. L'accettazione di un Executor
lo consente.
La creazione di un altro HandlerThread
o di un elemento simile al trampolino per motivi difensivi svantaggia questo caso d'uso auspicabile.
Se un'app eseguirà codice costoso da qualche parte nel proprio processo, lascialo fare. Le soluzioni alternative che gli sviluppatori di app troveranno per superare le limitazioni saranno molto più difficili da supportare a lungo termine.
Eccezione per un singolo callback: quando la natura degli eventi registrati richiede il supporto di una sola istanza di callback, utilizza il seguente stile:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Utilizza Executor anziché Handler
In passato, Handler
di Android veniva utilizzato come standard per reindirizzare l'esecuzione del callback a un thread Looper
specifico. Questo standard è stato modificato in modo da dare la preferenza a Executor
poiché la maggior parte degli sviluppatori di app gestisce i propri pool di thread, rendendo il thread principale o dell'interfaccia utente l'unico thread Executor
disponibile per l'app. Utilizza Executor
per dare agli sviluppatori il controllo necessario per riutilizzare i contesti di esecuzione esistenti/preferiti.Looper
Le librerie di concorrenza moderne come kotlinx.coroutines o RxJava forniscono i propri meccanismi di pianificazione che eseguono il proprio invio quando necessario, il che rende importante fornire la possibilità di utilizzare un'esecuzione diretta (ad esempioRunnable::run
) per evitare la latenza dei doppi salti di thread. Ad esempio, un hop per pubblicare in un thread Looper
utilizzando un Handler
seguito da un altro hop dal framework di concorrenza dell'app.
Le eccezioni a questa linea guida sono rare. Ecco alcuni dei motivi più comuni per richiedere un'eccezione:
Devo utilizzare un Looper
perché ho bisogno di un Looper
per epoll
per l'evento.
Questa richiesta di eccezione viene concessa perché i vantaggi di Executor
non possono essere ottenuti in questa situazione.
Non voglio che il codice dell'app blocchi la pubblicazione dell'evento nel mio thread. In genere, questa richiesta di eccezione non viene concessa per il codice eseguito in un processo dell'app. Le app che commettono questo errore si danneggiano solo, senza influire sulla salute complessiva del sistema. Le app che eseguono correttamente l'operazione o utilizzano un framework di concorrenza comune non dovrebbero pagare penalità di latenza aggiuntive.
Handler
è localmente coerente con altre API simili nella stessa classe.
Questa richiesta di eccezione viene concessa in base alla situazione. È preferibile aggiungere sovraccarichi basati su Executor
ed eseguire la migrazione delle implementazioni di Handler
in modo che utilizzino la nuova implementazione di Executor
. (myHandler::post
è un valore valido per Executor
). A seconda delle dimensioni della classe, del numero di metodi Handler
esistenti e della probabilità che gli sviluppatori debbano utilizzare i metodi basati su Handler
esistenti insieme al nuovo metodo, potrebbe essere concessa un'eccezione per aggiungere un nuovo metodo basato su Handler
.
Simmetria nella registrazione
Se esiste un modo per aggiungere o registrare qualcosa, deve esistere anche un modo perrimuoverlo/annullare la registrazione. Il metodo
registerThing(Thing)
deve avere una corrispondenza
unregisterThing(Thing)
Fornisci un identificatore della richiesta
Se è ragionevole che uno sviluppatore riutilizzi un callback, fornisci un oggetto identificativo per collegare il callback alla richiesta.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
Oggetti di callback con più metodi
I richiami di più metodi devono preferire i metodi interface
e utilizzare i metodi default
quando vengono aggiunti alle interfacce rilasciate in precedenza. In precedenza, questa linea guida consigliava abstract class
a causa della mancanza di metodi default
in Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
Utilizzare android.os.OutcomeReceiver per modellare una chiamata di funzione non bloccante
OutcomeReceiver<R,E>
segnala un valore di risultato R
in caso di esito positivo o E : Throwable
in caso contrario, ovvero le stesse operazioni che può eseguire una chiamata di metodo normale. Utilizza OutcomeReceiver
come tipo di callback
quando converti un metodo bloccante che restituisce un risultato o genera un'eccezione in un metodo asincrono non bloccante:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
I metodi asincroni convertiti in questo modo restituiscono sempre void
. Qualsiasi risultato restituito da requestFoo
viene invece riportato al OutcomeReceiver.onResult
del parametro callback
di requestFooAsync
chiamandolo nel executor
fornito.
Eventuali eccezioni generate da requestFoo
vengono invece segnalate al metodo OutcomeReceiver.onError
nello stesso modo.
L'utilizzo di OutcomeReceiver
per la generazione di report sui risultati dei metodi asincroni offre anche un wrapper Kotlinsuspend fun
per i metodi asincroni che utilizzano l'estensioneContinuation.asOutcomeReceiver
di androidx.core:core-ktx
:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
Estensioni come questa consentono ai client Kotlin di chiamare metodi asincroni non bloccanti con la praticità di una chiamata di funzione normale senza bloccare il thread di chiamata. Queste estensioni 1:1 per le API di piattaforma potrebbero essere offerte nell'ambito dell'artifact androidx.core:core-ktx
in Jetpack se combinate con i controlli e le considerazioni sulla compatibilità delle versioni standard. Per ulteriori informazioni, considerazioni sull'annullamento e esempi, consulta la documentazione relativa a asOutcomeReceiver.
I metodi asincroni che non corrispondono alla semantica di un metodo che restituisce un risultato o genera un'eccezione al termine del lavoro non devono utilizzare OutcomeReceiver
come tipo di callback. Valuta invece una delle altre opzioni elencate nella sezione seguente.
Preferisci le interfacce funzionali alla creazione di nuovi tipi di metodi astratti singoli (SAM)
Il livello 24 dell'API ha aggiunto i tipi java.util.function.*
(documentazione di riferimento), che offrono interfacce SAM generiche come Consumer<T>
, adatte per l'utilizzo come lambda di callback. In molti casi, la creazione di nuove interfacce SAM offre scarso valore in termini di sicurezza del tipo o di comunicazione dell'intenzione, ampliando al contempo inutilmente la superficie dell'API Android.
Valuta la possibilità di utilizzare queste interfacce generiche anziché crearne di nuove:
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- Molte altre sono disponibili nella documentazione di riferimento
Posizionamento dei parametri SAM
I parametri SAM devono essere posizionati per ultimi per consentire l'utilizzo idiomatico da Kotlin, anche se il metodo è sovraccaricato con parametri aggiuntivi.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
Documenti
Queste sono regole relative alla documentazione pubblica (Javadoc) per le API.
Tutte le API pubbliche devono essere documentate
Tutte le API pubbliche devono avere una documentazione sufficiente per spiegare come un sviluppatore userebbe l'API. Supponiamo che lo sviluppatore abbia trovato il metodo utilizzando la funzionalità di completamento automatico o mentre sfogliava la documentazione di riferimento dell'API e che abbia una quantità minima di contesto dall'interfaccia API adiacente (ad esempio la stessa classe).
Metodi
I parametri e i valori restituiti dei metodi devono essere documentati utilizzando rispettivamente le annotazioni dei documenti @param
e @return
. Formatta il corpo del Javadoc come se fosse preceduto da "Questo metodo…".
Se un metodo non accetta parametri, non prevede considerazioni speciali e restituisce ciò che indica il nome del metodo, puoi omettere @return
e scrivere documenti simili a:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Utilizza sempre i link in Javadoc
I documenti devono includere link ad altri documenti per costanti, metodi e altri elementi correlati. Utilizza i tag Javadoc (ad esempio @see
e {@link foo}
), non solo le parole in testo normale.
Per il seguente esempio di origine:
public static final int FOO = 0;
public static final int BAR = 1;
Non utilizzare testo non elaborato o caratteri di codice:
/**
* 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) { ... }
Utilizza invece i link:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
Tieni presente che l'utilizzo di un'annotazione IntDef
come @ValueType
su un parametro genera automaticamente la documentazione che specifica i tipi consentiti. Per scoprire di più su IntDef
, consulta le linee guida sulle annotazioni.
Esegui il target update-api o docs quando aggiungi Javadoc
Questa regola è particolarmente importante quando aggiungi i tag @link
o @see
e assicurati che l'output sia come previsto. L'output ERROR in Javadoc è spesso dovuto a link sbagliati. Questo controllo viene eseguito dal target di compilazione update-api
o docs
, ma il target docs
potrebbe essere più rapido se stai modificando solo il Javadoc e non devi eseguire il target update-api
.
Utilizza {@code foo} per distinguere i valori Java
Inserisci un a capo tra i valori Java come true
, false
e null
con {@code...}
per distinguerli dal testo della documentazione.
Quando scrivi la documentazione nelle sorgenti Kotlin, puoi inserire il codice tra virgolette a capo come faresti per Markdown.
I riepiloghi @param e @return devono essere costituiti da un singolo frammento di frase
I riepiloghi dei parametri e dei valori restituiti devono iniziare con un carattere minuscolo e contenere un solo frammento di frase. Se hai informazioni aggiuntive che superano una singola frase, spostale nel corpo del Javadoc del metodo:
/**
* @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.
*/
Deve essere sostituito con:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Le annotazioni dei documenti richiedono spiegazioni
Documenta il motivo per cui le annotazioni @hide
e @removed
sono nascoste nell'API pubblica.
Includi istruzioni su come sostituire gli elementi dell'API contrassegnati con l'annotazione@deprecated
.
Utilizza @throws per documentare le eccezioni
Se un metodo genera un'eccezione controllata, ad esempio IOException
, documenta l'eccezione con @throws
. Per le API basate su Kotlin destinate all'utilizzo da parte di client Java, annota le funzioni con @Throws
.
Se un metodo genera un'eccezione non controllata che indica un errore evitabile, ad esempio IllegalArgumentException
o IllegalStateException
, documenta l'eccezione spiegando il motivo per cui viene generata. L'eccezione sollevata deve anche indicare il motivo per cui è stata sollevata.
Alcuni casi di eccezioni non controllate sono considerati impliciti e non devono essere documentati, ad esempio NullPointerException
o IllegalArgumentException
se un argomento non corrisponde a un'annotazione @IntDef
o simile che incorpora il contratto dell'API nella firma del metodo:
/**
* ...
* @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.");
}
// ...
In 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")
}
}
// ...
Se il metodo richiama codice asincrono che potrebbe generare eccezioni, valuta come lo sviluppatore le rileva e risponde a queste eccezioni. In genere, questo comporta l'inoltro dell'eccezione a un callback e la documentazione delle eccezioni lanciate nel metodo che le riceve. Le eccezioni asincrone
non devono essere documentate con @throws
, a meno che non vengano effettivamente rilanciate dal
metodo annotato.
Termina la prima frase dei documenti con un punto
Lo strumento Doclava analizza i documenti in modo semplice, terminando il documento di sinossi (la prima frase, utilizzata nella descrizione rapida nella parte superiore dei documenti del corso) non appena vede un punto (.) seguito da uno spazio. Ciò causa due problemi:
- Se un breve documento non termina con un punto e se il relativo membro ha ereditato
documenti rilevati dallo strumento, la sinossi rileva anche questi
documenti ereditati. Ad esempio, consulta
actionBarTabStyle
nella documentazione diR.attr
, che contiene la descrizione della dimensione aggiunta alla sinossi. - Evita "ad es." nella prima frase per lo stesso motivo, perché Doclava termina
la documentazione della sinossi dopo "ad es.". Ad esempio, vedi
TEXT_ALIGNMENT_CENTER
inView.java
. Tieni presente che Metalava corregge automaticamente questo errore inserendo un spazio non inserito dopo il punto. Tuttavia, non fare questo errore in primo luogo.
Formattare i documenti da visualizzare in HTML
Javadoc viene visualizzato in HTML, quindi formatta questi documenti di conseguenza:
Le interruzioni di riga devono utilizzare un tag
<p>
esplicito. Non aggiungere un tag</p>
di chiusura.Non utilizzare ASCII per il rendering di elenchi o tabelle.
Per gli elenchi, utilizza
<ul>
o<ol>
rispettivamente per non ordinati e ordinati. Ogni elemento deve iniziare con un tag<li>
, ma non è necessario un tag di chiusura</li>
. È obbligatorio un tag di chiusura</ul>
o</ol>
dopo l'ultimo elemento.Le tabelle devono utilizzare
<table>
,<tr>
per le righe,<th>
per le intestazioni e<td>
per le celle. Tutti i tag della tabella richiedono tag di chiusura corrispondenti. Puoi utilizzareclass="deprecated"
su qualsiasi tag per indicare il ritiro.Per creare un carattere codice in linea, utilizza
{@code foo}
.Per creare blocchi di codice, utilizza
<pre>
.Tutto il testo all'interno di un blocco
<pre>
viene analizzato dal browser, quindi fai attenzione alle parentesi<>
. Puoi eseguire la loro sfuggita con le entità HTML<
e>
.In alternativa, puoi lasciare le parentesi graffe
<>
nello snippet di codice se{@code foo}
li inserisci nelle sezioni in violazione. Ad esempio:<pre>{@code <manifest>}</pre>
Segui la guida di stile del riferimento API
Per garantire la coerenza dello stile per i riepiloghi delle classi, le descrizioni dei metodi, le descrizioni dei parametri e altri elementi, segui i consigli riportati nelle linee guida ufficiali del linguaggio Java all'indirizzo Come scrivere commenti di documentazione per lo strumento Javadoc.
Regole specifiche per il framework Android
Queste regole riguardano API, pattern e strutture di dati specifici per le API e i comportamenti integrati nel framework Android (ad esempio Bundle
o
Parcelable
).
I creator di intent devono utilizzare il pattern create*Intent().
I creator di intent devono utilizzare metodi denominati createFooIntent()
.
Utilizza Bundle anziché creare nuove strutture di dati generiche
Evita di creare nuove strutture di dati generiche per rappresentare mappature arbitrarie di chiavi a valori di tipo. In alternativa, considera l'utilizzo di Bundle
.
Questo problema si verifica in genere quando si scrivono API di piattaforma che fungono da canali di comunicazione tra app e servizi non di piattaforma, in cui la piattaforma non legge i dati inviati tramite il canale e il contratto dell'API può essere definito parzialmente al di fuori della piattaforma (ad esempio in una libreria Jetpack).
Nei casi in cui la piattaforma legga i dati, evita di utilizzare Bundle
e preferisci una classe di dati fortemente tipizzata.
Le implementazioni di Parcelable devono avere un campo CREATOR pubblico
L'inflazione parcellabile è esposta tramite CREATOR
, non tramite i costruttori non elaborati. Se una classe implementa Parcelable
, il relativo campo CREATOR
deve essere anche un'API pubblica e il costruttore della classe che accetta un argomento Parcel
deve essere privato.
Utilizza CharSequence per le stringhe dell'interfaccia utente
Quando una stringa viene presentata in un'interfaccia utente, utilizza CharSequence
per consentire le istanze Spannable
.
Se si tratta solo di una chiave o di un'altra etichetta o di un valore non visibile agli utenti,String
va bene.
Evita di utilizzare gli enum
IntDef
deve essere utilizzato al posto degli enum in tutte le API di piattaforma e deve essere fortemente preso in considerazione
nelle API di librerie scomposte. Utilizza gli enum solo quando hai la certezza che non verranno aggiunti nuovi valori.
Vantaggi diIntDef
:
- Consente di aggiungere valori nel tempo
- Le istruzioni
when
di Kotlin possono non riuscire in fase di esecuzione se non sono più esaustive a causa di un valore enum aggiunto nella piattaforma.
- Le istruzioni
- Nessuna classe o oggetto utilizzato in fase di esecuzione, solo elementi primitivi
- Sebbene R8 o la minimizzazione possano evitare questo costo per le API della libreria non raggruppate, questa ottimizzazione non può influire sulle classi API della piattaforma.
Vantaggi di Enum
- Funzionalità di linguaggio idiomatico di Java, Kotlin
- Consente l'utilizzo esaustivo di istruzioni
when
e switch.- Nota: i valori non devono cambiare nel tempo, consulta l'elenco precedente
- Nomi con ambito chiaro e rilevabili
- Consente la verifica in fase di compilazione
- Ad esempio, un'istruzione
when
in Kotlin che restituisce un valore
- Ad esempio, un'istruzione
- È una classe funzionante che può implementare interfacce, avere helper statici, esporre metodi di membri o di estensione ed esporre campi.
Segui la gerarchia del layering dei pacchetti Android
La gerarchia dei pacchetti android.*
ha un ordine implicito, in cui i pacchetti di livello inferiore non possono dipendere da quelli di livello superiore.
Evita di fare riferimento a Google, ad altre aziende e ai relativi prodotti
La piattaforma Android è un progetto open source e mira a essere indipendente dal fornitore. L'API deve essere generica e ugualmente utilizzabile da integratori di sistema o app con le autorizzazioni richieste.
Le implementazioni di Parcelable devono essere finali
Le classi Parcelable definite dalla piattaforma vengono sempre caricate da
framework.jar
, pertanto non è valido per un'app provare a eseguire l'override di un'implementazione di Parcelable
.
Se l'app di invio estende un Parcelable
, l'app di ricezione non avrà l'implementazione personalizzata del mittente da decomprimere. Nota sulla compatibilità con le versioni precedenti: se in passato la tua classe non era finale, ma non aveva un costruttore disponibile pubblicamente, puoi comunque contrassegnarla come final
.
I metodi che chiamano il processo di sistema devono lanciare nuovamente RemoteException come RuntimeException
RemoteException
viene in genere generato dall'AIDL interno e indica che il processo di sistema è stato interrotto o che l'app sta tentando di inviare troppi dati. In entrambi i casi, l'API pubblica deve essere lanciata di nuovo come RuntimeException
per impedire alle app di mantenere le decisioni relative alla sicurezza o alle norme.
Se sai che l'altro lato di una chiamata Binder
è il processo di sistema, questo codice boilerplate è la best practice:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Lanciano eccezioni specifiche per le modifiche all'API
Il comportamento delle API pubbliche potrebbe cambiare in base ai livelli API e causare arresti anomali dell'app (ad esempio per applicare nuovi criteri di sicurezza).
Quando l'API deve generare un'eccezione per una richiesta precedentemente valida, genera una nuova eccezione specifica anziché una generica. Ad esempio, ExportedFlagRequired
instead of SecurityException
(and ExportedFlagRequired
can extendSecurityException
).
In questo modo, gli sviluppatori di app e gli strumenti potranno rilevare le modifiche al comportamento delle API.
Implementa il costruttore di copia anziché clone
L'utilizzo del metodo Java clone()
è vivamente sconsigliato a causa della mancanza di contratti API forniti dalla classe Object
e delle difficoltà intrinseche nell'estensione delle classi che utilizzano clone()
. Utilizza invece un costruttore di copia che accetta un oggetto
dello stesso tipo.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
Le classi che si basano su un costruttore per la costruzione dovrebbero prendere in considerazione l'aggiunta di un costruttore di copia del costruttore per consentire le modifiche alla copia.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Utilizza ParcelFileDescriptor anziché FileDescriptor
L'oggetto java.io.FileDescriptor
ha una definizione scadente della proprietà, che può comportare bug oscuri di utilizzo dopo la chiusura. Le API devono invece restituire o accettare istanze ParcelFileDescriptor
. Il codice precedente può convertire tra PFD e
FD, se necessario, utilizzando
dup()
o
getFileDescriptor().
Evita di utilizzare valori numerici di dimensioni dispari
Evita di utilizzare direttamente i valori short
o byte
, perché spesso limitano la modalità in cui potresti far evolvere l'API in futuro.
Evita di utilizzare BitSet
java.util.BitSet
è ideale per l'implementazione, ma non per le API pubbliche. È immutabile, richiede un'allocazione per le chiamate di metodi ad alta frequenza e non fornisce un significato semantico per ciò che rappresenta ogni bit.
Per scenari ad alte prestazioni, utilizza un int
o un long
con @IntDef
. Per scenari con prestazioni ridotte, valuta la possibilità di utilizzare un Set<EnumType>
. Per i dati binari non elaborati, utilizza
byte[]
.
Preferisci android.net.Uri
android.net.Uri
è l'incapsulamento preferito per gli URI nelle API Android.
Evita java.net.URI
, perché è troppo rigido nell'analisi degli URL, e non utilizzare mai
java.net.URL
, perché la sua definizione di uguaglianza è gravemente compromessa.
Nascondere le annotazioni contrassegnate come @IntDef, @LongDef o @StringDef
Le annotazioni contrassegnate come @IntDef
, @LongDef
o @StringDef
indicano un insieme di costanti valide che possono essere passate a un'API. Tuttavia, quando vengono esportate come API, il compilatore inserisce in linea le costanti e rimangono solo i valori (ora inutili) nello stub dell'API dell'annotazione (per la piattaforma) o nel file JAR (per le librerie).
Di conseguenza, l'utilizzo di queste annotazioni deve essere contrassegnato con l'annotazione @hide
docs
nella piattaforma o con l'annotazione @RestrictTo.Scope.LIBRARY)
code nelle
biblioteche. Devono essere contrassegnati come @Retention(RetentionPolicy.SOURCE)
in entrambi i casi per impedire che vengano visualizzati negli stub o nei JAR dell'API.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
Quando vengono creati gli AAR dell'SDK e della libreria della piattaforma, uno strumento estrae le annotazioni e le raggruppa separatamente dalle sorgenti compilate. Android Studio legge questo formato aggregato e applica le definizioni di tipo.
Non aggiungere nuove chiavi del provider di impostazioni
Non esporre nuove chiavi da
Settings.Global
,
Settings.System
o
Settings.Secure
.
Aggiungi invece un'API Java getter e setter appropriata in una classe pertinente, che in genere è una classe "manager". Aggiungi un meccanismo di ascolto o una trasmissione per notificare ai clienti le modifiche, se necessario.
Le impostazioni SettingsProvider
presentano una serie di problemi rispetto ai metodi Getter/Setter:
- Nessuna sicurezza dei tipi.
- Nessun modo unificato per fornire un valore predefinito.
- Nessun modo corretto per personalizzare le autorizzazioni.
- Ad esempio, non è possibile proteggere l'impostazione con un'autorizzazione personalizzata.
- Nessun modo corretto per aggiungere correttamente la logica personalizzata.
- Ad esempio, non è possibile modificare il valore dell'impostazione A in base al valore dell'impostazione B.
Esempio:
Settings.Secure.LOCATION_MODE
esiste da molto tempo, ma il team di localizzazione lo ha ritirato per un'API Java corretta
LocationManager.isLocationEnabled()
e la trasmissione
MODE_CHANGED_ACTION
, che ha dato al team molta più flessibilità e la semantica delle
API è ora molto più chiara.
Non estendere Activity e AsyncTask
AsyncTask
è un dettaglio di implementazione. Esponi invece un ascoltatore o, in androidx, un'API ListenableFuture
.
È impossibile comporre sottoclassi di Activity
. L'estensione dell'attività per la tua funzionalità la rende incompatibile con altre funzionalità che richiedono agli utenti di fare lo stesso. Utilizza invece la composizione con strumenti come
LifecycleObserver.
Utilizza getUser() del contesto
Le classi associate a un Context
, ad esempio qualsiasi elemento restituito da
Context.getSystemService()
, devono utilizzare l'utente associato al Context
anziché
esporre membri che hanno come target utenti specifici.
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);
}
}
Eccezione: un metodo può accettare un argomento utente se accetta valori che non rappresentano un singolo utente, ad esempio UserHandle.ALL
.
Utilizza UserHandle anziché semplici int
È preferibile utilizzare UserHandle
per garantire la sicurezza del tipo ed evitare di confondere gli ID utente con gli uid.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
Se inevitabile, un int
che rappresenta un ID utente deve essere annotato con
@UserIdInt
.
Foobar getFoobarForUser(@UserIdInt int user);
Preferisci gli ascoltatori o i callback per trasmettere gli intent
Gli intent di trasmissione sono molto efficaci, ma hanno generato comportamenti emergenti che possono influire negativamente sulla salute del sistema, pertanto i nuovi intent di trasmissione devono essere aggiunti con giudizio.
Ecco alcuni problemi specifici che ci inducono a scoraggiare l'introduzione di nuovi intent di trasmissione:
Quando invii trasmissioni senza il flag
FLAG_RECEIVER_REGISTERED_ONLY
, vengono avviate forzatamente tutte le app che non sono già in esecuzione. Sebbene a volte possa essere un risultato voluto, può comportare l'avvio di decine di app, con un impatto negativo sulla salute del sistema. Ti consigliamo di utilizzare strategie alternative, comeJobScheduler
, per coordinare meglio le varie precondizioni.Quando invii le trasmissioni, hai poche possibilità di filtrare o modificare i contenuti pubblicati nelle app. Ciò rende difficile o impossibile rispondere a futuri problemi di privacy o apportare modifiche al comportamento in base all'SDK di destinazione dell'app di destinazione.
Poiché le code di trasmissione sono una risorsa condivisa, possono sovraccaricarsi e l'evento potrebbe non essere pubblicato in tempo. Abbiamo osservato diverse code di trasmissione con una latenza end-to-end di 10 minuti o più.
Per questi motivi, invitiamo a prendere in considerazione l'utilizzo di ascoltatori o callback o di altri elementi come JobScheduler
anziché intent di trasmissione per le nuove funzionalità.
Se gli intent di trasmissione rimangono ancora il design ideale, ecco alcune best practice da prendere in considerazione:
- Se possibile, utilizza
Intent.FLAG_RECEIVER_REGISTERED_ONLY
per limitare la trasmissione alle app già in esecuzione. Ad esempio,ACTION_SCREEN_ON
utilizza questo design per evitare di riattivare le app. - Se possibile, utilizza
Intent.setPackage()
oIntent.setComponent()
per scegliere come target della trasmissione un'app specifica di interesse. Ad esempio,ACTION_MEDIA_BUTTON
utilizza questo design per concentrarsi sulla gestione dell'app corrente tramite i controlli di riproduzione. - Se possibile, definisci la trasmissione come
<protected-broadcast>
per impedire alle app dannose di rubare l'identità del sistema operativo.
Intent nei servizi per sviluppatori legati al sistema
I servizi che devono essere estesi dallo sviluppatore e vincolati dal sistema, ad esempio i servizi astratti come NotificationListenerService
, possono rispondere a un'azione Intent
del sistema. Questi servizi devono soddisfare i seguenti criteri:
- Definisci una costante di stringa
SERVICE_INTERFACE
nella classe contenente il nome completo della classe del servizio. Questa costante deve essere annotata con@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
. - Documenta nel corso che uno sviluppatore deve aggiungere un
<intent-filter>
al suoAndroidManifest.xml
per ricevere gli intent dalla piattaforma. - Ti consigliamo vivamente di aggiungere un'autorizzazione a livello di sistema per impedire alle app malintenzionate di inviare
Intent
ai servizi per sviluppatori.
Interoperabilità Kotlin-Java
Per un elenco completo delle linee guida, consulta la guida all'interoperabilità Kotlin-Java ufficiale di Android. Alcune linee guida sono state copiate in questa guida per migliorare la visibilità.
Visibilità API
Alcune API Kotlin, come le suspend fun
, non sono destinate a essere utilizzate dagli sviluppatori Java. Tuttavia, non tentare di controllare la visibilità specifica del linguaggio utilizzando @JvmSynthetic
, in quanto ha effetti collaterali sulla modalità di presentazione dell'API nei debugger che rendono più difficile il debug.
Per indicazioni specifiche, consulta la guida all'interoperabilità Kotlin-Java o la guida all'asynchronicità.
Oggetti companion
Kotlin utilizza companion object
per esporre i membri statici. In alcuni casi, questi metodi verranno visualizzati da Java in una classe interna denominata Companion
anziché nella classe contenente. I corsi Companion
potrebbero essere visualizzati come vuoti nei file di testo dell'API. Questo è normale.
Per massimizzare la compatibilità con Java, annota i
campi non costanti
degli oggetti companion con @JvmField
e
le funzioni pubbliche
con @JvmStatic
per esporli direttamente nella classe contenente.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Evoluzione delle API della piattaforma Android
Questa sezione descrive le norme relative ai tipi di modifiche che puoi apportare alle API Android esistenti e come implementarle per massimizzare la compatibilità con le app e le basi di codice esistenti.
Modifiche che causano errori nei file binari
Evita modifiche che comportano la rottura dei binari nelle API pubbliche definitive. Questi tipi di modifiche in genere generano errori durante l'esecuzione di make update-api
, ma potrebbero esserci casi limite che il controllo dell'API di Metalava non rileva. In caso di dubbi, consulta la guida Evolving Java-based APIs della Eclipse Foundation per una spiegazione dettagliata dei tipi di modifiche dell'API compatibili in Java. Le modifiche che comportano la rottura del codice binario nelle API nascoste (ad esempio di sistema) devono seguire il ciclo di ritiro/sostituzione.
Modifiche che comportano la rottura dell'origine
Sconsigliamo le modifiche che causano interruzioni del codice sorgente, anche se non causano interruzioni del codice binario. Un
esempio di modifica compatibile con il codice binario, ma che comporta una rottura del codice sorgente, è l'aggiunta di un generico a
una classe esistente, che è
compatibile con il codice binario
ma può introdurre errori di compilazione a causa di ereditarietà o riferimenti ambigui.
Le modifiche che comportano la rottura del codice sorgente non generano errori durante l'esecuzione di make update-api
, quindi
devi assicurarti di comprendere l'impatto delle modifiche alle firme API esistenti.
In alcuni casi, sono necessarie modifiche che rompono il codice sorgente per migliorare l'esperienza degli sviluppatori o la correttezza del codice. Ad esempio, l'aggiunta di annotazioni di nullità ai codici sorgente Java migliora l'interoperabilità con il codice Kotlin e riduce la probabilità di errori, ma spesso richiede modifiche, a volte significative, al codice sorgente.
Modifiche alle API private
Puoi modificare le API annotate con @TestApi
in qualsiasi momento.
Devi conservare le API annotate con @SystemApi
per tre anni. Devi rimuovere o eseguire il refactoring di un'API di sistema secondo la seguente pianificazione:
- API y - Aggiunta
- API y+1 - ritiro
- Contrassegna il codice con
@Deprecated
. - Aggiungi le sostituzioni e il link alla sostituzione nel Javadoc per il codice ritirato utilizzando l'annotazione della documentazione
@deprecated
. - Durante il ciclo di sviluppo, segnala i bug agli utenti interni informandoli che l'API verrà ritirata. In questo modo puoi verificare che le API di sostituzione siano adeguate.
- Contrassegna il codice con
- API y+2 - Rimozione soft
- Contrassegna il codice con
@removed
. - Se vuoi, puoi lanciare un'eccezione o eseguire un'operazione no-op per le app che hanno come target il livello SDK corrente per la release.
- Contrassegna il codice con
- API y+3 - Rimozione forzata
- Rimuovi completamente il codice dall'albero di origine.
Ritiro
Consideriamo il ritiro una modifica dell'API e può verificarsi in una release principale (come la lettera). Utilizza insieme l'annotazione del codice sorgente @Deprecated
e l'annotazione della documentazione @deprecated
<summary>
quando ritiri le API. Il riepilogo deve includere una strategia di migrazione. Questa strategia potrebbe includere un link a un'API sostitutiva o spiegare perché non dovresti utilizzare 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)
Devi anche ritirare le API definite in XML ed esposte in Java, inclusi gli attributi e le proprietà stilizzabili esposte nella classe android.R
, con un riepilogo:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
Quando ritirare un'API
I ritiri sono particolarmente utili per scoraggiare l'adozione di un'API nel nuovo codice.
Inoltre, ti chiediamo di contrassegnare le API come @deprecated
prima che diventino
@removed
, ma questo non fornisce una motivazione forte per spingere gli sviluppatori a eseguire la migrazione da un'API che stanno già utilizzando.
Prima di ritirare un'API, valuta l'impatto sugli sviluppatori. Gli effetti del ritiro di un'API includono:
javac
emette un avviso durante la compilazione.- Gli avvisi di ritiro non possono essere ignorati a livello globale o di riferimento, pertanto
gli sviluppatori che utilizzano
-Werror
devono correggere o ignorare singolarmente ogni utilizzo di un'API ritirata prima di poter aggiornare la versione dell'SDK di compilazione. - Gli avvisi di ritiro relativi alle importazioni di classi ritirate non possono essere suppressed. Di conseguenza, prima di poter aggiornare la versione dell'SDK di compilazione, gli sviluppatori devono inserire in linea il nome della classe completamente qualificato per ogni utilizzo di una classe deprecata.
- Gli avvisi di ritiro non possono essere ignorati a livello globale o di riferimento, pertanto
gli sviluppatori che utilizzano
- La documentazione su
d.android.com
mostra un avviso di ritiro. - Gli IDE come Android Studio mostrano un avviso sul sito di utilizzo dell'API.
- Gli IDE potrebbero ridurre il ranking o nascondere l'API dal completamento automatico.
Di conseguenza, il ritiro di un'API può scoraggiare gli sviluppatori più attenti alla salute del codice (ovvero quelli che utilizzano -Werror
) dall'adottare nuovi SDK.
Gli sviluppatori che non sono preoccupati per gli avvisi nel codice esistente probabilmente ignoreranno del tutto i ritiri.
Un SDK che introduce un numero elevato di ritiri peggiora entrambi i casi.
Per questo motivo, ti consigliamo di ritirare le API solo nei casi in cui:
- Abbiamo in programma di
@remove
l'API in una release futura. - L'utilizzo dell'API comporta un comportamento errato o non definito che non possiamo correggere senza rompere la compatibilità.
Quando ritiri un'API e la sostituisci con una nuova, consigliamo vivamente di aggiungere un'API di compatibilità corrispondente a una libreria Jetpack come androidx.core
per semplificare il supporto sia dei vecchi che dei nuovi dispositivi.
Non consigliamo di ritirare le API che funzionano come previsto nelle release attuali e future:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
Il ritiro è appropriato nei casi in cui le API non possono più mantenere i loro comportamenti documentati:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Modifiche alle API obsolete
Devi mantenere il comportamento delle API ritirate. Ciò significa che le implementazioni dei test devono rimanere invariate e i test devono continuare a superare dopo aver ritirato l'API. Se l'API non ha test, devi aggiungerli.
Non espandere le API ritirate nelle release future. Puoi aggiungere annotazioni di correttezza lint (ad esempio @Nullable
) a un'API esistente ritirata, ma non dovresti aggiungere nuove API.
Non aggiungere nuove API come deprecate. Se alcune API sono state aggiunte e successivamente ritirate in un ciclo di pre-release (e quindi inizialmente entreranno nell'interfaccia API pubblica come ritirate), devi rimuoverle prima di finalizzare l'API.
Rimozione temporanea
La rimozione soft è una modifica che comporta la rottura del codice sorgente e dovresti evitarla nelle API pubbliche
a meno che il Consiglio dell'API non la approvi esplicitamente.
Per le API di sistema, devi ritirare l'API per la durata di una release principale prima di una rimozione soft. Rimuovi tutti i riferimenti della documentazione alle API e utilizza l'annotazione della documentazione @removed <summary>
quando rimuovi le API in modo soft. Il riepilogo deve includere il motivo della rimozione e può includere una strategia di migrazione, come spiegato in Ritiro.
Il comportamento delle API rimosse in modo soft può essere mantenuto invariato, ma soprattutto deve essere preservato in modo che i chiamanti esistenti non si arrestino in modo anomalo quando chiamano l'API. In alcuni casi, potrebbe essere necessario preservare il comportamento.
La copertura dei test deve essere mantenuta, ma i contenuti dei test potrebbero dover essere modificati per adattarsi ai cambiamenti comportamentali. I test devono comunque verificare che i chiamanti esistenti non si arrestino in modo anomalo in fase di esecuzione. Puoi mantenere invariato il comportamento delle API rimosse in modo soft, ma, soprattutto, devi preservarlo in modo che i chiamanti esistenti non si arrestino in modo anomalo quando chiamano l'API. In alcuni casi, questo potrebbe significare preservare il comportamento.
Devi mantenere la copertura dei test, ma i contenuti dei test potrebbero dover essere modificati per adattarsi ai cambiamenti comportamentali. I test devono comunque verificare che i chiamanti esistenti non si arrestino in modo anomalo in fase di esecuzione.
A livello tecnico, rimuoviamo l'API dal JAR stub dell'SDK e dal percorso di classe compilato utilizzando l'annotazione Javadoc @remove
, ma l'API esiste ancora nel percorso di classe di runtime, in modo simile alle API @hide
:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
Dal punto di vista di uno sviluppatore di app, l'API non viene più visualizzata nel completamento automatico
e il codice sorgente che fa riferimento all'API non verrà compilato quando compileSdk
è equale o successivo all'SDK in cui è stata rimossa l'API. Tuttavia, il codice
sorgente continua a compilarsi correttamente con gli SDK precedenti e i file binari che fanno riferimento all'API continuano a funzionare.
Alcune categorie di API non devono essere rimosse in modo soft. Non devi rimuovere alcune categorie di API.
Metodi astratti
Non devi rimuovere in modo soft i metodi astratti dalle classi che gli sviluppatori potrebbero estendere. In questo modo, è impossibile per gli sviluppatori estendere correttamente la classe a tutti i livelli dell'SDK.
Nei rari casi in cui non è mai stato e non sarà mai possibile per gli sviluppatori estendere un'aula, puoi comunque rimuovere in modo soft i metodi astratti.
Rimozione forzata
La rimozione forzata è una modifica che comporta la rottura del codice binario e non deve mai verificarsi nelle API pubbliche.
Annotazione sconsigliata
Utilizziamo l'annotazione @Discouraged
per indicare che non consigliamo un'API
nella maggior parte dei casi (>95%). Le API non consigliate sono diverse da quelle obsolete in quanto esiste un caso d'uso critico limitato che ne impedisce il ritiro. Quando contrassegni un'API come sconsigliata, devi fornire una spiegazione e una soluzione alternativa:
@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);
}
Non devi aggiungere nuove API come non consigliate.
Modifiche al comportamento delle API esistenti
In alcuni casi, potresti voler modificare il comportamento di implementazione di un'API esistente. Ad esempio, in Android 7.0 abbiamo migliorato DropBoxManager
per comunicare chiaramente quando gli sviluppatori hanno provato a pubblicare eventi troppo grandi per essere inviati tramite Binder
.
Tuttavia, per evitare di causare problemi alle app esistenti, ti consigliamo vivamente di mantenere un comportamento sicuro per le app meno recenti. In passato, abbiamo monitorato queste modifiche di comportamento in base al ApplicationInfo.targetSdkVersion
dell'app, ma di recente abbiamo eseguito la migrazione per richiedere l'utilizzo del framework di compatibilità delle app. Ecco un esempio di come implementare una modifica del comportamento utilizzando questo nuovo 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'utilizzo di questo design del framework di compatibilità delle app consente agli sviluppatori di disattivare temporaneamente modifiche specifiche del comportamento durante le release di anteprima e beta nell'ambito del debugging delle app, anziché costringerli ad adeguarsi contemporaneamente a dozzine di modifiche del comportamento.
Compatibilità con le versioni successive
La compatibilità con le versioni successive è una caratteristica di progettazione che consente a un sistema di accettare input destinati a una versione successiva di se stesso. Nel caso della progettazione dell'API, devi prestare particolare attenzione al design iniziale e alle modifiche future, perché gli sviluppatori si aspettano di scrivere il codice una volta, testarlo una volta e farlo funzionare ovunque senza problemi.
Di seguito sono riportate le cause più comuni dei problemi di compatibilità futura in Android:
- L'aggiunta di nuove costanti a un insieme (ad esempio
@IntDef
oenum
) precedentemente presumibilmente completo (ad esempio, seswitch
ha undefault
che genera un'eccezione). - Aggiunta del supporto per una funzionalità non acquisita direttamente nell'interfaccia API
(ad esempio, il supporto per l'assegnazione di risorse di tipo
ColorStateList
in XML dove in precedenza erano supportate solo le risorse<color>
). - Allentamento delle limitazioni relative ai controlli di runtime, ad esempio la rimozione di un controllo
requireNotNull()
presente nelle versioni precedenti.
In tutti questi casi, gli sviluppatori scoprono che c'è un problema solo in fase di esecuzione. Peggio ancora, potrebbero scoprirlo a seguito di segnalazioni di arresti anomali provenienti da dispositivi meno recenti sul campo.
Inoltre, si tratta di modifiche all'API tecnicamente valide. Non compromettono la compatibilità del codice sorgente o del codice binario e lo strumento di lint dell'API non rileva nessuno di questi problemi.
Di conseguenza, i progettisti di API devono prestare molta attenzione quando modificano le classi esistenti. Chiedi: "Questa modifica causerà l'errore del codice scritto e testato solo con l'ultima versione della piattaforma nelle versioni inferiori?"
Schemi XML
Se uno schema XML funge da interfaccia stabile tra i componenti, deve essere specificato esplicitamente e deve evolversi in modo compatibile con le versioni precedenti, come per altre API Android. Ad esempio, la struttura degli elementi e degli attributi XML deve essere preservata in modo simile a come vengono gestiti i metodi e le variabili su altre piattaforme API Android.
Ritiro del formato XML
Se vuoi ritirare un elemento o un attributo XML, puoi aggiungere l'indicatore xs:annotation
, ma devi continuare a supportare tutti i file XML esistenti seguendo il ciclo di vita di evoluzione @SystemApi
.
<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>
I tipi di elementi devono essere conservati
Gli schemi supportano gli elementi sequence
, choice
e all
come elementi secondari dell'elemento complexType
. Tuttavia, questi elementi secondari differiscono per il numero e l'ordine dei propri elementi secondari, pertanto la modifica di un tipo esistente costituirebbe una modifica incompatibile.
Se vuoi modificare un tipo esistente, la best practice è ritirare il tipo precedente e introdurne uno nuovo per sostituirlo.
<!-- 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>
Pattern specifici per la versione principale
Mainline è un progetto che consente di aggiornare singolarmente i sottosistemi ("moduli principali") del sistema operativo Android, anziché l'intera immagine di sistema.
I moduli principali devono essere "sganciati" dalla piattaforma di base, il che significa che tutte le interazioni tra ciascun modulo e il resto del mondo devono essere eseguite utilizzando API formali (pubbliche o di sistema).
I moduli principali devono seguire determinati pattern di progettazione. Questa sezione descrive queste opzioni.
Il pattern <Module>FrameworkInitializer
Se un modulo principale deve esporre classi @SystemService
(ad esempio
JobScheduler
), utilizza il seguente pattern:
Esponi una classe
<YourModule>FrameworkInitializer
dal tuo modulo. Questa classe deve essere in$BOOTCLASSPATH
. Esempio: StatsFrameworkInitializerContrassegnalalo con
@SystemApi(client = MODULE_LIBRARIES)
.Aggiungi un metodo
public static void registerServiceWrappers()
.Utilizza
SystemServiceRegistry.registerContextAwareService()
per registrare un classe di gestore dei servizi quando ha bisogno di un riferimento a unContext
.Utilizza
SystemServiceRegistry.registerStaticService()
per registrare una classe di gestori di servizi quando non è necessario un riferimento aContext
.Chiama il metodo
registerServiceWrappers()
dall'inizializzatore statico diSystemServiceRegistry
.
Il pattern <Module>ServiceManager
In genere, per registrare oggetti di binder dei servizi di sistema o ottenerne i riferimenti, si utilizza ServiceManager
, ma i moduli principali non possono utilizzarlo perché è nascosto. Questa classe è nascosta
perché i moduli principali non devono registrare o fare riferimento a oggetti di binder
del servizio di sistema esposti dalla piattaforma statica o da altri moduli.
I moduli principali possono utilizzare invece il seguente pattern per poter registrare e ottenere riferimenti ai servizi di binder implementati all'interno del modulo.
Crea una classe
<YourModule>ServiceManager
seguendo il design di TelephonyServiceManagerEsponi la classe come
@SystemApi
. Se devi accedere solo da$BOOTCLASSPATH
o da classi di server di sistema, puoi utilizzare@SystemApi(client = MODULE_LIBRARIES)
; in caso contrario,@SystemApi(client = PRIVILEGED_APPS)
andrà bene.Questo corso è composto da:
- Un costruttore nascosto, quindi solo il codice della piattaforma statica può istanziarlo.
- Metodi getter pubblici che restituiscono un'istanza
ServiceRegisterer
per un nome specifico. Se hai un oggetto binder, devi avere un metodo getter. Se ne hai due, hai bisogno di due getter. - In
ActivityThread.initializeMainlineModules()
, inizializza questa classe e passala a un metodo statico esposto dal modulo. In genere, aggiungi un'API@SystemApi(client = MODULE_LIBRARIES)
statica al tuoFrameworkInitializer
che la gestisce.
Questo pattern impedirebbe ad altri moduli principali di accedere a queste API
perché non c'è modo per gli altri moduli di ottenere un'istanza di
<YourModule>ServiceManager
, anche se le API get()
e register()
sono
visibili.
Ecco come la telefonia ottiene un riferimento al servizio di telefonia: link alla ricerca del codice.
Se implementi un oggetto di collegamento del servizio in codice nativo, utilizza
le API native AServiceManager
.
Queste API corrispondono alle API Java di ServiceManager
, ma quelle native sono esposte direttamente ai moduli principali. Non utilizzarli per registrare o fare riferimento a oggetti binder che non sono di proprietà del tuo modulo. Se esponi un oggetto binder
dall'app nativa, <YourModule>ServiceManager.ServiceRegisterer
non ha bisogno di un
metodo register()
.
Definizioni delle autorizzazioni nei moduli Mainline
I moduli principali contenenti APK possono definire autorizzazioni (personalizzate) nel proprio APKAndroidManifest.xml
nello stesso modo di un normale APK.
Se l'autorizzazione definita viene utilizzata solo internamente all'interno di un modulo, il suo nome deve essere preceduto dal nome del pacchetto APK, ad esempio:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
Se l'autorizzazione definita deve essere fornita come parte di un'API di piattaforma aggiornabile ad altre app, il nome dell'autorizzazione deve avere il prefisso "android.permission". (come qualsiasi autorizzazione di piattaforma statica) più il nome del pacchetto del modulo, per indicare che si tratta di un'API di piattaforma di un modulo evitando conflitti di denominazione, ad esempio:
<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" />
Il modulo può quindi esporre questo nome di autorizzazione come costante dell'API nella sua API
superficie, ad esempio
HealthPermissions.READ_ACTIVE_CALORIES_BURNED
.