Ta strona ma pomóc programistom w zrozumieniu ogólnych zasad, których Rada ds. API przestrzega podczas sprawdzania interfejsów API.
Oprócz przestrzegania tych wytycznych podczas pisania interfejsów API deweloperzy powinni uruchamiać narzędzie API Lint, które zawiera wiele z tych reguł w postaci testów przeprowadzanych na interfejsach API.
Możesz to traktować jako przewodnik po regułach, których przestrzega to narzędzie Lint, a także ogólne porady dotyczące reguł, których nie można w nim zakodować z dużą dokładnością.
Narzędzie API Lint
API Lint jest zintegrowany z narzędziem do analizy statycznej Metalava i działa automatycznie podczas weryfikacji w CI. Możesz go uruchomić ręcznie z lokalnego procesu płatności na platformie za pomocą m
checkapi lub z lokalnego procesu płatności AndroidX za pomocą ./gradlew :path:to:project:checkApi.
Reguły interfejsu API
Platforma Android i wiele bibliotek Jetpack istniały już przed utworzeniem tego zestawu wytycznych, a zasady przedstawione w dalszej części tej strony są stale rozwijane, aby sprostać potrzebom ekosystemu Androida.
W związku z tym niektóre interfejsy API mogą nie być zgodne z wytycznymi. W innych przypadkach deweloperom aplikacji może być wygodniej, jeśli nowy interfejs API będzie spójny z dotychczasowymi interfejsami API, a nie ściśle zgodny z wytycznymi.
W razie trudnych pytań dotyczących interfejsu API, które wymagają rozwiązania, lub wytycznych, które należy zaktualizować, skontaktuj się z Radą ds. API.
Podstawowe informacje o interfejsie API
Ta kategoria dotyczy podstawowych aspektów interfejsu Android API.
Wszystkie interfejsy API muszą być wdrożone
Niezależnie od odbiorców interfejsu API (np. publicznych lub @SystemApi) wszystkie powierzchnie interfejsu API muszą być zaimplementowane, gdy są scalane lub udostępniane jako interfejs API. Nie scalaj stubów interfejsu API z implementacją, która zostanie wprowadzona w późniejszym terminie.
Interfejsy API bez implementacji mają wiele problemów:
- Nie ma gwarancji, że odpowiednia lub pełna powierzchnia została odsłonięta. Dopóki interfejs API nie zostanie przetestowany lub użyty przez klientów, nie ma możliwości sprawdzenia, czy klient ma odpowiednie interfejsy API, aby móc korzystać z tej funkcji.
- Interfejsów API bez implementacji nie można testować w wersjach przedpremierowych dla programistów.
- Interfejsów API bez implementacji nie można testować w pakiecie CTS.
Wszystkie interfejsy API muszą zostać przetestowane
Jest to zgodne z wymaganiami CTS platformy, zasadami AndroidX i ogólną zasadą, że interfejsy API muszą być zaimplementowane.
Testowanie interfejsów API daje podstawową gwarancję, że interfejs API jest użyteczny i że uwzględniliśmy oczekiwane przypadki użycia. Sprawdzenie, czy interfejs API istnieje, nie wystarczy. Musisz przetestować jego działanie.
Zmiana, która dodaje nowy interfejs API, powinna zawierać odpowiednie testy w tym samym CL lub temacie Gerrit.
Interfejsy API powinny być też testowalne. Powinieneś(-aś) umieć odpowiedzieć na pytanie: „Jak deweloper aplikacji może przetestować kod korzystający z Twojego interfejsu API?”.
Wszystkie interfejsy API muszą być udokumentowane
Dokumentacja jest kluczowym elementem użyteczności interfejsu API. Składnia interfejsu API może wydawać się oczywista, ale nowi klienci nie będą rozumieć semantyki, działania ani kontekstu interfejsu API.
Wszystkie wygenerowane interfejsy API muszą być zgodne z wytycznymi.
Interfejsy API wygenerowane przez narzędzia muszą być zgodne z tymi samymi wytycznymi co kod napisany ręcznie.
Narzędzia, których nie zalecamy do generowania interfejsów API:
AutoValue: narusza wytyczne na różne sposoby, np. nie ma możliwości implementacji klas wartości końcowych ani konstruktorów końcowych w sposób, w jaki działa AutoValue.
Styl kodu
Ta kategoria dotyczy ogólnego stylu kodu, którego deweloperzy powinni używać, zwłaszcza podczas pisania publicznych interfejsów API.
Przestrzegaj standardowych konwencji kodowania, z wyjątkiem sytuacji, w których podano inne informacje.
Konwencje kodowania w Androidzie są udokumentowane dla zewnętrznych współtwórców tutaj:
https://source.android.com/source/code-style.html
Ogólnie rzecz biorąc, stosujemy standardowe konwencje kodowania w językach Java i Kotlin.
Akronimy w nazwach metod nie powinny być pisane wielką literą
Na przykład nazwa metody powinna mieć postać runCtsTests, a nie runCTSTests.
Nazwy nie powinny kończyć się ciągiem znaków Impl
Ujawnia to szczegóły implementacji, więc należy tego unikać.
Zajęcia
W tej sekcji opisujemy reguły dotyczące klas, interfejsów i dziedziczenia.
dziedziczyć nowe klasy publiczne z odpowiedniej klasy bazowej;
Dziedziczenie udostępnia w klasie podrzędnej elementy interfejsu API, które mogą być nieodpowiednie.
Na przykład nowa publiczna podklasa FrameLayout wygląda tak: FrameLayout
plus nowe zachowania i elementy interfejsu API. Jeśli odziedziczony interfejs API nie jest odpowiedni w Twoim przypadku, odziedzicz go z klasy znajdującej się wyżej w drzewie, np. ViewGroup lub View.
Jeśli chcesz zastąpić metody z klasy bazowej, aby zgłosić wyjątek UnsupportedOperationException, zastanów się, której klasy bazowej używasz.
Używanie podstawowych klas kolekcji
Niezależnie od tego, czy kolekcja jest argumentem, czy zwracaną wartością, zawsze preferuj klasę bazową od konkretnej implementacji (np. zwracaj List<Foo> zamiast ArrayList<Foo>).
Użyj klasy bazowej, która wyraża odpowiednie ograniczenia interfejsu API. Na przykład użyj symbolu List w przypadku interfejsu API, którego kolekcja musi być uporządkowana, a symbolu Set w przypadku interfejsu API, którego kolekcja musi składać się z unikalnych elementów.
W języku Kotlin preferuj kolekcje niezmienne. Więcej informacji znajdziesz w sekcji Zmienność kolekcji.
Klasy abstrakcyjne a interfejsy
Java 8 dodaje obsługę domyślnych metod interfejsu, co pozwala projektantom interfejsów API dodawać metody do interfejsów przy zachowaniu zgodności binarnej. Kod platformy i wszystkie biblioteki Jetpack powinny być kierowane na Javę 8 lub nowszą.
W przypadku, gdy domyślna implementacja jest bezstanowa, projektanci interfejsów API powinni preferować interfejsy zamiast klas abstrakcyjnych – domyślne metody interfejsu można implementować jako wywołania innych metod interfejsu.
W przypadkach, gdy domyślna implementacja wymaga konstruktora lub stanu wewnętrznego, należy użyć klas abstrakcyjnych.
W obu przypadkach projektanci interfejsu API mogą pozostawić jedną metodę abstrakcyjną, aby uprościć użycie jako wyrażenia 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) { }
}
Nazwy klas powinny odzwierciedlać to, co rozszerzają
Na przykład klasy, które rozszerzają Service, powinny mieć nazwę FooService, aby były bardziej czytelne:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
Przyrostki ogólne
Unikaj używania ogólnych sufiksów nazw klas, takich jak Helper i Util, w przypadku kolekcji metod narzędziowych. Zamiast tego umieść metody bezpośrednio w powiązanych klasach lub w funkcjach rozszerzających Kotlin.
W przypadku metod łączących wiele klas nadaj klasie zawierającej rozpoznawalną nazwę, która wyjaśnia, co robi.
W bardzo rzadkich przypadkach użycie sufiksu Helper może być odpowiednie:
- Używana do tworzenia domyślnego działania
- Może obejmować delegowanie istniejącego zachowania do nowych klas
- Może wymagać stanu trwałego
- Zwykle obejmuje
View
Jeśli na przykład portowanie wsteczne etykietek wymaga zachowania stanu powiązanego z elementem View i wywołania kilku metod w View w celu zainstalowania portowania wstecznego, TooltipHelper będzie odpowiednią nazwą klasy.
Nie udostępniaj bezpośrednio kodu wygenerowanego przez IDL jako publicznych interfejsów API
Zachowaj kod wygenerowany przez IDL jako szczegóły implementacji. Obejmuje to protobuf, gniazda, FlatBuffers i inne interfejsy API inne niż Java i NDK. Większość IDL w Androidzie jest w AIDL, więc ta strona skupia się na AIDL.
Wygenerowane klasy AIDL nie spełniają wymagań przewodnika po stylu interfejsu API (np. nie mogą używać przeciążania), a narzędzie AIDL nie zostało zaprojektowane z myślą o zachowaniu zgodności interfejsu API z językiem, więc nie można ich osadzać w publicznym interfejsie API.
Zamiast tego dodaj publiczną warstwę interfejsu API na interfejsie AIDL, nawet jeśli początkowo będzie to tylko powłoka.
Interfejsy bindera
Jeśli interfejs Binder jest szczegółem implementacji, można go w przyszłości dowolnie zmieniać, a warstwa publiczna umożliwia zachowanie wymaganej zgodności wstecznej. Może być na przykład konieczne dodanie nowych argumentów do wywołań wewnętrznych lub zoptymalizowanie ruchu IPC przez użycie przetwarzania wsadowego lub strumieniowego, pamięci współdzielonej itp. Żadnej z tych czynności nie można wykonać, jeśli interfejs AIDL jest też publicznym interfejsem API.
Nie udostępniaj na przykład FooService bezpośrednio jako publicznego interfejsu API:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
Zamiast tego umieść interfejs Binder w klasie menedżera lub innej klasie:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
Jeśli później do tego wywołania będzie potrzebny nowy argument, interfejs wewnętrzny może być minimalny, a do publicznego interfejsu API można dodać wygodne przeciążenia. Warstwy opakowującej możesz używać do rozwiązywania innych problemów ze zgodnością wsteczną w miarę rozwoju implementacji:
/**
* @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);
}
}
W przypadku interfejsów Binder, które nie są częścią platformy Android (np. interfejsu usługi eksportowanego przez Usługi Google Play do użytku przez aplikacje), wymóg stabilnego, opublikowanego i wersjonowanego interfejsu IPC oznacza, że znacznie trudniej jest rozwijać sam interfejs. Warto jednak utworzyć warstwę otoki, aby dopasować ją do innych wytycznych dotyczących interfejsów API i ułatwić korzystanie z tego samego publicznego interfejsu API w przypadku nowej wersji interfejsu IPC, jeśli kiedykolwiek zajdzie taka potrzeba.
Nie używaj surowych obiektów Binder w publicznym interfejsie API
Obiekt Binder sam w sobie nie ma żadnego znaczenia, dlatego nie należy go używać w publicznym interfejsie API. Jednym z częstych przypadków użycia jest użycie Binder lub IBinder jako tokena, ponieważ ma on semantykę tożsamości. Zamiast używać surowego obiektu Binder, użyj klasy tokena opakowującego.
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() {...}
}
Klasy menedżera muszą być ostateczne
Klasy menedżera powinny być deklarowane jako final. Klasy menedżera komunikują się z usługami systemowymi i stanowią pojedynczy punkt interakcji. Nie ma potrzeby dostosowywania, więc zadeklaruj ją jako final.
Nie używaj interfejsów CompletableFuture ani Future
java.util.concurrent.CompletableFuture ma dużą powierzchnię interfejsu API, która umożliwia dowolną zmianę wartości przyszłej i ma domyślne ustawienia podatne na błędy.
Z kolei w przypadku java.util.concurrent.Future brakuje nasłuchiwania nieblokującego, co utrudnia korzystanie z kodu asynchronicznego.
W kodzie platformy i interfejsach API bibliotek niskiego poziomu używanych zarówno przez Kotlin, jak i Javę preferuj połączenie wywołania zwrotnego zakończenia Executor i, jeśli interfejs API obsługuje anulowanie, CancellationSignal.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
Jeśli kierujesz reklamy na język Kotlin, używaj funkcji suspend.
suspend fun asyncLoadFoo(): Foo
W bibliotekach integracji specyficznych dla Javy możesz używać biblioteki GuavaListenableFuture.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
Nie używaj opcjonalnych
Chociaż Optional może mieć zalety w niektórych interfejsach API, jest niespójny z obecnym obszarem interfejsu API Androida. @Nullable i @NonNull zapewniają narzędzia ułatwiające zachowanie bezpieczeństwa null, a Kotlin wymusza umowy dotyczące dopuszczalności wartości null na poziomie kompilatora, co sprawia, że Optional jest zbędne.
W przypadku opcjonalnych typów prostych używaj sparowanych metod has i get. Jeśli wartość nie jest ustawiona (has zwraca false), metoda get powinna zgłosić wyjątek IllegalStateException.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
Używanie prywatnych konstruktorów w przypadku klas, których nie można utworzyć
Klasy, które mogą być tworzone tylko przez Builder, klasy zawierające tylko stałe lub metody statyczne albo klasy, których nie można utworzyć, powinny zawierać co najmniej jeden konstruktor prywatny, aby zapobiec tworzeniu instancji za pomocą domyślnego konstruktora bez argumentów.
public final class Log {
// Not instantiable.
private Log() {}
}
Singletony
Singletony są odradzane, ponieważ mają te wady związane z testowaniem:
- Konstrukcja jest zarządzana przez klasę, co uniemożliwia używanie fałszywych danych.
- Testy nie mogą być hermetyczne ze względu na statyczny charakter pojedynczej instancji
- Aby obejść te problemy, deweloperzy muszą znać wewnętrzne szczegóły singletonu lub utworzyć wokół niego otokę.
Zalecamy stosowanie wzorca pojedynczej instancji, który wykorzystuje abstrakcyjną klasę bazową do rozwiązania tych problemów.
Jedna instancja
Klasy z jedną instancją używają abstrakcyjnej klasy bazowej z konstruktorem private lub internal i udostępniają statyczną metodę getInstance() do uzyskiwania instancji. Metoda getInstance() musi zwracać ten sam obiekt przy kolejnych wywołaniach.
Obiekt zwracany przez getInstance() powinien być prywatną implementacją abstrakcyjnej klasy bazowej.
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
}
}
}
Pojedyncza instancja różni się od singletonu tym, że deweloperzy mogą utworzyć fałszywą wersję SingleInstance i użyć własnego frameworka wstrzykiwania zależności do zarządzania implementacją bez konieczności tworzenia otoki. Biblioteka może też udostępniać własną fałszywą wersję w artefakcie -testing.
Klasy, które zwalniają zasoby, powinny implementować interfejs AutoCloseable
Klasy, które zwalniają zasoby za pomocą metod close, release, destroy lub podobnych, powinny implementować interfejs java.lang.AutoCloseable, aby umożliwić programistom automatyczne zwalnianie miejsca dla tych zasobów podczas korzystania z bloku try-with-resources.
Unikaj wprowadzania nowych podklas View w pakiecie android.*.
Nie wprowadzaj nowych klas, które dziedziczą bezpośrednio lub pośrednio z klasy
android.view.View w publicznym interfejsie API platformy (czyli w android.*).
Zestaw narzędzi interfejsu Androida jest teraz oparty na Compose. Nowe funkcje interfejsu udostępniane przez platformę powinny być udostępniane jako interfejsy API niższego poziomu, których można używać do implementowania komponentów interfejsu Jetpack Compose i opcjonalnie komponentów interfejsu opartych na widokach dla programistów w bibliotekach Jetpack. Udostępnianie tych komponentów w bibliotekach stwarza możliwości implementacji wstecznych, gdy funkcje platformy są niedostępne.
Pola
Te reguły dotyczą pól publicznych w klasach.
Nie udostępniaj nieprzetworzonych pól
Klasy Java nie powinny bezpośrednio udostępniać pól. Pola powinny być prywatne i dostępne tylko za pomocą publicznych metod pobierających i ustawiających, niezależnie od tego, czy są one ostateczne.
Rzadkie wyjątki obejmują podstawowe struktury danych, w przypadku których nie ma potrzeby ulepszania sposobu określania lub pobierania pola. W takich przypadkach pola powinny mieć nazwy zgodne ze standardowymi konwencjami nazewnictwa zmiennych, np. Point.x i Point.y.
Klasy Kotlin mogą udostępniać właściwości.
Pola udostępniane powinny być oznaczone jako finalne
Pola surowe są zdecydowanie odradzane (@see Nie udostępniaj pól surowych). Jeśli jednak w rzadkich przypadkach pole jest widoczne jako pole publiczne, oznacz je symbolem final.
Pola wewnętrzne nie powinny być widoczne
Nie odwołuj się do wewnętrznych nazw pól w publicznym interfejsie API.
public int mFlags;
Używanie dostępu publicznego zamiast chronionego
@see Use public instead of protected
Stałe
Są to reguły dotyczące stałych publicznych.
Stałe flagi nie powinny nakładać się na wartości int lub long
Flagi to bity, które można łączyć w pewną wartość sumy. W przeciwnym razie nie nazywaj zmiennej ani stałej 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;
Więcej informacji o definiowaniu stałych flag publicznych znajdziesz w sekcji @IntDef dotyczącej flag masek bitowych.
stałe statyczne powinny być zgodne z konwencją nazewnictwa z użyciem wielkich liter i podkreślników,
Wszystkie słowa w stałej powinny być pisane wielkimi literami, a poszczególne słowa powinny być oddzielone znakiem _. Przykład:
public static final int fooThing = 5
public static final int FOO_THING = 5
Używanie standardowych prefiksów dla stałych
Wiele stałych używanych w Androidzie dotyczy standardowych elementów, takich jak flagi, klucze i działania. Stałe te powinny mieć standardowe prefiksy, aby można je było łatwiej rozpoznać.
Na przykład dodatki do intencji powinny zaczynać się od znaku EXTRA_. Działania związane z intencjami powinny zaczynać się od ACTION_. Stałe używane z Context.bindService() powinny zaczynać się od BIND_.
Nazwy i zakresy kluczowych stałych
Wartości stałych ciągów znaków powinny być zgodne z samą nazwą stałej i zwykle powinny być ograniczone do pakietu lub domeny. Przykład:
public static final String FOO_THING = "foo"
nie ma spójnej nazwy ani odpowiedniego zakresu. Zamiast tego rozważ:
public static final String FOO_THING = "android.fooservice.FOO_THING"
Prefiksy android w stałych ciągach znaków o określonym zakresie są zarezerwowane dla Projektu Android Open Source.
Działania i dodatki intencji oraz wpisy pakietu powinny mieć przestrzeń nazw z nazwą pakietu, w którym są zdefiniowane.
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"
}
Używanie dostępu publicznego zamiast chronionego
@see Use public instead of protected
Używanie spójnych prefiksów
Powiązane stałe powinny zaczynać się od tego samego prefiksu. Na przykład w przypadku zbioru stałych do użycia z wartościami 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 Używanie standardowych prefiksów dla stałych
Używanie spójnych nazw zasobów
Identyfikatory publiczne, atrybuty i wartości muszą być nazwane zgodnie z konwencją camelCase, np. @id/accessibilityActionPageUp lub @attr/textAppearance, podobnie jak pola publiczne w języku Java.
W niektórych przypadkach publiczny identyfikator lub atrybut zawiera wspólny prefiks oddzielony podkreśleniem:
- Wartości konfiguracji platformy, np.
@string/config_recentsComponentNamew pliku config.xml - Atrybuty widoku specyficzne dla układu, takie jak
@attr/layout_marginStartw pliku attrs.xml
Publiczne motywy i style muszą być zgodne z hierarchiczną konwencją nazewnictwa PascalCase, np. @style/Theme.Material.Light.DarkActionBar lub @style/Widget.Material.SearchView.ActionBar, podobnie jak klasy zagnieżdżone w języku Java.
Zasoby układu i zasoby rysowalne nie powinny być udostępniane jako publiczne interfejsy API. Jeśli jednak muszą być udostępniane, publiczne układy i elementy rysunkowe muszą być nazwane zgodnie z konwencją nazewnictwa under_score, np. layout/simple_list_item_1.xml lub drawable/title_bar_tall.xml.
Gdy stałe wartości mogą się zmienić, ustaw je jako dynamiczne
Kompilator może wstawiać wartości stałe, więc zachowanie tych samych wartości jest uważane za część umowy API. Jeśli wartość stałej MIN_FOO lub MAX_FOO może się w przyszłości zmienić, rozważ użycie zamiast niej metody dynamicznej.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
Zadbaj o zgodność wywołań zwrotnych z przyszłymi wersjami
Stałe zdefiniowane w przyszłych wersjach interfejsu API nie są znane aplikacjom kierowanym na starsze interfejsy API. Z tego powodu stałe dostarczane do aplikacji powinny uwzględniać docelową wersję interfejsu API aplikacji i mapować nowsze stałe na spójną wartość. Rozważmy ten scenariusz:
Przykładowe źródło pakietu SDK:
// 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;
Hipotetyczna aplikacja z targetSdkVersion="22":
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
W tym przypadku aplikacja została zaprojektowana w ramach ograniczeń poziomu API 22 i przyjęła (dość) rozsądne założenie, że istnieją tylko 2 możliwe stany. Jeśli jednak aplikacja otrzyma nowo dodany kod STATUS_FAILURE_RETRY, zinterpretuje to jako sukces.
Metody zwracające stałe wartości mogą bezpiecznie obsługiwać takie przypadki, ograniczając dane wyjściowe do poziomu interfejsu API, na który jest kierowana aplikacja:
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;
}
Deweloperzy nie mogą przewidzieć, czy lista stałych może się w przyszłości zmienić. Jeśli zdefiniujesz interfejs API ze stałą UNKNOWN lub UNSPECIFIED, która wygląda jak stała ogólna, deweloperzy założą, że opublikowane stałe w momencie pisania aplikacji są wyczerpujące. Jeśli nie chcesz tego robić, zastanów się, czy stała typu catch-all jest dobrym rozwiązaniem w przypadku Twojego interfejsu API.
Biblioteki nie mogą też określać własnych targetSdkVersion niezależnie od aplikacji, a obsługa targetSdkVersion zmian zachowania w kodzie biblioteki jest skomplikowana i podatna na błędy.
Stała w postaci liczby całkowitej lub ciągu tekstowego
Używaj stałych całkowitych i @IntDef, jeśli przestrzeń nazw wartości nie jest rozszerzalna poza pakiet. Używaj stałych ciągów znaków, jeśli przestrzeń nazw jest współdzielona lub może być rozszerzana przez kod spoza pakietu.
Klasy danych
Klasy danych reprezentują zestaw niezmiennych właściwości i zapewniają mały i dobrze zdefiniowany zestaw funkcji narzędziowych do interakcji z tymi danymi.
Nie używaj data class w publicznych interfejsach API w Kotlinie, ponieważ kompilator tego języka nie gwarantuje zgodności interfejsu API języka ani zgodności binarnej wygenerowanego kodu. Zamiast tego ręcznie zaimplementuj wymagane funkcje.
Utworzenie instancji
W języku Java klasy danych powinny udostępniać konstruktor, gdy jest niewiele właściwości, lub używać wzorca Builder, gdy jest ich wiele.
W języku Kotlin klasy danych powinny udostępniać konstruktor z argumentami domyślnymi niezależnie od liczby właściwości. Klasy danych zdefiniowane w Kotlinie mogą również korzystać z udostępniania konstruktora podczas kierowania na klientów Java.
Modyfikowanie i kopiowanie
Jeśli dane wymagają modyfikacji, podaj klasę Builder z konstruktorem kopiującym (Java) lub funkcję copy() (Kotlin), która zwraca nowy obiekt.
Podczas podawania funkcji copy() w języku Kotlin argumenty muszą być zgodne z konstruktorem klasy, a wartości domyślne muszą być wypełniane przy użyciu bieżących wartości obiektu:
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
)
}
Dodatkowe zachowania
Klasy danych powinny implementować zarówno equals(), jak i hashCode(), a każda właściwość musi być uwzględniona w implementacjach tych metod.
Klasy danych mogą implementować toString() w zalecanym formacie
pasującym do implementacji klasy danych w Kotlinie
np. User(var1=Alex, var2=42).
Metody
Są to reguły dotyczące różnych szczegółów w metodach, parametrów, nazw metod, typów zwracanych i specyfikatorów dostępu.
Godzina
Określają one, jak w interfejsach API powinny być wyrażane pojęcia związane z czasem, takie jak daty i czas trwania.
W miarę możliwości używaj typów java.time.*
java.time.Duration, java.time.Instant i wiele innych typów java.time.* jest dostępnych we wszystkich wersjach platformy dzięki odcukrzaniu i powinny być preferowane podczas wyrażania czasu w parametrach interfejsu API lub zwracanych wartościach.
Zalecamy udostępnianie tylko tych wariantów interfejsu API, które akceptują lub zwracają java.time.Duration lub java.time.Instant, i pomijanie wariantów pierwotnych o tym samym zastosowaniu, chyba że domena interfejsu API jest taka, w której alokacja obiektów w zamierzonych wzorcach użycia miałaby niekorzystny wpływ na wydajność.
Metody wyrażające czas trwania powinny mieć nazwę duration
Jeśli wartość czasu wyraża czas trwania, nazwij parametr „duration”, a nie „time”.
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
Wyjątki:
Wartość „timeout” jest odpowiednia, gdy czas trwania dotyczy konkretnie wartości limitu czasu.
Wartość „time” o typie java.time.Instant jest odpowiednia, gdy odnosi się do konkretnego momentu w czasie, a nie do czasu trwania.
Metody wyrażające czas trwania lub czas jako typ prosty powinny mieć w nazwie jednostkę czasu i używać typu long.
Metody, które przyjmują lub zwracają czas trwania jako typ prosty, powinny mieć w nazwie przyrostek z odpowiednimi jednostkami czasu (np. Millis, Nanos, Seconds), aby zarezerwować nazwę bez przyrostka do użycia z java.time.Duration. Zobacz Czas.
Metody powinny być też odpowiednio oznaczone jednostką i podstawą czasu:
@CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba milisekund od 1970-01-01T00:00:00Z.@CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba sekund od 1970-01-01T00:00:00Z.@DurationMillisLong: wartość to nieujemny czas trwania w milisekundach.@ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa wSystemClock.elapsedRealtime().@UptimeMillisLong: wartość to nieujemna sygnatura czasowa wSystemClock.uptimeMillis().
Parametry czasu pierwotnego lub wartości zwracane powinny używać znaku long, a nie int.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
Metody wyrażające jednostki czasu powinny używać nieabbreviowanych skrótów nazw jednostek.
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
Dodawanie adnotacji do argumentów dotyczących długiego czasu
Platforma zawiera kilka adnotacji, które zapewniają silniejsze typowanie jednostek czasu typu long:
@CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba milisekund od1970-01-01T00:00:00Z, a więc w bazie czasuSystem.currentTimeMillis().@CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba sekund od1970-01-01T00:00:00Z.@DurationMillisLong: wartość to nieujemny czas trwania w milisekundach.@ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa wSystemClock#elapsedRealtime().@UptimeMillisLong: wartość to nieujemna sygnatura czasowa wSystemClock#uptimeMillis().
Jednostki miary
W przypadku wszystkich metod wyrażających jednostkę miary inną niż czas preferuj prefiksy jednostek SI w formacie CamelCase .
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
Umieszczaj opcjonalne parametry na końcu przeciążeń.
Jeśli masz przeciążenia metody z parametrami opcjonalnymi, umieść je na końcu i zachowaj spójną kolejność z innymi parametrami:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
Podczas dodawania przeciążeń dla argumentów opcjonalnych prostsze metody powinny działać dokładnie tak samo, jak gdyby do bardziej złożonych metod zostały przekazane argumenty domyślne.
Wniosek: nie przeciążaj metod, chyba że chcesz dodać argumenty opcjonalne lub akceptować różne typy argumentów, jeśli metoda jest polimorficzna. Jeśli przeciążona metoda robi coś zasadniczo innego, nadaj jej nową nazwę.
Metody z parametrami domyślnymi muszą być oznaczone adnotacją @JvmOverloads (tylko w przypadku języka Kotlin).
Metody i konstruktory z parametrami domyślnymi muszą być oznaczone adnotacją
@JvmOverloads, aby zachować zgodność binarną.
Więcej informacji znajdziesz w sekcji Function overloads for defaults w oficjalnym przewodniku po interoperacyjności Kotlin-Java.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
Nie usuwaj domyślnych wartości parametrów (tylko Kotlin)
Jeśli metoda została dostarczona z parametrem o wartości domyślnej, usunięcie tej wartości jest zmianą powodującą niezgodność kodu źródłowego.
Najbardziej charakterystyczne i identyfikujące parametry metody powinny być podane jako pierwsze.
Jeśli masz metodę z wieloma parametrami, umieść na początku te, które są najbardziej istotne. Parametry określające flagi i inne opcje są mniej ważne niż te, które opisują obiekt, na którym wykonywana jest operacja. Jeśli istnieje wywołanie zwrotne zakończenia, umieść je na końcu.
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);
Zobacz też: Umieszczanie parametrów opcjonalnych na końcu w przypadku przeciążeń
Budowniczowie
Wzorzec Builder jest zalecany do tworzenia złożonych obiektów Java i jest powszechnie używany w Androidzie w przypadkach, gdy:
- Właściwości wynikowego obiektu powinny być niezmienne.
- Wymaganych jest wiele właściwości, np. wiele argumentów konstruktora.
- Istnieje złożona relacja między usługami w momencie tworzenia, np. wymagany jest krok weryfikacji. Pamiętaj, że ten poziom złożoności często wskazuje na problemy z użytecznością interfejsu API.
Zastanów się, czy potrzebujesz montera. Obiekty Builder są przydatne w interfejsie API, jeśli służą do:
- skonfigurować tylko kilka z potencjalnie dużego zbioru opcjonalnych parametrów tworzenia,
- skonfigurować wiele różnych opcjonalnych lub wymaganych parametrów tworzenia, czasami podobnych lub pasujących do siebie, w przypadku których witryny wywołań mogłyby być trudne do odczytania lub podatne na błędy podczas pisania.
- Skonfiguruj tworzenie obiektu przyrostowo, gdzie kilka różnych fragmentów kodu konfiguracyjnego może wywoływać konstruktora jako szczegóły implementacji.
- Zezwalaj na rozwijanie typu przez dodawanie dodatkowych opcjonalnych parametrów tworzenia w przyszłych wersjach interfejsu API.
Jeśli masz typ z maksymalnie 3 wymaganymi parametrami i bez parametrów opcjonalnych, prawie zawsze możesz pominąć narzędzie do tworzenia i użyć zwykłego konstruktora.
W przypadku klas pochodzących z KOTLIN-a należy preferować konstruktory z adnotacją @JvmOverloads z argumentami domyślnymi zamiast konstruktorów, ale można też poprawić użyteczność dla klientów Java, udostępniając konstruktory w opisanych wcześniej przypadkach.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
Klasy narzędzi do tworzenia muszą zwracać narzędzie do tworzenia
Klasy narzędzia do tworzenia muszą umożliwiać łączenie metod przez zwracanie obiektu narzędzia do tworzenia (np. this) z każdej metody z wyjątkiem build(). Dodatkowe wbudowane obiekty należy przekazywać jako argumenty – nie zwracaj konstruktora innego obiektu.
Przykład:
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();
}
}
W rzadkich przypadkach, gdy klasa bazowa kreatora musi obsługiwać rozszerzenie, użyj ogólnego typu zwracanego:
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);
}
Klasy narzędzi do tworzenia muszą być tworzone za pomocą konstruktora.
Aby zachować spójność tworzenia narzędzi do tworzenia za pomocą interfejsu Android API, wszystkie narzędzia muszą być tworzone za pomocą konstruktora, a nie statycznej metody tworzenia. W przypadku interfejsów API opartych na Kotlinie pole Builder musi być publiczne, nawet jeśli użytkownicy Kotlina mają niejawnie korzystać z konstruktora za pomocą metody fabrykującej lub mechanizmu tworzenia w stylu DSL. Biblioteki nie mogą używać adnotacji @PublishedApi internal do selektywnego ukrywania konstruktora klasy Builder przed klientami Kotlin.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
Wszystkie argumenty konstruktorów narzędzi do tworzenia muszą być wymagane (np. @NonNull).
Opcjonalne argumenty, np. @Nullable, należy przenieść do metod ustawiających.
Konstruktor narzędzia do tworzenia powinien zgłaszać wyjątek NullPointerException (rozważ użycie Objects.requireNonNull), jeśli nie podano żadnych wymaganych argumentów.
Klasy budujące powinny być statycznymi klasami wewnętrznymi typu finalnego
Ze względu na logiczną organizację w pakiecie klasy konstruktora powinny być zwykle udostępniane jako wewnętrzne klasy końcowe typów, które tworzą, np. Tone.Builder zamiast ToneBuilder.
Konstruktory mogą zawierać konstruktor do tworzenia nowej instancji na podstawie istniejącej instancji.
Kreatory mogą zawierać konstruktor kopiujący, który tworzy nową instancję kreatora na podstawie istniejącego kreatora lub utworzonego obiektu. Nie powinni udostępniać alternatywnych metod tworzenia instancji konstruktora na podstawie istniejących konstruktorów lub obiektów do tworzenia.
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);
}
}
Metody ustawiające w klasie Builder powinny przyjmować argumenty @Nullable, jeśli klasa Builder ma konstruktor kopiujący
Resetowanie jest niezbędne, jeśli na podstawie istniejącej instancji może zostać utworzona nowa instancja narzędzia do tworzenia. Jeśli nie jest dostępny żaden konstruktor kopiujący, konstruktor może mieć argumenty @Nullable lub @NonNullable.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
Metody ustawiające w klasie Builder mogą przyjmować argumenty @Nullable w przypadku właściwości opcjonalnych.
Często łatwiej jest użyć wartości dopuszczającej wartość null w przypadku danych wejściowych drugiego stopnia, zwłaszcza w języku Kotlin, który zamiast konstruktorów i przeciążeń używa argumentów domyślnych.
Dodatkowo @Nullable ustawiające będą pasować do ich pobierających, które muszą być @Nullable w przypadku właściwości opcjonalnych.
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();
}
Typowe użycie w języku Kotlin:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
Wartość domyślna (jeśli funkcja ustawiająca nie jest wywoływana) i znaczenie null muszą być odpowiednio udokumentowane zarówno w funkcji ustawiającej, jak i pobierającej.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
W przypadku właściwości, które można zmieniać, można podać ustawiające funkcje konstruktora, jeśli w zbudowanej klasie są dostępne funkcje ustawiające.
Jeśli Twoja klasa ma właściwości, które można zmieniać, i potrzebuje klasy Builder, najpierw zastanów się, czy Twoja klasa rzeczywiście powinna mieć właściwości, które można zmieniać.
Następnie, jeśli masz pewność, że potrzebujesz właściwości modyfikowalnych, zdecyduj, który z tych scenariuszy lepiej pasuje do Twojego przypadku użycia:
Zbudowany obiekt powinien być od razu gotowy do użycia, dlatego dla wszystkich odpowiednich właściwości, zarówno modyfikowalnych, jak i niemodyfikowalnych, należy podać metody ustawiające.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());Zanim utworzony obiekt będzie przydatny, może być konieczne wykonanie dodatkowych wywołań, dlatego w przypadku właściwości modyfikowalnych nie należy udostępniać setterów.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
Nie mieszaj tych 2 scenariuszy.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
Klasy Builder nie powinny mieć metod pobierających
Metoda pobierająca powinna znajdować się w utworzonym obiekcie, a nie w obiekcie budującym.
Ustawiające funkcje budujące muszą mieć odpowiednie funkcje pobierające w budowanej klasie.
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();
}
Nazewnictwo metod kreatora
Nazwy metod konstruktora powinny być zgodne ze stylem setFoo(), addFoo() lub clearFoo().
Klasy Builder powinny deklarować metodę build()
Klasy kreatora powinny deklarować metodę build(), która zwraca instancję utworzonego obiektu.
Metody build() w klasie Builder muszą zwracać obiekty @NonNull
Metoda konstruktora build() powinna zwracać instancję utworzonego obiektu, która nie ma wartości null. Jeśli nie można utworzyć obiektu z powodu nieprawidłowych parametrów, weryfikację można odłożyć do metody tworzenia i należy zgłosić wyjątek IllegalStateException.
Nie udostępniaj wewnętrznych blokad
Metody w publicznym interfejsie API nie powinny używać słowa kluczowego synchronized. Ten słowo kluczowe powoduje, że obiekt lub klasa są używane jako blokada. Ponieważ jest ono widoczne dla innych, możesz napotkać nieoczekiwane efekty uboczne, jeśli inny kod poza Twoją klasą zacznie go używać do blokowania.
Zamiast tego wykonaj wymagane blokowanie na wewnętrznym, prywatnym obiekcie.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
Metody w stylu funkcji dostępu powinny być zgodne z wytycznymi dotyczącymi właściwości w Kotlinie.
Metody w stylu dostępu – te, które używają prefiksów get, set lub is – będą też dostępne jako właściwości Kotlin, gdy są wyświetlane ze źródeł Kotlin.
Na przykład pole int getField() zdefiniowane w kodzie Java jest dostępne w kodzie Kotlin jako właściwość val field: Int.
Z tego powodu i aby spełnić oczekiwania deweloperów dotyczące zachowania akcesorów, metody używające prefiksów akcesorów powinny zachowywać się podobnie do pól w języku Java. Unikaj prefiksów w stylu metod dostępu, gdy:
- Metoda ma efekty uboczne – wybierz bardziej opisową nazwę metody
- Metoda wymaga dużych nakładów obliczeniowych – preferuj
compute - Metoda polega na blokowaniu lub innym długotrwałym działaniu w celu zwrócenia wartości, np. IPC lub innego wejścia/wyjścia – preferuj
fetch - Metoda blokuje wątek, dopóki nie może zwrócić wartości – preferuj
await - Metoda zwraca nowe wystąpienie obiektu przy każdym wywołaniu – zalecamy używanie
create - Metoda może nie zwrócić wartości. Zalecamy użycie
request
Pamiętaj, że wykonanie kosztownej obliczeniowo pracy raz i zapisanie w pamięci podręcznej wartości na potrzeby kolejnych wywołań nadal jest uznawane za wykonanie kosztownej obliczeniowo pracy. Zacinanie się nie jest rozłożone na klatki.
Używaj prefiksu „is” w przypadku metod dostępu do wartości logicznych
Jest to standardowa konwencja nazewnictwa metod i pól logicznych w języku Java. Zazwyczaj nazwy metod i zmiennych logicznych powinny być zapisywane w formie pytań, na które odpowiada zwracana wartość.
Metody dostępu do wartości logicznych w języku Java powinny być zgodne ze schematem nazewnictwa set/is, a pola powinny preferować is, np.:
// 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;
Użycie metod dostępu set/is w przypadku Javy lub pól is w przypadku Javy umożliwi używanie ich jako właściwości w Kotlinie:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
Właściwości i metody dostępu powinny mieć nazwy o pozytywnym znaczeniu, np. Enabled zamiast Disabled. Używanie negatywnej terminologii odwraca znaczenie tagów true i false oraz utrudnia wnioskowanie o zachowaniu.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
W przypadku, gdy wartość logiczna opisuje włączenie lub własność usługi, możesz użyć słowa has zamiast is. Nie będzie to jednak działać w przypadku składni właściwości 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();
Niektóre alternatywne prefiksy, które mogą być bardziej odpowiednie, to can i 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();
Metody, które przełączają zachowania lub funkcje, mogą używać prefiksu is i sufiksu 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()
Podobnie metody, które wskazują zależność od innych zachowań lub funkcji, mogą używać prefiksu is i sufiksu Supported lub 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()
Zazwyczaj nazwy metod powinny być zapisywane w formie pytań, na które odpowiada wartość zwracana.
Metody właściwości w języku Kotlin
W przypadku właściwości klasy var foo: Foo Kotlin wygeneruje metody get/set
zgodnie z tą samą regułą: przed nazwą metody pobierającej dodaj prefiks get i zmień pierwszą literę na wielką, a przed nazwą metody ustawiającej dodaj prefiks set i zmień pierwszą literę na wielką. Deklaracja właściwości wygeneruje odpowiednio metody o nazwach public Foo getFoo() i public void setFoo(Foo foo).
Jeśli właściwość jest typu Boolean, w przypadku generowania nazwy obowiązuje dodatkowa reguła: jeśli nazwa właściwości zaczyna się od is, do nazwy metody pobierającej nie jest dodawany prefiks get, a sama nazwa właściwości jest używana jako metoda pobierająca.
Dlatego warto nadawaćBoolean właściwościom isprefiks, aby zachować zgodność z wytycznymi dotyczącymi nazewnictwa:
var isVisible: Boolean
Jeśli Twoja usługa należy do wspomnianych wyjątków i zaczyna się od odpowiedniego prefiksu, użyj w niej adnotacji @get:JvmName, aby ręcznie określić odpowiednią nazwę:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
Akcesoria do masek bitowych
Więcej informacji o definiowaniu flag bitowych w interfejsie API znajdziesz w artykule Używanie @IntDef w przypadku flag bitowych.
Setery
Należy udostępnić 2 metody ustawiające: jedną, która przyjmuje pełny ciąg bitów i zastępuje wszystkie istniejące flagi, oraz drugą, która przyjmuje niestandardową maskę bitową, aby zapewnić większą elastyczność.
/**
* 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);
Gettery
Aby uzyskać pełną maskę bitową, należy podać jeden 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();
Używanie dostępu publicznego zamiast chronionego
W publicznym interfejsie API zawsze preferuj public zamiast protected. Długoterminowo dostęp chroniony jest uciążliwy, ponieważ osoby wdrażające muszą zastępować go publicznymi metodami dostępu w przypadkach, w których domyślny dostęp zewnętrzny byłby równie dobry.
Pamiętaj, że protectedwidocznośćnie uniemożliwia deweloperom wywoływania interfejsu API, a jedynie utrudnia to zadanie.
Zaimplementuj obie metody equals() i hashCode() lub żadnej z nich.
Jeśli zastąpisz jeden, musisz zastąpić też drugi.
Implementowanie metody toString() w klasach danych
Klasy danych powinny zastępować metodę toString(), aby ułatwić deweloperom debugowanie kodu.
Dokumentuj, czy dane wyjściowe służą do określania zachowania programu czy do debugowania.
Zdecyduj, czy działanie programu ma zależeć od Twojej implementacji. Na przykład UUID.toString() i File.toString() dokumentują swój konkretny format, który mogą wykorzystywać programy. Jeśli udostępniasz informacje tylko na potrzeby debugowania, np. Intent, dziedzicz dokumentację z klasy nadrzędnej.
Nie podawaj dodatkowych informacji
Wszystkie informacje dostępne w toString() powinny być też dostępne w publicznym interfejsie API obiektu. W przeciwnym razie zachęcasz programistów do analizowania i korzystania z Twoich toString(), co uniemożliwi wprowadzenie przyszłych zmian. Dobrym rozwiązaniem jest implementowanie toString() tylko za pomocą publicznego interfejsu API obiektu.
Zniechęcanie do polegania na danych wyjściowych debugowania
Nie można uniemożliwić deweloperom korzystania z danych wyjściowych debugowania, ale uwzględnienie System.identityHashCode obiektu w jego danych wyjściowych toString() sprawi, że jest mało prawdopodobne, aby 2 różne obiekty miały takie same dane wyjściowe toString().
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
Może to skutecznie zniechęcić programistów do pisania w Twoich obiektach asercji testowych, takich jak assertThat(a.toString()).isEqualTo(b.toString()).
Używaj funkcji createFoo, gdy zwracasz nowo utworzone obiekty
W przypadku metod, które będą tworzyć wartości zwracane, np. przez konstruowanie nowych obiektów, używaj prefiksu create, a nie get ani new.
Jeśli metoda ma utworzyć obiekt do zwrócenia, wyraźnie zaznacz to w nazwie metody.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
Metody akceptujące obiekty File powinny też akceptować strumienie
Lokalizacje przechowywania danych na Androidzie nie zawsze są plikami na dysku. Na przykład treści przekazywane między użytkownikami są reprezentowane jako content:// Uri. Aby umożliwić przetwarzanie różnych źródeł danych, interfejsy API, które akceptują obiekty File, powinny też akceptować obiekty InputStream, OutputStream lub oba te typy.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
Zamiast wersji opakowanych pobieraj i zwracaj surowe typy proste
Jeśli chcesz przekazać informacje o brakujących lub zerowych wartościach, użyj -1, Integer.MAX_VALUE lub Integer.MIN_VALUE.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
Unikanie klasowych odpowiedników typów prostych pozwala uniknąć narzutu pamięciowego tych klas, dostępu do wartości za pomocą metod i, co ważniejsze, automatycznego pakowania, które wynika z rzutowania między typami prostymi a obiektowymi. Unikanie tych zachowań oszczędza pamięć i tymczasowe przydziały, które mogą prowadzić do kosztownych i częstszych operacji odśmiecania.
Używaj adnotacji, aby wyjaśnić prawidłowe parametry i wartości zwracane.
Dodaliśmy adnotacje dla deweloperów, aby wyjaśnić dopuszczalne wartości w różnych sytuacjach. Ułatwia to narzędziom pomaganie deweloperom w przypadku podania nieprawidłowych wartości (np. przekazania dowolnego znaku int, gdy platforma wymaga jednej z określonego zestawu stałych wartości). W odpowiednich przypadkach używaj dowolnych z tych adnotacji:
Dopuszczalność wartości null
W przypadku interfejsów API Java wymagane są jawne adnotacje dopuszczalności wartości null, ale koncepcja dopuszczalności wartości null jest częścią języka Kotlin i adnotacji dopuszczalności wartości null nigdy nie należy używać w interfejsach API Kotlin.
@Nullable: oznacza, że dana wartość zwracana, parametr lub pole może mieć wartość null:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull: oznacza, że dana wartość zwracana, parametr lub pole nie może mieć wartości null. Oznaczanie elementów jako @Nullable jest stosunkowo nową funkcją Androida, więc większość metod interfejsu API Androida nie jest konsekwentnie udokumentowana. Dlatego mamy 3 stany: „nieznany, @Nullable, @NonNull”. Z tego powodu @NonNull jest częścią wytycznych dotyczących interfejsu API:
@NonNull
public String getName()
public void setName(@NonNull String name)
W przypadku dokumentacji platformy Android dodanie adnotacji do parametrów metody spowoduje automatyczne wygenerowanie dokumentacji w formie „Ta wartość może mieć wartość null”, chyba że słowo „null” zostanie wyraźnie użyte w innym miejscu dokumentu parametru.
Istniejące metody „nie do końca dopuszczające wartość null”: istniejące metody w API bez zadeklarowanej adnotacji @Nullable mogą mieć adnotację @Nullable, jeśli metoda może zwracać wartość null w określonych, oczywistych okolicznościach (np. findViewById()). Deweloperom, którzy nie chcą sprawdzać wartości null, należy udostępnić metody towarzyszące @NotNull requireFoo(), które zgłaszają wyjątek IllegalArgumentException.
Metody interfejsu: nowe interfejsy API powinny dodawać odpowiednią adnotację podczas implementowania metod interfejsu, np. Parcelable.writeToParcel() (tzn. metoda w klasie implementującej powinna być writeToParcel(@NonNull Parcel,
int), a nie writeToParcel(Parcel, int)); istniejące interfejsy API, w których brakuje adnotacji, nie muszą być „naprawiane”.
Egzekwowanie dopuszczalności wartości null
W języku Java zaleca się przeprowadzanie walidacji danych wejściowych dla parametrów za pomocą Objects.requireNonNull() i zgłaszanie wyjątku NullPointerException, gdy parametry mają wartość null.@NonNull Jest to wykonywane automatycznie w języku Kotlin.
Zasoby
Identyfikatory zasobów: parametry całkowite, które oznaczają identyfikatory konkretnych zasobów, powinny być oznaczone odpowiednią definicją typu zasobu.
Adnotacja jest dostępna dla każdego typu zasobu, np. @StringRes, @ColorRes i @AnimRes, a także dla typu catch-all @AnyRes. Przykład:
public void setTitle(@StringRes int resId)
@IntDef w przypadku zbiorów stałych
Stałe magiczne: parametry String i int, które mają otrzymywać jedną z skończonego zbioru możliwych wartości oznaczonych stałymi publicznymi, powinny być odpowiednio oznaczone za pomocą @StringDef lub @IntDef. Te adnotacje umożliwiają utworzenie nowej adnotacji, która działa jak typedef dla dozwolonych parametrów. Przykład:
/** @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);
Metody są zalecane do sprawdzania poprawności adnotowanych parametrów i zgłaszania błędu IllegalArgumentException, jeśli parametr nie jest częścią @IntDef.
@IntDef dla flag maski bitowej
Adnotacja może też określać, że stałe są flagami, i może być łączona ze znakami & i 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 dla zbiorów stałych ciągów tekstowych
Istnieje też adnotacja @StringDef, która jest dokładnie taka sama jak @IntDef w poprzedniej sekcji, ale dotyczy stałych String. Możesz uwzględnić wiele wartości „prefix”, które są używane do automatycznego generowania dokumentacji wszystkich wartości.
@SdkConstant dla stałych pakietu SDK
@SdkConstant Dodaj adnotację do pól publicznych, jeśli mają jedną z tych wartości: SdkConstantACTIVITY_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";
Zapewnianie zgodnej dopuszczalności wartości null w przypadku zastąpień
W przypadku zgodności interfejsu API dopuszczalność wartości null w zastąpieniach powinna być zgodna z bieżącą dopuszczalnością wartości null w elemencie nadrzędnym. W tabeli poniżej znajdziesz oczekiwania dotyczące zgodności. Oczywiście zastąpienia powinny być co najmniej tak samo restrykcyjne jak element, który zastępują.
| Typ | Rodzic | dla dziecka, |
|---|---|---|
| Zwracany typ | Nieoznaczone | Nieoznaczone lub niepuste |
| Zwracany typ | Dopuszczalna wartość null | Dopuszczanie wartości null lub nie |
| Zwracany typ | Nonnull | Nonnull |
| Zabawny argument | Nieoznaczone | Nieoznaczony lub dopuszczający wartość null |
| Zabawny argument | Dopuszczalna wartość null | Dopuszczalna wartość null |
| Zabawny argument | Nonnull | Dopuszczanie wartości null lub nie |
W miarę możliwości używaj argumentów, które nie mogą mieć wartości null (np. @NonNull).
Jeśli metody są przeciążone, preferuj, aby wszystkie argumenty były niepuste.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
Ta reguła dotyczy też przeciążonych setterów właściwości. Główny argument nie powinien mieć wartości null, a czyszczenie właściwości powinno być implementowane jako osobna metoda. Zapobiega to „bezsensownym” wywołaniom, w przypadku których deweloper musi ustawiać parametry końcowe, mimo że nie są one wymagane.
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()
Preferuj typy zwracane, które nie dopuszczają wartości null (np. @NonNull) w przypadku kontenerów.
W przypadku typów kontenerów takich jak Bundle lub Collection zwróć pusty kontener, a w odpowiednich przypadkach – niezmienny. W przypadkach, w których null służy do rozróżniania dostępności kontenera, rozważ udostępnienie osobnej metody logicznej.
@NonNull
public Bundle getExtras() { ... }
Adnotacje dotyczące dopuszczalności wartości null dla par get i set muszą być zgodne
Pary metod pobierania i ustawiania dla pojedynczej właściwości logicznej powinny zawsze być zgodne w adnotacjach dotyczących dopuszczalności wartości null. Niezastosowanie się do tej wskazówki spowoduje naruszenie składni właściwości w języku Kotlin, a dodanie niezgodnych adnotacji o dopuszczalności wartości null do istniejących metod właściwości będzie zmianą powodującą niezgodność kodu źródłowego dla użytkowników języka Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
Wartość zwracana w przypadku niepowodzenia lub błędu
Wszystkie interfejsy API powinny umożliwiać aplikacjom reagowanie na błędy. Zwracanie wartości false, -1, null lub innych ogólnych wartości typu „coś poszło nie tak” nie dostarcza deweloperowi wystarczających informacji o błędzie, aby mógł on określić oczekiwania użytkowników lub dokładnie śledzić niezawodność aplikacji w terenie. Podczas projektowania interfejsu API wyobraź sobie, że tworzysz aplikację. Jeśli napotkasz błąd, czy interfejs API dostarczy Ci wystarczająco dużo informacji, aby przedstawić go użytkownikowi lub odpowiednio zareagować?
- Możesz (a nawet powinieneś) umieszczać szczegółowe informacje w komunikacie o wyjątku, ale programiści nie powinni musieć go analizować, aby odpowiednio obsłużyć błąd. Szczegółowe kody błędów lub inne informacje powinny być udostępniane jako metody.
- Upewnij się, że wybrana opcja obsługi błędów zapewnia elastyczność, która pozwoli Ci w przyszłości wprowadzać nowe typy błędów. W przypadku
@IntDefoznacza to uwzględnienie wartościOTHERlubUNKNOWN. Podczas zwracania nowego kodu możesz sprawdzićtargetSdkVersionwywołującego, aby uniknąć zwracania kodu błędu, którego aplikacja nie zna. W przypadku wyjątków używaj wspólnej klasy nadrzędnej, którą implementują wyjątki, aby każdy kod obsługujący ten typ mógł też przechwytywać i obsługiwać podtypy. - Deweloper nie powinien mieć możliwości przypadkowego zignorowania błędu – jeśli błąd jest zgłaszany przez zwrócenie wartości, oznacz metodę adnotacją
@CheckResult.
W przypadku osiągnięcia stanu błędu lub niepowodzenia z powodu błędu popełnionego przez programistę, np. zignorowania ograniczeń dotyczących parametrów wejściowych lub niepowodzenia w sprawdzeniu stanu obserwowalnego, zalecamy zgłaszanie wyjątku ? extends RuntimeException.
Metody ustawiające lub działania (np. perform) mogą zwracać kod stanu w postaci liczby całkowitej, jeśli działanie może się nie powieść z powodu stanu aktualizowanego asynchronicznie lub warunków niezależnych od dewelopera.
Kody stanu powinny być zdefiniowane w klasie zawierającej jako pola public static final z prefiksem ERROR_ i wyliczone w adnotacji @hide @IntDef.
Nazwy metod powinny zawsze zaczynać się od czasownika, a nie od podmiotu.
Nazwa metody powinna zawsze zaczynać się od czasownika (np. get, create, reload itp.), a nie od obiektu, na którym wykonujesz działanie.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
Preferuj typy Collection zamiast tablic jako typ zwracany lub typ parametru.
Interfejsy kolekcji o ogólnym typie mają kilka zalet w porównaniu z tablicami, w tym bardziej rygorystyczne umowy API dotyczące unikalności i kolejności, obsługę typów ogólnych oraz wiele wygodnych metod dla programistów.
Wyjątek w przypadku typów prostych
Jeśli elementy są typami prostymi, używaj raczej tablic, aby uniknąć kosztów automatycznego pakowania. Zobacz Zamiast wersji w pudełku używaj pierwotnych typów danych
Wyjątek w przypadku kodu wrażliwego na wydajność
W niektórych przypadkach, gdy interfejs API jest używany w kodzie wrażliwym na wydajność (np. w interfejsach API grafiki lub innych interfejsach API pomiaru, układu lub rysowania), dopuszczalne jest używanie tablic zamiast kolekcji w celu zmniejszenia liczby alokacji i fragmentacji pamięci.
Wyjątek dla języka Kotlin
Tablice w Kotlinie są niezmienne, a język Kotlin udostępnia wiele interfejsów API do obsługi tablic, więc tablice są porównywalne z List i Collection w przypadku interfejsów API Kotlina przeznaczonych do uzyskiwania dostępu z Kotlina.
Preferowanie kolekcji @NonNull
W przypadku obiektów kolekcji zawsze preferuj @NonNull. Podczas zwracania pustej kolekcji użyj odpowiedniej metody Collections.empty, aby zwrócić niedrogi, prawidłowo wpisany i niezmienny obiekt kolekcji.
W miejscach, w których obsługiwane są adnotacje typu, zawsze preferuj @NonNull w przypadku elementów kolekcji.
W przypadku tablic zamiast kolekcji używaj też @NonNull (patrz poprzedni punkt). Jeśli alokacja obiektów jest problemem, utwórz stałą i przekaż ją dalej – w końcu pusta tablica jest niezmienna. Przykład:
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;
}
Zmienność kolekcji
Interfejsy API w Kotlinie powinny domyślnie zwracać kolekcje tylko do odczytu (nie Mutable) chyba że umowa interfejsu API wymaga zwracania typu modyfikowalnego.
Interfejsy API w języku Java powinny jednak domyślnie zwracać zmienne typy, ponieważ implementacja interfejsów API w języku Java na platformie Android nie zapewnia jeszcze wygodnej implementacji kolekcji niezmiennych. Wyjątek stanowią typy zwracaneCollections.empty, które są niezmienne. W przypadkach, gdy klienci mogą wykorzystać zmienność – celowo lub przez pomyłkę – do naruszenia zamierzonego wzorca użycia interfejsu API, interfejsy API w języku Java powinny zdecydowanie rozważyć zwracanie płytkiej kopii kolekcji.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
Typy zwracane, które można modyfikować
Interfejsy API, które zwracają kolekcje, nie powinny modyfikować zwróconego obiektu kolekcji po jego zwróceniu. Jeśli zwrócona kolekcja musi zostać zmieniona lub ponownie użyta w jakiś sposób – np. w postaci dostosowanego widoku zmiennego zbioru danych – należy wyraźnie udokumentować dokładne zachowanie w momencie, gdy zawartość może ulec zmianie, lub zastosować ustalone konwencje nazewnictwa interfejsu API.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
Konwencja Kotlin .asFoo() jest opisana poniżej i umożliwia zmianę kolekcji zwracanej przez .asList(), jeśli zmieni się oryginalna kolekcja.
Zmienność zwracanych obiektów typu danych
Podobnie jak w przypadku interfejsów API zwracających kolekcje, interfejsy API zwracające obiekty typu danych nie powinny modyfikować właściwości zwróconego obiektu po jego zwróceniu.
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)
}
W wyjątkowo rzadkich przypadkach niektóre fragmenty kodu, od których zależy wydajność, mogą skorzystać z puli obiektów lub ponownego użycia. Nie pisz własnej struktury danych puli obiektów i nie udostępniaj ponownie używanych obiektów w publicznych interfejsach API. W obu przypadkach zachowaj szczególną ostrożność podczas zarządzania jednoczesnym dostępem.
Używanie typu parametru vararg
Zarówno interfejsy API w języku Kotlin, jak i interfejsy API w języku Java powinny używać adnotacji vararg w przypadkach, gdy deweloper prawdopodobnie utworzy tablicę w miejscu wywołania wyłącznie w celu przekazania wielu powiązanych parametrów tego samego typu.
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);
Kopie ochronne
Implementacje parametrów vararg w Javie i Kotlinie są kompilowane do tego samego kodu bajtowego opartego na tablicy, dzięki czemu można je wywoływać z kodu Java za pomocą tablicy modyfikowalnej. Projektanci interfejsów API są gorąco zachęcani do tworzenia defensywnej, płytkiej kopii parametru tablicy w przypadkach, gdy będzie on zapisywany w polu lub anonimowej klasie wewnętrznej.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
Pamiętaj, że utworzenie kopii obronnej nie zapewnia ochrony przed jednoczesną modyfikacją między początkowym wywołaniem metody a utworzeniem kopii ani nie chroni przed mutacją obiektów zawartych w tablicy.
Podawanie prawidłowej semantyki za pomocą parametrów typu kolekcji lub zwracanych typów
List<Foo> to opcja domyślna, ale możesz rozważyć inne typy, aby dodać dodatkowe znaczenie:
Użyj
Set<Foo>, jeśli kolejność elementów nie ma znaczenia dla interfejsu API i nie zezwala on na duplikaty lub duplikaty nie mają znaczenia.Collection<Foo>,– jeśli interfejs API nie uwzględnia kolejności i umożliwia duplikaty.
Funkcje konwersji w języku Kotlin
W języku Kotlin często używa się operatorów .toFoo() i .asFoo(), aby uzyskać obiekt innego typu z istniejącego obiektu, gdzie Foo to nazwa typu zwracanego konwersji. Jest to zgodne z dobrze znanym JDKObject.toString(). Kotlin idzie o krok dalej i używa go do konwersji typów prostych, takich jak 25.toFloat().
Różnica między konwersjami o nazwach .toFoo() i .asFoo() jest istotna:
Używaj funkcji .toFoo() podczas tworzenia nowego, niezależnego obiektu
Podobnie jak w przypadku funkcji .toString(), konwersja „to” zwraca nowy, niezależny obiekt. Jeśli oryginalny obiekt zostanie później zmodyfikowany, nowy obiekt nie będzie odzwierciedlać tych zmian.
Podobnie, jeśli nowy obiekt zostanie później zmodyfikowany, stary obiekt nie będzie odzwierciedlać tych zmian.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
Podczas tworzenia zależnego opakowania, udekorowanego obiektu lub rzutowania używaj funkcji .asFoo()
Rzutowanie w języku Kotlin odbywa się za pomocą słowa kluczowego as. Odzwierciedla to zmianę interfejsu, ale nie tożsamości. Gdy jest używany jako prefiks w funkcji rozszerzającej, symbol .asFoo() dekoruje odbiorcę. Zmiana w oryginalnym obiekcie odbiorcy zostanie odzwierciedlona w obiekcie zwróconym przez asFoo().
Zmiana w nowym obiekcie Foo może zostać odzwierciedlona w obiekcie pierwotnym.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
Funkcje konwersji powinny być zapisywane jako funkcje rozszerzające.
Zapisywanie funkcji konwersji poza definicjami klasy odbiornika i klasy wyniku zmniejsza powiązanie między typami. Idealna konwersja wymaga tylko publicznego dostępu do interfejsu API oryginalnego obiektu. Pokazuje to na przykładzie, że deweloper może też tworzyć analogiczne konwersje do własnych preferowanych typów.
zgłaszanie odpowiednich wyjątków,
Metody nie mogą zgłaszać ogólnych wyjątków, takich jak java.lang.Exception lub
java.lang.Throwable. Zamiast tego należy używać odpowiedniego, konkretnego wyjątku, np. java.lang.NullPointerException, aby umożliwić deweloperom obsługę wyjątków
bez nadmiernego uogólniania.
Błędy niezwiązane z argumentami przekazanymi bezpośrednio do metody wywoływanej publicznie powinny zgłaszać wyjątek java.lang.IllegalStateException zamiast java.lang.IllegalArgumentException lub java.lang.NullPointerException.
Detektory i wywołania zwrotne
Oto zasady dotyczące klas i metod używanych w przypadku mechanizmów odbiornika i wywołania zwrotnego.
Nazwy klas wywołań zwrotnych powinny być w liczbie pojedynczej
Używaj zasady MyObjectCallback zamiast MyObjectCallbacks.
Nazwy metod wywołania zwrotnego powinny mieć format on .
onFooEvent oznacza, że FooEvent ma miejsce i że wywołanie zwrotne powinno zareagować.
Czas przeszły i teraźniejszy powinny opisywać zachowanie czasowe.
Metody wywołania zwrotnego dotyczące zdarzeń powinny mieć nazwy wskazujące, czy zdarzenie już nastąpiło, czy jest w trakcie realizacji.
Jeśli na przykład metoda jest wywoływana po wykonaniu czynności kliknięcia:
public void onClicked()
Jeśli jednak metoda odpowiada za wykonanie działania kliknięcia:
public boolean onClick()
Rejestracja wywołania zwrotnego
Gdy do obiektu można dodać lub z niego usunąć odbiornik lub wywołanie zwrotne, powiązane metody powinny mieć nazwy add i remove lub register i unregister. Zachowaj spójność z dotychczasową konwencją stosowaną w przypadku tej klasy lub innych klas w tym samym pakiecie. Jeśli nie ma takiego precedensu, wybierz dodawanie i usuwanie.
Metody związane z rejestrowaniem lub wyrejestrowywaniem wywołań zwrotnych powinny określać pełną nazwę typu wywołania zwrotnego.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
Unikaj metod pobierających w przypadku wywołań zwrotnych
Nie dodawaj metod getFooCallback(). Jest to kuszące rozwiązanie w przypadku, gdy deweloperzy chcą połączyć istniejące wywołanie zwrotne z własnym zamiennikiem, ale jest ono niestabilne i utrudnia deweloperom komponentów określenie bieżącego stanu. Na przykład
- Deweloper A dzwoni do
setFooCallback(a) - Deweloper B dzwoni pod numer
setFooCallback(new B(getFooCallback())) - Deweloper A chce usunąć wywołanie zwrotne
a, ale nie może tego zrobić, ponieważ nie zna typuB, aBnie zostało zaprojektowane tak, aby umożliwiać takie modyfikacje opakowanego wywołania zwrotnego.
Akceptowanie obiektu Executor do sterowania wysyłaniem wywołań zwrotnych
Podczas rejestrowania wywołań zwrotnych, które nie mają wyraźnych oczekiwań dotyczących wątków (praktycznie wszędzie poza zestawem narzędzi interfejsu), zdecydowanie zaleca się uwzględnienie parametru Executor w ramach rejestracji, aby umożliwić programiście określenie wątku, w którym będą wywoływane wywołania zwrotne.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
W wyjątku od naszych zwykłych wytycznych dotyczących parametrów opcjonalnych dopuszczalne jest podanie przeciążenia pomijającego parametr Executor, mimo że nie jest on ostatnim argumentem na liście parametrów. Jeśli argument Executor nie zostanie podany, wywołanie zwrotne powinno zostać wywołane w głównym wątku za pomocą argumentu Looper.getMainLooper(), a informacja o tym powinna być udokumentowana w powiązanej przeciążonej metodzie.
/**
* ...
* 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)
Pułapki związane z implementacją Executor: pamiętaj, że poniższy kod jest prawidłowym wykonawcą.
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Oznacza to, że podczas implementowania interfejsów API, które mają taką formę, implementacja przychodzącego obiektu Binder po stronie procesu aplikacji musi wywołać Binder.clearCallingIdentity() przed wywołaniem wywołania zwrotnego aplikacji w Executor dostarczonym przez aplikację. Dzięki temu każdy kod aplikacji, który używa tożsamości bindera (np. Binder.getCallingUid()) do sprawdzania uprawnień, prawidłowo przypisuje uruchomiony kod do aplikacji, a nie do procesu systemowego wywołującego aplikację. Jeśli użytkownicy interfejsu API chcą uzyskać informacje o identyfikatorze UID lub PID wywołującego, powinny one być wyraźnie częścią interfejsu API, a nie wynikać z tego, gdzie został uruchomiony dostarczony przez nich kod Executor.
Określanie Executor powinno być obsługiwane przez interfejs API. W przypadkach, w których wydajność ma kluczowe znaczenie, aplikacje mogą potrzebować natychmiastowego lub synchronicznego uruchamiania kodu z informacjami zwrotnymi z interfejsu API. Zaakceptowanie Executor na to zezwala.
Obronne tworzenie dodatkowego HandlerThread lub podobnego do trampoliny z
niweczy ten pożądany przypadek użycia.
Jeśli aplikacja ma uruchamiać kosztowny kod w ramach własnego procesu, pozwól jej na to. Obejścia, które deweloperzy aplikacji znajdą, aby pokonać Twoje ograniczenia, będą znacznie trudniejsze do obsługi w dłuższej perspektywie.
Wyjątek dotyczący pojedynczego wywołania zwrotnego: jeśli charakter zgłaszanych zdarzeń wymaga obsługi tylko jednego wywołania zwrotnego, użyj tego stylu:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
Używanie interfejsu Executor zamiast interfejsu Handler
W przeszłości Handler w Androidzie było używane jako standard przekierowywania wykonania wywołania zwrotnego do konkretnego wątku Looper. Zmieniliśmy ten standard na preferowanie
Executor, ponieważ większość deweloperów aplikacji zarządza własnymi pulami wątków, przez co główny wątek lub wątek UI jest jedynym wątkiem Looper dostępnym dla aplikacji. Użyj Executor, aby
zapewnić deweloperom kontrolę potrzebną do ponownego wykorzystania istniejących lub preferowanych kontekstów wykonania.
Nowoczesne biblioteki współbieżności, takie jak kotlinx.coroutines czy RxJava, udostępniają własne mechanizmy planowania, które w razie potrzeby wykonują własne wysyłanie. Dlatego ważne jest, aby zapewnić możliwość używania bezpośredniego wykonawcy (np. Runnable::run), aby uniknąć opóźnień wynikających z podwójnego przeskoku wątków. Na przykład jeden przeskok
do opublikowania posta w wątku Looper za pomocą Handler, a następnie kolejny przeskok z platformy współbieżności aplikacji.
Wyjątki od tej wytycznej są rzadkie. Częste odwołania od decyzji o wyjątku:
Muszę użyć Looper, ponieważ potrzebuję Looper, aby epoll na wydarzenie.
Ta prośba o wyjątek została zaakceptowana, ponieważ w tej sytuacji nie można korzystać z funkcji Executor.
Nie chcę, aby kod aplikacji blokował wątek publikujący zdarzenie. Prośba o wyjątek nie jest zwykle uwzględniana w przypadku kodu, który jest uruchamiany w procesie aplikacji. Aplikacje, które to robią nieprawidłowo, szkodzą tylko sobie, a nie ogólnej kondycji systemu. Aplikacje, które działają prawidłowo lub korzystają z popularnego frameworka współbieżności, nie powinny ponosić dodatkowych kar za opóźnienia.
Handler jest lokalnie spójny z innymi podobnymi interfejsami API w tej samej klasie.
Prośba o wyjątek jest rozpatrywana w zależności od sytuacji. Preferowane jest dodawanie przeciążeń opartych na Executor, a także migracja implementacji Handler w celu korzystania z nowej implementacji Executor. (myHandler::post jest prawidłowym znakiem
Executor!). W zależności od rozmiaru klasy, liczby istniejących metod Handler
i prawdopodobieństwa, że deweloperzy będą musieli używać istniejących metod opartych na Handler
wraz z nową metodą, może zostać przyznany wyjątek umożliwiający dodanie nowej metody opartej na Handler.
Symetria w rejestracji
Jeśli istnieje sposób na dodanie lub zarejestrowanie czegoś, powinien też istnieć sposób na usunięcie lub wyrejestrowanie tego. Metoda
registerThing(Thing)
powinien mieć pasujący
unregisterThing(Thing)
Podaj identyfikator prośby
Jeśli programista może ponownie wykorzystać wywołanie zwrotne, podaj obiekt identyfikatora, aby powiązać wywołanie zwrotne z żądaniem.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
Obiekty wywołania zwrotnego z wieloma metodami
W przypadku wywołań zwrotnych z wieloma metodami preferowane są metody interface, a metody default należy stosować podczas dodawania do wcześniej opublikowanych interfejsów. Wcześniej te wytyczne zalecały abstract class ze względu na brak metod default w języku Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
Używaj android.os.OutcomeReceiver podczas modelowania wywołania funkcji nieblokującej
OutcomeReceiver<R,E>
zwraca wartość wyniku R w przypadku powodzenia lub E : Throwable w przeciwnym razie – tak samo jak zwykłe wywołanie metody. Użyj OutcomeReceiver jako typu wywołania zwrotnego
podczas przekształcania metody blokującej, która zwraca wynik lub zgłasza wyjątek, w nieblokującą metodę asynchroniczną:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
Metody asynchroniczne przekonwertowane w ten sposób zawsze zwracają wartość void. Każdy wynik, któryrequestFoo zostałby zwrócony, jest zamiast tego przekazywany do parametru requestFooAsynccallbackOutcomeReceiver.onResult przez wywołanie go w podanym executor.
Każdy wyjątek, który requestFoo zgłosiłby, jest zamiast tego zgłaszany do metody OutcomeReceiver.onError w ten sam sposób.
Używanie OutcomeReceiver do raportowania wyników metody asynchronicznej zapewnia też otoczkę Kotlinsuspend fun dla metod asynchronicznych korzystających z rozszerzenia Continuation.asOutcomeReceiver z androidx.core:core-ktx:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
Takie rozszerzenia umożliwiają klientom Kotlin wywoływanie nieblokujących metod asynchronicznych
wygodnie, jak zwykłych funkcji, bez blokowania wątku wywołującego. Te rozszerzenia 1:1 interfejsów API platformy mogą być oferowane jako artefakt androidx.core:core-ktx w Jetpacku w połączeniu ze standardowymi kontrolami i wskazówkami dotyczącymi zgodności wersji. Więcej informacji, kwestie związane z anulowaniem i przykłady znajdziesz w dokumentacji funkcji asOutcomeReceiver.
Metody asynchroniczne, które nie pasują do semantyki metody zwracającej wynik lub zgłaszającej wyjątek po zakończeniu pracy, nie powinny używać typu wywołania zwrotnego OutcomeReceiver. Zamiast tego rozważ jedną z opcji wymienionych w następnej sekcji.
Preferuj interfejsy funkcyjne zamiast tworzenia nowych typów pojedynczych metod abstrakcyjnych (SAM)
W interfejsie API na poziomie 24 dodano typy java.util.function.*(dokumentacja), które oferują ogólne interfejsy SAM, takie jak Consumer<T>, które nadają się do użycia jako wywołania zwrotne lambda. W wielu przypadkach tworzenie nowych interfejsów SAM nie przynosi korzyści w zakresie bezpieczeństwa typów ani przekazywania intencji, a niepotrzebnie zwiększa obszar interfejsu API Androida.
Zamiast tworzyć nowe interfejsy, rozważ użycie tych ogólnych:
Runnable:() -> UnitSupplier<R>:() -> RConsumer<T>:(T) -> UnitFunction<T,R>:(T) -> RPredicate<T>:(T) -> Boolean- wiele innych dostępnych w dokumentacji
Umieszczanie parametrów SAM
Parametry SAM powinny być umieszczane na końcu, aby umożliwić idiomatyczne użycie w języku Kotlin, nawet jeśli metoda jest przeciążona dodatkowymi parametrami.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
Dokumenty
Są to zasady dotyczące publicznej dokumentacji (Javadoc) interfejsów API.
Wszystkie publiczne interfejsy API muszą być udokumentowane
Wszystkie publiczne interfejsy API muszą mieć wystarczającą dokumentację, która wyjaśnia, jak deweloper może z nich korzystać. Załóż, że programista znalazł metodę za pomocą autouzupełniania lub podczas przeglądania dokumentacji API i ma minimalną ilość kontekstu z sąsiedniej powierzchni interfejsu API (np. z tej samej klasy).
Metody
Parametry metody i wartości zwracane muszą być udokumentowane za pomocą adnotacji @param i @return. Sformatuj treść Javadoc tak, jakby poprzedzało ją zdanie „This method...”.
Jeśli metoda nie przyjmuje żadnych parametrów, nie ma specjalnych wymagań i zwraca to, co sugeruje jej nazwa, możesz pominąć @return i napisać dokumentację w podobny sposób:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Zawsze używaj linków w Javadoc
Dokumentacja powinna zawierać linki do innych dokumentów dotyczących powiązanych stałych, metod i innych elementów. Używaj tagów Javadoc (np. @see i {@link foo}), a nie zwykłych słów.
W przypadku tego przykładu:
public static final int FOO = 0;
public static final int BAR = 1;
Nie używaj zwykłego tekstu ani czcionki kodu:
/**
* 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) { ... }
Zamiast tego używaj linków:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
Pamiętaj, że użycie adnotacji IntDef, np. @ValueType, w przypadku parametru automatycznie generuje dokumentację określającą dozwolone typy. Więcej informacji o IntDef znajdziesz w wytycznych dotyczących adnotacji.
Podczas dodawania dokumentacji Javadoc uruchom polecenie update-api lub docs.
Ta zasada jest szczególnie ważna podczas dodawania tagów @link lub @see. Upewnij się, że wynik wygląda zgodnie z oczekiwaniami. Błąd w danych wyjściowych Javadoc jest często spowodowany nieprawidłowymi linkami. Sprawdzanie wykonuje cel update-api lub docs Make, ale cel docs może być szybszy, jeśli zmieniasz tylko dokumentację Javadoc i nie musisz uruchamiać celu update-api.
Używaj {@code foo}, aby odróżniać wartości w języku Java.
Wartości Java, takie jak true, false i null, należy umieszczać w {@code...}, aby odróżnić je od tekstu dokumentacji.
Podczas pisania dokumentacji w kodzie źródłowym Kotlin możesz umieszczać kod w odwrotnych apostrofach, tak jak w przypadku Markdown.
Podsumowania @param i @return powinny być pojedynczym fragmentem zdania
Podsumowania parametrów i wartości zwracanych powinny zaczynać się od małej litery i zawierać tylko fragment jednego zdania. Jeśli masz dodatkowe informacje, które wykraczają poza jedno zdanie, przenieś je do treści dokumentacji Javadoc metody:
/**
* @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.
*/
należy zmienić na:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Adnotacje w Dokumentach wymagają wyjaśnienia
Wyjaśnij, dlaczego adnotacje @hide i @removed są ukryte w publicznym interfejsie API.
Dołącz instrukcje dotyczące zastępowania elementów interfejsu API oznaczonych adnotacją @deprecated.
Używanie tagu @throws do dokumentowania wyjątków
Jeśli metoda zgłasza sprawdzony wyjątek, np. IOException, udokumentuj go za pomocą adnotacji @throws. W przypadku interfejsów API pochodzących z języka Kotlin, które są przeznaczone do użycia przez klientów Java, oznacz funkcje adnotacją @Throws.
Jeśli metoda zgłasza nieobsługiwany wyjątek wskazujący na błąd, któremu można zapobiec, np. IllegalArgumentException lub IllegalStateException, udokumentuj wyjątek, wyjaśniając, dlaczego jest zgłaszany. Wyrzucony wyjątek powinien też wskazywać, dlaczego został wyrzucony.
Niektóre przypadki nieobsłużonych wyjątków są uważane za domyślne i nie wymagają dokumentowania, np. NullPointerException lub IllegalArgumentException, gdy argument nie pasuje do adnotacji @IntDef lub podobnej, która osadza kontrakt API w sygnaturze metody:
/**
* ...
* @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.");
}
// ...
Lub w języku 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")
}
}
// ...
Jeśli metoda wywołuje kod asynchroniczny, który może zgłaszać wyjątki, zastanów się, w jaki sposób deweloper dowie się o takich wyjątkach i jak na nie zareaguje. Zazwyczaj polega to na przekazaniu wyjątku do wywołania zwrotnego i udokumentowaniu wyjątków zgłoszonych w metodzie, która je odbiera. Wyjątki asynchroniczne nie powinny być dokumentowane za pomocą adnotacji @throws, chyba że są one ponownie zgłaszane z metody z adnotacjami.
Pierwsze zdanie dokumentu zakończ kropką.
Narzędzie Doclava analizuje dokumenty w prosty sposób, kończąc dokument z podsumowaniem (pierwsze zdanie, używane w krótkim opisie u góry dokumentów klasy) natychmiast po napotkaniu kropki (.) i spacji. Powoduje to 2 problemy:
- Jeśli krótki dokument nie kończy się kropką i jeśli ten członek ma odziedziczone dokumenty, które są wykrywane przez narzędzie, to streszczenie również je wykrywa. Na przykład w
R.attrdokumentach zobaczactionBarTabStyle, gdzie opis wymiaru został dodany do streszczenia. - Z tego samego powodu unikaj używania skrótu „np.” w pierwszym zdaniu, ponieważ Doclava kończy dokumenty z podsumowaniem po literze „g”. Na przykład zobacz
TEXT_ALIGNMENT_CENTERwView.java. Pamiętaj, że Metalava automatycznie poprawia ten błąd, wstawiając po kropce spację nierozdzielającą. Nie popełniaj jednak tego błędu.
Formatowanie dokumentów do renderowania w HTML
Dokumentacja Javadoc jest renderowana w formacie HTML, więc sformatuj ją odpowiednio:
Podziały wierszy powinny używać jawnego tagu
<p>. Nie dodawaj tagu zamykającego</p>.Nie używaj znaków ASCII do renderowania list ani tabel.
Listy nieuporządkowane powinny używać znacznika
<ul>, a listy uporządkowane – znacznika<ol>. Każdy element powinien zaczynać się od tagu<li>, ale nie musi mieć tagu zamykającego</li>. Po ostatnim elemencie wymagany jest tag zamykający</ul>lub</ol>.W tabelach należy używać tagów
<table>i<tr>dla wierszy,<th>dla nagłówków i<td>dla komórek. Wszystkie tagi tabeli wymagają pasujących tagów zamykających. Możesz użyć taguclass="deprecated"w dowolnym tagu, aby oznaczyć go jako wycofany.Aby utworzyć czcionkę kodu wbudowanego, użyj znaku
{@code foo}.Aby utworzyć bloki kodu, użyj znaku
<pre>.Cały tekst w bloku
<pre>jest analizowany przez przeglądarkę, więc uważaj na nawiasy<>. Możesz je pominąć za pomocą encji HTML<i>.Możesz też pozostawić w fragmencie kodu nawiasy kwadratowe
<>, jeśli otoczysz problematyczne sekcje tagami{@code foo}. Przykład:<pre>{@code <manifest>}</pre>
Postępuj zgodnie z przewodnikiem po stylu dokumentacji API
Aby zachować spójność stylu w przypadku podsumowań klas, opisów metod, opisów parametrów i innych elementów, postępuj zgodnie z zaleceniami zawartymi w oficjalnych wytycznych dotyczących języka Java, które znajdziesz na stronie How to Write Doc Comments for the Javadoc Tool (Jak pisać komentarze do dokumentacji dla narzędzia Javadoc).
Reguły dotyczące platformy Android
Te reguły dotyczą interfejsów API, wzorców i struktur danych, które są specyficzne dla interfejsów API i zachowań wbudowanych w platformę Android (np. Bundle lub Parcelable).
Kreatory intencji powinni używać wzorca create*Intent()
Twórcy intencji powinni używać metod o nazwie createFooIntent().
Używaj pakietu zamiast tworzyć nowe struktury danych do zwykłych obciążeń
Unikaj tworzenia nowych struktur danych do zwykłych obciążeń do reprezentowania dowolnych mapowań klucza na wartość o określonym typie. Zamiast tego możesz użyć Bundle.
Zwykle pojawia się to podczas pisania interfejsów API platformy, które służą jako kanały komunikacji między aplikacjami i usługami spoza platformy, w przypadku których platforma nie odczytuje danych przesyłanych przez kanał, a umowa interfejsu API może być częściowo zdefiniowana poza platformą (np. w bibliotece Jetpack).
W przypadkach, gdy platforma odczytuje dane, unikaj używania Bundle i stosuj klasy danych o ściśle określonym typie.
Implementacje Parcelable muszą mieć publiczne pole CREATOR
Inflacja obiektów Parcelable jest udostępniana za pomocą CREATOR, a nie konstruktorów surowych. Jeśli klasa implementuje interfejs Parcelable, jej pole CREATOR musi być publicznym interfejsem API, a konstruktor klasy przyjmujący argument Parcel musi być prywatny.
Używanie CharSequence w ciągach interfejsu
Gdy ciąg znaków jest wyświetlany w interfejsie, użyj CharSequence, aby zezwolić na wystąpienia Spannable.
Jeśli jest to tylko klucz lub inna etykieta bądź wartość, która nie jest widoczna dla użytkowników, String jest w porządku.
Unikaj używania wyliczeń
IntDef
musi być używany zamiast wyliczeń we wszystkich interfejsach API platformy i należy go rozważyć
w przypadku interfejsów API bibliotek, które nie są częścią pakietu. Wyliczeń używaj tylko wtedy, gdy masz pewność, że nie zostaną dodane nowe wartości.
Korzyści zIntDef:
- Umożliwia dodawanie wartości z upływem czasu.
- Instrukcje Kotlin
whenmogą zawodzić w czasie działania, jeśli staną się niepełne z powodu dodania wartości wyliczeniowej na platformie.
- Instrukcje Kotlin
- W czasie działania nie są używane żadne klasy ani obiekty, tylko typy proste.
- R8 lub minifikacja mogą uniknąć tego kosztu w przypadku interfejsów API biblioteki bez pakietu, ale ta optymalizacja nie ma wpływu na klasy interfejsu API platformy.
Zalety typu wyliczeniowego
- Idiomatyczna funkcja języka Java, Kotlin
- Umożliwia wyczerpujące przełączanie,
whenużycie instrukcji- Uwaga – wartości nie mogą się zmieniać z czasem (patrz poprzednia lista).
- Jasno określony zakres i łatwe do znalezienia nazwy
- Włącza weryfikację w czasie kompilacji.
- Na przykład instrukcja
whenw języku Kotlin, która zwraca wartość.
- Na przykład instrukcja
- Jest działającą klasą, która może implementować interfejsy, mieć statyczne funkcje pomocnicze, udostępniać metody członkowskie lub rozszerzające oraz udostępniać pola.
Postępuj zgodnie z hierarchią warstw pakietu na Androida
android.* Hierarchia pakietów ma domyślne uporządkowanie, w którym pakiety niższego poziomu nie mogą zależeć od pakietów wyższego poziomu.
Unikaj odwoływania się do Google, innych firm i ich produktów.
Platforma Android to projekt open source, który ma być niezależny od dostawcy. Interfejs API powinien być ogólny i równie przydatny dla integratorów systemów lub aplikacji z wymaganymi uprawnieniami.
Implementacje Parcelable powinny być ostateczne
Klasy Parcelable zdefiniowane przez platformę są zawsze ładowane z framework.jar, więc aplikacja nie może zastąpić implementacji Parcelable.
Jeśli aplikacja wysyłająca rozszerzy Parcelable, aplikacja odbierająca nie będzie miała niestandardowej implementacji nadawcy do rozpakowania. Uwaga dotycząca zgodności wstecznej: jeśli Twoja klasa historycznie nie była ostateczna, ale nie miała publicznie dostępnego konstruktora, nadal możesz oznaczyć ją symbolem final.
Metody wywołujące proces systemowy powinny ponownie zgłaszać wyjątek RemoteException jako RuntimeException
RemoteException jest zwykle zgłaszany przez wewnętrzny interfejs AIDL i wskazuje, że proces systemowy został zakończony lub aplikacja próbuje wysłać zbyt dużo danych. W obu przypadkach publiczny interfejs API powinien ponownie zgłosić wyjątek RuntimeException, aby uniemożliwić aplikacjom utrwalanie decyzji dotyczących bezpieczeństwa lub zasad.
Jeśli wiesz, że po drugiej stronie wywołania Binder znajduje się proces systemowy, ten powtarzalny kod jest najlepszym rozwiązaniem:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
Zgłaszanie konkretnych wyjątków w przypadku zmian w interfejsie API
Działanie publicznych interfejsów API może się zmieniać w zależności od poziomu interfejsu API i powodować awarie aplikacji (np. w celu egzekwowania nowych zasad bezpieczeństwa).
Jeśli interfejs API musi zgłosić błąd w przypadku żądania, które było wcześniej prawidłowe, zgłoś nowy, konkretny wyjątek zamiast ogólnego. Na przykład ExportedFlagRequired zamiast SecurityException (i ExportedFlagRequired może się rozciągać na SecurityException).
Pomoże to deweloperom aplikacji i narzędzi wykrywać zmiany w działaniu interfejsu API.
Implementowanie konstruktora kopiującego zamiast klonowania
Używanie metody clone() w języku Java jest zdecydowanie odradzane ze względu na brak umów API udostępnianych przez klasę Object i trudności związane z rozszerzaniem klas, które używają metody clone(). Zamiast tego użyj konstruktora kopiującego, który przyjmuje obiekt tego samego typu.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
Klasy, które do tworzenia instancji używają klasy Builder, powinny rozważyć dodanie konstruktora kopiującego Builder, aby umożliwić modyfikowanie kopii.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
Używanie ParcelFileDescriptor zamiast FileDescriptor
Obiekt java.io.FileDescriptor ma słabo zdefiniowaną własność, co może prowadzić do niejasnych błędów typu „use-after-close”. Zamiast tego interfejsy API powinny zwracać lub akceptować instancje ParcelFileDescriptor. Starszy kod może w razie potrzeby konwertować PFD na FD za pomocą funkcji dup() lub getFileDescriptor().
Unikaj używania wartości liczbowych o dziwnych rozmiarach.
Unikaj bezpośredniego używania wartości short lub byte, ponieważ często ograniczają one możliwości rozwoju interfejsu API w przyszłości.
Unikaj używania klasy BitSet
java.util.BitSet świetnie nadaje się do implementacji, ale nie do publicznego interfejsu API. Jest ona modyfikowalna, wymaga przydzielenia pamięci na potrzeby wywołań metod o wysokiej częstotliwości i nie zawiera informacji o znaczeniu poszczególnych bitów.
W przypadku scenariuszy o wysokiej wydajności używaj int lub long z @IntDef. W przypadku scenariuszy o niskiej skuteczności rozważ Set<EnumType>. W przypadku nieprzetworzonych danych binarnych użyj
byte[].
Preferowanie android.net.Uri
android.net.Uri to preferowany sposób hermetyzacji identyfikatorów URI w interfejsach API Androida.
Unikaj java.net.URI, ponieważ jest zbyt rygorystyczny w parsowaniu URI, i nigdy nie używaj java.net.URL, ponieważ jego definicja równości jest poważnie wadliwa.
Ukrywanie adnotacji oznaczonych jako @IntDef, @LongDef lub @StringDef
Adnotacje oznaczone jako @IntDef, @LongDef lub @StringDef oznaczają zestaw prawidłowych stałych, które można przekazać do interfejsu API. Jednak gdy są one eksportowane jako interfejsy API, kompilator wstawia stałe w kodzie, a w szkielecie interfejsu API adnotacji (na platformie) lub w pliku JAR (w przypadku bibliotek) pozostają tylko (teraz bezużyteczne) wartości.
Dlatego użycie tych adnotacji musi być oznaczone adnotacją @hide docs w platformie lub adnotacją @RestrictTo.Scope.LIBRARY) code w bibliotekach. W obu przypadkach muszą być oznaczone symbolem @Retention(RetentionPolicy.SOURCE), aby nie pojawiały się w szkieletach interfejsu API ani w plikach JAR.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
Podczas tworzenia pakietu SDK platformy i biblioteki AAR narzędzie wyodrębnia adnotacje i pakuje je oddzielnie od skompilowanych źródeł. Android Studio odczytuje ten format pakietu i wymusza definicje typów.
Nie dodawaj nowych kluczy dostawcy ustawień
Nie udostępniaj nowych kluczy z Settings.Global, Settings.System ani Settings.Secure.
Zamiast tego dodaj odpowiedni interfejs API Java do pobierania i ustawiania w odpowiedniej klasie, która jest zwykle klasą „menedżera”. Dodaj mechanizm nasłuchiwania lub transmisję, aby w razie potrzeby powiadamiać klientów o zmianach.
W porównaniu z metodami pobierającymi i ustawiającymi wartości ustawienia SettingsProvider mają kilka problemów:
- Brak bezpieczeństwa typów.
- Nie ma ujednoliconego sposobu podawania wartości domyślnej.
- Brak możliwości dostosowania uprawnień.
- Nie można na przykład chronić ustawienia za pomocą niestandardowego uprawnienia.
- Brak odpowiedniego sposobu na prawidłowe dodanie niestandardowej logiki.
- Nie można na przykład zmienić wartości ustawienia A w zależności od wartości ustawienia B.
Przykład:
Settings.Secure.LOCATION_MODE
istnieje od dawna, ale zespół ds. lokalizacji wycofał go na rzecz odpowiedniego interfejsu API Java
LocationManager.isLocationEnabled()
i MODE_CHANGED_ACTION
transmisji, co dało zespołowi znacznie większą elastyczność, a semantyka interfejsów API jest teraz znacznie jaśniejsza.
Nie rozszerzaj klas Activity i AsyncTask
AsyncTask to szczegół implementacji. Zamiast tego udostępnij odbiornik lub w przypadku biblioteki androidx interfejs ListenableFuture API.
Nie można utworzyć podklas Activity. Wydłużenie aktywności w przypadku funkcji powoduje, że staje się ona niezgodna z innymi funkcjami, które wymagają od użytkowników tego samego działania. Zamiast tego polegaj na kompozycji, używając narzędzi takich jak LifecycleObserver.
Używanie metody getUser() obiektu Context
Klasy powiązane z Context, np. wszystko, co jest zwracane z Context.getSystemService(), powinny używać użytkownika powiązanego z Context zamiast udostępniać elementy, które są kierowane na konkretnych użytkowników.
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);
}
}
Wyjątek: metoda może akceptować argument użytkownika, jeśli akceptuje wartości, które nie reprezentują pojedynczego użytkownika, np. UserHandle.ALL.
Używanie UserHandle zamiast zwykłych liczb całkowitych
UserHandle jest preferowane, aby zapewnić bezpieczeństwo typów i uniknąć łączenia identyfikatorów użytkowników z identyfikatorami UID.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
W przypadku, gdy jest to nieuniknione, identyfikator użytkownika int musi być oznaczony adnotacją @UserIdInt.
Foobar getFoobarForUser(@UserIdInt int user);
Preferuj detektory lub wywołania zwrotne zamiast intencji rozgłaszania
Intencje rozgłaszania są bardzo przydatne, ale mogą powodować nieoczekiwane zachowania, które negatywnie wpływają na stan systemu. Dlatego nowe intencje rozgłaszania należy dodawać z rozwagą.
Oto niektóre konkretne obawy, które sprawiają, że odradzamy wprowadzanie nowych intencji transmisji:
Wysyłanie transmisji bez flagi
FLAG_RECEIVER_REGISTERED_ONLYpowoduje wymuszone uruchomienie wszystkich aplikacji, które nie są jeszcze uruchomione. Chociaż czasami jest to zamierzony efekt, może prowadzić do jednoczesnego uruchamiania dziesiątek aplikacji, co negatywnie wpływa na stan systemu. Zalecamy stosowanie alternatywnych strategii, np.JobScheduler, aby lepiej koordynować spełnianie różnych warunków wstępnych.Podczas wysyłania transmisji nie ma możliwości filtrowania ani dostosowywania treści dostarczanych do aplikacji. Utrudnia to lub uniemożliwia reagowanie na przyszłe kwestie dotyczące prywatności lub wprowadzanie zmian w zachowaniu na podstawie docelowego pakietu SDK aplikacji odbierającej.
Kolejki transmisji są zasobem współdzielonym, więc mogą być przeciążone i nie zapewnić terminowego dostarczenia wydarzenia. Zaobserwowaliśmy kilka kolejek transmisji, w których opóźnienie od początku do końca wynosi 10 minut lub więcej.
Z tych powodów zachęcamy do korzystania w nowych funkcjach z odbiorników, wywołań zwrotnych lub innych narzędzi, takich jak JobScheduler, zamiast intencji rozgłaszania.
Jeśli intencje transmisji nadal są idealnym rozwiązaniem, warto wziąć pod uwagę te sprawdzone metody:
- Jeśli to możliwe, użyj
Intent.FLAG_RECEIVER_REGISTERED_ONLY, aby ograniczyć transmisję do aplikacji, które są już uruchomione. Na przykładACTION_SCREEN_ONużywa tego rozwiązania, aby zapobiegać wybudzaniu aplikacji. - Jeśli to możliwe, użyj tagu
Intent.setPackage()lubIntent.setComponent(), aby kierować transmisję na konkretną aplikację, która Cię interesuje. Na przykładACTION_MEDIA_BUTTONużywa tego projektu, aby skupić się na bieżącej aplikacji obsługującej elementy sterujące odtwarzaniem. - Jeśli to możliwe, zdefiniuj transmisję jako
<protected-broadcast>, aby uniemożliwić złośliwym aplikacjom podszywanie się pod system operacyjny.
Intencje w usługach dla programistów powiązanych z systemem
Usługi, które mają być rozszerzane przez dewelopera i są powiązane z systemem, np. usługi abstrakcyjne takie jak NotificationListenerService, mogą odpowiadać na działanie Intent z systemu. Takie usługi powinny spełniać te kryteria:
- Zdefiniuj stałą ciągu tekstowego
SERVICE_INTERFACEw klasie zawierającej pełną nazwę klasy usługi. Ta stała musi być oznaczona adnotacją@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION). - Dokument dotyczący klasy, do której deweloper musi dodać
<intent-filter>, aby otrzymywać intencje z platformy.AndroidManifest.xml - Zdecydowanie rozważ dodanie uprawnień na poziomie systemu, aby zapobiec wysyłaniu przez nieuczciwe aplikacje
Intentdo usług dla deweloperów.
Współdziałanie języków Kotlin i Java
Pełną listę wytycznych znajdziesz w oficjalnym przewodniku Androida dotyczącym współdziałania Kotlinu i Javy. Wybrane wytyczne zostały skopiowane do tego przewodnika, aby zwiększyć wykrywalność.
Widoczność interfejsu API
Niektóre interfejsy API Kotlin, takie jak suspend funs, nie są przeznaczone do używania przez programistów Java. Nie próbuj jednak kontrolować widoczności specyficznej dla języka za pomocą @JvmSynthetic, ponieważ ma to wpływ na sposób prezentowania interfejsu API w debuggerach, co utrudnia debugowanie.
Szczegółowe wskazówki znajdziesz w przewodniku po współdziałaniu języków Kotlin i Java lub w przewodniku po asynchroniczności.
Obiekty towarzyszące
Kotlin używa znaku companion object do udostępniania statycznych elementów. W niektórych przypadkach będą one widoczne w kodzie Java w klasie wewnętrznej o nazwie Companion, a nie w klasie zawierającej. Companion zajęcia mogą być wyświetlane jako puste w plikach tekstowych interfejsu API – tak ma być.
Aby zmaksymalizować zgodność z Javą, oznacz pola niebędące stałymi w obiektach towarzyszących adnotacją @JvmField, a funkcje publiczne adnotacją @JvmStatic, aby udostępnić je bezpośrednio w klasie zawierającej.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Ewolucja interfejsów API platformy Android
W tej sekcji opisujemy zasady dotyczące typów zmian, które możesz wprowadzać w istniejących interfejsach API Androida, oraz sposobu ich wdrażania, aby zmaksymalizować zgodność z istniejącymi aplikacjami i bazami kodu.
Zmiany powodujące niezgodność binarną
Unikaj zmian powodujących niezgodność binarną w ukończonych publicznych interfejsach API. Tego typu zmiany zwykle powodują błędy podczas uruchamiania make update-api, ale mogą wystąpić przypadki graniczne, których kontrola interfejsu API Metalava nie wykryje. W razie wątpliwości zapoznaj się z przewodnikiem Evolving Java-based APIs (Rozwijanie interfejsów API opartych na Javie) opublikowanym przez Eclipse Foundation, w którym znajdziesz szczegółowe wyjaśnienie, jakie typy zmian w interfejsie API są zgodne z językiem Java. Zmiany powodujące niezgodność binarną w ukrytych interfejsach API (np. systemowych) powinny być wprowadzane zgodnie z cyklem wycofania/zastąpienia.
Zmiany powodujące niezgodność ze źródłem
Odradzamy wprowadzanie zmian powodujących niezgodność kodu źródłowego, nawet jeśli nie powodują one niezgodności binarnej. Przykładem zmiany, która jest zgodna binarnie, ale powoduje niezgodność na poziomie kodu źródłowego, jest dodanie typu ogólnego do istniejącej klasy. Jest to zgodne binarnie, ale może powodować błędy kompilacji z powodu dziedziczenia lub niejednoznacznych odwołań.
Zmiany powodujące niezgodność z kodem źródłowym nie będą powodować błędów podczas uruchamiania polecenia make update-api, dlatego musisz dokładnie zrozumieć wpływ zmian na istniejące sygnatury interfejsu API.
W niektórych przypadkach zmiany powodujące niezgodność z kodem źródłowym są konieczne, aby poprawić komfort pracy deweloperów lub poprawność kodu. Na przykład dodanie do źródeł w Javie adnotacji o dopuszczalności wartości null poprawia interoperacyjność z kodem w Kotlinie i zmniejsza prawdopodobieństwo wystąpienia błędów, ale często wymaga zmian w kodzie źródłowym – czasami znacznych.
Zmiany w prywatnych interfejsach API
Interfejsy API oznaczone symbolem @TestApi możesz w każdej chwili zmienić.
Interfejsy API z adnotacją @SystemApi musisz przechowywać przez 3 lata. Musisz usunąć lub zmodyfikować interfejs API systemu zgodnie z tym harmonogramem:
- API y - Added
- API y+1 - Wycofanie
- Oznacz kod za pomocą
@Deprecated. - Dodaj zamienniki i utwórz do nich link w dokumentacji Javadoc dla wycofanego kodu za pomocą adnotacji
@deprecateddocs. - W trakcie cyklu rozwoju zgłaszaj błędy użytkownikom wewnętrznym, informując ich o wycofaniu interfejsu API. Pomaga to potwierdzić, że interfejsy API zastępujące są odpowiednie.
- Oznacz kod za pomocą
- API y+2 – łagodne wycofanie
- Oznacz kod za pomocą
@removed. - Opcjonalnie zgłaszaj wyjątek lub nie rób nic w przypadku aplikacji, których docelowy poziom to bieżący poziom pakietu SDK w wersji.
- Oznacz kod za pomocą
- API y+3 - trwałe usunięcie
- Całkowicie usuń kod z drzewa źródłowego.
Wycofanie
Wycofanie interfejsu API traktujemy jako zmianę, która może nastąpić w wersji głównej (np. oznaczonej literą). Podczas wycofywania interfejsów API używaj jednocześnie adnotacji @Deprecated source i adnotacji @deprecated
<summary> docs. Podsumowanie musi zawierać strategię migracji. Ta strategia może zawierać link do interfejsu API, który zastępuje wycofany interfejs, lub wyjaśnienie, dlaczego nie należy go używać:
/**
* 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)
Musisz też oznaczyć jako wycofane interfejsy API zdefiniowane w XML i udostępnione w Javie, w tym atrybuty i właściwości, które można stylizować, udostępnione w klasie android.R, wraz z podsumowaniem:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
Kiedy wycofać interfejs API
Wycofanie jest najbardziej przydatne do zniechęcania do używania interfejsu API w nowym kodzie.
Wymagamy też, aby przed @removed oznaczać interfejsy API jako @deprecated, ale nie stanowi to dla deweloperów wystarczającej motywacji do migracji z interfejsu API, którego już używają.
Zanim wycofasz interfejs API, zastanów się, jaki będzie to miało wpływ na deweloperów. Wycofanie interfejsu API ma następujące skutki:
javacemituje ostrzeżenie podczas kompilacji.- Ostrzeżeń o wycofaniu nie można wyłączyć globalnie ani ustalić dla nich wartości bazowych, więc programiści korzystający z
-Werrormuszą indywidualnie naprawić lub wyłączyć każde użycie wycofanego interfejsu API, zanim będą mogli zaktualizować wersję pakietu SDK do kompilacji. - Ostrzeżeń o wycofaniu w przypadku importowania wycofanych klas nie można pominąć. W rezultacie deweloperzy muszą wstawić w kodzie pełną i jednoznaczną nazwę klasy za każdym razem, gdy używają wycofanej klasy, zanim będą mogli zaktualizować wersję pakietu SDK do kompilacji.
- Ostrzeżeń o wycofaniu nie można wyłączyć globalnie ani ustalić dla nich wartości bazowych, więc programiści korzystający z
- Dokumentacja dotycząca
d.android.comzawiera informację o wycofaniu. - Środowiska IDE, takie jak Android Studio, wyświetlają ostrzeżenie w miejscu użycia interfejsu API.
- Środowiska IDE mogą obniżyć ranking interfejsu API lub ukryć go w autouzupełnianiu.
W rezultacie wycofanie interfejsu API może zniechęcić deweloperów, którzy najbardziej dbają o stan kodu (czyli tych, którzy używają -Werror), do wdrażania nowych pakietów SDK.
Deweloperzy, którzy nie przejmują się ostrzeżeniami w dotychczasowym kodzie, prawdopodobnie będą całkowicie ignorować wycofania.
Pakiet SDK, który wprowadza dużą liczbę wycofań, pogarsza oba te przypadki.
Z tego powodu zalecamy wycofywanie interfejsów API tylko w przypadkach, gdy:
- W przyszłej wersji planujemy
@removeten interfejs API. - Użycie interfejsu API prowadzi do nieprawidłowego lub niezdefiniowanego zachowania, którego nie możemy naprawić bez utraty zgodności.
Gdy wycofujesz interfejs API i zastępujesz go nowym, zdecydowanie zalecamy dodanie odpowiedniego interfejsu API zgodności do biblioteki Jetpack, np. androidx.core, aby uprościć obsługę zarówno starych, jak i nowych urządzeń.
Nie zalecamy wycofywania interfejsów API, które działają zgodnie z założeniami w obecnych i przyszłych wersjach:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
Wycofanie jest odpowiednie w przypadkach, gdy interfejsy API nie mogą już zachowywać się zgodnie z dokumentacją:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
Zmiany dotyczące wycofanych interfejsów API
Musisz zachować działanie wycofanych interfejsów API. Oznacza to, że implementacje testów muszą pozostać takie same, a testy muszą nadal przechodzić po wycofaniu interfejsu API. Jeśli interfejs API nie ma testów, dodaj je.
Nie rozszerzaj wycofanych interfejsów API w przyszłych wersjach. Do istniejącego wycofanego interfejsu API możesz dodać adnotacje dotyczące poprawności (np. @Nullable), ale nie dodawaj nowych interfejsów API.
Nie dodawaj nowych interfejsów API jako wycofanych. Jeśli w cyklu przedpremierowym dodano interfejsy API, które następnie zostały wycofane (czyli początkowo pojawiły się w publicznym interfejsie API jako wycofane), musisz je usunąć przed sfinalizowaniem interfejsu API.
Miękkie usunięcie
Usunięcie warunkowe jest zmianą powodującą niezgodność z kodem źródłowym, więc w publicznych interfejsach API należy go unikać, chyba że Rada ds. API wyraźnie na to zezwoli.
W przypadku interfejsów API systemu musisz wycofać interfejs API na czas trwania wersji głównej przed jego usunięciem. Usuń wszystkie odniesienia do interfejsów API w dokumentach i użyj adnotacji @removed <summary> w dokumentacji, gdy interfejsy API są usuwane w sposób nietrwały. Podsumowanie musi zawierać powód usunięcia i może obejmować strategię migracji, jak wyjaśniliśmy w sekcji Wycofywanie.
Zachowanie interfejsów API usuniętych w sposób nietrwały może pozostać bez zmian, ale co ważniejsze, musi zostać zachowane w taki sposób, aby istniejące wywołania nie powodowały awarii podczas wywoływania interfejsu API. W niektórych przypadkach może to oznaczać zachowanie dotychczasowego działania.
Pokrycie testami musi zostać zachowane, ale zawartość testów może wymagać zmiany, aby uwzględnić zmiany w zachowaniu. Testy muszą nadal sprawdzać, czy istniejący rozmówcy nie ulegają awarii w czasie działania. Możesz zachować dotychczasowe działanie interfejsów API usuniętych w sposób nietrwały, ale co ważniejsze, musisz je zachować w taki sposób, aby istniejące wywołania nie powodowały awarii podczas wywoływania interfejsu API. W niektórych przypadkach może to oznaczać zachowanie dotychczasowego sposobu działania.
Musisz zachować pokrycie testami, ale zawartość testów może wymagać zmiany, aby uwzględnić zmiany w zachowaniu. Testy muszą nadal sprawdzać, czy istniejący rozmówcy nie ulegają awarii w czasie działania.
Od strony technicznej usuwamy interfejs API z pliku JAR z elementami zastępczymi pakietu SDK i ze ścieżki klas w czasie kompilacji za pomocą adnotacji @remove Javadoc, ale nadal istnieje on w ścieżce klas w czasie działania – podobnie jak interfejsy API @hide:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
Z perspektywy dewelopera interfejs API nie będzie już widoczny w funkcji autouzupełniania, a kod źródłowy, który się do niego odwołuje, nie będzie się kompilować, gdy compileSdk będzie równe lub nowsze niż pakiet SDK, w którym interfejs API został usunięty. Kod źródłowy będzie się jednak nadal kompilować w przypadku starszych pakietów SDK, a pliki binarne, które się do niego odwołują, będą nadal działać.
Niektórych kategorii interfejsów API nie wolno usuwać w sposób nietrwały. Nie możesz usuwać niektórych kategorii interfejsów API.
Metody abstrakcyjne
Nie wolno usuwać metod abstrakcyjnych w klasach, które deweloperzy mogą rozszerzać. Uniemożliwia to deweloperom rozszerzenie klasy na wszystkich poziomach pakietu SDK.
W rzadkich przypadkach, gdy nigdy i nie będzie możliwe rozszerzenie klasy przez deweloperów, możesz nadal usunąć metody abstrakcyjne.
Trwałe usunięcie
Trwałe usunięcie jest zmianą powodującą brak zgodności binarnej i nigdy nie powinno występować w publicznych interfejsach API.
Adnotacja odradzana
Adnotacji @Discouraged używamy, aby wskazać, że w większości przypadków (>95%) nie zalecamy korzystania z danego interfejsu API. Od wycofanych interfejsów API różnią się tym, że istnieje wąski, krytyczny przypadek użycia, który uniemożliwia ich wycofanie. Gdy oznaczysz interfejs API jako odradzany, musisz podać wyjaśnienie i alternatywne rozwiązanie:
@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);
}
Nie należy dodawać nowych interfejsów API, które są odradzane.
Zmiany w działaniu istniejących interfejsów API
W niektórych przypadkach możesz chcieć zmienić sposób działania implementacji istniejącego interfejsu API. Na przykład w Androidzie 7.0 ulepszyliśmy DropBoxManager, aby wyraźnie informować, kiedy deweloperzy próbowali publikować zdarzenia, które były zbyt duże, aby wysłać je przez Binder.
Aby jednak uniknąć problemów z dotychczasowymi aplikacjami, zdecydowanie zalecamy zachowanie bezpiecznego działania w przypadku starszych aplikacji. W przeszłości chroniliśmy te zmiany w zachowaniu na podstawie ApplicationInfo.targetSdkVersion aplikacji, ale niedawno przeszliśmy na wymaganie korzystania z platformy zgodności aplikacji. Oto przykład wdrożenia zmiany zachowania za pomocą tych nowych ram:
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
}
}
}
Korzystanie z tej struktury zgodności aplikacji umożliwia deweloperom tymczasowe wyłączanie określonych zmian w zachowaniu aplikacji podczas wersji podglądowych i beta w ramach debugowania aplikacji, zamiast zmuszać ich do dostosowywania się do dziesiątek zmian w zachowaniu jednocześnie.
Zgodność w przyszłości
Zgodność w przyszłość to cecha konstrukcyjna, która umożliwia systemowi akceptowanie danych wejściowych przeznaczonych dla jego późniejszej wersji. W przypadku projektowania interfejsu API musisz zwrócić szczególną uwagę na projekt początkowy, a także na przyszłe zmiany, ponieważ programiści oczekują, że napiszą kod raz, przetestują go raz i będzie on działać wszędzie bez problemów.
Najczęstsze problemy ze zgodnością z przyszłymi wersjami Androida powodują:
- Dodawanie nowych stałych do zbioru (np.
@IntDeflubenum), który wcześniej był uznawany za kompletny (np. gdyswitchmadefault, który zgłasza wyjątek). - Dodanie obsługi funkcji, która nie jest bezpośrednio uwzględniona w interfejsie API (np. obsługa przypisywania zasobów typu
ColorStateListw XML, gdzie wcześniej obsługiwane były tylko zasoby typu<color>). - złagodzenie ograniczeń dotyczących sprawdzania w czasie działania, np. usunięcie sprawdzania
requireNotNull(), które było obecne w starszych wersjach;
We wszystkich tych przypadkach deweloperzy dowiadują się, że coś jest nie tak, dopiero w czasie działania programu. Co gorsza, mogą się o tym dowiedzieć z raportów o awariach ze starszych urządzeń w terenie.
Dodatkowo wszystkie te przypadki są technicznie prawidłowe. Nie naruszają one zgodności binarnej ani zgodności kodu źródłowego, a narzędzie API Lint nie wykrywa żadnych z tych problemów.
Dlatego projektanci interfejsów API muszą zachować szczególną ostrożność podczas modyfikowania istniejących klas. Zadaj sobie pytanie: „Czy ta zmiana spowoduje, że kod napisany i przetestowany tylko pod kątem najnowszej wersji platformy nie będzie działać w starszych wersjach?”.
Schematy XML
Jeśli schemat XML służy jako stabilny interfejs między komponentami, musi być wyraźnie określony i musi ewoluować w sposób wstecznie zgodny, podobnie jak inne interfejsy API Androida. Na przykład struktura elementów i atrybutów XML musi być zachowana podobnie jak metody i zmienne na innych powierzchniach interfejsu Android API.
Wycofanie XML
Jeśli chcesz wycofać element lub atrybut XML, możesz dodać znacznik
xs:annotation, ale musisz nadal obsługiwać wszystkie istniejące pliki XML
zgodnie z typowym cyklem życia @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>
Typy elementów muszą zostać zachowane
Schematy obsługują elementy sequence, choice i all jako elementy podrzędne elementu complexType. Elementy podrzędne różnią się jednak liczbą i kolejnością elementów podrzędnych, więc zmiana istniejącego typu byłaby niekompatybilna.
Jeśli chcesz zmodyfikować istniejący typ, najlepszym rozwiązaniem jest wycofanie starego typu i wprowadzenie nowego, który go zastąpi.
<!-- 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>
Wzorce specyficzne dla magistrali
Mainline to projekt, który umożliwia aktualizowanie podsystemów („modułów Mainline”) systemu operacyjnego Android oddzielnie, a nie całego obrazu systemu.
Moduły główne muszą być „odłączone” od platformy podstawowej, co oznacza, że wszystkie interakcje między poszczególnymi modułami a resztą świata muszą odbywać się za pomocą formalnych interfejsów API (publicznych lub systemowych).
Moduły główne powinny być zgodne z określonymi wzorcami projektowymi. Opisujemy je w tej sekcji.
Wzorzec <Module>FrameworkInitializer
Jeśli moduł główny musi udostępniać klasy @SystemService (np. JobScheduler), użyj tego wzorca:
Udostępnij klasę
<YourModule>FrameworkInitializerz modułu. Ta klasa musi znajdować się w$BOOTCLASSPATH. Przykład: StatsFrameworkInitializerOznacz go symbolem
@SystemApi(client = MODULE_LIBRARIES).Dodaj do niego metodę
public static void registerServiceWrappers().Użyj
SystemServiceRegistry.registerContextAwareService(), aby zarejestrować klasę menedżera usługi, gdy potrzebuje ona odwołania doContext.Użyj
SystemServiceRegistry.registerStaticService(), aby zarejestrować klasę menedżera usługi, gdy nie potrzebuje ona odwołania doContext.Wywołaj metodę
registerServiceWrappers()z inicjatora statycznego klasySystemServiceRegistry.
Wzorzec <Module>ServiceManager
Zwykle do rejestrowania obiektów powiązań usług systemowych lub uzyskiwania do nich odwołań używa się funkcji ServiceManager, ale moduły główne nie mogą jej używać, ponieważ jest ukryta. Ta klasa jest ukryta, ponieważ moduły główne nie powinny rejestrować ani odwoływać się do obiektów interfejsu usługi systemowej udostępnianych przez platformę statyczną lub inne moduły.
Moduły główne mogą zamiast tego używać tego wzorca, aby rejestrować usługi Binder i uzyskiwać do nich odwołania.
Utwórz klasę
<YourModule>ServiceManager, która będzie zgodna z projektem klasy TelephonyServiceManager.Udostępniasz zajęcia jako
@SystemApi. Jeśli potrzebujesz dostępu do niego tylko z klas$BOOTCLASSPATHlub klas serwera systemowego, możesz użyć@SystemApi(client = MODULE_LIBRARIES). W innych przypadkach sprawdzi się@SystemApi(client = PRIVILEGED_APPS).Te zajęcia będą obejmować:
- Ukryty konstruktor, więc tylko statyczny kod platformy może go utworzyć.
- Publiczne metody pobierające, które zwracają instancję
ServiceRegistererdla określonej nazwy. Jeśli masz 1 obiekt bindera, potrzebujesz 1 metody pobierającej. Jeśli masz 2 właściwości, potrzebujesz 2 metod pobierających. - W
ActivityThread.initializeMainlineModules()utwórz instancję tej klasy i przekaż ją do metody statycznej udostępnianej przez moduł. Zwykle dodajesz statyczny interfejs API@SystemApi(client = MODULE_LIBRARIES)w klasieFrameworkInitializer, która go przyjmuje.
Ten wzorzec uniemożliwia innym modułom głównym dostęp do tych interfejsów API, ponieważ nie ma możliwości uzyskania przez nie instancji <YourModule>ServiceManager, mimo że interfejsy API get() i register() są dla nich widoczne.
Oto jak usługa telefoniczna uzyskuje odwołanie do usługi telefonicznej: link do wyszukiwania kodu.
Jeśli implementujesz obiekt bindera usługi w kodzie natywnym, używasz natywnych interfejsów API AServiceManager.
Te interfejsy API odpowiadają interfejsom API Javy ServiceManager, ale natywne interfejsy API są bezpośrednio udostępniane modułom głównym. Nie używaj ich do rejestrowania ani odwoływania się do obiektów Binder, które nie należą do Twojego modułu. Jeśli udostępniasz obiekt Binder z kodu natywnego, Twój interfejs <YourModule>ServiceManager.ServiceRegisterer nie musi mieć metody register().
Definicje uprawnień w modułach magistrali
Moduły Mainline zawierające pliki APK mogą definiować uprawnienia (niestandardowe) w pliku APKAndroidManifest.xml w taki sam sposób jak zwykły plik APK.
Jeśli zdefiniowane uprawnienie jest używane tylko wewnętrznie w module, jego nazwa powinna mieć prefiks w postaci nazwy pakietu APK, np.:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
Jeśli zdefiniowane uprawnienie ma być udostępniane innym aplikacjom w ramach interfejsu API platformy, który można aktualizować, jego nazwa powinna zaczynać się od „android.permission.”. (jak w przypadku każdego statycznego uprawnienia platformy) oraz nazwę pakietu modułu, aby zasygnalizować, że jest to interfejs API platformy z modułu, unikając przy tym konfliktów nazw, np.:
<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" />
Moduł może następnie udostępnić nazwę tego uprawnienia jako stałą interfejsu API w swoim interfejsie API, np. HealthPermissions.READ_ACTIVE_CALORIES_BURNED.