Wytyczne dotyczące interfejsu API Androida

Ta strona ma służyć deweloperom jako przewodnik po ogólnych zasadach, które Rada Interfejsów API egzekwuje podczas sprawdzania interfejsów API.

Oprócz przestrzegania tych wskazówek podczas tworzenia interfejsów API deweloperzy powinni uruchomić narzędzie API Lint, które koduje wiele z tych reguł w sprawdzeniach przeprowadzanych na interfejsach API.

Możesz traktować to jako przewodnik po regułach przestrzeganych przez to narzędzie Lint oraz ogólne porady dotyczące reguł, których nie można z dużą dokładnością skodyfikować w tym narzędziu.

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 poziomu lokalnego procesu płatności za pomocą m checkapi lub lokalnego procesu płatności AndroidX za pomocą ./gradlew :path:to:project:checkApi.

Reguły interfejsu API

Platforma Android i wiele bibliotek Jetpacka istniały już przed utworzeniem tych wytycznych, a zasady opisane dalej na tej stronie są stale ulepszane, aby spełniać potrzeby ekosystemu Androida.

W związku z tym niektóre istniejące interfejsy API mogą nie być zgodne z tymi wytycznymi. W innych przypadkach może być wygodniej dla deweloperów aplikacji, jeśli nowy interfejs API będzie zgodny z dotychczasowymi interfejsami API, a nie będzie ściśle przestrzegać tych wytycznych.

W razie trudnych pytań dotyczących interfejsu API, które wymagają rozwiązania, lub wytycznych, które wymagają aktualizacji, skontaktuj się z Radą interfejsów API.

Podstawy interfejsu API

Ta kategoria dotyczy podstawowych aspektów interfejsu API Androida.

Wszystkie interfejsy API muszą być zaimplementowane

Niezależnie od tego, do kogo skierowany jest interfejs API (np. do wszystkich lub do @SystemApi), wszystkie jego interfejsy muszą być implementowane po złączeniu lub udostępniane jako interfejs API. Nie łącz szablonów interfejsu API z implementacją, która ma nastąpić w późniejszym terminie.

Interfejsy API bez implementacji mają kilka problemów:

  • Nie możemy zagwarantować, że została odsłonięta odpowiednia lub pełna powierzchnia. 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 wersji dla deweloperów.
  • Interfejsów API bez implementacji nie można testować w CTS.

Wszystkie interfejsy API muszą zostać przetestowane

Jest to zgodne z wymaganiami platformy CTS, zasadami AndroidX oraz ogólną zasadą, że interfejsy API muszą być implementowane.

Testowanie interfejsów API zapewnia podstawową gwarancję, że interfejs API jest użyteczny i że uwzględniliśmy oczekiwane przypadki użycia. Testowanie istnienia nie wystarcza. Należy przetestować zachowanie samego interfejsu API.

Zmiana, która dodaje nowy interfejs API, powinna zawierać odpowiednie testy w tym samym temacie CL lub Gerrit.

Interfejsy API powinny być też testowalne. Powinieneś znać odpowiedź na pytanie „Jak deweloper aplikacji będzie testować kod korzystający z Twojego interfejsu API?”.

Wszystkie interfejsy API muszą być udokumentowane

Dokumentacja jest kluczowym elementem użyteczności interfejsu API. Chociaż składnia interfejsu API może wydawać się oczywista, nowi klienci nie będą rozumieć semantyki, zachowania ani kontekstu interfejsu API.

Wszystkie wygenerowane interfejsy API muszą być zgodne z tymi wytycznymi.

Interfejsy API generowane 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 można zaimplementować klas wartości końcowych ani końcowych budowniczych w sposób, w jaki działa AutoValue.

Styl kodu

Ta kategoria dotyczy ogólnego stylu kodu, którego powinni używać deweloperzy, w szczególności podczas pisania publicznych interfejsów API.

Z wyjątkiem miejsc, w których jest to zaznaczone, należy przestrzegać standardowych konwencji kodowania

Konwencje kodowania Androida są opisane tutaj:

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

Ogólnie przestrzegamy standardowych konwencji kodowania Java i Kotlin.

Akronimy nie powinny być pisane wielką literą w nazwach metod

Na przykład nazwa metody powinna być runCtsTests, a nie runCTSTests.

Nazwy nie mogą się kończyć na Impl

To ujawnia szczegóły implementacji. Unikaj tego.

Zajęcia

W tej sekcji opisano reguły dotyczące klas, interfejsów i dziedziczenia.

dziedziczyć nowe publiczne klasy z odpowiedniej klasy bazowej;

Dziedziczenie ujawnia w podklasie elementy interfejsu API, które mogą być nieodpowiednie. Na przykład nowa publiczna podklasa FrameLayout wygląda jak FrameLayout, ale ma też nowe zachowania i elementy interfejsu API. Jeśli odziedziczony interfejs API nie jest odpowiedni do Twojego przypadku użycia, odziedź go z klasy wyżej w drzewie, np. ViewGroup lub View.

Jeśli chcesz zastąpić metody z klasy bazowej, aby rzucać UnsupportedOperationException, zastanów się, której klasy bazowej używasz.

Korzystanie z klas kolekcji podstawowych

Niezależnie od tego, czy kolekcja jest podawana jako argument czy zwracana jako wartość, zawsze preferuj klasę podstawową zamiast 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 List w przypadku interfejsu API, którego kolekcja musi być posortowana, i użyj Set w przypadku interfejsu API, którego kolekcja musi składać się z elementów unikalnych.

W Kotlinie preferuj niezmienne kolekcje. Więcej informacji znajdziesz w artykule 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 Jetpacka powinny być przeznaczone do wersji Java 8 lub nowszej.

W przypadku implementacji bezstanowej projektanci interfejsów API powinni preferować interfejsy zamiast klas abstrakcyjnych. Oznacza to, że domyślne metody interfejsu mogą być implementowane 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 ułatwić jej używanie jako funkcji 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ą klasę Service, powinny mieć nazwę FooService, aby zachować przejrzystość:

public class IntentHelper extends Service {}
public class IntentService extends Service {}

Sufiksy ogólne

Unikaj stosowania ogólnych przyrostków nazw klas, takich jak HelperUtil, w przypadku kolekcji metod pomocniczych. Zamiast tego umieść metody bezpośrednio w powiązanych klasach lub w funkcjach rozszerzeń Kotlina.

Jeśli metody łączą kilka klas, nadaj klasie zawierającej zrozumiałą nazwę, która wyjaśnia, do czego służy.

W bardzo ograniczonych przypadkach można użyć sufiksu Helper:

  • Służy do tworzenia domyślnego zachowania
  • Może to wymagać przekazania istniejącego zachowania nowym zajęciom.
  • Może wymagać trwałego stanu
  • Zazwyczaj obejmuje View

Jeśli na przykład przenoszenie tooltipów wymaga zachowania stanu związanego z View i wywołania kilku metod na obiekcie View, aby zainstalować przenoszony tooltip, TooltipHelper będzie akceptowalną nazwą klasy.

Nie udostępniaj kodu wygenerowanego przez IDL bezpośrednio jako publicznych interfejsów API.

Zachowaj kod wygenerowany przez IDL jako szczegóły implementacji. Dotyczy to interfejsów protobuf, gniazd, FlatBuffers i innych interfejsów API innych niż Java i NDK. Jednak większość funkcji IDL w Androidzie jest dostępna w AIDL, więc na tej stronie skupiamy się na AIDL.

Wygenerowane klasy AIDL nie spełniają wymagań przewodnika po stylu interfejsu API (np. nie można w nich używać przeciążeń), a narzędzie AIDL nie zostało zaprojektowane w celu zapewnienia zgodności z interfejsem API języka, więc nie można ich umieszczać w publicznych interfejsach API.

Zamiast tego dodaj publiczną warstwę interfejsu API na wierzchu interfejsu AIDL, nawet jeśli początkowo jest to płytka warstwa opakowania.

Interfejsy Binder

Jeśli interfejs Binder jest szczegółem implementacji, można go swobodnie zmieniać w przyszłości, a warstwa publiczna pozwoli zachować wymaganą zgodność wsteczną. Może być na przykład konieczne dodanie nowych argumentów do wywołań wewnętrznych lub zoptymalizowanie ruchu IPC za pomocą grupowania lub przesyłania strumieniowego, używania współdzielonej pamięci itp. Nie możesz tego zrobić, jeśli interfejs AIDL jest też publicznym interfejsem API.

Nie udostępniaj na przykład interfejsu FooService bezpośrednio jako publicznego interfejsu API:

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

Zamiast tego owiń interfejs Binder w menedżera lub inną klasę:

/**
 * @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. W miarę ulepszania implementacji możesz używać warstwy opakowywania do rozwiązywania innych problemów związanych ze zgodnością wsteczną:

/**
 * @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. interfejs usługi eksportowany przez usługi Google Play do użytku w aplikacjach), wymóg stabilnego, opublikowanego i wersyfikowanego interfejsu IPC oznacza, że znacznie trudniej jest ulepszać sam interfejs. Warto jednak umieścić wokół niego warstwę opakowania, aby pasował do innych wytycznych dotyczących interfejsów API i ułatwić używanie tego samego publicznego interfejsu API w nowej wersji interfejsu IPC, jeśli zajdzie taka potrzeba.

Nie używaj nieprzetworzonych obiektów Bindera w publicznych interfejsach API

Obiekt Binder nie ma żadnego znaczenia, dlatego nie należy go używać w interfejsie API publicznego. Jednym z częstych zastosowań jest użycie jako tokena wartości Binder lub IBinder, ponieważ mają one semantykę tożsamości. Zamiast używać obiektu Binder w postaci surowych danych, użyj klasy zawijającej tokenów.

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 należy zadeklarować jako final. Klasy menedżera komunikują się z usługami systemu i stanowią jedyny punkt interakcji. Nie musisz dostosowywać tego elementu, więc zadeklaruj go jako final.

Nie używaj CompletableFuture ani Future.

java.util.concurrent.CompletableFuture ma duży interfejs API, który umożliwia arbitralną mutację wartości przyszłej, a także ma domyślne wartości podatne na błędy.

Z drugiej strony, java.util.concurrent.Future nie ma funkcji nieblokującego słuchania, przez co trudno jest go używać z kodem asynchronicznym.

W kodzie platformy i interfejsach API bibliotek niskiego poziomu używanych zarówno w Kotlin, jak i w Javie, zalecamy połączenie funkcji zwrotnej po zakończeniu 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 się na Kotlin, używaj funkcji suspend.

suspend fun asyncLoadFoo(): Foo

W bibliotekach integracji dla Javy możesz używać biblioteki GuavaListenableFuture.

public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();

Nie używaj opcji Opcjonalne

Chociaż Optional może mieć zalety w niektórych interfejsach API, jest on niezgodny z dotychczasowymi interfejsami API Androida. @Nullable@NonNull zapewniają pomoc w narzędziach w zakresie bezpieczeństwa null, a Kotlin wymusza umowy dotyczące możliwości null na poziomie kompilatora, co powoduje, że Optional staje się zbędny.

W przypadku opcjonalnych prymitywów używaj parowanych metod hasget. Jeśli wartość nie jest ustawiona (has zwraca false), metoda get powinna wyrzucić błąd 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 1 konstruktor prywatny, aby zapobiec tworzeniu instancji za pomocą domyślnego konstruktora bez argumentów.

public final class Log {
  // Not instantiable.
  private Log() {}
}

Singletony

Nie zalecamy korzystania z testów typu singleton, ponieważ mają one te wady:

  1. Budowa jest zarządzana przez zajęcia, co uniemożliwia używanie podróbek.
  2. Testy nie mogą być hermetyczne ze względu na statyczną naturę pojedynczego obiektu
  3. Aby obejść te problemy, deweloperzy muszą znać wewnętrzne szczegóły klasy singleton lub utworzyć dla niej osłonę.

Preferuj wzór pojedyncza instancja, który opiera się na abstrakcyjnej klasie bazowej.

Pojedyncza instancja

Klasy pojedynczych wystąpień używają abstrakcyjnej klasy bazowej z konstruktorem private lub internal i zawierają stałą metodę getInstance(), która umożliwia uzyskanie wystąpienia. Metoda getInstance() musi zwracać ten sam obiekt w kolejnych wywołaniach.

Obiekt zwracany przez funkcję 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 singletona tym, że deweloperzy mogą utworzyć fałszywą wersję SingleInstance i użyć własnego frameworku do wstrzykiwania zależności, aby zarządzać implementacją bez konieczności tworzenia opakowania, lub biblioteka może dostarczyć własną fałszywą wersję w postaci artefaktu -testing.

Klasy, które uwalniają zasoby, powinny implementować AutoCloseable

Klasy, które uwalniają zasoby za pomocą metod close, release, destroy lub podobnych, powinny implementować metodę java.lang.AutoCloseable, aby umożliwić deweloperom automatyczne usuwanie tych zasobów podczas korzystania z bloku try-with-resources.

Unikaj wprowadzania nowych podklas widoku na Androidzie*.

Nie wprowadzaj nowych klas, które dziedziczą bezpośrednio lub pośrednio z android.view.View w publicznym interfejsie API platformy (czyli w android.*).

Zestaw narzędzi interfejsu Androida jest teraz oparty na Compose. Nowe funkcje interfejsu użytkownika 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 użytkownika Jetpack Compose (i opcjonalnie komponentów opartych na widoku) w bibliotekach Jetpacka. Udostępnianie tych komponentów w bibliotekach daje możliwość wdrożenia wstecznego, gdy funkcje platformy są niedostępne.

Fieldsem

Te reguły dotyczą pól publicznych w klasach.

Nie udostępniaj pól nieprzetworzonych

Klasy Java nie powinny bezpośrednio udostępniać pól. Pola powinny być prywatne i dostępne tylko za pomocą publicznych metod getter i setter, niezależnie od tego, czy są to pola finalne.

Rzadkie wyjątki obejmują podstawowe struktury danych, w których nie ma potrzeby ulepszania zachowania podczas określania lub pobierania pola. W takich przypadkach pola powinny być nazwane zgodnie ze standardowymi konwencjami nazewnictwa zmiennych, np. Point.xPoint.y.

Klasy Kotlina mogą udostępniać właściwości.

Pola wyświetlane powinny być oznaczone jako ostateczne

Nie zalecamy używania pól nieprzetworzonych (patrz: Nie udostępniaj pól nieprzetworzonych). W rzadkich przypadkach, gdy pole jest widoczne jako pole publiczne, zaznacz je final.

Pola wewnętrzne nie powinny być widoczne

Nie odwołuj się do nazw pól wewnętrznych w publicznym interfejsie API.

public int mFlags;

Użyj opcji publicznie dostępne zamiast chronione.

@zobacz Używanie publicznych, a nie chronionych plików

Stałe

To reguły dotyczące stałych publicznych.

Flagi stałych nie mogą nakładać się na wartości int ani long

Flagi oznaczają bity, które można połączyć w wartość zjednoczenia. Jeśli tak nie jest, nie wywołuj 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;

Aby dowiedzieć się więcej o definiowaniu publicznych stałych flag, zobacz @IntDef (flagi bitowe).

Stałe końcowe w klasie statycznej powinny być zapisywane w wielkiej literze i oddzielone od siebie podkreślnikiem.

Wszystkie słowa w konstancie powinny być pisane wielkimi literami, a kilka słów powinno być rozdzielonych znakiem _. Na 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 służy do standardowych elementów, takich jak flagi, klucze i działania. Te stałe powinny mieć standardowe prefiksy, aby można je było łatwiej zidentyfikować.

Na przykład dodatkowe informacje o intencji powinny zaczynać się od EXTRA_. Działania związane z zamiarem powinny zaczynać się od ACTION_. Wartości stałe używane w funkcji Context.bindService() powinny zaczynać się od BIND_.

Nazwy i zakresy kluczowych stałych

Wartości stałych ciągu znaków powinny być zgodne z nazwą stałej i zazwyczaj powinny być ograniczone do pakietu lub domeny. Na przykład:

public static final String FOO_THING = "foo"

nie ma spójnej nazwy ani odpowiedniego zakresu. Zamiast tego:

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

Współczynniki android w konstantach ciągu znaków z ograniczeniem są zarezerwowane dla projektu Android Open Source.

Działania i dodatkowe informacje dotyczące intencji, a także wpisy w pakiecie powinny być umieszczone w przestrzeni nazw za pomocą nazwy 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żyj opcji publicznie dostępne zamiast chronione.

@zobacz Używanie publicznych, a nie chronionych plików

Używaj 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 flagi:

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;

@zobacz Używanie standardowych prefiksów dla stałych

Używanie spójnych nazw zasobów

Publiczne identyfikatory, atrybuty i wartości muszą mieć nazwy zgodne z konwencją nazewnictwa camelCase, np. @id/accessibilityActionPageUp lub @attr/textAppearance, podobnie jak publiczne pola w języku Java.

W niektórych przypadkach publiczny identyfikator lub atrybut zawiera wspólny prefiks oddzielony znakiem podkreślenia:

  • wartości konfiguracji platformy, np. @string/config_recentsComponentName w pliku config.xml;
  • atrybuty widoku specyficzne dla układu, takie jak @attr/layout_marginStart w pliku attrs.xml;

Publiczne motywy i style muszą być nazywane zgodnie z hierarchiczną konwencją nazewnictwa PascalCase, np. @style/Theme.Material.Light.DarkActionBar lub @style/Widget.Material.SearchView.ActionBar, podobnie jak zagnieżdżone klasy w języku Java.

Zasoby układu i zasoby do rysowania nie powinny być udostępniane jako publiczne interfejsy API. Jeśli jednak muszą być one widoczne, publiczne układy i elementy rysowane muszą mieć nazwy zgodne z konwencją nazewnictwa podkreślenie_podkreślenie, np. layout/simple_list_item_1.xml lub drawable/title_bar_tall.xml.

Jeśli stałe mogą się zmieniać, zrób je dynamiczne.

Kompilator może wstawiać wartości statyczne w kod źródłowy, więc zachowanie tych wartości jest uważane za część umowy dotyczącej interfejsu API. Jeśli wartość stałej MIN_FOO lub MAX_FOO może się w przyszłości zmienić, rozważ użycie zamiast nich metod dynamicznych.

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

Zadbaj o współpracę wstecz wywołań zwrotnych

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:

Hipotetyczne ź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 funkcją targetSdkVersion="22":

if (result == STATUS_FAILURE) {
  // Oh no!
} else {
  // Success!
}

W tym przypadku aplikacja została zaprojektowana z uwzględnieniem ograniczeń poziomu API 22 i zastosowano w niej (raczej) rozsądne założenie, że istnieją tylko 2 możliwe stany. Jeśli jednak aplikacja otrzyma nowo dodaną wartość STATUS_FAILURE_RETRY, zinterpretuje to jako sukces.

Metody zwracające stałe mogą bezpiecznie obsługiwać takie przypadki, ograniczając dane wyjściowe do poziomu interfejsu API docelowego dla aplikacji:

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 wartości może się w przyszłości zmienić. Jeśli zdefiniujesz interfejs API z konstantą UNKNOWN lub UNSPECIFIED, która wygląda jak uniwersalna, deweloperzy założą, że opublikowane przez Ciebie stałe są wyczerpujące. Jeśli nie chcesz określać tego oczekiwania, zastanów się, czy w przypadku Twojego interfejsu API stosowanie uniwersalnej stałej jest dobrym pomysłem.

Biblioteki nie mogą też określać własnych wartości targetSdkVersion niezależnie od aplikacji, a obsługa zmian zachowania targetSdkVersion w kodzie biblioteki jest skomplikowana i może powodować błędy.

Ciąg znaków lub liczba całkowita

Użyj stałych liczb całkowitych i @IntDef, jeśli nazwa przestrzeni dla wartości nie jest rozszerzalna poza Twój pakiet. Używaj stałych ciągu znaków, jeśli przestrzeń nazw jest współdzielona lub może być rozszerzona przez kod spoza pakietu.

Klasy danych

Klasy danych reprezentują zestaw niezmiennych właściwości i zapewniają niewielki, dobrze zdefiniowany zestaw funkcji pomocniczych do interakcji z tymi danymi.

Nie używaj interfejsów API Kotlina w publicznych interfejsach API, ponieważ kompilator Kotlina nie gwarantuje zgodności z interfejsem API języka ani zgodności binarnej wygenerowanego kodu.data class Zamiast tego ręcznie zaimplementuj wymagane funkcje.

Tworzenie instancji

W języku Java klasy danych powinny zawierać konstruktor, gdy mają niewiele właściwości, lub używać wzorca Builder, gdy mają wiele właściwości.

W Kotlinie klasy danych powinny udostępniać konstruktor z argumentami domyślnymi niezależnie od liczby właściwości. Klasy danych zdefiniowane w Kotlinie mogą też korzystać z budowniczego podczas kierowania na klientów w Java.

Modyfikowanie i kopiowanie

W przypadku, gdy dane wymagają modyfikacji, podaj klasę Builder z konstruktorem kopiowania (Java) lub funkcję członkowską copy() (Kotlin), która zwraca nowy obiekt.

W przypadku funkcji copy() w Kotlinie argumenty muszą być zgodne z konstruktorem klasy, a wartości domyślne muszą być wypełnione za pomocą 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 interfejs equals(), jak i hashCode(), a w implementacjach tych metod należy uwzględnić wszystkie właściwości.

Klasy danych mogą implementować toString() w zależnym od formatu zalecanym dla implementacji klasy danych w Kotlinie, np. User(var1=Alex, var2=42).

Metody

Są to reguły dotyczące różnych szczegółów metod, parametrów, nazw metod, typów zwracanych wartości i specyfikatorów dostępu.

Godzina

Te reguły określają, jak w interfejsach API należy wyrażać pojęcia czasu, takie jak daty i czas trwania.

W miarę możliwości używaj typów java.time.*

Typy java.time.Duration, java.time.Instant i wiele innych typów java.time.* są dostępne na wszystkich wersjach platformy dzięki desugaringowi i powinny być preferowane podczas wyrażania czasu w parametrach interfejsu API lub wartościach zwracanych.

Preferuj udostępnianie tylko wariantów interfejsu API, które akceptują lub zwracają java.time.Duration lub java.time.Instant, i pomijaj prymitywne warianty w tym samym przypadku użycia, chyba że domena interfejsu API jest domeną, w której przydzielanie obiektów w ramach zamierzonych wzorców użycia miałoby negatywny wpływ na wydajność.

Metody wyrażające czas trwania powinny mieć nazwę „czas trwania”.

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 wartości czasu oczekiwania.

Typ „time” (czas) o typie java.time.Instant jest odpowiedni, gdy odnosi się do konkretnego punktu w czasie, a nie do jego trwania.

Metody wyrażające czas trwania lub czas jako obiekt podstawowy powinny być nazwane z jednostką czasu i użyciem long.

Metody akceptujące lub zwracające czas trwania jako typ prymitywny powinny dodawać do nazwy metody odpowiednie jednostki czasu (takie jak Millis, Nanos, Seconds), aby zarezerwować nieozdobioną nazwę do użycia z java.time.Duration. Patrz Czas.

Metody powinny być również odpowiednio oznaczone jednostką i podstawą czasową:

  • @CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa wyrażona w liczbie milisekund od 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa wyrażona w liczbie sekund od 1970-01-01T00:00:00Z.
  • @DurationMillisLong: wartość jest nieujemnym czasem w milisekundach.
  • @ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa w podstawie czasowej SystemClock.elapsedRealtime().
  • @UptimeMillisLong: wartość to nieujemna sygnatura czasowa w bazie czasowej SystemClock.uptimeMillis().

Pierwotne parametry czasu lub wartości zwracane powinny używać long, a nie int.

ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);

Metody wyrażające jednostki czasu powinny preferować nieskrótkowane nazwy jednostek.

public void setIntervalNs(long intervalNs);

public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);

public void setTimeoutMicros(long timeoutMicros);

Dodawanie adnotacji do długich argumentów czasu

Platforma zawiera kilka adnotacji, które zapewniają większą precyzję w przypadku jednostek czasu typu long:

  • @CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa wyrażona w milisekundach od 1970-01-01T00:00:00Z, czyli w podstawie czasowej System.currentTimeMillis().
  • @CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa wyrażona w sekundach od 1970-01-01T00:00:00Z.
  • @DurationMillisLong: wartość jest nieujemnym czasem w milisekundach.
  • @ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa w podstawie czasowej SystemClock#elapsedRealtime().
  • @UptimeMillisLong: wartość to nieujemna sygnatura czasowa w podstawie czasowej SystemClock#uptimeMillis().

Jednostki miary

W przypadku wszystkich metod wyrażania jednostki miary innej niż czas, zaleca się stosowanie prefiksów jednostek SI w alfabetycznym układzie.

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

umieszczanie opcjonalnych parametrów na końcu przeciążeń;

Jeśli masz przeciążenia metody z opcjonalnymi parametrami, pozostaw je na końcu i zachowaj spójny porządek z pozostałymi 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 zachowanie prostszych metod powinno być takie samo, jak gdyby bardziej rozbudowane metody zawierały argumenty domyślne.

Wniosek: nie przeciążaj metod, chyba że chcesz dodać argumenty opcjonalne lub zaakceptować różne typy argumentów, jeśli metoda jest polimorficzna. Jeśli przeciążona metoda robi coś zupełnie innego, nadaj jej nową nazwę.

Metody z parametrami domyślnymi muszą być opatrzone adnotacjami @JvmOverloads (dotyczy tylko Kotlina).

Metody i konstruktory z parametrami domyślnymi muszą być opatrzone adnotacją @JvmOverloads, aby zachować zgodność binarną.

Więcej informacji znajdziesz w oficjalnym przewodniku po interoperacyjności Kotlina i Java (w języku angielskim) na temat przeciążeń funkcji domyślnych.

class Greeting @JvmOverloads constructor(
  loudness: Int = 5
) {
  @JvmOverloads
  fun sayHello(prefix: String = "Dr.", name: String) = // ...
}

Nie usuwaj domyślnych wartości parametrów (dotyczy tylko języka Kotlin)

Jeśli metoda została dostarczona z parametrem o wartości domyślnej, usunięcie tej wartości domyślnej powoduje przerwanie zgodności ze źródłem.

Najbardziej charakterystyczne i wyróżniające metodę parametry powinny być podawane jako pierwsze.

Jeśli metoda ma wiele parametrów, na początku umieść te, które są najbardziej odpowiednie. Parametry, które określają flagi i inne opcje, są mniej ważne niż te, które opisują obiekt, na którym wykonywane są działania. Jeśli jest 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 przeciążeniach

Builders

W przypadku tworzenia złożonych obiektów Java zalecamy użycie wzorca Builder. Jest on często używany w Androidzie w takich przypadkach:

  • Właściwości wynikowego obiektu powinny być niezmienne.
  • Istnieje duża liczba wymaganych właściwości, na przykład wiele argumentów konstruktora.
  • W momencie tworzenia istnieje złożona relacja między usługami, na przykład wymagany jest krok weryfikacji. Pamiętaj, że ten poziom złożoności często świadczy o problemach z użytecznością interfejsu API.

Zastanów się, czy potrzebujesz kreatora. W przypadku interfejsu API kreatory są przydatne, jeśli służą do:

  • Skonfiguruj tylko kilka z potencjalnie dużego zestawu opcjonalnych parametrów tworzenia.
  • Skonfiguruj wiele różnych opcjonalnych lub wymaganych parametrów tworzenia, czasami o podobnych lub identycznych typach, w przypadku których strony wywołania mogą być trudne do odczytania lub podatne na błędy podczas zapisywania.
  • Konfigurowanie tworzenia obiektu stopniowo, gdzie kilka różnych fragmentów kodu konfiguracji może wywoływać konstruktor jako szczegóły implementacji.
  • umożliwić rozszerzanie typu przez dodanie dodatkowych opcjonalnych parametrów tworzenia w przyszłych wersjach interfejsu API;

Jeśli masz typ z 3 lub mniej wymaganymi parametrami i bez parametrów opcjonalnych, możesz prawie zawsze pominąć konstruktor i użyć zwykłego konstruktora.

Klasy pochodzące z Kotlina powinny preferować konstruktory z oznaczeniem @JvmOverloads i argumentami domyślnymi zamiast konstruktorów Builder, ale mogą też zwiększyć użyteczność dla klientów Java, udostępniając konstruktory Builder w przypadkach opisanych wcześniej.

class Tone @JvmOverloads constructor(
  val duration: Long = 1000,
  val frequency: Int = 2600,
  val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
  class Builder {
    // ...
  }
}

Klasy typu Builder muszą zwracać obiekt typu Builder

Klasy typu Builder muszą umożliwiać łańcuchowanie metod, zwracając obiekt Builder (np. this) z każdej metody oprócz build(). Dodatkowe utworzone obiekty powinny być przekazywane jako argumenty – nie zwracaj konstruktora innego obiektu. Na 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 podstawowa klasa 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 konstruktora muszą być tworzone za pomocą konstruktora

Aby zachować spójność podczas tworzenia obiektów Builder za pomocą interfejsu API Androida, wszystkie takie obiekty muszą być tworzone za pomocą konstruktora, a nie statycznej metody konstruktora. W przypadku interfejsów API opartych na języku Kotlin metoda Builder musi być publiczna, nawet jeśli użytkownicy Kotlina mają domyślnie polegać na konstruktorze za pomocą metody fabrycznej lub mechanizmu tworzenia w stylu DSL. Biblioteki nie mogą używać funkcji @PublishedApi internal do selektywnego ukrywania konstruktora klasy Builder przed klientami Kotlina.

public class Tone {
  public static Builder builder();
  public static class Builder {
  }
}
public class Tone {
  public static class Builder {
    public Builder();
  }
}

Wszystkie argumenty konstruktorów buildera muszą być wymagane (np. @NonNull)

Opcjonalne argumenty, np. @Nullable, należy przenieść do metod ustawiania. Konstruktor buildera powinien rzucać NullPointerException (rozważ użycie Objects.requireNonNull), jeśli nie są określone żadne wymagane argumenty.

Klasy konstruktora powinny być finalnymi statycznymi klasami wewnętrznymi swoich typów.

Ze względu na logiczną organizację w pakiecie klasy konstruktora powinny być zwykle udostępniane jako ostateczne klasy wewnętrzne swoich typów, na przykład Tone.Builder zamiast ToneBuilder.

Twórcy mogą dołączyć konstruktor, aby utworzyć nową instancję na podstawie istniejącej.

Kreatory mogą zawierać konstruktor kopiujący, który tworzy nową instancję kreatora na podstawie istniejącego kreatora lub utworzonego obiektu. Nie powinny udostępniać alternatywnych metod tworzenia instancji kreatora z dotychczasowych kreatorów lub obiektów kreatora.

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);
  }
}
Jeśli konstruktor kopiujący zawiera argumenty typu @Nullable, metody settera w budującym powinny przyjmować argumenty typu @Nullable.

Resetowanie jest niezbędne, jeśli nowa instancja kreatora może zostać utworzona na podstawie istniejącej. Jeśli nie ma dostępnego konstruktora kopii, w budującym może być argument @Nullable lub @NonNullable.

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
W przypadku metod settera w budującym mogą występować argumenty @Nullable dla właściwości opcjonalnych

Często łatwiej jest użyć wartości null w przypadku danych wejściowych drugiego stopnia, zwłaszcza w Kotlinie, który wykorzystuje argumenty domyślne zamiast konstruktorów i przeciążeń.

Dodatkowo metody @Nullable będą dopasowywały się do metod @Nullable, 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 zastosowanie w Kotlinie:

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 nie wywołano metody ustawiającej) i znaczenie null muszą być odpowiednio udokumentowane zarówno w metodzie ustawiającej, jak i metodie pobierającej.

/**
 * ...
 *
 * <p>Defaults to {@code null}, which means the optional value won't be used.
 */

Właściwości konstruktora można ustawiać w przypadku właściwości zmiennych, dla których w klasie wygenerowanej dostępne są metody settera.

Jeśli klasa ma właściwości, które można zmienić, i potrzebuje klasy Builder, zastanów się, czy naprawdę powinna mieć takie właściwości.

Jeśli masz pewność, że potrzebujesz właściwości zmiennych, wybierz jeden z tych scenariuszy, który lepiej pasuje do Twojego przypadku użycia:

  1. Obiekt utworzony powinien być od razu gotowy do użycia, dlatego należy zapewnić metody mutatora dla wszystkich odpowiednich właściwości, niezależnie od tego, czy są one zmienne, czy niezmienne.

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. Zanim skompilowany obiekt będzie przydatny, może być konieczne wykonanie dodatkowych wywołań, dlatego w przypadku właściwości zmiennych nie należy podawać metod ustawiania.

    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 dwóch scenariuszy.

Value v = new Value.Builder(requiredValue)
    .setImmutableProperty(immutableValue)
    .setUsefulMutableProperty(usefulValue)
    .build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);

Budowniki nie powinny mieć getterów

Metoda getter powinna być w obiekcie utworzonym na podstawie buildera, a nie w builderze.

Metody settera w ramach konstruktora muszą mieć odpowiadające im metody gettera w klasie wygenerowanej.

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 kreatora powinny być w stylu setFoo(), addFoo() lub clearFoo().

Klasy konstruktora powinny deklarować metodę build().

Klasy kreatora powinny deklarować metodę build(), która zwraca instancję utworzonego obiektu.

Metody Builder build() muszą zwracać obiekty @NonNull

Metoda build() w klasie konstruktora powinna zwracać instancję utworzonego obiektu, która nie jest równa null. Jeśli obiekt nie może zostać utworzony z powodu nieprawidłowych parametrów, weryfikację można odroczyć do metody build i wyrzucić błądIllegalStateException.

Nie udostępniaj blokad wewnętrznych

Metody w interfejsie API publicznego nie powinny używać słowa kluczowego synchronized. To słowo kluczowe powoduje, że obiekt lub klasa jest używany jako blokada. Ponieważ jest on dostępny dla innych, możesz napotkać nieoczekiwane skutki uboczne, jeśli inny kod poza Twoją klasą zacznie go używać do blokowania.

Zamiast tego zastosuj wymagane blokowanie w przypadku wewnętrznego obiektu prywatnego.

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

W źródłach kodu Kotlin metody w stylu akcesora (czyli te, które używają prefiksów get, set lub is) będą też dostępne jako właściwości Kotlina. Na przykład int getField() zdefiniowana w języku Java jest dostępna w Kotlinie jako właściwość val field: Int.

Z tego powodu oraz aby spełnić oczekiwania programistów dotyczące zachowania metod akcesorów, metody korzystające z prefiksów metod akcesorów powinny działać podobnie jak pola Java. Unikaj prefiksów w przypadku metod dostępu, gdy:

  • Metoda ma efekty uboczne – preferuj bardziej opisową nazwę metody
  • Ta metoda wymaga dużych nakładów obliczeniowych – lepiej użyć compute
  • Metoda polega na zablokowaniu lub przedłużeniu czasu działania w celu zwrócenia wartości, takiej jak IPC lub inne operacje wejścia/wyjścia – preferowany jest fetch
  • Metoda blokuje wątek, dopóki nie może zwrócić wartości – preferuj await
  • Metoda zwraca nową instancję obiektu przy każdym wywołaniu – preferuj create
  • Metoda może nie zwrócić wartości – lepiej użyć request

Pamiętaj, że wykonanie raz pracochłonnego działania i zapisanie wartości w pamięci podręcznej na potrzeby kolejnych wywołań nadal liczy się jako wykonanie pracochłonnego działania. Jank nie jest amortyzowany w poszczególnych klatkach.

Używanie prefiksu is w przypadku metod dostępu do atrybutów logicznych

Jest to standardowa konwencja nazewnictwa metod i pól logicznych w języku Java. Na ogół nazwy metod i zmiennych logicznych powinny być zapisane jako pytania, na które odpowiada wartość zwracana.

Metody dostępu do atrybutów logicznych w języku Java powinny stosować schemat 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 set/is w przypadku metod dostępu w języku Java lub is w przypadku pól w języku Java umożliwi ich użycie 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 zwykle używać pozytywnych nazw, na przykład Enabled zamiast Disabled. Używanie negatywnej terminologii odwraca znaczenie truefalse oraz utrudnia analizowanie zachowań.

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

W przypadku, gdy wyrażenie logiczne opisuje włączenie lub własność w usłudze, możesz użyć has zamiast is. Nie będzie to jednak działać w przypadku składni właściwości języka 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 przedrostki, które mogą być bardziej odpowiednie, to canshould:

// "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ą na 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()

Nazwa metody powinna być zazwyczaj sformułowana jako pytanie, na które odpowiada zwracana wartość.

Metody właściwości w Kotlinie

W przypadku właściwości klasy var foo: Foo Kotlin wygeneruje metody get/set, stosując spójną regułę: w przypadku metody gettera doda get i uczyni pierwszą literę dużą, a w przypadku metody settera doda set i uczyni pierwszą literę dużą. Deklaracja właściwości wygeneruje metody o nazwach public Foo getFoo()public void setFoo(Foo foo).

Jeśli właściwość ma typ Boolean, podczas generowania nazwy obowiązuje dodatkowe reguła: jeśli nazwa właściwości zaczyna się od is, przed nazwą metody gettera nie dołącza się get, a jako getter używa się samej nazwy właściwości. Dlatego preferuj nadawanie obiektom Boolean prefiksu is, aby zachować spójność z wytycznymi dotyczącymi nazewnictwa:

var isVisible: Boolean

Jeśli Twoja usługa jest jednym z wymienionych wyżej wyjątków i zaczyna się od odpowiedniego prefiksu, użyj adnotacji @get:JvmName w usłudze, aby ręcznie podać odpowiednią nazwę:

@get:JvmName("hasTransientState")
var hasTransientState: Boolean

@get:JvmName("canRecord")
var canRecord: Boolean

@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean

Akcesoria bitmask

Aby poznać wytyczne dotyczące definiowania flag bitowych w interfejsie API, przeczytaj artykuł Używanie flag bitowych @IntDef.

Setters

Należy podać 2 metody settera: jedną, która przyjmuje pełny ciąg bitów i zapisuje wszystkie istniejące flagi, oraz drugą, która przyjmuje niestandardową maskę bitów, 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ć jedną funkcję gettera.

/**
 * 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żyj opcji publicznie dostępne zamiast chronione.

W publicznych interfejsach API zawsze preferuj public zamiast protected. Zabezpieczony dostęp okazuje się w długim okresie kłopotliwy, ponieważ implementatorzy muszą zastąpić go publiczną metodą dostępu w przypadkach, gdy dostęp zewnętrzny domyślnie byłby równie dobry.

Pamiętaj, że protected widoczność nie uniemożliwia deweloperom wywoływania interfejsu API – tylko nieco ją utrudnia.

Nie wdrażaj ani jednej, ani obu metod equals() i hashCode().

Jeśli zastąpisz jeden, musisz zastąpić też drugi.

Implementacja metody toString() w klasach danych

Zachęcamy do zastąpienia wartości toString() wartościami z klasy danych, aby ułatwić deweloperom debugowanie kodu.

Zapisz, czy dane wyjściowe są przeznaczone do działania programu czy debugowania.

Zdecyduj, czy działanie programu ma zależeć od Twojej implementacji. Na przykład funkcje UUID.toString() i File.toString() dokumentują format, którego używają programy. Jeśli informacje są udostępniane tylko do debugowania, na przykład Intent, to załóż, że dokumenty dziedziczą z superklasy.

Nie podawaj dodatkowych informacji

Wszystkie informacje dostępne w usłudze toString() powinny być też dostępne za pomocą publicznego interfejsu API obiektu. W przeciwnym razie zachęcasz deweloperów do analizowania i korzystania z danych wyjściowych funkcji toString(), co uniemożliwi wprowadzenie zmian w przyszłości. Dobrą praktyką jest implementowanie funkcji toString() tylko za pomocą publicznego interfejsu API obiektu.

Nie zalecamy polegania na danych wyjściowych debugowania.

Nie można uniemożliwić deweloperom korzystania z wyjścia debugowania, ale uwzględnienie System.identityHashCode obiektu w wyjściu toString() sprawi, że bardzo mało prawdopodobne będzie, aby 2 różne obiekty miały takie samo wyjście toString().

@Override
public String toString() {
  return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}

Może to skutecznie zniechęcić deweloperów do tworzenia stwierdzeń testowych, takich jak assertThat(a.toString()).isEqualTo(b.toString()), w Twoich obiektach.

Używanie funkcji createFoo podczas zwracania nowo utworzonych obiektów

W przypadku metod, które zwracają wartości, np. przez tworzenie nowych obiektów, używaj prefiksu create, a nie get ani new.

Jeśli metoda ma utworzyć obiekt, który ma zwrócić, wyraźnie to zaznacz 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)

Przejmowanie i zwracanie nieprzetworzonych prymitywów zamiast wersji w pudełku

Jeśli chcesz przekazać brakujące lub puste wartości, użyj właściwości -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 korzystania z klas odpowiadających typom prymitywnym pozwala uniknąć nadmiernego obciążenia pamięcią tych klas, dostępu metod do wartości, a co ważniejsze, automatycznego zamieniania typów prymitywnych na typy obiektów. Unikanie tych zachowań pozwala oszczędzać pamięć i zasoby tymczasowe, co może prowadzić do kosztownych i częstszych zbiorów elementów do usunięcia.

Używanie adnotacji do wyjaśniania prawidłowych wartości parametrów i zwracanych

Dodaliśmy adnotacje dla deweloperów, aby wyjaśnić dozwolone wartości w różnych sytuacjach. Ułatwia to narzędziom pomaganie deweloperom, gdy podają one nieprawidłowe wartości (np. gdy podają dowolną wartość int, a platforma wymaga jednej z wartości z konkretnego zestawu stałych wartości). W razie potrzeby używaj wszystkich tych adnotacji:

Dopuszczalność wartości null

W przypadku interfejsów API Javy wymagane są adnotacje dotyczące możliwości wystąpienia wartości null, ale koncepcja możliwości wystąpienia wartości null jest częścią języka Kotlin, dlatego w interfejsach API w Kotlinie nie należy używać adnotacji dotyczących możliwości wystąpienia wartości null.

@Nullable: wskazuje, że dane zwracane, parametr lub pole mogą być puste:

@Nullable
public String getName()

public void setName(@Nullable String name)

@NonNull: wskazuje, że dana wartość zwracana, parametr lub pole nie może być null. Oznaczanie elementów jako @Nullable jest stosunkowo nową funkcją w Androidzie, dlatego większość metod interfejsu API Androida nie jest odpowiednio udokumentowana. Dlatego mamy 3 stany: „unknown, @Nullable, @NonNull”, dlatego @NonNull jest częścią wytycznych dotyczących interfejsu API:

@NonNull
public String getName()

public void setName(@NonNull String name)

W przypadku dokumentacji platformy Android adnotowanie parametrów metody spowoduje automatyczne wygenerowanie dokumentacji w formie „Ta wartość może być nullem”, chyba że „null” jest używane w innym miejscu w dokumentacji parametrów.

Istniejące metody „niezupełnie nullowalne”: istniejące metody w interfejsie API bez zadeklarowanej adnotacji @Nullable mogą zostać opatrzone adnotacją @Nullable, jeśli metoda może zwracać null w określonych, oczywistych okolicznościach (np. findViewById()). Dla deweloperów, którzy nie chcą sprawdzać wartości null, należy dodać dodatkowe metody @NotNull requireFoo(), które rzucają IllegalArgumentException.

Metody interfejsu: nowe interfejsy API powinny dodawać odpowiednią adnotację podczas implementowania metod interfejsu, np. Parcelable.writeToParcel() (czyli metoda w klasie implementacji powinna być writeToParcel(@NonNull Parcel, int), a nie writeToParcel(Parcel, int)). Istniejące interfejsy API, które nie mają adnotacji, nie muszą jednak być „naprawiane”.

Egzekwowanie dopuszczalności

W języku Java zaleca się stosowanie metod do weryfikacji danych wejściowych w przypadku parametrów @NonNull za pomocą Objects.requireNonNull() i wywoływania NullPointerException, gdy parametry są puste. W języku Kotlin jest to wykonywane automatycznie.

Materiały

Identyfikatory zasobów: parametry całkowite, które oznaczają identyfikatory konkretnych zasobów, powinny być opatrzone odpowiednią definicją typu zasobu. Oprócz uniwersalnego zasobu @AnyRes istnieją adnotacje dla każdego typu zasobu, np. @StringRes, @ColorRes i @AnimRes. Przykład:

public void setTitle(@StringRes int resId)

@IntDef dla zestawów stałych

Magiczne stałe: parametry Stringint, które mają przyjmować jedną z ograniczonego zbioru możliwych wartości oznaczonych za pomocą publicznych stałych, 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. Na 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);

Zalecamy stosowanie metod, aby sprawdzać poprawność adnotowanych parametrów i wyrzucać IllegalArgumentException, jeśli parametr nie należy do @IntDef

@IntDef dla flag bitowych

W adnotacji można też określić, że stałe są flagami i można je łączyć za pomocą operatorów „&” 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 zestawów stałych ciągu tekstowego

Jest też adnotacja @StringDef, która jest dokładnie taka sama jak @IntDef w poprzedniej sekcji, ale dotyczy stałych String. Możesz podać wiele wartości „prefix”, które są używane do automatycznego generowania dokumentacji dla wszystkich wartości.

@SdkConstant dla stałych pakietu SDK

@SdkConstant Dodaj adnotacje do publicznych pól, gdy mają jedną z tych wartości: SdkConstant: ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE.

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

Zapewnij zgodność z opcją null dla zastąpień.

Ze względu na zgodność z interfejsem API wartość null zastąpień powinna być zgodna z obecną wartością null nadrzędnego. W tabeli poniżej znajdziesz informacje o oczekiwanej zgodności. Zastąpienia powinny być równie restrykcyjne lub bardziej restrykcyjne niż element, który zastępują.

Typ Rodzic Dziecko
Typ zwracanej wartości Nieotagowane Nieotagowane lub nienullowe
Typ zwracanej wartości Dopuszczalna wartość null Wartość null lub nienull
Typ zwracanej wartości Nonnull Nonnull
Zabawny argument Nieotagowane bez adnotacji lub z możliwością pominięcia
Argument oparty na zabawie Dopuszczalna wartość null Dopuszczalna wartość null
Argument oparty na zabawie Nonnull Wartość null lub nienull

Tam, gdzie to możliwe, preferuj argumenty, które nie mogą być puste (np. @NonNull).

Gdy metody są przeciążone, preferuj, aby wszystkie argumenty były niezerowe.

public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }

Ta reguła dotyczy również przeciążonych metod seterów właściwości. Główny argument nie powinien być równy null, a czyszczenie właściwości powinno być zaimplementowane jako osobna metoda. Zapobiega to „bezsensownym” wywołaniom, w których deweloper musi ustawiać końcowe parametry, 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()

W przypadku kontenerów preferuj typy zwracane, które nie mogą być puste (np. @NonNull).

W przypadku typów kontenerów takich jak Bundle lub Collection zwracaj pusty (a w stosownych przypadkach niezmienny) kontener. W przypadkach, gdy null ma służyć do rozróżniania dostępności kontenera, rozważ podanie osobnej metody logicznej.

@NonNull
public Bundle getExtras() { ... }

adnotacje nieważności dla par get i set muszą być zgodne

Pary metod get i set dotyczące jednej właściwości logicznej powinny zawsze zawierać zgodne adnotacje dotyczące możliwości wystąpienia wartości null. Nieprzestrzeganie tej wskazówki spowoduje nieprawidłowe działanie składni właściwości w Kotlinie, a dodanie do istniejących metod właściwości adnotacji z niezgodnymi adnotacjami dotyczącymi możliwości wystąpienia wartości null spowoduje zmianę źródła dla użytkowników Kotlina.

@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }

Zwracanie wartości w przypadku niepowodzenia lub błędów

Wszystkie interfejsy API powinny umożliwiać aplikacjom reagowanie na błędy. Zwracanie wartości false, -1, null lub innych uniwersalnych wartości „coś poszło nie tak” nie daje deweloperowi wystarczających informacji o nieudanym określeniu oczekiwań użytkownika ani dokładnym śledzeniu niezawodności aplikacji w tym polu. Podczas projektowania interfejsu API wyobraź sobie, że tworzysz aplikację. Jeśli wystąpi błąd, czy interfejs API zawiera wystarczającą ilość informacji, aby wyświetlić go użytkownikowi lub zareagować odpowiednio?

  1. Dobrze (a nawet zalecamy) jest umieszczanie szczegółowych informacji w wiadomości o wyjątkach, ale deweloperzy nie powinni ich analizować, aby odpowiednio obsłużyć błąd. Szczegółowe kody błędów lub inne informacje powinny być udostępniane jako metody.
  2. Upewnij się, że wybrana opcja obsługi błędów daje Ci możliwość wprowadzania nowych typów błędów w przyszłości. W przypadku @IntDef oznacza to uwzględnienie wartości OTHER lub UNKNOWN – zwracając nowy kod, możesz sprawdzić parametr targetSdkVersion wywołującego, aby uniknąć zwracania kodu błędu, którego aplikacja nie zna. W przypadku wyjątków należy zaimplementować wspólną superklasę, aby wszystkie kody obsługujące dany typ mogły również wychwytywać i obsługiwać podtypy.
  3. Powinien być trudny lub niemożliwy do przypadkowego zignorowania przez dewelopera. Jeśli błąd jest przekazywany przez zwróconą wartość, dodaj do metody adnotację @CheckResult.

Zaleca się stosowanie instrukcji ? extends RuntimeException, gdy wystąpi błąd lub warunek błędu z powodu błędu programisty, np. zignorowania ograniczeń parametrów wejściowych lub braku sprawdzenia stanu obserwowalnego.

Metody ustawiające lub metody działania (np. perform) mogą zwracać kod stanu całkowitego, jeśli działanie może się nie udać z powodu asynchronicznie aktualizowanego stanu 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 tematu

Nazwa metody powinna zawsze zaczynać się od czasownika (np. get, create, reload itp.), a nie od obiektu, na którym działasz.

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

Jako typ zwracanego wyniku lub parametru preferuj typy kolekcji zamiast tablic

Interfejsy kolekcji o uniwersalnym typie mają kilka zalet w porównaniu z tablicami, takich jak silniejsze umowy interfejsu API dotyczące unikalności i kolejności, obsługa elementów ogólnych oraz kilka metod ułatwiających pracę programistom.

Wyjątek w przypadku elementów podstawowych

Jeśli elementy są prymitywne, używaj tablic, aby uniknąć kosztów automatycznego zaokrąglenia. Zobacz przetwarzanie i zwracanie nieprzetworzonych prymitywów zamiast wersji w opakowaniu.

Wyjątek dla 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 dotyczących grafiki lub innych interfejsów API do pomiaru, układu lub rysowania), dopuszczalne jest używanie tablic zamiast kolekcji w celu zmniejszenia alokacji i zmienności pamięci.

Wyjątek dla Kotlin

Tablice w Kotlinie są niezmienne, a język Kotlin udostępnia liczne interfejsy API do obsługi tablic, więc tablice są porównywalne z interfejsami API ListCollection w Kotlinie, do których można uzyskać dostęp z Kotlina.

Preferuj kolekcje @NonNull

W przypadku obiektów kolekcji zawsze preferuj @NonNull. Zwracając pustą kolekcję, użyj odpowiedniej metody Collections.empty, aby zwrócić niskokosztowe, poprawnie sformatowane i niezmienne obiekty kolekcji.

W przypadku obsługiwanych adnotacji typu zawsze preferuj @NonNull dla elementów kolekcji.

Tablice powinny być też preferowane przed kolekcjami (patrz poprzedni punkt).@NonNull Jeśli masz wątpliwości co do przypisywania obiektów, utwórz stałą i przekaż ją dalej – w szake pusty 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 języku Kotlin powinny domyślnie preferować typy zwracane kolekcji tylko do odczytu (a nie Mutable), chyba że kontrakt interfejsu API wyraźnie wymaga zmiennego typu zwracanego.

Interfejsy API w języku Java powinny jednak domyślnie preferować zmienność typów zwracanych, ponieważ implementacja interfejsów API w języku Java na platformie Android nie zapewnia jeszcze wygodnego sposobu implementacji niezmiennych kolekcji. Wyjątkiem są typy zwrotu Collections.empty, które są niezmienne. W przypadku, gdy zmienność może zostać wykorzystana przez klientów (celowo lub przez pomyłkę) do złamania zamierzonego wzorca użycia interfejsu API, interfejsy Java API powinny zwracać płytką kopię kolekcji.

@Nullable
public PermissionInfo[] getGrantedPermissions() {
  return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
  if (mPermissions == null) {
    return Collections.emptySet();
  }
  return new ArraySet<>(mPermissions);
}

Typy zwracanych wartości, które można wyraźnie zmieniać

Interfejsy API, które zwracają kolekcje, nie powinny modyfikować zwróconego obiektu kolekcji po zwróceniu. Jeśli zwrócona kolekcja musi ulec zmianie lub zostać ponownie użyta w jakiś sposób (np. jako dostosowany widok zbioru danych, który można zmieniać), dokładne zachowanie w momencie zmiany zawartości musi być wyraźnie udokumentowane lub zgodne z utrwałą konwencją nazewnictwa interfejsu API.

/**
 * Returns a view of this object as a list of [Item]s.
 */
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)

Konwencja Kotlina .asFoo() jest opisana poniżej i pozwala na zmianę kolekcji zwracanej przez funkcję .asList(), jeśli zmieni się oryginalna kolekcja.

Zmienne obiekty zwracane przez typ 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 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)
}

bardzo ograniczonych przypadkach niektóry kod wrażliwy na wydajność może korzystać z poolowania obiektów lub ich ponownego użycia. Nie twórz własnej struktury danych puli obiektów i nie udostępniaj wielokrotnie używanych obiektów w publicznych interfejsach API. W obu przypadkach należy bardzo uważnie zarządzać jednoczesnym dostępem.

Używanie typu parametru vararg

W przypadku interfejsów API w językach Kotlin i Java zalecamy używanie vararg w sytuacjach, gdy deweloper prawdopodobnie utworzy tablicę w miejscu wywołania tylko po to, aby przekazać wiele 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 obronne

Zarówno implementacje w Javie, jak i w Kotlinie parametrów vararg kompilują się do tego samego bajtkodu z tablicami, dzięki czemu można je wywoływać z kodu Javy za pomocą tablicy zmiennej. Projektantom interfejsów API zdecydowanie zalecamy tworzenie kopii skojarzonego parametru tablicowego w przypadku, gdy parametr ma być 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 ochronnej nie zapewnia żadnej ochrony przed jednoczesną modyfikacją między początkowym wywołaniem metody a utworzeniem kopii ani nie chroni przed mutacją obiektów zawartych w tablicy.

Upewnij się, że parametry typu kolekcji lub typy zwracane mają prawidłową semantykę.

List<Foo> to opcja domyślna, ale rozważ użycie innych typów, aby nadać dodatkowe znaczenie:

  • Użyj atrybutu Set<Foo>, jeśli Twój interfejs API jest obojętny co do kolejności elementów i nie zezwala na duplikaty lub duplikaty są bez znaczenia.

  • Collection<Foo>,, jeśli interfejs API jest obojętny co do kolejności i umożliwia duplikaty.

Funkcje konwersji w Kotlinie

Kotlin często używa funkcji .toFoo().asFoo(), aby uzyskać obiekt innego typu niż istniejący obiekt, gdzie Foo to nazwa typu zwracanego przez konwersję. Jest to zgodne ze znanym JDKObject.toString(). Kotlin idzie jeszcze dalej, używając go do konwersji prymitywnych, takich jak 25.toFloat().

Różnica między konwersjami o nazwach .toFoo().asFoo() jest istotna:

Użyj metody .toFoo() podczas tworzenia nowego, niezależnego obiektu.

Podobnie jak w przypadku funkcji .toString(), funkcja „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 uwzględni tych zmian.

fun Foo.toBundle(): Bundle = Bundle().apply {
    putInt(FOO_VALUE_KEY, value)
}

Użyj funkcji .asFoo() podczas tworzenia zależnego opakowania, ozdobionego obiektu lub odlewu.

Przesyłanie w Kotlinie jest wykonywane za pomocą słowa kluczowego as. Odzwierciedla ona zmianę interfejsu, ale nie zmianę tożsamości. Gdy jest używany jako prefiks w funkcji rozszerzenia, .asFoo() ozdabia odbiornik. Mutacja w pierwotnym obiekcie odbiorcy zostanie odzwierciedlona w obiekcie zwróconym przez funkcję asFoo(). Mutacja nowego obiektu Foo może być odzwierciedlona w pierwotnym obiekcie.

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

Funkcje konwersji powinny być pisane jako funkcje rozszerzeń

Pisanie funkcji konwersji poza definicjami klasy odbiorczej i klasy wyniku zmniejsza sprzężenie między typami. Konwersja idealna wymaga tylko dostępu do publicznego interfejsu API do oryginalnego obiektu. Przykład ten pokazuje, że deweloper może też pisać konwersje analogiczne do swoich preferowanych typów.

Wyjątki

Metody nie mogą zgłaszać ogólnych wyjątków, takich jak java.lang.Exception czy java.lang.Throwable. Zamiast tego należy użyć odpowiedniego wyjątku, np. java.lang.NullPointerException, aby umożliwić deweloperom obsługę wyjątków bez nadmiernego ich rozszerzania.

Błędy niezwiązane z argumentami przekazanymi bezpośrednio do wywoływanej publicznie metody powinny zgłaszać błąd java.lang.IllegalStateException zamiast java.lang.IllegalArgumentException lub java.lang.NullPointerException.

Listener i wywołania zwrotne

Oto reguły dotyczące klas i metod używanych w mechanizmach listenera i wywołania zwrotnego.

Nazwy wywołań zwrotnych powinny być w rodzaju męskim.

Zamiast MyObjectCallbacks użyj MyObjectCallback.

Nazwy metod wywołania zwrotnego powinny mieć format on.

onFooEvent oznacza, że FooEvent jest w trakcie wykonywania i że wywołanie zwrotne powinno działać w odpowiedzi na to.

Czas przeszły a teraźniejszy powinien opisywać zachowanie związane z czasem

Nazwy metod wywołania dotyczących zdarzeń powinny wskazywać, czy zdarzenie już nastąpiło, czy dopiero się dzieje.

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 związanego z kliknięciem:

public boolean onClick()

Rejestrowanie wywołania zwrotnego

Jeśli listener lub callback można dodać do obiektu lub z niego usunąć, powiązane metody powinny mieć nazwy add i remove lub register i unregister. Zachowaj spójność z dotychczasową konwencją używaną przez zajęcia lub inne zajęcia w tym samym pakiecie. Jeśli nie ma takiego precedensu, preferuj dodawanie i usuwanie.

Metody związane z rejestrowaniem i odrejestrowywaniem 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 getterów w przypadku funkcji zwrotnych

Nie dodawaj metod getFooCallback(). Jest to kusząca opcja w przypadkach, gdy deweloperzy chcą połączyć istniejący callback z własnym zamiennikiem, ale jest on niestabilny i utrudnia deweloperom komponentów zrozumienie bieżącego stanu. Na przykład

  • Deweloper A dzwoni do setFooCallback(a)
  • Deweloper B dzwoni do setFooCallback(new B(getFooCallback()))
  • Deweloper A chce usunąć wywołanie zwrotne a, ale nie ma możliwości zrobienia tego bez znajomości typu B, a B nie został utworzony w taki sposób, aby umożliwiał takie modyfikacje zawiniętego wywołania zwrotnego.

Zaakceptuj wykonawcę, aby kontrolować wysyłanie wywołania zwrotnego

Podczas rejestrowania wywołań zwrotnych, które nie mają wyraźnych oczekiwań dotyczących wątków (czyli prawie wszędzie poza interfejsem UI Toolkit), zdecydowanie zalecamy uwzględnienie parametru Executor w ramach rejestracji, aby umożliwić deweloperowi określenie wątku, w którym będą wywoływane wywołania zwrotne.

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

Jako wyjątek od naszych wytycznych dotyczących parametrów opcjonalnych dopuszczalne jest podanie przeciążenia pomijając Executor, nawet jeśli nie jest to ostatni argument na liście parametrów. Jeśli Executor nie jest podany, wywołanie zwrotne powinno być wywoływane w wątku głównym za pomocą Looper.getMainLooper(). Należy to udokumentować 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)

Executor pułapki implementacyjne: zwróć uwagę, ż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 przyjmują tę postać, implementacja obiektu binder przychodzącego po stronie procesu aplikacji musi wywołać Binder.clearCallingIdentity(), zanim wywoła wywołanie zwrotne aplikacji na parametrze 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ń, poprawnie przypisuje kod do aplikacji, a nie do procesu systemowego wywołującego aplikację. Jeśli użytkownicy Twojego interfejsu API chcą znać identyfikator UID lub PID wywołującego, należy to uwzględnić w interfejsie API, a nie domyślnie na podstawie miejsca uruchomienia dostarczonego przez nich Binder.getCallingUid().Executor

Interfejs API powinien obsługiwać określanie Executor. W przypadku krytycznych dla wydajności aplikacji może być konieczne natychmiastowe lub synchroniczne uruchamianie kodu z informacjami zwrotnymi z interfejsu API. Akceptacja Executor umożliwia to. Utworzenie dodatkowego HandlerThread lub podobnego do trampoliny w celu ochrony przed atakami nie spełnia tego oczekiwanego przypadku użycia.

Jeśli aplikacja ma gdzieś w swoim procesie uruchomić drogi kod, poinformuj o tym dewelopera. Sposoby obejścia ograniczeń, które znajdą deweloperzy aplikacji, będą znacznie trudniejsze do obsługi w długim okresie.

Wyjątek dotyczący pojedynczego wywołania zwrotnego: jeśli charakter zgłaszanych zdarzeń wymaga obsługi tylko pojedynczego wywołania zwrotnego, użyj tego stylu:

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

public void clearFooCallback()

Używanie Executora zamiast Handlera

W Androidzie metoda Handler była w przeszłości używana jako standardowa metoda przekierowywania wywołania zwrotnego do określonego wątku Looper. Ten standard został zmieniony, aby preferować Executor, ponieważ większość deweloperów aplikacji zarządza własnymi pulami wątków, co powoduje, że główny wątek lub wątek interfejsu użytkownika jest jedynym wątkiem Looper dostępnym dla aplikacji. Używaj Executor, aby zapewnić deweloperom kontrolę potrzebną do ponownego użycia istniejących lub preferowanych kontekstów wykonania.

Nowoczesne biblioteki obsługi równoległości, takie jak kotlinx.coroutines czy RxJava, zapewniają własne mechanizmy harmonogramowania, które w razie potrzeby wykonują własne rozsyłanie. Dlatego ważne jest, aby umożliwić używanie bezpośredniego wykonawcy (takiego jak Runnable::run), aby uniknąć opóźnień spowodowanych podwójnym przeskokiem wątku. Na przykład jeden przeskok do opublikowania w wątku Looper za pomocą elementu Handler, a następnie kolejny przeskok z ramy współbieżności aplikacji.

Wyjątki od tej zasady są rzadkie. Oto kilka przykładów odwołań dotyczących wyjątków:

Muszę użyć Looper, ponieważ potrzebuję Looper, aby epoll na potrzeby wydarzenia. Ta prośba o wyjątek została rozpatrzona pozytywnie, ponieważ w tej sytuacji nie można skorzystać z zalet usługi Executor.

Nie chcę, aby kod aplikacji blokował publikowanie zdarzenia przez mój wątek. Zwykle nie zezwalamy na to w przypadku kodu, który działa w procesie aplikacji. Aplikacje, które nieprawidłowo interpretują te dane, szkodzą tylko sobie, a nie wpływają na ogólną kondycję systemu. Aplikacje, które działają prawidłowo lub używają wspólnego mechanizmu obsługi równoczeństwa, nie powinny być karane za dodatkowe opóźnienia.

Handler jest lokalnie spójny z innymi podobnymi interfejsami API tej samej klasy. Prośba o wyjątek zostanie rozpatrzona w stosunku do konkretnej sytuacji. Preferowane jest dodanie przeciążeń na podstawie Executor i przeniesienie implementacji Handler na implementację nowej wersji Executor. (myHandler::post to prawidłowa wartość Executor!). W zależności od rozmiaru klasy, liczby istniejących metod Handler i prawdopodobieństwo, że deweloperzy będą musieli używać istniejących metod opartych na Handler, a także nowej metody, możemy zezwolić na wyjątek i dodanie nowej metody opartej na Executor.Handler

Symetria w rejestracji

Jeśli istnieje sposób dodawania lub rejestrowania czegoś, powinien też istnieć sposób usuwania lub wyrejestrowywania. Metoda

registerThing(Thing)

powinien mieć pasujące

unregisterThing(Thing)

Podaj identyfikator żądania

Jeśli ponowne użycie wywołania zwrotnego przez dewelopera jest uzasadnione, 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 dodawania metody do interfejsu, który został już opublikowany, należy preferować metody interface i używać metod default. Wcześniej te wytyczne zalecały abstract class ze względu na brak metod default w 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 – robi to samo, co zwykłe wywołanie metody. Użyj typu OutcomeReceiver jako typu funkcji zwracającej wartość lub wyrzucającej wyjątek, aby przekonwertować metodę blokującą na 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ą void. Każdy wynik zwracany przez funkcję requestFoo jest zamiast tego przekazywany do parametru callback funkcji requestFooAsync o nazwie OutcomeReceiver.onResult, który jest wywoływany za pomocą parametru executor. Każdy wyjątek, który requestFoo wyrzuciłby w sposób identyczny, jest zamiast tego przekazywany do metody OutcomeReceiver.onError.

Używanie OutcomeReceiver do raportowania wyników metod asynchronicznych umożliwia też użycie owijaka Kotlina suspend fun dla metod asynchronicznych za pomocą rozszerzenia Continuation.asOutcomeReceiver z poziomu androidx.core:core-ktx:

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

Dzięki takim rozszerzeniom klienci Kotlina mogą wywoływać asynchroniczne metody nieblokujące z wygodą wywołania zwykłej funkcji bez blokowania wątku wywołującego. Te rozszerzenia 1:1 dla interfejsów API platformy mogą być oferowane jako część artefaktu androidx.core:core-ktx w Jetpacku w połączeniu ze standardowymi weryfikacjami i uwagami dotyczącymi zgodności wersji. Aby dowiedzieć się więcej o anulowaniu i zobaczyć przykłady, zapoznaj się z dokumentacją funkcji asOutcomeReceiver.

Metody asynchroniczne, które nie pasują do semantyki metody zwracającej wynik lub rzucającej wyjątek po zakończeniu działania, nie powinny używać typu OutcomeReceiver jako typu funkcji wywołania zwrotnego. Zamiast tego rozważ jedną z innych opcji wymienionych w następującej sekcji.

Zamiast tworzenia nowych typów abstrakcyjnych metod (SAM) preferuj interfejsy funkcyjne

W poziomie API 24 dodano typy java.util.function.* (dokumentacja referencyjna), które oferują ogólne interfejsy SAM, takie jak Consumer<T>, które nadają się do używania jako funkcje zwracające lambda. W wielu przypadkach tworzenie nowych interfejsów SAM nie przynosi większych korzyści w zakresie bezpieczeństwa typów ani przekazywania intencji, a jednocześnie niepotrzebnie zwiększa obszar interfejsu API Androida.

Zamiast tworzyć nowe interfejsy, rozważ użycie tych ogólnych:

Umiejscowienie parametrów SAM

Parametry SAM powinny być umieszczane na końcu, aby umożliwić idiomatyczne korzystanie z języka 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 publicznych dokumentów (Javadoc) interfejsów API.

Wszystkie publiczne interfejsy API muszą być udokumentowane

Wszystkie publiczne interfejsy API muszą mieć wystarczającą dokumentację, aby wyjaśnić, jak deweloper ma z nich korzystać. Załóżmy, że deweloper znalazł metodę za pomocą autouzupełniania lub podczas przeglądania dokumentów na temat interfejsu API i ma minimalną ilość kontekstu z sąsiedniej powierzchni interfejsu API (np. tej samej klasy).

Metody

Parametry metody i wartości zwracane muszą być udokumentowane odpowiednio za pomocą adnotacji @param@return. Sformatuj treść Javadoc tak, jakby poprzedzała ją sekwencja „Ta metoda…”.

W przypadku metody, która nie przyjmuje parametrów, nie ma specjalnych wymagań i zwraca to, co wynika z jej nazwy, możesz pominąć @return i napisać dokumentację podobną do tej:

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

Dokumenty powinny zawierać linki do innych dokumentów z powiązanymi stałymi, metodami i innymi elementami. Używaj tagów Javadoc (np. @see{@link foo}), a nie tylko zwykłych słów.

W przypadku tego przykładowego źródła:

public static final int FOO = 0;
public static final int BAR = 1;

Nie używaj czcionki kodu ani zwykłego tekstu:

/**
 * 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żyj 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, takiej jak @ValueType, w przypadku parametru automatycznie generuje dokumentację z wyszczególnieniem dozwolonych typów. Więcej informacji o IntDef znajdziesz we wskazówkach dotyczących adnotacji.

Uruchom polecenie update-api lub docs podczas dodawania Javadoc

To reguła jest szczególnie ważna w przypadku dodawania tagów @link lub @see, ponieważ zapewnia, że dane wyjściowe będą wyglądać tak, jak powinny. Błędy w wyjściu Javadoc są często spowodowane nieprawidłowymi linkami. Ta weryfikacja jest wykonywana przez cel Make albo update-api, ale cel update-api może być szybszy, jeśli zmieniasz tylko Javadoc i nie musisz uruchamiać celu update-api.docsdocs

Używanie znaku {@code foo} do rozróżniania wartości w języku Java

Aby odróżnić je od tekstu dokumentacji, użyj {@code...} do nawinięcia wartości Java, takich jak true, falsenull.

Podczas pisania dokumentacji w źródłach Kotlina możesz otaczać kod cudzysłowami, tak jak w Markdown.

Podsumowania @param i @return powinny być wyrażone w jednym zdaniu

Podsumowania parametrów i wartości zwracanych powinny zaczynać się małą literą i zawierać tylko jeden fragment zdania. Jeśli masz dodatkowe informacje, które nie mieszczą się w jednym zdaniu, przenieś je do opisu metody w dokumentacji Javadoc:

/**
 * @param e The element to be appended to the list. This must not be
 *       null. If the list contains no entries, this element will
 *       be added at the beginning.
 * @return This method returns true on success.
 */

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śnień;

Udokumentuj, dlaczego adnotacje @hide@removed są ukryte w publicznym interfejsie API. Dołącz instrukcje, jak zastąpić elementy interfejsu API oznaczone adnotacją @deprecated.

Używanie adnotacji @throws do dokumentowania wyjątków

Jeśli metoda rzuca sprawdzony wyjątek, na przykład IOException, udokumentuj wyjątek za pomocą @throws. W przypadku interfejsów API opartych na kodzie Kotlin przeznaczonych do użycia przez klientów Java dodaj adnotacje do funkcji za pomocą @Throws.

Jeśli metoda zgłasza niezatwierdzone wyjątki wskazujące na błąd, którego można uniknąć, na przykład IllegalArgumentException lub IllegalStateException, zdokumentuj ten wyjątek, podając wyjaśnienie, dlaczego został zgłoszony. Wyjątek powinien też wskazywać przyczynę.

Niektóre przypadki niezaznaczonych wyjątków są uważane za domyślne i nie wymagają udokumentowania, np. NullPointerException lub IllegalArgumentException, gdy argument nie pasuje do adnotacji @IntDef lub podobnej, która umieszcza umowę API w signaturze 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 Kotlinie:

/**
 * ...
 * @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ę, jak deweloper może się dowiedzieć o takich wyjątkach i jak na nie zareagować. Zwykle oznacza to przekazanie wyjątku do funkcji zwracającej wywołanie zwrotne i udokumentowanie wyjątków zgłaszanych przez metodę, która je otrzymuje. Wyjątki asynchroniczne nie powinny być dokumentowane za pomocą @throws, chyba że są rzeczywiście zgłaszane z metody z komentarzem.

Zakończ pierwsze zdanie kropką

Narzędzie Doclava parsuje dokumenty w prosty sposób, kończąc dokument z synopsą (pierwsze zdanie, używane w krótkim opisie u góry dokumentu zajęć) od razu po wykryciu kropki (.) poprzedzonej spacjami. Powoduje to 2 problemy:

  • Jeśli krótki dokument nie kończy się kropką, a jego użytkownik ma dokumenty dziedziczone, które są rozpoznawane przez narzędzie, to streszczenie uwzględnia też te dokumenty dziedziczone. Przykładowo actionBarTabStyle w dokumentach R.attr zawiera opis wymiaru dodany do streszczenia.
  • Z tego samego powodu unikaj słów „np.” w pierwszym zdaniu, ponieważ Doclava kończy dokumenty z synopsją po słowie „g”. Na przykład TEXT_ALIGNMENT_CENTERView.java. Pamiętaj, że Metalava automatycznie poprawia ten błąd, wstawiając po kropce spację niełamiącą wiersza. Nie popełniaj jednak tego błędu.

Formatowanie dokumentów do renderowania w HTML

Javadoc jest renderowany w formacie HTML, więc sformatuj te dokumenty odpowiednio:

  • Do podziałów wierszy należy używać wyraźnego tagu <p>. Nie dodawaj zamykającego tagu </p>.

  • Nie używaj ASCII do renderowania list ani tabel.

  • W listach nieuporządkowanych należy używać znacznika <ul>, a w uporządkowanych – <ol>. Każdy element powinien zaczynać się od tagu <li>, ale nie musi mieć tagu zamykającego </li>. Po ostatnim elemencie musi znajdować się tag zamykający </ul> lub </ol>.

  • W tabelach należy używać <table>, <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ć tagu class="deprecated" w dowolnym tagu, aby oznaczyć go jako przestarzały.

  • Aby utworzyć czcionkę kodu wbudowanego, użyj {@code foo}.

  • Aby utworzyć bloki kodu, użyj <pre>.

  • Cały tekst w bloku <pre> jest analizowany przez przeglądarkę, więc należy uważać na nawiasy <>. Możesz je zastąpić odpowiednimi elementami HTML: &lt;&gt;.

  • Możesz też pozostawić w fragmentach kodu niesformatowane nawiasy <>, jeśli otoczysz problematyczne sekcje w ramach {@code foo}. Na przykład:

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

Postępuj zgodnie z wytycznymi dotyczącymi stylu w 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 w oficjalnych wytycznych dotyczących języka Java, które znajdziesz w artykule Jak pisać komentarze do pliku Doc w narzędzie 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 (na przykład Bundle lub Parcelable).

Twórcy intencji powinni używać wzorca create*Intent().

Twórcy intencji powinni używać metod o nazwie createFooIntent().

Zamiast tworzyć nowe uniwersalne struktury danych, użyj pakietu.

Unikaj tworzenia nowych uniwersalnych struktur danych, które reprezentują dowolne mapowania kluczy do typów wartości. Zamiast tego użyj interfejsu Bundle.

Zwykle ma to miejsce podczas pisania interfejsów API platformy, które służą jako kanały komunikacji między aplikacjami i usługami spoza platformy, w których platforma nie odczytuje danych przesyłanych przez kanał, a umowa dotycząca interfejsu API może być częściowo zdefiniowana poza platformą (np. w bibliotece Jetpack).

W przypadku, gdy platforma czyta dane, unikaj używania Bundle i wolno preferować klasę danych o ściśle określonym typie.

Implementacje Parcelable muszą mieć publiczne pole CREATOR

Funkcja parcelable inflation jest dostępna za pomocą CREATOR, a nie za pomocą nieprzetworzonych konstruktorów. Jeśli klasa implementuje interfejs Parcelable, jej pole CREATOR musi też być publiczną usługą API, a konstruktor klasy przyjmujący argument Parcel musi być prywatny.

Używanie obiektu CharSequence do ciągów znaków w interfejsie

Gdy ciąg tekstowy jest wyświetlany w interfejsie, użyj CharSequence, aby umożliwić wystąpienia Spannable.

Jeśli jest to tylko klucz lub inna etykieta lub wartość, która nie jest widoczna dla użytkowników, znak String jest prawidłowy.

Unikaj korzystania z typów enumeracji

IntDef należy używać zamiast typów wyliczeń we wszystkich interfejsach API platform. Należy go również rozważyć w przypadku niespakowanych interfejsów API bibliotek. Używaj typów wyliczeniowych tylko wtedy, gdy masz pewność, że nie zostaną dodane nowe wartości.

ZaletyIntDef:

  • Umożliwia dodawanie wartości w ciągu czasu.
    • Instrukcje Kotlina when mogą zawieść w czasie wykonywania, jeśli nie są już wyczerpujące z powodu dodania wartości z enumeracji na platformie.
  • Brak klas ani obiektów używanych w czasie wykonywania kodu, tylko typy proste
    • Chociaż R8 lub minifikacja mogą zapobiec tym kosztom w przypadku niespakowanych bibliotek interfejsów API, ta optymalizacja nie może wpływać na klasy interfejsów API platformy.

Zalety typu wyliczeniowego

  • Idiotyczne funkcje języka Java i Kotlin
  • Włącza wyczerpujące przełączanie, whenużycie instrukcji.
    • Uwaga: wartości nie mogą się zmieniać w czasie (patrz poprzednia lista).
  • wyraźnie określony zakres i możliwość znalezienia nazwy;
  • Umożliwia weryfikację w czasie kompilacji.
    • Na przykład instrukcja when w Kotlinie, która zwraca wartość.
  • Jest to działająca klasa, która może implementować interfejsy, mieć pomocnicze metody statyczne, udostępniać pola i metody rozszerzeń oraz elementy członkowskie.

Postępowanie zgodnie z hierarchią warstw pakietu na Androida

Hierarchia pakietów android.* ma ukryte uporządkowanie, w którym pakiety niższego poziomu nie mogą zależeć od pakietów wyższego poziomu.

Nie odwołuj się do Google, innych firm i ich produktów.

Platforma Android to projekt typu open source, który ma być niezależny od dostawcy. Interfejs API powinien być uniwersalny 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 próbować zastąpić implementacji Parcelable.

Jeśli aplikacja wysyłająca rozszerza Parcelable, aplikacja odbierająca nie będzie miała implementacji niestandardowej nadawcy, której mogłaby użyć do rozpakowania. Uwaga na temat zgodności wstecznej: jeśli Twoja klasa nie była wcześniej finalna, ale nie miała publicznie dostępnego konstruktora, nadal możesz ją oznaczyć jako final.

Metody wywołujące proces systemowy powinny ponownie rzucać RemoteException jako RuntimeException

RemoteException jest zwykle wywoływany przez wewnętrzny AIDL i wskazuje, że proces systemowy został przerwany lub aplikacja próbuje wysłać zbyt dużo danych. W obu przypadkach interfejs publiczny API powinien zostać ponownie wywołany jako RuntimeException, aby uniemożliwić aplikacjom utrwalanie decyzji dotyczących zabezpieczeń lub zasad.

Jeśli wiesz, że po drugiej stronie wywołania Binder znajduje się proces systemowy, sprawdzoną metodą jest użycie tego szablonu kodu:

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

Wyjątki dotyczące zmian w interfejsie API

Zachowanie publicznych interfejsów API może się zmieniać w zależności od poziomu interfejsu API i może powodować awarie aplikacji (na przykład w celu egzekwowania nowych zasad bezpieczeństwa).

Jeśli interfejs API musi wyrzucić błąd w przypadku żądania, które było wcześniej prawidłowe, zamiast ogólnego błędu wyrzuć nowy, specyficzny błąd. Na przykład ExportedFlagRequired zamiast SecurityException (i ExportedFlagRequired może rozszerzać SecurityException).

Pomoże to deweloperom aplikacji i narzędziom wykrywać zmiany w zachowaniu interfejsu API.

Zamiast metody clone zaimplementuj konstruktor kopiujący

Nie zalecamy używania metody Java clone() ze względu na brak umów interfejsu API udostępnianych przez klasę Object oraz trudności związane z rozszerzaniem klas, które używają clone(). Zamiast tego użyj konstruktora kopii, który przyjmuje obiekt tego samego typu.

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

Klasy, które korzystają z Buildera do tworzenia, powinny mieć konstruktor kopii Buildera, aby umożliwić modyfikację 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

Definicja własności obiektu java.io.FileDescriptor jest niejasna, co może powodować niejasne błędy związane z użyciem po zamknięciu. Zamiast tego interfejsy API powinny zwracać lub akceptować instancje ParcelFileDescriptor. Kod starszy może w razie potrzeby konwertować pliki PFD na FDF i odwrotnie za pomocą funkcji dup() lub getFileDescriptor().

Unikaj stosowania wartości liczbowych o nieparzystej wielkości.

Unikaj bezpośredniego używania wartości short lub byte, ponieważ często ograniczają one możliwości ulepszania interfejsu API w przyszłości.

Unikaj używania BitSet

java.util.BitSet jest świetny do implementacji, ale nie do publicznego interfejsu API. Jest zmienny, wymaga alokacji na potrzeby wywołań metod o wysokiej częstotliwości i nie zapewnia semantycznego znaczenia dla każdego bitu.

W przypadku scenariuszy o wysokiej wydajności użyj int lub long@IntDef. W przypadku scenariuszy o niskiej wydajności rozważ użycie Set<EnumType>. W przypadku nieprzetworzonych danych binarnych użyj byte[].

Preferuj android.net.Uri

android.net.Uri to preferowana forma opakowania identyfikatorów URI w interfejsach API Androida.

Unikaj funkcji java.net.URI, ponieważ zbyt rygorystycznie analizuje URI, i nigdy nie używaj funkcji java.net.URL, ponieważ jej definicja równości jest bardzo nieprawidłowa.

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ą eksportowane jako interfejsy API, kompilator umieszcza je w kodze źródłowym, a w zawartym w adnotacji interfejsie API (w przypadku platformy) lub pliku JAR (w przypadku bibliotek) pozostają tylko (teraz nieprzydatne) wartości.

Dlatego ich użycie musi być oznaczone adnotacją @hide docs na platformie lub adnotacją @RestrictTo.Scope.LIBRARY) code w bibliotekach. W obu przypadkach muszą być one oznaczone jako @Retention(RetentionPolicy.SOURCE), aby nie pojawiały się w zaczepkach interfejsu API ani 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 pakietów AAR platformy i biblioteki narzędzie wyodrębnia adnotacje i odrębnia je od skompilowanych źródeł. Android Studio odczytuje ten format pakietu i zastosuje definicje typów.

Nie dodawaj nowych kluczy dostawcy ustawień

Nie udostępniaj nowych kluczy z Settings.Global, Settings.SystemSettings.Secure.

Zamiast tego dodaj odpowiednie metody Java API do pobierania i ustawiania wartości w odpowiedniej klasie, która jest zwykle klasą „manager”. Dodaj mechanizm odbiorczy lub funkcję przesyłania danych, aby w razie potrzeby informować klientów o zmianach.

Ustawienia SettingsProvider mają kilka problemów w porównaniu z metodami getter i setter:

  • Brak zabezpieczeń typów.
  • Brak ujednoliconego sposobu podawania wartości domyślnej.
  • Brak odpowiedniego sposobu dostosowywania uprawnień.
    • Nie można na przykład chronić ustawień za pomocą niestandardowych uprawnień.
  • Brak odpowiedniego sposobu na prawidłowe dodawanie logiki niestandardowej.
    • 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 istniał od dawna, ale zespół ds. lokalizacji wycofał go na rzecz prawidłowego interfejsu Java API LocationManager.isLocationEnabled() i MODE_CHANGED_ACTION transmisji, co dało zespołowi większą elastyczność, a semantyka interfejsów API stała się znacznie bardziej przejrzysta.

Nie rozszerzaj klas Activity i AsyncTask

AsyncTask to szczegóły implementacji. Zamiast tego udostępnij interfejs listener lub w przypadku androidx – interfejs API ListenableFuture.

Podklasy Activity nie można skompilować. Rozszerzenie zakresu działania funkcji powoduje jej niezgodność z innymi funkcjami, które wymagają od użytkowników takiego samego działania. Zamiast tego użyj narzędzia takiego jak LifecycleObserver.

Używanie metody getUser() w kontekście

Klasy powiązane z elementem Context, np. wszystko zwracane przez funkcję Context.getSystemService(), powinny używać użytkownika powiązanego z elementem Context zamiast ujawniać elementy kierowane do 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 przyjmować argument user, jeśli akceptuje wartości, które nie reprezentują pojedynczego użytkownika, np. UserHandle.ALL.

Zamiast zwykłych liczb używaj obiektów UserHandle.

Wartość UserHandle jest preferowana, ponieważ zapewnia bezpieczeństwo typów i unika pomylenia identyfikatorów użytkowników z identyfikatorami użytkowników.

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

Jeśli jest to nieuniknione, wartość int reprezentująca identyfikator użytkownika musi być opatrzona adnotacją @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

preferowanie metody listener lub callback do przesyłania intencji;

Intencje transmisji są bardzo skuteczne, ale mogą powodować nieoczekiwane zachowania, które mogą negatywnie wpłynąć na bezpieczeństwo systemu. Dlatego nowe intencje transmisji należy dodawać rozważnie.

Oto kilka konkretnych obaw, które powodują, że nie zachęcamy do wprowadzania nowych intencji transmisji:

  • Podczas wysyłania transmisji bez flagi FLAG_RECEIVER_REGISTERED_ONLY wymuszają one uruchomienie wszystkich aplikacji, które nie są jeszcze uruchomione. Czasami jest to zamierzone działanie, ale może też spowodować zablokowanie wielu aplikacji i tym samym negatywnie wpłynąć na kondycję systemu. Zalecamy stosowanie alternatywnych strategii, takich jak JobScheduler, aby lepiej koordynować działania po spełnieniu różnych warunków wstępnych.

  • Podczas wysyłania transmisji nie można filtrować ani dostosowywać treści przesyłanych do aplikacji. Utrudnia to lub uniemożliwia reagowanie na przyszłe problemy związane z prywatnością lub wprowadzanie zmian w zachowaniu na podstawie docelowego pakietu SDK aplikacji odbiorczej.

  • Kolejki transmisji to zasoby współdzielone, które mogą zostać przeciążone i nie doprowadzić do terminowego dostarczenia wydarzenia. Zauważyliśmy, że w przypadku niektórych kolejek transmisji czas opóźnienia od początku do końca wynosi 10 minut lub więcej.

Z tego powodu zachęcamy, aby zamiast intencji rozgłoszeniowych w przypadku nowych funkcji używać odsłuchiwania lub wywołań zwrotnych lub innych funkcji, takich jak JobScheduler.

W przypadku, gdy intencje transmisji nadal są najlepszym rozwiązaniem, warto wziąć pod uwagę te sprawdzone metody:

  • Jeśli to możliwe, użyj opcji Intent.FLAG_RECEIVER_REGISTERED_ONLY, aby ograniczyć transmisję do aplikacji, które są już uruchomione. Na przykład ACTION_SCREEN_ONużywa tej funkcji, aby uniknąć wybudzania aplikacji.
  • Jeśli to możliwe, użyj tagu Intent.setPackage() lub Intent.setComponent(), aby kierować transmisję na konkretną aplikację. Na przykład ACTION_MEDIA_BUTTON używa tego projektu, aby skupić uwagę na elementach sterujących odtwarzaniem w bieżącej aplikacji.
  • Jeśli to możliwe, zdefiniuj transmisję jako <protected-broadcast>, aby zapobiec podszywanie się złośliwych aplikacji pod system operacyjny.

Intencje w usługach dla deweloperów związanych z systemem

Usługi, które mają być rozszerzane przez dewelopera i powiązane z systemem, np. usługi abstrakcyjne, takie jak NotificationListenerService, mogą reagować na działanie Intent systemu. Takie usługi powinny spełniać te kryteria:

  1. Zdefiniuj w klasie stałą ciągu znaków SERVICE_INTERFACE zawierającą pełną i jednoznaczną nazwę klasy usługi. Ta stała musi być oznaczona jako @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION).
  2. Dokumentacja zajęć: deweloper musi dodać <intent-filter> do AndroidManifest.xml, aby otrzymywać intencje z platformy.
  3. Zdecydowanie zalecamy dodanie uprawnienia na poziomie systemu, aby zapobiec wysyłaniu Intent do usług dla deweloperów przez złośliwe aplikacje.

Współdziałanie Kotlina z Javo

Pełną listę wytycznych znajdziesz w oficjalnym przewodniku po interoperacyjności Kotlina i Java na Androida. Aby zwiększyć wykrywalność, do tego przewodnika skopiowano wybrane wytyczne.

Widoczność interfejsu API

Niektóre interfejsy API w Kotlinie, np. suspend fun, nie są przeznaczone dla programistów Java. Nie należy jednak próbować kontrolować widoczności w języku za pomocą interfejsu @JvmSynthetic, ponieważ może to mieć wpływ na sposób wyświetlania interfejsu API w debugerach, co utrudnia debugowanie.

Więcej informacji znajdziesz w przewodniku po interoperacyjności Kotlina i Java lub w przewodniku po asynchroniczności.

Obiekty towarzyszące

Kotlin używa companion object do udostępniania elementów statycznych. W niektórych przypadkach te metody będą widoczne w języku Java w klasie wewnętrznej o nazwie Companion, a nie w klasie zawierającej. CompanionZajęcia mogą być wyświetlane jako puste zajęcia w plikach tekstowych interfejsu API. Jest to działanie zgodne z oczekiwaniami.

Aby zmaksymalizować zgodność z językiem Java, dodaj adnotacje do niestałych pól obiektów towarzyszących za pomocą elementu @JvmField oraz do publicznych funkcji za pomocą elementu @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 opisaliśmy zasady dotyczące tego, jakie zmiany możesz wprowadzić w dotychczasowych interfejsach API na Androida i jak je wdrożyć, aby zmaksymalizować zgodność z dotychczasowymi aplikacjami i kodem źródłowym.

Zmiany powodujące niezgodność binarną

Unikaj zmian, które powodują niekompatybilność binarną w publicznych interfejsach API. Tego typu zmiany zwykle powodują błędy podczas uruchamiania make update-api, ale mogą wystąpić przypadki, których Metalava nie wykryje za pomocą interfejsu API. W razie wątpliwości zapoznaj się z przewodnikiem Evolving Java-based APIs (w języku angielskim) fundacji Eclipse, aby uzyskać szczegółowe informacje o tym, jakie typy zmian interfejsu API są zgodne z językiem Java. Zmiany w interfejsach API, które powodują przerwanie działania binarnych, powinny być wprowadzane zgodnie z cyklem wycofania/zastąpienia.

Zmiany powodujące przerwanie działania źródła

Nie zalecamy wprowadzania zmian, które powodują niedziałanie kodu źródłowego, nawet jeśli nie powodują one niedziałania binarnego. Przykładem zmiany zgodnej z binarną wersją, ale nie ze źródłową, jest dodanie typu ogólnego do istniejącej klasy. Jest on zgodny z binarną wersją, ale może powodować błędy kompilacji z powodu dziedziczenia lub niejednoznacznych odwołań. Zmiany, które powodują niekompatybilność z kodem źródłowym, nie spowodują błędów podczas wykonywania make update-api, dlatego musisz dokładnie przeanalizować wpływ zmian na istniejące sygnatury interfejsu API.

W niektórych przypadkach konieczne może być wprowadzenie zmian, które spowodują przerwanie działania źródła, aby ulepszyć środowisko programisty lub poprawność kodu. Na przykład dodanie adnotacji dotyczących możliwości wystąpienia wartości null do źródeł kodu Java poprawia współdziałanie z kodem Kotlina i zmniejsza prawdopodobieństwo wystąpienia błędów, ale często wymaga wprowadzenia zmian w kodzie źródłowym (czasem znaczących).

Zmiany w prywatnych interfejsach API

Interfejsy API oznaczone @TestApi możesz zmienić w każdej chwili.

Interfejsy API oznaczone adnotacjami @SystemApi musisz zachować przez 3 lata. Musisz usunąć lub przerobić interfejs API systemu zgodnie z tym harmonogramem:

  • API y – dodano
  • Interfejs API y+1 – wycofanie
    • Oznacz kod symbolem @Deprecated.
    • Dodaj zastąpienia i utwórz link do zastąpienia w dokumentacji Javadoc dla wycofanego kodu, używając adnotacji @deprecated.
    • Podczas cyklu programowania zgłaszaj błędy użytkownikom wewnętrznym, informując ich o wycofaniu interfejsu API. Pomoże to sprawdzić, czy interfejsy API zastępcze są odpowiednie.
  • Interfejs API y+2 – usunięcie tymczasowe
    • Oznacz kod symbolem @removed.
    • Opcjonalnie wyrzucaj wyjątki lub nie wykonuj kodu w przypadku aplikacji kierowanych na bieżący poziom pakietu SDK dla danej wersji.
  • API y+3 – trwałe usunięcie
    • całkowicie usunąć kod z drzewa źródłowego;

Wycofanie

Uznaliśmy, że wycofanie jest zmianą interfejsu API i może nastąpić w ramach wersji głównej (np. letter). Podczas wycofywania interfejsów API używaj adnotacji źródła @Deprecated i adnotacji dokumentów @deprecated <summary>. Podsumowanie musi zawierać strategię migracji. Ta strategia może zawierać link do interfejsu API zastępczego lub wyjaśnienie, dlaczego nie należy używać danego interfejsu API:

/**
 * Simple version of ...
 *
 * @deprecated Use the {@link androidx.fragment.app.DialogFragment}
 *             class with {@link androidx.fragment.app.FragmentManager}
 *             instead.
 */
@Deprecated
public final void showDialog(int id)

Musisz też wycofać interfejsy API zdefiniowane w XML i wyświetlane w Javie, w tym atrybuty i właściwości stylizowane wyświetlane w klasie android.R, podając podsumowanie:

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

Kiedy wycofać interfejs API

Ostrzeżenia o zaprzestaniu stosowania są najbardziej przydatne, gdy chcesz zniechęcić do korzystania z interfejsu API w nowym kodzie.

Wymagamy też, aby interfejsy API zostały oznaczone jako @deprecated, zanim staną się @removed, ale nie stanowi to silnej motywacji dla deweloperów do zaprzestania korzystania z interfejsu API, którego używają.

Zanim wycofasz interfejs API, zastanów się nad jego wpływem na programistów. Skutki wycofania interfejsu API:

  • javac generuje ostrzeżenie podczas kompilacji.
    • Ostrzeżenia o wycofaniu nie można wyłączyć globalnie ani ustawić jako domyślnych, dlatego deweloperzy korzystający z -Werror muszą osobno poprawić lub wyłączyć każde użycie wycofanego interfejsu API, zanim będą mogli zaktualizować wersję kompilowanego pakietu SDK.
    • Ostrzeżenia o wycofaniu w przypadku importowania wycofanych klas nie mogą być ignorowane. Dlatego zanim zaktualizują wersję pakietu SDK kompilacji, deweloperzy muszą wstawić pełną nazwę kwalifikowaną klasy w każdym miejscu, w którym jest używana wycofana klasa.
  • Dokumentacja d.android.com zawiera powiadomienie o wycofaniu.
  • Środowiska IDE, takie jak Android Studio, wyświetlają ostrzeżenie na stronie dotyczącej użycia interfejsu API.
  • Interfejs IDE może obniżyć ranking interfejsu API lub ukryć go z autouzupełniania.

W efekcie wycofanie interfejsu API może zniechęcić deweloperów, którzy najbardziej przejmują się jakością kodu (czyli tych, którzy korzystają z -Werror), do wdrażania nowych pakietów SDK. Deweloperzy, których niepokoją ostrzeżenia w dotychczasowym kodzie, prawdopodobnie całkowicie zignorują wycofanie.

Pakiet SDK, który wprowadza dużą liczbę wycofanych funkcji, pogarsza oba te przypadki.

Z tego powodu zalecamy wycofywanie interfejsów API tylko w takich przypadkach:

  • W przyszłej wersji interfejsu API planujemy @remove.
  • Korzystanie z interfejsu API prowadzi do nieprawidłowego lub nieokreślonego zachowania, którego nie możemy naprawić bez łamania zgodności.

Gdy wycofujesz interfejs API i zastąpisz go nowym, zdecydowanie zalecamy dodanie do biblioteki Jetpack, takiej jak androidx.core, odpowiedniego interfejsu API zapewniającego zgodność, aby uprościć obsługę zarówno starszych, jak i nowych urządzeń.

Nie zalecamy wycofywania interfejsów API, które działają zgodnie z oczekiwaniami w bieżących 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 przypadku, gdy interfejsy API nie mogą już zachować swoich udokumentowanych zachowań:

/**
 * ...
 * @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ć zachowanie 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, musisz je dodać.

Nie rozszerzaj wycofanych interfejsów API w kolejnych wersjach. Do dotychczasowego interfejsu API, którego używanie zostało wycofane, możesz dodać adnotacje poprawności lint (np. @Nullable), ale nie możesz dodawać 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 (a więc początkowo byłyby dostępne jako wycofane), musisz je usunąć przed sfinalizowaniem interfejsu API.

Przeniesienie do kosza

Miękkie usunięcie jest zmianą powodującą przerwanie działania źródła. Należy unikać tego w publicznych interfejsach API, chyba że Rada Interfejsów API wyraźnie zatwierdzi taką zmianę. W przypadku interfejsów API systemu musisz wycofać interfejs API na czas trwania głównej wersji przed jego wycofaniem. Podczas usuwania interfejsów API o niskim priorytecie usuń wszystkie odwołania do nich w dokumentach i użyj adnotacji @removed <summary>. Podsumowanie musi zawierać przyczynę usunięcia. Możesz też podać strategię migracji, jak wyjaśniliśmy w artykule Wycofanie.

Zachowanie interfejsów API usuniętych w miękki sposób może pozostać bez zmian, ale co ważniejsze, musi być zachowane, aby dotychczasowi wywołujący nie ulegli awarii podczas wywoływania interfejsu API. W niektórych przypadkach może to oznaczać zachowanie zachowania.

Zakres testów musi być zachowany, ale treść testów może wymagać zmiany, aby uwzględnić zmiany w zachowaniu. Testy muszą nadal sprawdzać, czy istniejące wywołania nie ulegają awarii w czasie wykonywania. Zachowasz zachowanie interfejsów API usuniętych w międzyczasie w takim stanie, ale co ważniejsze, musisz zachować je 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 zachowania.

Musisz zachować pokrycie testów, ale ich treść może wymagać zmiany, aby uwzględnić zmiany w zachowaniu. Testy muszą nadal sprawdzać, czy istniejące wywołania nie ulegają awarii w czasie wykonywania.

Na poziomie technicznym usuwamy interfejs API ze szkieletu pakietu SDK w pliku JAR i z ścieżki klas w czasie kompilacji za pomocą adnotacji @remove w języku Javadoc, ale nadal jest on obecny na ścieżce klas w czasie wykonywania – podobnie jak w przypadku interfejsów API @hide:

/**
 * Ringer volume. This is ...
 *
 * @removed Not functional since API 2.
 */
public static final String VOLUME_RING = ...

Z perspektywy dewelopera aplikacji interfejs API nie będzie już wyświetlany w autouzupełnianiu, a kod źródłowy odwołujący się do tego interfejsu nie będzie się kompilować, gdy compileSdk jest równe lub nowsze niż pakiet SDK, w którym interfejs API został usunięty. Kod źródłowy nadal będzie się jednak kompilować w ramach wcześniejszych pakietów SDK, a binarne pliki danych odwołujące się do tego interfejsu API będą nadal działać.

Niektórych kategorii interfejsów API nie wolno usuwać w ramach miękkiego usuwania. Nie wolno usuwać niektórych kategorii interfejsów API.

Abstrakcyjne metody

Nie wolno usuwać metod abstrakcyjnych w klasach, które mogą być rozszerzane przez programistów. W przeciwnym razie deweloperzy nie będą mogli rozszerzyć klasy na wszystkich poziomach pakietu SDK.

W rzadkich przypadkach, gdy rozszerzanie klasy było niemożliwe i nadal będzie, możesz łagodnie usunąć abstrakcyjne metody.

Usuwanie twarde

Twarde usunięcie jest zmianą powodującą niekompatybilność binarną i nigdy nie powinno występować w publicznych interfejsach API.

Adnotacja o niezachęcaniu

Używamy adnotacji @Discouraged, aby wskazać, że w większości przypadków (powyżej 95%) nie zalecamy używania danego interfejsu API. Interfejsy API, których używanie nie jest zalecane, różnią się od wycofanych interfejsów API tym, że istnieją przypadki, w których ich używanie jest niezbędne. Jeśli oznaczysz interfejs API jako niewskazany, musisz podać wyjaśnienie i alternatywę:

@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 musisz dodawać nowych interfejsów API jako nie zalecanych.

zmiany w działaniu istniejących interfejsów API;

W niektórych przypadkach możesz chcieć zmienić sposób implementacji istniejącego interfejsu API. Na przykład w Androidzie 7.0 ulepszono DropBoxManager, aby wyraźnie informować deweloperów o próbach opublikowania zdarzeń, które są zbyt duże, aby można je było wysłać przez Binder.

Aby jednak uniknąć problemów z dotychczasowymi aplikacjami, zdecydowanie zalecamy zachowanie bezpiecznego działania w przypadku starszych aplikacji. Do tej pory chroniliśmy te zmiany zachowania na podstawie ApplicationInfo.targetSdkVersion aplikacji, ale niedawno zaczęliśmy wymagać używania interfejsu App Compatability Framework. Oto przykład wdrożenia zmiany zachowania za pomocą tego nowego frameworku:

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
    }
  }
}

Dzięki tej architekturze deweloperzy mogą tymczasowo wyłączać określone zmiany zachowania podczas wersji w podglądzie i wersji beta w ramach debugowania aplikacji, zamiast zmuszać się do dostosowywania do kilkunastu zmian zachowania jednocześnie.

Zgodność wsteczna

Zgodność wstecz to cecha projektu, która pozwala systemowi akceptować dane przeznaczone dla nowszej wersji. W przypadku projektowania interfejsu API musisz zwrócić szczególną uwagę na początkowy projekt oraz przyszłe zmiany, ponieważ deweloperzy oczekują, że kod zostanie napisany raz, przetestowany raz i będzie działać wszędzie bez problemów.

Najczęstsze problemy z kompatybilnością wsteczną w Androidzie:

  • Dodawanie nowych stałych do zbioru (takich jak @IntDef lub enum), który wcześniej był uważany za kompletny (na przykład gdy switch zawiera default, 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 ColorStateList w pliku XML, gdzie wcześniej obsługiwane były tylko zasoby typu <color>).
  • Zmniejszenie ograniczeń dotyczących kontroli w czasie wykonywania, np. usunięcie kontroli requireNotNull(), która była obecna w starszych wersjach.

We wszystkich tych przypadkach deweloperzy dowiadują się, że coś jest nie tak, dopiero w czasie wykonywania. Co gorsza, mogą się o tym dowiedzieć na podstawie raportów o awariach pochodzących ze starszych urządzeń w polu.

Ponadto w tych przypadkach wszystkie zmiany w interfejsie API są technicznie poprawne. Nie powodują one niezgodności binarnej ani zgodności ze źródłem, a program API Lint nie wykryje żadnych z tych problemów.

W związku z tym projektanci interfejsu API muszą zachować szczególną ostrożność podczas modyfikowania istniejących klas. Zadaj pytanie: „Czy ta zmiana spowoduje, że kod napisany i przetestowany tylko w 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 zapewniający zgodność wsteczną, podobnie jak inne interfejsy API na Androida. Na przykład struktura elementów i atrybutów XML musi być zachowana w sposób podobny do sposobu, w jaki metody i zmienna są zachowywane w innych interfejsach API Androida.

Wycofanie obsługi 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, stosując typowy cykl ż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ą być zachowane

Schematy obsługują elementy sequence, choiceall jako elementy podrzędne elementu complexType. Jednak te elementy różnią się liczbą i kolejnością elementów podrzędnych, więc modyfikacja istniejącego typu byłaby niezgodna z standardami.

Jeśli chcesz zmodyfikować istniejący typ, najlepiej jest wycofać stary typ i wprowadzić nowy typ, 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 dotyczące magistrali

Mainline to projekt umożliwiający aktualizowanie poszczególnych podsystemów („modułów głównej linii”) systemu Android, zamiast aktualizowania całego obrazu systemu.

Moduły główne muszą być „odłączone” od głównej platformy, co oznacza, że wszystkie interakcje między poszczególnymi modułami a resztą świata muszą odbywać się za pomocą oficjalnych (publicznych lub systemowych) interfejsów API.

Istnieją pewne wzorce projektowania, których należy przestrzegać w przypadku modułów głównych. W tej sekcji je opisujemy.

Wzór <Module>FrameworkInitializer

Jeśli moduł główny musi udostępniać klasy @SystemService (np. JobScheduler), użyj tego wzoru:

  • Wyświetl klasę <YourModule>FrameworkInitializer z modulu. Ta klasa musi być w $BOOTCLASSPATH. Przykład: StatsFrameworkInitializer

  • Oznacz go @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 do Context.

  • Użyj funkcji SystemServiceRegistry.registerStaticService(), aby zarejestrować klasę menedżera usług, gdy nie wymaga ona odwołania do funkcji Context.

  • Wywołaj metodę registerServiceWrappers() w inicjalizacji statycznej klasy SystemServiceRegistry.

Wzór <Module>ServiceManager

Zazwyczaj, aby zarejestrować obiekty systemu binder service lub uzyskać do nich odwołania, należy użyć ServiceManager, ale moduły główne nie mogą z niego korzystać, ponieważ jest on ukryty. Ta klasa jest ukryta, ponieważ moduły główne nie powinny rejestrować ani odwoływać się do obiektów systemu bindera usługi udostępnianych przez platformę statyczną lub inne moduły.

Zamiast tego w przypadku modułów głównych można użyć tego wzorca, aby rejestrować i otrzymywać odwołania do usług bindera zaimplementowanych w module.

  • Utwórz klasę <YourModule>ServiceManager zgodnie z projektowaniem TelephonyServiceManager.

  • Udostępnij zajęcia jako @SystemApi. Jeśli dostęp do niego jest potrzebny tylko z poziomu klas $BOOTCLASSPATH lub klas serwera systemu, możesz użyć @SystemApi(client = MODULE_LIBRARIES). W innym przypadku możesz użyć @SystemApi(client = PRIVILEGED_APPS).

  • Te zajęcia obejmują:

    • Ukryty konstruktor, który może utworzyć instancję tylko w ramach statycznego kodu platformy.
    • Publiczne metody gettera, które zwracają wystąpienie ServiceRegisterer dla określonej nazwy. Jeśli masz jeden obiekt binder, potrzebujesz jednej metody getter. Jeśli masz 2 elementy, potrzebujesz 2 metod gettera.
    • ActivityThread.initializeMainlineModules() utwórz wystąpienie tej klasy i przekaż ją do metody statycznej udostępnionej przez Twój moduł. Zwykle dodajesz statyczny interfejs API @SystemApi(client = MODULE_LIBRARIES) w klasie FrameworkInitializer, który go obsługuje.

Ten wzorzec uniemożliwiłby innym modułom głównym dostęp do tych interfejsów API, ponieważ nie ma sposobu na uzyskanie instancji interfejsu <YourModule>ServiceManager przez inne moduły, mimo że interfejsy get()register() są dla nich widoczne.

Oto jak usługa telefonii uzyskuje odwołanie do usługi telefonicznej: link do wyszukiwania kodu.

Jeśli implementujesz obiekt usługi w kodzie natywnym, używasz natywnego interfejsu API AServiceManager. Te interfejsy API odpowiadają interfejsom API ServiceManager w języku Java, ale interfejsy natywne są bezpośrednio udostępniane modułom głównym. Nie używaj ich do rejestrowania ani odwoływania się do obiektów bindera, które nie należą do Twojego modułu. Jeśli obiekt binder jest udostępniany z poziomu natywnej aplikacji, metoda <YourModule>ServiceManager.ServiceRegisterer nie musi zawierać metody register().

Definicje uprawnień w modułach magistrali

Moduł główny zawierający pliki APK może definiować uprawnienia (niestandardowe) w plikach APKAndroidManifest.xml w taki sam sposób jak w zwykłych plikach APK.

Jeśli zdefiniowane uprawnienie jest używane tylko wewnętrznie w module, przed nazwą uprawnienia należy umieścić nazwę 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, przed nazwą uprawnienia należy umieścić prefiks „android.permission”. (jak każde statyczne uprawnienie platformy) oraz nazwa pakietu modułu, aby wskazać, że jest to interfejs API platformy z modułu, unikając przy tym konfliktów nazw, na przykład:

<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" />

Następnie moduł może udostępnić tę nazwę uprawnienia jako stałą interfejsu API na swojej interfejsie API, na przykład HealthPermissions.READ_ACTIVE_CALORIES_BURNED.