Questa pagina ha lo scopo di fornire agli sviluppatori una guida per comprendere i principi generali che l'API Council applica 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 che esegue sulle API.
Consideralo come la guida alle regole rispettate da questo strumento Lint, oltre a consigli generali sulle regole che non possono essere codificate nello 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 checkout della piattaforma locale utilizzando m
checkapi
o da un checkout 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 stabilite più avanti 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 offrire una migliore esperienza utente agli sviluppatori di app se una nuova API rimane coerente con le API esistenti anziché rispettare rigorosamente le linee guida.
Utilizza il tuo giudizio e contatta l'API Council se ci sono 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 principali di un'API Android.
Tutte le API devono essere implementate
Indipendentemente dal pubblico di un'API (ad esempio, pubblico o @SystemApi
), tutte le superfici API devono essere implementate quando vengono unite o esposte come API. Non unire gli stub API
con l'implementazione in un secondo momento.
Le superfici 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 anteprime per sviluppatori.
- 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, le norme AndroidX e, in generale, l'idea che le API devono essere implementate.
I test delle superfici API forniscono una garanzia di base che la superficie API sia utilizzabile e che abbiamo affrontato i casi d'uso previsti. Il test dell'esistenza non è sufficiente, deve essere testato il comportamento dell'API stessa.
Una modifica che aggiunge una nuova API deve includere i test corrispondenti nella stessa CL o nello stesso argomento Gerrit.
Le API devono anche essere testabili. Dovresti essere in grado di rispondere alla domanda: "Come testerà il codice che utilizza la tua API uno sviluppatore di app?"
Tutte le API devono essere documentate
La documentazione è una parte fondamentale dell'usabilità dell'API. Sebbene la sintassi di una superficie 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é builder 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 essere scritti in maiuscolo nei nomi dei metodi
Ad esempio, il nome del metodo deve essere runCtsTests
e non runCTSTests
.
I nomi non devono terminare con Impl
In questo modo vengono esposti i dettagli dell'implementazione, quindi evita di farlo.
Classi
Questa sezione descrive le regole relative a classi, interfacce ed ereditarietà.
Ereditare nuove classi pubbliche dalla classe base appropriata
L'ereditarietà espone elementi API nella sottoclasse che potrebbero non essere appropriati.
Ad esempio, una nuova sottoclasse pubblica di FrameLayout
ha l'aspetto di FrameLayout
più i nuovi comportamenti e gli elementi API. Se l'API ereditata non è appropriata
per il tuo caso d'uso, esegui l'ereditarietà da una classe più in alto nell'albero, ad esempio,
ViewGroup
o View
.
Se sei tentato di eseguire l'override dei metodi della classe base per generare
UnsupportedOperationException
, riconsidera la classe base che stai utilizzando.
Utilizzare le classi di raccolte di base
Quando prendi una raccolta come argomento o la restituisci come valore, preferisci sempre
la classe base all'implementazione specifica (ad esempio, restituisci
List<Foo>
anziché ArrayList<Foo>
).
Utilizza una classe base che esprima vincoli appropriati per l'API. Ad esempio, utilizza List
per un'API la cui raccolta deve essere ordinata e utilizza Set
per un'API la cui raccolta deve essere costituita da elementi univoci.
In Kotlin, preferisci le raccolte immutabili. Per ulteriori dettagli, consulta la sezione Modificabilità della raccolta.
Classi astratte e interfacce
Java 8 aggiunge il supporto per i metodi di interfaccia predefiniti, che consentono 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 è senza stato, i progettisti di API devono preferire le interfacce alle 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 è 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 semplificare 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
chiarezza:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
Suffissi generici
Evita di utilizzare suffissi generici per i nomi delle 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.
Nei casi in cui i metodi collegano più classi, assegna alla classe contenitore un nome significativo che ne spieghi 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 nuove classi
- Potrebbe essere necessario uno stato persistente
- In genere, prevede
View
Ad esempio, se le descrizioni comando di backporting richiedono di rendere persistente lo stato associato
a un View
e di chiamare diversi metodi su View
per installare il backporting,
TooltipHelper
sarebbe un nome di classe accettabile.
Non esporre direttamente il codice generato da IDL come API pubbliche
Conserva il codice generato da IDL come dettagli di implementazione. Sono inclusi protobuf, socket, FlatBuffers o qualsiasi altra superficie API non Java e non NDK. Tuttavia, la maggior parte dell'IDL in Android è in AIDL, quindi questa pagina si concentra su AIDL.
Le classi AIDL generate non soddisfano i requisiti della guida di stile delle API (ad esempio, non possono utilizzare l'overload) e lo strumento AIDL non è progettato esplicitamente per mantenere la compatibilità delle API di linguaggio, pertanto non puoi incorporarle in un'API pubblica.
Aggiungi invece un livello API pubblico sopra l'interfaccia AIDL, anche se inizialmente è un wrapper superficiale.
Interfacce Binder
Se l'interfaccia Binder
è un dettaglio di implementazione, può essere modificata liberamente
in futuro, mentre il livello pubblico consente di mantenere la compatibilità
con le versioni precedenti richiesta. 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 la tua 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, racchiudi l'interfaccia Binder
all'interno di un gestore o di 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, l'interfaccia interna può essere minimale e comode sovraccariche aggiunte all'API pubblica. 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 controllo delle versioni significa che è
molto più difficile far evolvere l'interfaccia stessa. Tuttavia, vale comunque la pena
avere un livello wrapper intorno, per rispettare le altre linee guida dell'API e per semplificare
l'utilizzo della stessa API pubblica per una nuova versione dell'interfaccia IPC, se
dovesse mai diventare necessario.
Non utilizzare oggetti Binder non elaborati nell'API pubblica
Un oggetto Binder
non ha alcun significato di per sé 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,
utilizza 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() {...}
}
Le classi di gestione devono essere finali
Le classi di gestione devono essere dichiarate come final
. Le classi di gestione comunicano con i servizi di sistema e sono l'unico punto di interazione. Non è necessaria alcuna personalizzazione, quindi dichiaralo come final
.
Non utilizzare CompletableFuture o Future
java.util.concurrent.CompletableFuture
ha un'ampia superficie API che consente
la mutazione arbitraria del valore del futuro e ha valori predefiniti soggetti a errori
.
Al contrario, java.util.concurrent.Future
non dispone dell'ascolto non bloccante,
il che rende difficile l'utilizzo con codice asincrono.
Nel codice della piattaforma e nelle API delle 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 hai 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 presentare vantaggi in alcune superfici API, non è coerente
con l'area della superficie API Android esistente. @Nullable
e @NonNull
forniscono
assistenza per gli strumenti per la sicurezza di null
e Kotlin applica i contratti di nullabilità
a livello di compilatore, rendendo Optional
non necessario.
Per i primitivi facoltativi, utilizza i metodi accoppiati has
e get
. Se il valore non è
impostato (has
restituisce false
), il metodo get
deve generare un'eccezione
IllegalStateException
.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
Utilizzare costruttori privati per classi non istanziabili
Le classi che possono essere create solo da Builder
, le classi contenenti solo
costanti o metodi statici o classi non istanziabili devono
includere almeno un costruttore privato per impedire l'istanziamento utilizzando il
costruttore predefinito senza argomenti.
public final class Log {
// Not instantiable.
private Log() {}
}
Singleton
I singleton sono sconsigliati perché presentano i seguenti svantaggi relativi 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 intorno a esso.
Preferisci il pattern singola istanza, che si basa su una classe base astratta per risolvere questi problemi.
Singola istanza
Le classi a singola istanza utilizzano una classe base astratta con un costruttore private
o
internal
e forniscono un metodo statico getInstance()
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 differisce dal singleton in quanto gli sviluppatori
possono creare una versione fittizia di SingleInstance
e utilizzare il proprio framework di
inserimento delle dipendenze per gestire l'implementazione senza dover creare un
wrapper oppure la libreria può fornire il proprio fake in un artefatto -testing
.
Le classi che rilasciano risorse devono implementare AutoCloseable
Le classi che rilasciano risorse tramite close
, release
, destroy
o metodi simili
devono implementare java.lang.AutoCloseable
per consentire agli sviluppatori di
pulire automaticamente queste risorse quando utilizzano un blocco try-with-resources
.
Evita di introdurre nuove 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 la UI di Android ora è Compose-first. 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 UI basati su View per gli sviluppatori nelle librerie Jetpack. L'offerta di questi componenti nelle librerie offre opportunità per implementazioni di backporting quando le funzionalità della piattaforma non sono disponibili.
Campi
Queste regole riguardano i campi pubblici delle classi.
Non esporre i campi non elaborati
Le classi Java non devono esporre direttamente i campi. I campi devono essere privati e accessibili solo tramite getter e setter pubblici, indipendentemente dal fatto che questi campi siano finali o meno.
Rari casi includono strutture di dati di base in cui non è necessario migliorare
il comportamento di specifica o recupero di un campo. In questi casi, i campi devono
essere denominati utilizzando le convenzioni standard di denominazione delle variabili, ad esempio Point.x
e
Point.y
.
Le classi Kotlin possono esporre proprietà.
I campi esposti devono essere contrassegnati come finali
I campi non elaborati sono fortemente sconsigliati (@see
Non esporre campi non elaborati). Tuttavia, nella rara situazione in cui un
campo viene esposto come campo pubblico, contrassegnalo con final
.
I campi interni non devono essere esposti
Non fare riferimento ai nomi dei campi interni nell'API pubblica.
public int mFlags;
Utilizzare la modalità pubblica anziché quella protetta
@see Use public instead of protected
Costanti
Queste sono regole relative alle costanti pubbliche.
Le costanti flag non devono sovrapporsi ai valori int o long
Flag indica i bit che possono essere combinati in un valore di 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 maggiori informazioni sulla definizione delle costanti dei flag pubblici, consulta @IntDef
per i flag bitmask.
Le costanti final static devono utilizzare la convenzione di denominazione con tutte le lettere maiuscole e separate da trattini bassi
Tutte le parole nella costante devono essere scritte in maiuscolo e le parole multiple devono essere
separate da _
. Ad esempio:
public static final int fooThing = 5
public static final int FOO_THING = 5
Utilizzare i prefissi standard per le costanti
Molte delle costanti utilizzate in Android riguardano elementi standard, come flag, chiavi e azioni. Queste costanti devono avere prefissi standard per renderle più identificabili come tali.
Ad esempio, gli extra intent devono iniziare con EXTRA_
. Le azioni di intent devono
iniziare con ACTION_
. Le costanti utilizzate con Context.bindService()
devono iniziare
con BIND_
.
Nomi e ambiti delle costanti chiave
I valori delle costanti stringa devono essere coerenti con il nome della costante stessa e in genere devono essere limitati al pacchetto o al dominio. Ad esempio:
public static final String FOO_THING = "foo"
non è denominato in modo coerente né ha un ambito appropriato. In alternativa, valuta:
public static final String FOO_THING = "android.fooservice.FOO_THING"
I prefissi di android
nelle costanti stringa con ambito sono riservati al progetto
open source Android.
Le azioni e gli extra degli intent, nonché le voci del bundle, devono essere namespace 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"
}
Utilizzare la modalità pubblica anziché quella protetta
@see Use public instead of protected
Utilizzare prefissi coerenti
Tutte le costanti correlate devono iniziare 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;
@see Utilizzare i prefissi standard per le costanti
Utilizzare nomi delle 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 identificatore o un attributo pubblico include un prefisso comune separato da un trattino basso:
- Valori di configurazione della piattaforma come
@string/config_recentsComponentName
in config.xml - Attributi di visualizzazione specifici del layout, ad esempio
@attr/layout_marginStart
in attrs.xml
I temi e gli stili pubblici devono seguire la convenzione di denominazione PascalCase gerarchica, 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 disegnabili non devono essere esposte come API pubbliche. Se devono essere esposti, tuttavia, i layout e le risorse disegnabili pubblici devono essere denominati utilizzando la convenzione di denominazione under_score, ad esempio layout/simple_list_item_1.xml
o drawable/title_bar_tall.xml
.
Quando le costanti potrebbero cambiare, rendile dinamiche
Il compilatore potrebbe incorporare i valori costanti, quindi mantenere gli stessi valori è
considerato parte del contratto API. Se il valore di una costante MIN_FOO
o MAX_FOO
potrebbe cambiare in futuro, valuta la possibilità di renderli metodi dinamici.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
Considera la compatibilità futura per i callback
Le costanti definite nelle versioni future dell'API non sono note alle app che hanno come target API precedenti. Per questo motivo, le costanti fornite alle app devono tenere conto della versione API target dell'app e mappare le costanti più recenti a un valore coerente. Considera il seguente scenario:
Origine 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 entro i limiti del livello API 22 e
ha fatto un'ipotesi (in qualche modo) ragionevole secondo cui esistevano solo due possibili
stati. Se l'app riceve il nuovo STATUS_FAILURE_RETRY
, lo interpreta come esito positivo.
I metodi che restituiscono costanti possono gestire in modo sicuro casi come questo limitando il loro output in modo che corrisponda al livello API di destinazione dell'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 un catch-all, gli sviluppatori presumono che le costanti pubblicate quando
hanno scritto la loro app siano esaustive. Se non vuoi impostare questa aspettativa,
valuta se una costante generica è una buona idea per la tua API.
Inoltre, le librerie non possono specificare il proprio targetSdkVersion
separato dall'app e la gestione delle modifiche al comportamento di targetSdkVersion
dal codice della libreria è complicata e soggetta a errori.
Costante intera o stringa
Utilizza costanti intere e @IntDef
se lo spazio dei nomi per i valori non è
estensibile al di fuori del pacchetto. Utilizza costanti stringa se lo spazio dei nomi è
condiviso o può essere esteso da codice al di fuori del 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à binaria o dell'API del linguaggio per il codice generato. In alternativa,
implementa 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 ci sono molte proprietà.
In Kotlin, le classi di dati devono fornire un costruttore con argomenti predefiniti indipendentemente dal numero di proprietà. Le classi di dati definite in Kotlin potrebbero trarre vantaggio anche dalla fornitura di un builder quando si utilizzano client Java.
Modifica e copia
Nei casi in cui 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
che corrisponde all'implementazione
della classe di dati di Kotlin, ad esempio User(var1=Alex, var2=42)
.
Metodi
Si tratta di regole relative a varie specifiche nei metodi, relative a parametri, nomi dei metodi, tipi restituiti e specificatori di accesso.
Tempo
Queste regole riguardano il modo in cui i concetti di tempo come date e durata devono essere espressi nelle API.
Preferisci i tipi java.time.*, se possibile
java.time.Duration
, java.time.Instant
e molti altri tipi di java.time.*
sono
disponibili in tutte le versioni della piattaforma tramite
desugaring e
devono essere preferiti quando si esprime l'ora nei parametri API o nei valori restituiti.
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 API non sia uno in cui l'allocazione di oggetti nei pattern di utilizzo previsti avrebbe un impatto negativo sulle prestazioni.
I metodi che esprimono durate devono essere denominati duration
Se un valore temporale esprime la durata del tempo coinvolto, 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 il 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 una primitiva devono essere denominati con la loro unità di tempo e utilizzare long
I metodi che accettano o restituiscono durate come primitive devono avere come suffisso del 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 anche essere annotati in modo appropriato con l'unità e la base temporale:
@CurrentTimeMillisLong
: il valore è un timestamp non negativo misurato come il numero di millisecondi dal 1970-01-01T00:00:00Z.@CurrentTimeSecondsLong
: il valore è un timestamp non negativo misurato come il numero di secondi trascorsi dal giorno 01/01/1970T00:00:00Z.@DurationMillisLong
: il valore è una durata non negativa in millisecondi.@ElapsedRealtimeLong
: il valore è un timestamp non negativo nella base temporaleSystemClock.elapsedRealtime()
.@UptimeMillisLong
: il valore è un timestamp non negativo nella base temporaleSystemClock.uptimeMillis()
.
I parametri temporali 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 la notazione abbreviata non abbreviata 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 argomenti di lungo periodo
La piattaforma include diverse annotazioni per fornire una digitazione più efficace per le unità di tempo di tipo long
:
@CurrentTimeMillisLong
: il valore è un timestamp non negativo misurato come numero di millisecondi a partire da1970-01-01T00:00:00Z
, quindi nella base temporaleSystem.currentTimeMillis()
.@CurrentTimeSecondsLong
: il valore è un timestamp non negativo misurato come il numero di secondi trascorsi dal1970-01-01T00:00:00Z
.@DurationMillisLong
: il valore è una durata non negativa in millisecondi.@ElapsedRealtimeLong
: il valore è un timestamp non negativo nella base temporaleSystemClock#elapsedRealtime()
.@UptimeMillisLong
: il valore è un timestamp non negativo nella base temporaleSystemClock#uptimeMillis()
.
Unità di misura
Per tutti i metodi che esprimono un'unità di misura diversa dal tempo, preferisci i prefissi delle unità SI in formato CamelCase.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
Inserisci i parametri facoltativi alla fine degli overload
Se hai overload di un metodo con parametri facoltativi, mantieni questi parametri alla fine e mantieni un 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 overload per gli argomenti facoltativi, il comportamento dei metodi più semplici deve essere esattamente lo stesso di quello dei metodi più elaborati a cui sono stati forniti argomenti predefiniti.
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 sovraccarico 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 saperne di più, consulta la sezione Function overloads for defaults 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 dei parametri predefiniti (solo Kotlin)
Se un metodo è stato spedito con un parametro con un valore predefinito, la rimozione del valore predefinito è una modifica che causa interruzioni della compatibilità con le origini.
I parametri del metodo più distintivi e identificativi devono essere i primi
Se hai un metodo con più parametri, inserisci prima quelli più pertinenti. I parametri che specificano i flag e altre opzioni sono meno importanti di quelli che descrivono l'oggetto su cui viene eseguita l'azione. 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: Inserire i parametri facoltativi alla fine degli overload
Costruttori
Il pattern Builder è consigliato per la creazione di oggetti Java complessi ed è comunemente utilizzato in Android nei casi in cui:
- Le proprietà dell'oggetto risultante devono essere immutabili
- Esiste un numero elevato di proprietà obbligatorie, ad esempio molti argomenti del costruttore
- Esiste una relazione complessa tra le proprietà al momento della creazione, 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 builder. I builder sono utili in una superficie API se vengono utilizzati per:
- Configura solo alcuni di un insieme potenzialmente ampio di parametri di creazione facoltativi
- Configura molti parametri di creazione facoltativi o obbligatori diversi, a volte di tipi simili o corrispondenti, in cui i siti di chiamata potrebbero altrimenti diventare confusi da leggere o soggetti a errori di scrittura
- Configura la creazione di un oggetto in modo incrementale, in cui diversi pezzi di codice di configurazione potrebbero effettuare chiamate al builder come dettagli di implementazione
- Consenti a un tipo di crescere aggiungendo parametri di creazione facoltativi aggiuntivi nelle versioni future dell'API
Se hai un tipo con tre o meno parametri obbligatori e nessun parametro facoltativo, puoi quasi sempre saltare un builder e utilizzare un costruttore semplice.
Le classi di origine Kotlin devono preferire i costruttori annotati con @JvmOverloads
con argomenti predefiniti rispetto ai builder, ma possono 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 Builder devono restituire il builder
Le classi Builder devono abilitare l'incatenamento dei metodi restituendo l'oggetto Builder
(ad esempio this
) da ogni metodo, tranne build()
. Gli oggetti integrati aggiuntivi
devono essere passati come argomenti. Non restituire il builder 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 base builder deve supportare l'estensione, utilizza un tipo 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 Builder devono essere create tramite un costruttore
Per mantenere una creazione coerente dei builder tramite la superficie API Android, tutti i
builder devono essere creati tramite un costruttore e non un metodo di creazione
statico. Per le API basate su Kotlin, Builder
deve essere pubblico anche se gli utenti Kotlin
devono fare affidamento implicitamente sul builder tramite un meccanismo di creazione in stile DSL/metodo factory. Le librerie non devono utilizzare @PublishedApi internal
per
nascondere selettivamente il costruttore della classe Builder
dai 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 di builder devono essere obbligatori (ad esempio @NonNull)
Gli argomenti facoltativi, ad esempio @Nullable
, devono essere spostati nei metodi setter.
Il costruttore deve generare un'eccezione NullPointerException
(valuta la possibilità di utilizzare
Objects.requireNonNull
) se non vengono specificati argomenti obbligatori.
Le classi Builder devono essere classi interne statiche finali dei tipi creati
Per una corretta organizzazione all'interno di un pacchetto, le classi builder devono
in genere essere esposte come classi interne finali dei tipi creati, ad esempio
Tone.Builder
anziché ToneBuilder
.
I builder possono includere un costruttore per creare una nuova istanza da una esistente
I builder possono includere un costruttore di copia per creare una nuova istanza del builder da un builder o un oggetto creato esistente. Non devono fornire metodi alternativi per creare istanze del builder da builder o oggetti di build 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 builder da un'istanza esistente. Se non è disponibile alcun costruttore di copia, il builder potrebbe
avere argomenti @Nullable
o @NonNullable
.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
I setter del builder possono accettare argomenti @Nullable per le proprietà facoltative
Spesso è più semplice utilizzare un valore Nullable per l'input di secondo grado, soprattutto in Kotlin, che utilizza argomenti predefiniti anziché builder e overload.
Inoltre, i setter @Nullable
corrisponderanno ai 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 setter non viene chiamato) e il significato di null
devono
essere documentati correttamente sia nel setter che nel getter.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
I setter del builder possono essere forniti per le proprietà modificabili in cui sono disponibili setter nella classe integrata
Se la tua classe ha proprietà modificabili e richiede una classe Builder
, chiediti
innanzitutto se la tua classe deve effettivamente avere proprietà modificabili.
Successivamente, se hai la certezza di aver bisogno di proprietà modificabili, decidi quale dei seguenti scenari è più adatto al tuo caso d'uso previsto:
L'oggetto creato deve essere immediatamente utilizzabile, pertanto i setter devono essere forniti per tutte le proprietà pertinenti, modificabili o immutabili.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
Potrebbe essere necessario effettuare alcune chiamate aggiuntive prima che l'oggetto creato possa essere utile, pertanto i setter non devono essere forniti per le proprietà modificabili.
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 mescolare 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 builder non devono avere getter
Il getter deve trovarsi nell'oggetto creato, non nel builder.
I setter del builder devono avere getter corrispondenti nella classe creata
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 del metodo builder
I nomi dei metodi del builder devono utilizzare lo stile setFoo()
, addFoo()
o clearFoo()
.
Le classi Builder devono dichiarare un metodo build()
Le classi 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 builder deve restituire un'istanza non nulla dell'oggetto costruito. Nel caso in cui l'oggetto non possa 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 i blocchi interni
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 per scopi di blocco.
Esegui invece qualsiasi blocco richiesto su un oggetto interno privato.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
I metodi in stile Accessor devono seguire le linee guida per le proprietà Kotlin
Se visualizzati dalle origini Kotlin, i metodi in stile accessore, 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 funzione di accesso quando:
- Il metodo ha effetti collaterali. Preferisci un nome di metodo più descrittivo.
- Il metodo prevede un lavoro computazionalmente costoso. Preferisci
compute
- Il metodo prevede il blocco o un altro lavoro a lunga esecuzione per restituire un
valore, ad esempio IPC o altre operazioni di I/O. Preferisci
fetch
- Il metodo blocca il thread finché non può restituire un valore. Preferisci
await
- Il metodo restituisce una nuova istanza dell'oggetto a ogni chiamata. Preferisci
create
- Il metodo potrebbe non restituire correttamente un valore. Preferisci
request
Tieni presente che eseguire un'operazione costosa dal punto di vista computazionale una sola volta e memorizzare nella cache il valore per le chiamate successive viene comunque conteggiato come esecuzione di un'operazione costosa dal punto di vista computazionale. Il jank non viene ammortizzato tra i frame.
Utilizza il prefisso is per i metodi di accesso booleani
Si tratta della convenzione di denominazione standard per i metodi e i campi booleani in Java. In genere, i nomi di metodi e variabili booleani devono essere scritti come domande a cui risponde il 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
Le proprietà e i metodi di accesso devono in genere utilizzare una denominazione positiva, 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 descrive l'inclusione o la proprietà di una proprietà, puoi utilizzare has anziché is; tuttavia, questo non funziona con la sintassi delle 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 includono 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 risponde il valore restituito.
Metodi delle 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
getter e anteponi set
e metti in maiuscolo il primo carattere per il setter. La dichiarazione
della proprietà produrrà metodi denominati public Foo getFoo()
e
public void setFoo(Foo foo)
, rispettivamente.
Se la proprietà è di tipo Boolean
, si applica una regola aggiuntiva nella generazione del nome: se il nome della proprietà inizia con is
, get
non viene anteposto per il nome del metodo getter, ma viene utilizzato il nome della proprietà stesso.
Pertanto, preferisci denominare le proprietà Boolean
con un prefisso is
per
rispettare le linee guida per la denominazione:
var isVisible: Boolean
Se la tua proprietà rientra tra le eccezioni sopra menzionate e inizia con un
prefisso appropriato, utilizza l'annotazione @get:JvmName
sulla 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
Funzioni di accesso alle maschere di bit
Consulta Utilizzare @IntDef
per i flag bitmask per le linee guida
dell'API relative alla definizione dei flag bitmask.
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);
Getters
Per ottenere la maschera di bit completa, deve essere fornito un getter.
/**
* 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();
Utilizzare la modalità pubblica anziché quella protetta
Preferisci sempre public
a protected
nell'API pubblica. L'accesso protetto alla fine
risulta doloroso a lungo termine, perché gli implementatori devono eseguire l'override per fornire
funzioni di accesso pubbliche 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 leggermente più fastidiosa.
Implementa nessuno o entrambi i metodi equals() e hashCode()
Se ne esegui l'override di uno, devi eseguire l'override anche dell'altro.
Implementa toString() per le classi di dati
È consigliabile che le classi di dati eseguano l'override di toString()
per aiutare gli sviluppatori a eseguire il debug
del codice.
Documenta se l'output è per il comportamento del programma o per 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 formato specifico da utilizzare per i programmi. Se esponi informazioni solo per il debug, ad esempio Intent, allora implica l'ereditarietà dei documenti dalla superclasse.
Non includere informazioni aggiuntive
Tutte le informazioni disponibili da toString()
devono essere disponibili anche tramite
l'API pubblica dell'oggetto. In caso contrario, incoraggi gli sviluppatori ad analizzare
e fare affidamento all'output toString()
, il che impedirà modifiche future. Una buona
prassi è implementare toString()
utilizzando solo l'API pubblica dell'oggetto.
Scoraggiare l'affidamento all'output di debug
Sebbene sia impossibile impedire agli sviluppatori di fare affidamento all'output di debug,
inclusa la System.identityHashCode
del tuo oggetto nel suo output toString()
,
sarà 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 dallo scrivere asserzioni di test come
assertThat(a.toString()).isEqualTo(b.toString())
sui tuoi oggetti.
Utilizzare createFoo quando vengono restituiti oggetti appena creati
Utilizza il prefisso create
, non get
o new
, per i metodi che creeranno valori di ritorno, ad esempio costruendo 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 gli stream
Le posizioni di archiviazione dei dati su Android non sono sempre file su disco. Ad esempio, i contenuti trasferiti tra i confini degli utenti sono rappresentati come content://
Uri
. Per
attivare 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 in scatola
Se devi comunicare valori mancanti o nulli, 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 l'overhead di memoria di queste classi, l'accesso ai valori dei metodi e, soprattutto, l'autoboxing che deriva dal casting tra tipi primitivi e di oggetti. Evitare questi comportamenti consente di risparmiare memoria e allocazioni temporanee che possono portare a costose e più frequenti operazioni di Garbage Collection.
Utilizza le annotazioni per chiarire i valori validi di parametri e restituiti
Sono state aggiunte annotazioni per gli sviluppatori per chiarire i valori consentiti in varie
situazioni. In questo modo, gli strumenti possono aiutare più facilmente gli sviluppatori quando forniscono
valori errati (ad esempio, quando trasmettono un int
arbitrario quando il framework
richiede uno di un insieme specifico di valori costanti). Utilizza una o tutte le
seguenti annotazioni, se appropriate:
Supporto di valori Null
Le annotazioni di nullabilità esplicite sono richieste per le API Java, ma il concetto di nullabilità fa parte del linguaggio Kotlin e le annotazioni di nullabilità non devono mai essere utilizzate nelle API Kotlin.
@Nullable
: indica che un determinato valore restituito, parametro o campo può essere
nullo:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: indica che un determinato valore restituito, parametro o campo non può
essere null. La classificazione degli elementi come @Nullable
è una funzionalità relativamente nuova di Android, pertanto la maggior parte dei metodi API di Android non è documentata in modo coerente. Pertanto, abbiamo uno
stato a tre vie "sconosciuto, @Nullable
, @NonNull
", motivo per cui @NonNull
fa parte
delle linee guida per le 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 nella forma "Questo valore può essere nullo", a meno che "null" non venga utilizzato esplicitamente altrove nella documentazione dei parametri.
Metodi "non realmente annullabili" esistenti: i metodi esistenti nell'API senza
un'annotazione @Nullable
dichiarata possono essere annotati @Nullable
se il metodo può
restituire null
in circostanze specifiche e ovvie (ad esempio findViewById()
).
Per gli sviluppatori che non vogliono eseguire il controllo di nullità, devono essere aggiunti metodi @NotNull requireFoo()
complementari che generano IllegalArgumentException
.
Metodi di interfaccia:le nuove API devono aggiungere l'annotazione corretta durante l'implementazione dei metodi di interfaccia, ad esempio Parcelable.writeToParcel()
(ovvero, il metodo nella classe di implementazione deve essere writeToParcel(@NonNull Parcel,
int)
, non writeToParcel(Parcel, int)
); le API esistenti che non hanno le annotazioni non devono essere "corrette".
Applicazione della nullabilità
In Java, i metodi sono consigliati per eseguire la convalida dell'input per i parametri @NonNull
utilizzando Objects.requireNonNull()
e generare un NullPointerException
quando i parametri sono nulli. Questa operazione viene
eseguita automaticamente in Kotlin.
Risorse
Identificatori di 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 all'annotazione generica @AnyRes
. Per
esempio:
public void setTitle(@StringRes int resId)
@IntDef per i set di costanti
Costanti magiche:i parametri String
e int
che devono 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 bitmask
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 set di costanti stringa
Esiste anche l'annotazione @StringDef
, che è esattamente come @IntDef
nella
sezione precedente, ma per le costanti String
. Puoi includere più valori "prefix" che vengono utilizzati per generare automaticamente la documentazione per tutti i valori.
@SdkConstant per le costanti dell'SDK
@SdkConstant Annotate i 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 compatibilità con i valori null per gli override
Per la compatibilità delle API, l'annullabilità degli override deve essere compatibile con l'annullabilità attuale dell'elemento principale. La tabella seguente mostra le aspettative di compatibilità. In altre parole, gli override devono essere restrittivi o più restrittivi dell'elemento che sostituiscono.
Tipo | Genitore | Figlio |
---|---|---|
Tipo restituito | Senza annotazioni | Non annotato o non nullo |
Tipo restituito | Nullable | Accettabile o non accettabile |
Tipo restituito | Nonnull | Nonnull |
Argomento divertente | Senza annotazioni | Non annotato o annullabile |
Argomento divertente | Nullable | Nullable |
Argomento divertente | Nonnull | Accettabile o non accettabile |
Preferisci argomenti non nullabili (ad esempio @NonNull) ove possibile
Quando i metodi sono sovraccarichi, preferisci che tutti gli argomenti siano non nulli.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
Questa regola si applica anche ai setter di proprietà sovraccarichi. L'argomento principale deve essere non nullo e l'eliminazione della proprietà deve essere implementata come metodo separato. In questo modo si evitano chiamate "non sense" in cui lo sviluppatore deve impostare parametri finali 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 restituiti non nullabili (ad esempio @NonNull) per i contenitori
Per i 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, valuta la possibilità di fornire un metodo
booleano separato.
@NonNull
public Bundle getExtras() { ... }
Le annotazioni di nullabilità per le coppie get e set devono corrispondere
Le coppie di metodi get e set per una singola proprietà logica devono sempre concordare nelle annotazioni di nullabilità. Se non segui questa linea guida, la sintassi delle proprietà di Kotlin non funzionerà e l'aggiunta di annotazioni di nullabilità in disaccordo ai metodi delle proprietà esistenti è quindi una modifica che interrompe la compatibilità con le origini per gli utenti Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Valore restituito in caso di errore o guasto
Tutte le API devono consentire alle app di reagire agli errori. Restituire false
, -1
, null
o altri valori generici di "si è verificato un errore" non fornisce a uno sviluppatore informazioni sufficienti sull'errore per gestire le aspettative degli utenti o monitorare con precisione l'affidabilità della propria app sul campo. Quando progetti un'API, immagina di
creare un'app. Se si verifica un errore, l'API ti fornisce informazioni
sufficienti per presentarlo all'utente o reagire in modo appropriato?
- È consigliabile includere informazioni dettagliate in un messaggio di eccezione, ma gli sviluppatori non devono analizzarle per gestire l'errore in modo appropriato. 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
introdurre nuovi tipi di errori in futuro. Per
@IntDef
, ciò significa includere un valoreOTHER
oUNKNOWN
. Quando restituisci un nuovo codice, puoi controllaretargetSdkVersion
del chiamante per evitare di restituire un codice di errore che l'app non conosce. Per le eccezioni, utilizza una superclasse comune implementata dalle eccezioni, in modo che qualsiasi codice che gestisce quel tipo rilevi e gestisca anche i sottotipi. - Per uno sviluppatore dovrebbe essere difficile o impossibile ignorare per errore
un errore. Se l'errore viene comunicato restituendo un valore, annota
il metodo con
@CheckResult
.
Preferisci generare 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 setter o di azione (ad esempio perform
) possono restituire un codice di 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 contenitore come campi public static final
, con il prefisso ERROR_
ed enumerati 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 agli array come tipo restituito o di parametro
Le interfacce di raccolta con tipi generici offrono diversi vantaggi rispetto agli array, tra cui contratti API più solidi in termini di unicità e ordinamento, supporto per i generici e una serie di metodi pratici per gli sviluppatori.
Eccezione per i tipi primitivi
Se gli elementi sono primitivi, preferisci gli array per evitare il costo del boxing automatico. Vedi Utilizzare e restituire primitive non elaborate anziché versioni in scatola
Eccezione per il codice sensibile alle prestazioni
In alcuni scenari, in cui l'API viene utilizzata in codice sensibile alle prestazioni (come API grafiche o di misurazione/layout/disegno), è accettabile utilizzare array anziché raccolte per ridurre le allocazioni e l'utilizzo della memoria.
Eccezione per Kotlin
Gli array Kotlin sono invarianti e il linguaggio Kotlin fornisce numerose API di utilità
intorno agli array, quindi gli array sono alla pari con List
e Collection
per le API Kotlin
destinate all'accesso da Kotlin.
Preferire 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 a basso costo, con tipo corretto e immutabile.
Dove sono supportate le annotazioni di tipo, preferisci sempre @NonNull
per gli elementi della raccolta.
Dovresti anche preferire @NonNull
quando utilizzi array anziché raccolte (vedi
elemento precedente). Se l'allocazione degli oggetti
è un problema, crea una costante e passala, dopotutto 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;
}
Modificabilità della raccolta
Per impostazione predefinita, le API Kotlin devono preferire i tipi restituiti di sola lettura (non Mutable
) per le raccolte, a meno che il contratto API non richieda specificamente un tipo restituito modificabile.
Le API Java, tuttavia, dovrebbero preferire i tipi restituiti modificabili per impostazione predefinita perché l'implementazione delle API Java della piattaforma Android non fornisce ancora un'implementazione conveniente delle raccolte immutabili. L'eccezione a questa regola sono i tipi di reso
Collections.empty
, che non sono modificabili. Nei casi in cui la mutabilità
potrebbe essere sfruttata dai client, intenzionalmente o per errore, per interrompere il pattern di utilizzo previsto dell'API, le API Java devono prendere seriamente in considerazione la restituzione di 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 restituiti esplicitamente modificabili
Idealmente, le API che restituiscono raccolte non devono modificare l'oggetto raccolta restituito dopo la restituzione. Se la raccolta restituita deve essere modificata o riutilizzata in qualche modo, ad esempio una visualizzazione adattata di un set di dati modificabile, il comportamento preciso di quando i contenuti possono cambiare deve essere documentato esplicitamente o seguire le convenzioni di denominazione delle API consolidate.
/**
* 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 la raccolta originale cambia.
Modificabilità degli oggetti del tipo di dati restituiti
Analogamente alle API che restituiscono raccolte, le API che restituiscono oggetti di tipo di dati idealmente non devono modificare le proprietà dell'oggetto restituito dopo la restituzione.
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 possono trarre vantaggio dal pooling o dal riutilizzo degli oggetti. Non scrivere la tua struttura di dati del pool di oggetti e non esporre gli oggetti riutilizzati nelle API pubbliche. In entrambi i casi, fai molta attenzione alla gestione dell'accesso simultaneo.
Utilizzo del tipo di parametro vararg
È consigliabile utilizzare le API Kotlin e Java vararg
nei casi in cui lo
sviluppatore creerebbe probabilmente 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
Le implementazioni Java e Kotlin dei parametri vararg
vengono compilate nello stesso
bytecode basato su array e di conseguenza possono essere chiamate dal codice Java con un
array modificabile. I progettisti di API sono fortemente incoraggiati a creare una copia
superficiale difensiva del parametro array nei casi in cui verrà reso persistente 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 al metodo iniziale e la creazione della copia, né protegge dalla mutazione degli oggetti contenuti nell'array.
Fornisci la semantica corretta con i parametri del 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 i duplicati non hanno significato.Collection<Foo>,
se la tua API è indifferente all'ordine e consente i duplicati.
Funzioni di conversione 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 restituito della conversione. Ciò è coerente con la JDK
Object.toString()
. Kotlin fa un ulteriore passo avanti utilizzandolo per le conversioni primitive, ad esempio 25.toFloat()
.
La distinzione tra le conversioni denominate .toFoo()
e .asFoo()
è
significativa:
Utilizza .toFoo() quando crei 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.
Allo stesso modo, 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 cast
Il casting in Kotlin viene eseguito utilizzando la parola chiave as
. Riflette una modifica all'interfaccia, ma non all'identità. Se utilizzato come prefisso in una
funzione di estensione, .asFoo()
decora il destinatario. Una mutazione nell'oggetto ricevitore originale si rifletterà nell'oggetto restituito da asFoo()
.
Una mutazione nel nuovo oggetto Foo
potrebbe essere riportata nell'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 del destinatario e del risultato riduce l'accoppiamento tra i tipi. Una conversione ideale richiede solo l'accesso all'API pubblica all'oggetto originale. Questo dimostra con un esempio che uno 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 generici.
Gli errori non correlati agli argomenti forniti direttamente al metodo richiamato 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 al singolare
Utilizza MyObjectCallback
anziché MyObjectCallbacks
.
I nomi dei metodi di callback devono essere nel formato on
onFooEvent
indica che FooEvent
è in corso e che il callback deve
agire di conseguenza.
Il tempo passato e 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 del callback
Quando un listener o un callback può essere aggiunto o rimosso da un oggetto, i metodi associati devono essere denominati add e remove o register e unregister. Rispetta la convenzione esistente utilizzata dal corso o da altri corsi dello stesso pacchetto. Quando non esiste un precedente, preferisci l'aggiunta e la rimozione.
I metodi che prevedono la registrazione o l'annullamento della registrazione 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()
. Si tratta di una soluzione di emergenza allettante per
i casi in cui gli sviluppatori potrebbero voler concatenare 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 il numero
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 tali modifiche al callback di wrapping.
Accetta l'esecutore per controllare l'invio del callback
Quando registri callback che non hanno aspettative di threading esplicite (praticamente ovunque al di fuori del toolkit UI), ti consigliamo vivamente di includere un parametro Executor
come parte della 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)
In deroga alle nostre linee guida sui parametri facoltativi, è accettabile fornire un overload omettendo Executor
anche se non è l'argomento finale nell'elenco dei parametri. Se Executor
non viene fornito, il callback
deve essere richiamato sul thread principale utilizzando Looper.getMainLooper()
e questo
deve essere documentato nel metodo di overload 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 accettano questo modulo, l'implementazione dell'oggetto Binder in entrata sul lato del processo dell'app deve chiamare Binder.clearCallingIdentity()
prima di richiamare il callback dell'app sul 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 del chiamante, queste devono essere una parte esplicita della superficie dell'API, anziché implicita in base a dove è stato eseguito il Executor
fornito.
La specifica di un Executor
deve essere supportata dalla tua API. Nei casi
critici per le prestazioni, le app potrebbero dover eseguire il codice immediatamente o
in modo sincrono con il feedback della tua API. L'accettazione di un Executor
lo consente.
La creazione difensiva di un HandlerThread
aggiuntivo o simile a un trampolino da
vanifica questo caso d'uso auspicabile.
Se un'app deve eseguire codice costoso in un punto qualsiasi del proprio processo, lasciala fare. Le soluzioni alternative che gli sviluppatori di app troveranno per superare le tue limitazioni saranno molto più difficili da supportare a lungo termine.
Eccezione per il singolo callback: quando la natura degli eventi segnalati richiede il supporto di una sola istanza di callback, utilizza lo stile seguente:
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 per preferire
Executor
, in quanto la maggior parte degli sviluppatori di app gestisce i propri pool di thread, rendendo il thread principale
o dell'interfaccia utente l'unico thread Looper
disponibile per l'app. Utilizza Executor
per
dare agli sviluppatori il controllo necessario per riutilizzare i contesti di esecuzione esistenti/preferiti.
Le librerie di concorrenza moderne come kotlinx.coroutines o RxJava forniscono meccanismi di pianificazione propri che eseguono l'invio quando necessario, il che rende importante fornire la possibilità di utilizzare un executor diretto (come Runnable::run
) per evitare la latenza dovuta a doppi salti di thread. Ad esempio, un hop
per pubblicare un post 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. I ricorsi più comuni per un'eccezione includono:
Devo usare 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
realizzati in questa situazione.
Non voglio che il codice dell'app blocchi la pubblicazione dell'evento nel mio thread. Questa richiesta di eccezione in genere non viene concessa per il codice eseguito in un processo dell'app. Le app che sbagliano questo aspetto danneggiano solo se stesse, senza influire sullo stato generale del sistema. Le app che lo fanno correttamente o utilizzano un framework di concorrenza comune non dovrebbero pagare penalità di latenza aggiuntive.
Handler
è coerente a livello locale con altre API simili della stessa classe.
Questa richiesta di eccezione viene concessa in base alla situazione. La preferenza è che vengano aggiunti overload basati su Executor
, eseguendo la migrazione delle implementazioni Handler
per utilizzare la nuova implementazione Executor
. (myHandler::post
è un Executor
valido.) A seconda delle dimensioni della classe, del numero di metodi Handler
esistenti e della probabilità che gli sviluppatori debbano utilizzare i metodi Handler
esistenti insieme al nuovo metodo, potrebbe essere concessa un'eccezione per aggiungere un nuovo metodo basato su Handler
.
Simmetria nella registrazione
Se è possibile aggiungere o registrare qualcosa, dovrebbe essere possibile anche rimuoverlo/annullarne la registrazione. Il metodo
registerThing(Thing)
deve avere un
unregisterThing(Thing)
Fornisci un identificatore della richiesta
Se è ragionevole che uno sviluppatore riutilizzi un callback, fornisci un oggetto identificatore 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 callback con più metodi devono preferire interface
e utilizzare i metodi default
quando vengono aggiunti a interfacce rilasciate in precedenza. In precedenza, queste linee guida
consigliavano 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.
}
}
Utilizza android.os.OutcomeReceiver per modellare una chiamata di funzione non bloccante
OutcomeReceiver<R,E>
restituisce un valore di risultato R
in caso di esito positivo o E : Throwable
in caso contrario. Le
stesse operazioni che può eseguire una semplice chiamata di metodo. Utilizza OutcomeReceiver
come tipo di callback
quando converti un metodo di blocco 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 che
requestFoo
restituirebbe viene invece segnalato al requestFooAsync
callback
del parametro
OutcomeReceiver.onResult
chiamandolo sul executor
fornito.
Qualsiasi eccezione generata da requestFoo
viene invece segnalata 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 Kotlin
suspend fun
per i metodi asincroni che utilizzano l'estensione
Continuation.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 comodità di una semplice chiamata di funzione senza bloccare il thread
chiamante. Queste estensioni 1:1 per le API della piattaforma possono essere offerte come parte dell'artefatto androidx.core:core-ktx
in Jetpack se combinate con i controlli e le considerazioni standard sulla compatibilità delle versioni. Per ulteriori informazioni, considerazioni sull'annullamento ed esempi, consulta la documentazione relativa ad
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 API 24 ha aggiunto i tipi java.util.function.*
(documenti di riferimento)
, che offrono interfacce SAM generiche come Consumer<T>
adatte all'uso come lambda di callback. In molti casi, la creazione di nuove interfacce SAM
offre un valore limitato in termini di sicurezza dei tipi o di comunicazione dell'intent, mentre
espande inutilmente l'area della 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
- Molti altri 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 viene 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 disporre di una documentazione sufficiente per spiegare come uno sviluppatore utilizzerebbe l'API. Supponiamo che lo sviluppatore abbia trovato il metodo utilizzando il completamento automatico o durante la navigazione nella documentazione di riferimento dell'API e disponga di un contesto minimo dalla superficie API adiacente (ad esempio, la stessa classe).
Metodi
I parametri e i valori restituiti del metodo devono essere documentati utilizzando le annotazioni @param
e
@return
, rispettivamente. Formatta il corpo di Javadoc come se fosse
preceduto da "Questo metodo...".
Nei casi in cui un metodo non accetta parametri, non presenta considerazioni speciali e
restituisce ciò che indica il nome del metodo, puoi omettere @return
e
scrivere una documentazione simile a:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Utilizzare sempre i link in Javadoc
I documenti devono rimandare ad altri documenti per costanti, metodi e altri elementi correlati. Utilizza i tag Javadoc (ad esempio @see
e {@link foo}
), non solo
parole in formato testo normale.
Per il seguente esempio di origine:
public static final int FOO = 0;
public static final int BAR = 1;
Non utilizzare testo normale 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 seguenti 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 saperne di più su IntDef
, consulta le indicazioni sulle annotazioni.
Esegui update-api o docs target quando aggiungi Javadoc
Questa regola è particolarmente importante quando aggiungi tag @link
o @see
e assicurati che l'output abbia l'aspetto previsto. L'output ERROR in Javadoc è spesso dovuto a link non validi. Il controllo viene eseguito da update-api
o docs
Make target, ma
il target docs
potrebbe essere più rapido se stai modificando solo Javadoc e non
devi altrimenti eseguire il target update-api
.
Utilizza {@code foo} per distinguere i valori Java
Racchiudi i valori Java come true
, false
e null
tra {@code...}
per
distinguerli dal testo della documentazione.
Quando scrivi la documentazione nelle origini Kotlin, puoi racchiudere il codice tra apici inversi come faresti per Markdown.
I riepiloghi di @param e @return devono essere un frammento di frase
I riepiloghi dei parametri e dei valori restituiti devono iniziare con un carattere minuscolo e contenere solo un frammento di frase. Se hai informazioni aggiuntive che vanno oltre 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 perché le annotazioni @hide
e @removed
sono nascoste all'API pubblica.
Includi istruzioni su come sostituire gli elementi API contrassegnati con l'annotazione
@deprecated
.
Utilizzare @throws per documentare le eccezioni
Se un metodo genera un'eccezione controllata, ad esempio IOException
, documenta l'eccezione con @throws
. Per le API di origine Kotlin destinate all'uso da parte di
client Java, annota le funzioni con
@Throws
.
Se un metodo genera un'eccezione non controllata che indica un errore prevenibile, ad esempio IllegalArgumentException
o IllegalStateException
, documenta l'eccezione con una spiegazione del motivo per cui viene generata. L'eccezione
generata deve anche indicare il motivo per cui è stata generata.
Alcuni casi di eccezione non controllata sono considerati impliciti e non devono
essere documentati, ad esempio NullPointerException
o IllegalArgumentException
in cui un argomento non corrisponde a un'annotazione @IntDef
o simile che incorpora
il contratto 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.");
}
// ...
Oppure, 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
in che modo lo sviluppatore viene a conoscenza di queste eccezioni e risponde. In genere
ciò comporta l'inoltro dell'eccezione a un callback e la documentazione delle
eccezioni generate nel metodo che le riceve. Le eccezioni asincrone non devono essere documentate con @throws
, a meno che non vengano effettivamente generate di nuovo dal metodo annotato.
Termina la prima frase dei documenti con un punto.
Lo strumento Doclava analizza la documentazione in modo semplificato, terminando il documento di riepilogo (la prima frase, utilizzata nella descrizione rapida nella parte superiore della documentazione della classe) non appena vede un punto (.) seguito da uno spazio. Ciò causa due problemi:
- Se un documento breve non termina con un punto e se il membro ha ereditato documenti rilevati dallo strumento, anche la sinossi rileva questi documenti ereditati. Ad esempio, vedi
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
i documenti di riepilogo dopo "g.". Ad esempio, vedi
TEXT_ALIGNMENT_CENTER
inView.java
. Tieni presente che Metalava corregge automaticamente questo errore inserendo uno spazio non separabile dopo il punto. Tuttavia, non commettere questo errore in primo luogo.
Formattare i documenti per il rendering 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.
Gli elenchi devono utilizzare
<ul>
o<ol>
per gli elenchi non ordinati e ordinati, rispettivamente. Ogni elemento deve iniziare con un tag<li>
, ma non è necessario un tag</li>
di chiusura. Dopo l'ultimo elemento è necessario un tag di chiusura</ul>
o</ol>
.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 la deprecazione.Per creare un carattere di 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 eseguirne l'escape con le entità HTML<
e>
.In alternativa, puoi lasciare le parentesi quadre non elaborate
<>
nello snippet di codice se racchiudi le sezioni problematiche in{@code foo}
. 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 How to Write Doc Comments for the Javadoc Tool.
Regole specifiche per Android Framework
Queste regole riguardano API, pattern e strutture di dati specifici per
API e comportamenti integrati nel framework Android (ad esempio, Bundle
o
Parcelable
).
I generatori di intent devono utilizzare il pattern create*Intent()
I creator per gli 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 da chiave a
valore digitato. In alternativa, valuta la possibilità di utilizzare Bundle
.
Ciò si verifica in genere quando si scrivono API della 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 API potrebbe essere parzialmente definito al di fuori della piattaforma (ad esempio, in una libreria Jetpack).
Nei casi in cui la piattaforma legge i dati, evita di utilizzare Bundle
e
preferisci una classe di dati fortemente tipizzata.
Le implementazioni di Parcelable devono avere il campo CREATOR pubblico
L'inflazione di Parcelable è esposta tramite CREATOR
, non tramite costruttori non elaborati. Se una
classe implementa Parcelable
, anche il relativo campo CREATOR
deve essere 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
Spannable
istanze.
Se si tratta solo di una chiave o di un'altra etichetta o valore non visibile agli utenti,
String
va bene.
Evita di utilizzare le enumerazioni
IntDef
deve essere utilizzato al posto degli enum in tutte le API della piattaforma e deve essere preso in seria considerazione
nelle API di libreria non raggruppate. Utilizza le enumerazioni solo quando hai la certezza che non verranno aggiunti nuovi valori.
Vantaggi di IntDef
:
- Consente di aggiungere valori nel tempo
- Le istruzioni Kotlin
when
possono non riuscire in fase di runtime se non sono più esaustive a causa di un valore enum aggiunto nella piattaforma.
- Le istruzioni Kotlin
- Nessuna classe o oggetto utilizzato in fase di runtime, solo primitive
- Anche se R8 o la minimizzazione possono evitare questo costo per le API di librerie non raggruppate, questa ottimizzazione non può influire sulle classi API della piattaforma.
Vantaggi dell'enumerazione
- Funzionalità di linguaggio idiomatico di Java, Kotlin
- Attiva l'utilizzo esaustivo dell'istruzione switch
when
- Nota: i valori non devono cambiare nel tempo, vedi l'elenco precedente
- Nomenclatura chiaramente definita e rilevabile
- 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 di stratificazione dei pacchetti Android
La gerarchia dei pacchetti android.*
ha un ordinamento 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 loro prodotti
La piattaforma Android è un progetto open source e mira a essere indipendente dal fornitore. L'API deve essere generica e utilizzabile in egual misura da integratori di sistemi o app con le autorizzazioni necessarie.
Le implementazioni Parcelable devono essere final
Le classi Parcelable definite dalla piattaforma vengono sempre caricate da
framework.jar
, pertanto non è valido che un'app tenti di 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 la tua classe non era final, ma non aveva un
costruttore disponibile pubblicamente, puoi comunque contrassegnarla come final
.
I metodi che chiamano il processo di sistema devono generare nuovamente RemoteException come RuntimeException
RemoteException
viene in genere generato da AIDL interno e indica che il
processo di sistema è terminato o che l'app sta tentando di inviare troppi dati. In entrambi i casi, l'API pubblica deve generare nuovamente un'eccezione RuntimeException
per impedire alle app di mantenere le decisioni relative alla sicurezza o alle norme.
Se sai che l'altra parte di una chiamata Binder
è il processo di sistema, questo
codice boilerplate è la best practice:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Generare eccezioni specifiche per le modifiche all'API
I comportamenti delle API pubbliche potrebbero cambiare a seconda dei livelli API e causare arresti anomali delle app (ad esempio per applicare nuove norme di sicurezza).
Quando l'API deve generare un errore per una richiesta precedentemente valida, genera una nuova eccezione specifica anziché una generica. Ad esempio, ExportedFlagRequired
invece di SecurityException
(e ExportedFlagRequired
può estendersi
SecurityException
).
In questo modo, gli sviluppatori di app e gli strumenti possono rilevare le modifiche al comportamento delle API.
Implementa il costruttore di copia anziché clone
L'utilizzo del metodo Java clone()
è fortemente 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 builder per la costruzione devono prendere in considerazione l'aggiunta di un costruttore di copie del builder per consentire modifiche alla copia.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Utilizzare ParcelFileDescriptor anziché FileDescriptor
L'oggetto java.io.FileDescriptor
ha una definizione di proprietà scarsa, il che
può causare bug oscuri di tipo use-after-close. Invece, le API devono restituire o
accettare istanze ParcelFileDescriptor
. Il codice legacy può eseguire la conversione 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 possibilità di evolvere l'API in futuro.
Evita di utilizzare BitSet
java.util.BitSet
è ideale per l'implementazione, ma non per l'API pubblica. È
modificabile, 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 a basso rendimento, 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é è eccessivamente rigoroso nell'analisi degli URI, e non utilizzare mai
java.net.URL
, perché la sua definizione di uguaglianza è gravemente errata.
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 trasmesse a un'API. Tuttavia, quando vengono esportate come
API, il compilatore in linea le costanti e solo i valori (ora inutili)
rimangono nello stub API dell'annotazione (per la piattaforma) o nel file JAR (per
le librerie).
Pertanto, gli utilizzi di queste annotazioni devono essere contrassegnati con l'annotazione @hide
docs
nella piattaforma o con l'annotazione di codice @RestrictTo.Scope.LIBRARY)
nelle
librerie. Devono essere contrassegnati con @Retention(RetentionPolicy.SOURCE)
in entrambi i casi per evitare che vengano visualizzati negli stub API o nei file JAR.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
Quando vengono create le librerie AAR e l'SDK della piattaforma, uno strumento estrae le annotazioni e le raggruppa separatamente dalle origini compilate. Android Studio legge questo formato in bundle e applica le definizioni dei tipi.
Non aggiungere nuove chiavi del fornitore di impostazioni
Non esporre nuove chiavi da
Settings.Global
,
Settings.System
,
o
Settings.Secure
.
Aggiungi invece un getter e un setter Java API appropriati in una classe pertinente, che in genere è una classe "manager". Aggiungi un meccanismo di ascolto o una trasmissione per notificare ai client le modifiche in base alle esigenze.
Le impostazioni SettingsProvider
presentano una serie di problemi rispetto a
getter/setter:
- Nessuna sicurezza dei tipi.
- Non esiste un 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 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 l'ha ritirata per una
corretta API Java
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. Esporre invece un listener o, in
androidx, un'API ListenableFuture
.
Impossibile comporre le sottoclassi di Activity
. L'estensione dell'attività per la tua funzionalità la rende incompatibile con altre funzionalità che richiedono agli utenti di fare la stessa cosa. Affidati invece alla composizione utilizzando strumenti come
LifecycleObserver.
Utilizzare getUser() di Context
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é numeri interi semplici
UserHandle
è preferito per garantire la sicurezza dei tipi 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);
Preferire listener o callback agli intent di trasmissione
Gli intent di trasmissione sono molto potenti, ma hanno portato a comportamenti emergenti che possono influire negativamente sull'integrità del sistema, pertanto i nuovi intent di trasmissione devono essere aggiunti con giudizio.
Di seguito sono riportati alcuni problemi specifici che ci portano a sconsigliare l'introduzione di nuovi intent di trasmissione:
Quando invii trasmissioni senza il flag
FLAG_RECEIVER_REGISTERED_ONLY
, forzano l'avvio di tutte le app che non sono già in esecuzione. Sebbene a volte possa essere un risultato intenzionale, può comportare l'apertura di decine di app, con un impatto negativo sull'integrità del sistema. Ti consigliamo di utilizzare strategie alternative, comeJobScheduler
, per coordinare meglio il momento in cui vengono soddisfatte varie precondizioni.Quando invii trasmissioni, hai poche possibilità di filtrare o modificare i contenuti inviati alle app. In questo modo è difficile o impossibile rispondere a futuri problemi di privacy o introdurre modifiche del comportamento in base all'SDK di destinazione dell'app ricevente.
Poiché le code di trasmissione sono una risorsa condivisa, possono sovraccaricarsi e potrebbero non garantire la consegna tempestiva del tuo evento. Abbiamo osservato diverse code di trasmissione in natura con una latenza end-to-end di 10 minuti o più.
Per questi motivi, incoraggiamo le nuove funzionalità a prendere in considerazione l'utilizzo di listener o
callback o altre funzionalità come JobScheduler
anziché intent di trasmissione.
Nei casi in cui gli intent di trasmissione rimangono 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 attivare le app. - Se possibile, utilizza
Intent.setPackage()
oIntent.setComponent()
per scegliere come target la trasmissione in un'app specifica di interesse. Ad esempio,ACTION_MEDIA_BUTTON
utilizza questo design per concentrarsi sulla gestione dell'app corrente controlli di riproduzione. - Se possibile, definisci la trasmissione come
<protected-broadcast>
per impedire alle app dannose di impersonare il sistema operativo.
Intent nei servizi per sviluppatori associati al sistema
I servizi che devono essere estesi dallo sviluppatore e vincolati dal sistema, ad esempio i servizi astratti come NotificationListenerService
, potrebbero rispondere a un'azione Intent
del sistema. Questi servizi devono soddisfare i seguenti criteri:
- Definisci una costante stringa
SERVICE_INTERFACE
nella classe contenente il nome della classe completo del servizio. Questa costante deve essere annotata con@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
. - Documento sul corso a cui uno sviluppatore deve aggiungere un
<intent-filter>
al proprioAndroidManifest.xml
per ricevere intent dalla piattaforma. - Valuta seriamente la possibilità di aggiungere un'autorizzazione a livello di sistema per impedire alle app non autorizzate
di inviare
Intent
ai servizi per sviluppatori.
Interoperabilità Kotlin-Java
Per un elenco completo delle linee guida, consulta la guida ufficiale all'interoperabilità Kotlin-Java di Android. Alcune linee guida sono state copiate in questa guida per migliorare la visibilità.
Visibilità API
Alcune API Kotlin, come suspend fun
, non sono destinate all'uso da parte di sviluppatori Java; tuttavia, non tentare di controllare la visibilità specifica della lingua utilizzando @JvmSynthetic
, in quanto ha effetti collaterali sul modo in cui l'API viene presentata nei debugger, rendendo più difficile il debug.
Consulta la guida all'interoperabilità Kotlin-Java o la guida asincrona per indicazioni specifiche.
Oggetti companion
Kotlin utilizza companion object
per esporre i membri statici. In alcuni casi, questi
vengono visualizzati da Java in una classe interna denominata Companion
anziché nella
classe contenitore. I corsi Companion
potrebbero essere visualizzati come corsi vuoti nei file di testo dell'API, il che funziona come previsto.
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 contenitore.
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 i codebase esistenti.
Modifiche che causano errori binari
Evita modifiche che causano interruzioni binarie nelle superfici delle API pubbliche finalizzate. Questi tipi di
modifiche in genere generano errori durante l'esecuzione di make update-api
, ma potrebbero
esistere casi limite che il controllo 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 alle API compatibili in Java. Le modifiche che causano interruzioni binarie nelle API nascoste (ad esempio, di sistema) devono seguire
il ciclo di ritiro/sostituzione.
Modifiche che causano interruzioni della compatibilità con le origini
Sconsigliamo le modifiche che interrompono la compatibilità con l'origine anche se non interrompono la compatibilità binaria. Un esempio di modifica compatibile a livello binario, ma che causa problemi a livello di codice sorgente, è l'aggiunta di un generico a una classe esistente, che è compatibile a livello binario, ma può introdurre errori di compilazione a causa di ereditarietà o riferimenti ambigui.
Le modifiche che causano interruzioni dell'origine non generano errori durante l'esecuzione di make update-api
, quindi
devi prestare attenzione a comprendere l'impatto delle modifiche alle firme API esistenti.
In alcuni casi, le modifiche che causano interruzioni dell'origine diventano necessarie per migliorare l'esperienza dello sviluppatore o la correttezza del codice. Ad esempio, l'aggiunta di annotazioni di nullabilità alle sorgenti 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 - Added
- API y+1 - Ritiro
- Contrassegna il codice con
@Deprecated
. - Aggiungi sostituzioni e link alla sostituzione nella documentazione Javadoc per il codice
ritirato utilizzando l'annotazione
@deprecated
docs. - Durante il ciclo di sviluppo, segnala bug agli utenti interni comunicando che l'API verrà ritirata. Ciò consente di verificare che le API sostitutive siano adeguate.
- Contrassegna il codice con
- API y+2 - Rimozione temporanea
- Contrassegna il codice con
@removed
. - Se vuoi, genera un'eccezione o esegui 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 definitiva
- Rimuovi completamente il codice dall'albero delle origini.
Ritiro
Consideriamo la deprecazione una modifica dell'API, che può verificarsi in una release principale (ad esempio
una lettera). Utilizza l'annotazione di origine @Deprecated
e l'annotazione di documentazione @deprecated
<summary>
insieme quando ritiri le API. Il riepilogo deve
includere una strategia di migrazione. Questa strategia potrebbe collegarsi a un'API sostitutiva o
spiegare perché non devi 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
attributi e proprietà modificabili nello stile esposti 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 più utili per scoraggiare l'adozione di un'API nel nuovo codice.
Inoltre, richiediamo di contrassegnare le API come @deprecated
prima che vengano
@removed
, ma questo non fornisce una forte motivazione agli
sviluppatori per 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
genera un avviso durante la compilazione.- Gli avvisi di ritiro non possono essere eliminati a livello globale o di base, pertanto
gli sviluppatori che utilizzano
-Werror
devono correggere o eliminare 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 eliminati. Di conseguenza, gli sviluppatori devono incorporare il nome della classe completamente qualificato per ogni utilizzo di una classe ritirata prima di poter aggiornare la versione dell'SDK di compilazione.
- Gli avvisi di ritiro non possono essere eliminati a livello globale o di base, pertanto
gli sviluppatori che utilizzano
- La documentazione relativa a
d.android.com
mostra un avviso di ritiro. - Gli IDE come Android Studio mostrano un avviso nel sito di utilizzo dell'API.
- Gli IDE potrebbero declassare o nascondere l'API dal completamento automatico.
Di conseguenza, il ritiro di un'API può scoraggiare gli sviluppatori più
interessati all'integrità del codice (quelli che utilizzano -Werror
) dall'adottare nuovi SDK.
Gli sviluppatori che non si preoccupano degli avvisi nel codice esistente probabilmente
ignoreranno completamente le deprecazioni.
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 versione futura. - L'utilizzo dell'API comporta un comportamento errato o indefinito che non possiamo correggere senza interrompere la compatibilità.
Quando ritiri un'API e la sostituisci con una nuova, ti consigliamo vivamente
di aggiungere un'API di compatibilità corrispondente a una libreria Jetpack come
androidx.core
per semplificare il supporto di dispositivi vecchi e nuovi.
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 comportamenti documentati:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Modifiche alle API deprecate
Devi mantenere il comportamento delle API ritirate. Ciò significa che le implementazioni dei test devono rimanere invariate e i test devono continuare a essere superati dopo il ritiro dell'API. Se l'API non ha test, devi aggiungerli.
Non espandere le superfici API ritirate nelle versioni future. Puoi aggiungere annotazioni di correttezza
lint (ad esempio @Nullable
) a un'API esistente
ritirata, ma non devi aggiungere nuove API.
Non aggiungere nuove API come obsolete. Se sono state aggiunte API e successivamente ritirate durante un ciclo di pre-release (quindi inizialmente entrerebbero nella superficie dell'API pubblica come ritirate), devi rimuoverle prima di finalizzare l'API.
Rimozione temporanea
La rimozione temporanea è una modifica che interrompe la compatibilità con l'origine e dovresti evitarla nelle API pubbliche
a meno che il consiglio per le 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 temporanea. Rimuovi tutti i riferimenti ai documenti
alle API e utilizza l'annotazione dei documenti @removed <summary>
quando rimuovi temporaneamente
le API. Il riepilogo deve includere il motivo della rimozione e può includere una
strategia di migrazione, come spiegato in Ritiro.
Il comportamento delle API rimosse temporaneamente può essere mantenuto così com'è, ma, cosa più importante, deve essere preservato in modo che i chiamanti esistenti non si arrestino in modo anomalo quando chiamano l'API. In alcuni casi, ciò potrebbe significare preservare il comportamento.
La copertura dei test deve essere mantenuta, ma i contenuti dei test potrebbero dover cambiare per adattarsi ai cambiamenti comportamentali. I test devono comunque verificare che i chiamanti esistenti non si arrestino in modo anomalo in fase di runtime. Puoi mantenere il comportamento delle API rimosse temporaneamente così com'è, ma, cosa più importante, devi preservarlo in modo che i chiamanti esistenti non si arrestino in modo anomalo quando chiamano l'API. In alcuni casi, ciò potrebbe significare preservare il comportamento.
Devi mantenere la copertura dei test, ma il contenuto dei test potrebbe dover cambiare per adattarsi ai cambiamenti comportamentali. I test devono comunque verificare che i chiamanti esistenti non si arrestino in modo anomalo in fase di runtime.
A livello tecnico, rimuoviamo l'API dal file JAR stub dell'SDK e dal classpath in fase di compilazione utilizzando l'annotazione Javadoc @remove
, ma è ancora presente nel classpath in fase di esecuzione, 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
è
uguale o successivo all'SDK in cui l'API è stata rimossa. Tuttavia, il codice
sorgente continua a essere compilato correttamente rispetto agli SDK precedenti e i binari che
fanno riferimento all'API continuano a funzionare.
Determinate categorie di API non devono essere rimosse temporaneamente. Non devi rimuovere in modo soft determinate categorie di API.
Metodi astratti
Non devi rimuovere in modo soft i metodi astratti nelle classi che gli sviluppatori potrebbero estendere. In questo modo, gli sviluppatori non possono estendere correttamente la classe a tutti i livelli dell'SDK.
Nei rari casi in cui non è mai stato e non sarà possibile per gli sviluppatori estendere una classe, puoi comunque rimuovere in modo soft i metodi astratti.
Rimozione definitiva
La rimozione definitiva è una modifica che interrompe la compatibilità binaria 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 sconsigliate differiscono da quelle obsolete in quanto
esiste un caso d'uso critico ristretto che impedisce l'obsolescenza. 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 sconsigliate.
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 tentato di 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 protetto queste
modifiche del comportamento in base al ApplicationInfo.targetSdkVersion
dell'app, ma
di recente abbiamo eseguito la migrazione per richiedere l'utilizzo dell'App Compatibility Framework. 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 framework di compatibilità delle app consente agli sviluppatori di disattivare temporaneamente modifiche specifiche del comportamento durante le versioni di anteprima e beta nell'ambito del debug delle loro app, anziché costringerli ad adattarsi a decine di modifiche del comportamento contemporaneamente.
Compatibilità futura
La compatibilità futura è una caratteristica di progettazione che consente a un sistema di accettare input destinati a una versione successiva. Nel caso della progettazione di API, devi prestare particolare attenzione alla progettazione iniziale e alle modifiche future, perché gli sviluppatori si aspettano di scrivere il codice una volta, testarlo una volta ed eseguirlo ovunque senza problemi.
Di seguito sono riportate le cause dei problemi di compatibilità con le versioni successive più comuni in Android:
- L'aggiunta di nuove costanti a un insieme (ad esempio
@IntDef
oenum
) precedentemente considerato completo (ad esempio, doveswitch
ha undefault
che genera un'eccezione). - Aggiunta del supporto per una funzionalità non acquisita direttamente nella superficie dell'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 sui controlli in fase di esecuzione, ad esempio rimozione di un controllo
requireNotNull()
presente nelle versioni precedenti.
In tutti questi casi, gli sviluppatori scoprono che qualcosa non va solo in fase di esecuzione. Peggio ancora, potrebbero scoprirlo a seguito di report sugli arresti anomali di dispositivi meno recenti sul campo.
Inoltre, in tutti questi casi si tratta di modifiche all'API tecnicamente valide. Non interrompono la compatibilità binaria o di origine e lo strumento di analisi lint API non rileva nessuno di questi problemi.
Di conseguenza, i progettisti di API devono prestare molta attenzione quando modificano le classi esistenti. Poni la domanda: "Questa modifica causerà l'interruzione del codice scritto e testato solo rispetto all'ultima versione della piattaforma nelle versioni precedenti?"
Schemi XML
Se uno schema XML funge da interfaccia stabile tra i componenti, deve essere specificato in modo esplicito e deve evolvere in modo compatibile con le versioni precedenti, in modo simile ad altre API Android. Ad esempio, la struttura degli elementi e degli attributi XML deve essere mantenuta in modo simile a come vengono mantenuti i metodi e le variabili in altre superfici API Android.
Ritiro di XML
Se vuoi ritirare un elemento o un attributo XML, puoi aggiungere il
marcatore xs:annotation
, ma devi continuare a supportare tutti i file XML esistenti
seguendo il ciclo di vita tipico dell'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 l'elemento sequence
, l'elemento choice
e gli elementi all
come
elementi secondari dell'elemento complexType
. Tuttavia, questi elementi secondari differiscono per il numero e l'ordine degli elementi secondari, quindi la modifica di un tipo esistente sarebbe una modifica incompatibile.
Se vuoi modificare un tipo esistente, la best practice è ritirare il tipo precedente e introdurre un nuovo tipo 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 di Mainline
Mainline è un progetto che consente di aggiornare i sottosistemi ("moduli mainline") del sistema operativo Android singolarmente, anziché aggiornare l'intera immagine di sistema.
I moduli Mainline devono essere "separati" dalla piattaforma principale, 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).
Esistono determinati pattern di progettazione che i moduli mainline devono seguire. Questa sezione li descrive.
Il pattern <Module>FrameworkInitializer
Se un modulo mainline deve esporre classi @SystemService
(ad esempio,
JobScheduler
), utilizza il seguente pattern:
Esporre una classe
<YourModule>FrameworkInitializer
dal modulo. Questa classe deve essere in$BOOTCLASSPATH
. Esempio: StatsFrameworkInitializerContrassegnalo con
@SystemApi(client = MODULE_LIBRARIES)
.Aggiungi un metodo
public static void registerServiceWrappers()
.Utilizza
SystemServiceRegistry.registerContextAwareService()
per registrare una classe di service manager quando ha bisogno di un riferimento a unContext
.Utilizza
SystemServiceRegistry.registerStaticService()
per registrare una classe di service manager quando non è necessario un riferimento a unContext
.Chiama il metodo
registerServiceWrappers()
dall'inizializzatore statico diSystemServiceRegistry
.
Il pattern <Module>ServiceManager
Normalmente, per registrare oggetti binder di servizi di sistema o ottenere riferimenti
a questi, si utilizzerebbe
ServiceManager
,
ma i moduli mainline non possono utilizzarlo perché è nascosto. Questa classe è nascosta
perché i moduli principali non devono registrare o fare riferimento a oggetti binder di servizi di sistema
esposti dalla piattaforma statica o da altri moduli.
I moduli Mainline possono utilizzare il seguente pattern per registrarsi e ottenere riferimenti ai servizi binder implementati all'interno del modulo.
Crea una classe
<YourModule>ServiceManager
seguendo la progettazione di TelephonyServiceManagerEsporre la classe come
@SystemApi
. Se devi accedervi solo dalle classi$BOOTCLASSPATH
o dalle classi del server di sistema, puoi utilizzare@SystemApi(client = MODULE_LIBRARIES)
; altrimenti,@SystemApi(client = PRIVILEGED_APPS)
funzionerà.Questo corso è composto da:
- Un costruttore nascosto, in modo che solo il codice della piattaforma statica possa 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, ti servono due getter. - In
ActivityThread.initializeMainlineModules()
, crea un'istanza di questa classe e passala a un metodo statico esposto dal tuo modulo. In genere, aggiungi un'API@SystemApi(client = MODULE_LIBRARIES)
statica nella classeFrameworkInitializer
che la prende.
Questo pattern impedisce ad altri moduli mainline di accedere a queste API
perché non esiste un modo per 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 binder di servizio nel codice nativo, utilizza
le API native AServiceManager
.
Queste API corrispondono alle API Java ServiceManager
, ma quelle native sono
esposte direttamente ai moduli mainline. Non utilizzarli per registrare o fare riferimento a
oggetti binder che non sono di proprietà del tuo modulo. Se esponi un oggetto binder
da nativo, il tuo <YourModule>ServiceManager.ServiceRegisterer
non ha bisogno di un
metodo register()
.
Definizioni delle autorizzazioni nei moduli Mainline
I moduli Mainline contenenti APK possono definire autorizzazioni (personalizzate) nel proprio APK
AndroidManifest.xml
nello stesso modo di un APK normale.
Se l'autorizzazione definita viene utilizzata solo internamente a un modulo, il suo nome deve avere come prefisso il 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 statica della piattaforma) più il nome del pacchetto del modulo, per segnalare che si tratta di un'API della 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 API nella sua superficie API, ad esempio
HealthPermissions.READ_ACTIVE_CALORIES_BURNED
.