Wytyczne dotyczące interfejsu API Androida

Ta strona ma pomóc deweloperom w zrozumieniu ogólnych zasad, których Rada ds. API przestrzega podczas sprawdzania interfejsów API.

Oprócz przestrzegania tych wytycznych podczas pisania interfejsów API deweloperzy powinni uruchamiać narzędzie API Lint, które zawiera wiele z tych reguł w postaci testów przeprowadzanych na interfejsach API.

Możesz to traktować jako przewodnik po regułach, których przestrzega to narzędzie Lint, a także ogólne porady dotyczące reguł, których nie można 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 uruchamia się automatycznie podczas weryfikacji w CI. Możesz go uruchomić ręcznie z lokalnego procesu płatności na platformie za pomocą m checkapi lub z lokalnego procesu płatności AndroidX za pomocą ./gradlew :path:to:project:checkApi.

Reguły interfejsu API

Platforma Android i wiele bibliotek Jetpack istniały już przed utworzeniem tego zestawu wytycznych, a zasady przedstawione w dalszej części tej strony są stale rozwijane, aby sprostać potrzebom ekosystemu Androida.

W związku z tym niektóre interfejsy API mogą nie być zgodne z wytycznymi. W innych przypadkach deweloperzy aplikacji mogą uznać, że lepsze wrażenia użytkowników zapewni nowy interfejs API, który będzie spójny z dotychczasowymi interfejsami API, a nie ściśle zgodny z wytycznymi.

Kieruj się własnym osądem i w razie trudnych pytań dotyczących interfejsu API, które wymagają rozwiązania, lub wytycznych, które należy zaktualizować, skontaktuj się z Radą ds. API.

Podstawowe informacje o interfejsie API

Ta kategoria dotyczy podstawowych aspektów interfejsu Android API.

Wszystkie interfejsy API muszą być zaimplementowane

Niezależnie od odbiorców interfejsu API (np. publicznych lub @SystemApi) wszystkie powierzchnie interfejsu API muszą być zaimplementowane, gdy są scalane lub udostępniane jako interfejs API. Nie scalaj stubów interfejsu API z implementacją, która zostanie wprowadzona w późniejszym terminie.

Interfejsy API bez implementacji mają kilka problemów:

  • Nie ma gwarancji, że odpowiednia lub pełna powierzchnia została odsłonięta. Dopóki interfejs API nie zostanie przetestowany lub użyty przez klientów, nie ma możliwości sprawdzenia, czy klient ma odpowiednie interfejsy API, aby móc korzystać z danej funkcji.
  • Interfejsów API bez implementacji nie można testować w wersjach deweloperskich.
  • Interfejsów API bez implementacji nie można testować w pakiecie CTS.

Wszystkie interfejsy API muszą zostać przetestowane

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

Testowanie interfejsów API daje podstawową gwarancję, że interfejs API jest użyteczny i że uwzględniliśmy oczekiwane przypadki użycia. Sprawdzenie, czy interfejs API istnieje, nie wystarczy. Musisz przetestować jego działanie.

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

Interfejsy API powinny być też testowalne. Powinieneś(-aś) umieć odpowiedzieć na pytanie: „Jak deweloper aplikacji może przetestować kod korzystający z Twojego interfejsu API?”.

Wszystkie interfejsy API muszą być udokumentowane

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

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

Interfejsy API generowane przez narzędzia muszą być zgodne z tymi samymi wytycznymi co kod pisany ręcznie.

Narzędzia, których nie zalecamy do generowania interfejsów API:

  • AutoValue: narusza wytyczne na różne sposoby, np. nie ma możliwości wdrożenia klas wartości końcowych ani konstruktorów końcowych w sposób, w jaki działa AutoValue.

Styl kodu

Ta kategoria dotyczy ogólnego stylu kodu, którego powinni używać programiści, zwłaszcza podczas pisania publicznych interfejsów API.

Przestrzegaj standardowych konwencji kodowania, z wyjątkiem sytuacji, w których podano inne informacje.

Konwencje kodowania w Androidzie są udokumentowane dla zewnętrznych współtwórców tutaj:

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

Ogólnie rzecz biorąc, stosujemy standardowe konwencje kodowania w językach Java i Kotlin.

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

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

Nazwy nie powinny kończyć się ciągiem znaków Impl

Ujawnia to szczegóły implementacji, więc unikaj tego.

Zajęcia

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

Dziedziczenie nowych klas publicznych z odpowiedniej klasy bazowej

Dziedziczenie udostępnia w klasie podrzędnej elementy interfejsu API, które mogą być nieodpowiednie. Na przykład nowa publiczna podklasa FrameLayout wygląda tak: FrameLayout plus nowe zachowania i elementy interfejsu API. Jeśli odziedziczony interfejs API nie jest odpowiedni w Twoim przypadku, odziedzicz go z klasy znajdującej się wyżej w drzewie, np. ViewGroup lub View.

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

Używanie podstawowych klas kolekcji

Niezależnie od tego, czy kolekcja jest argumentem, czy zwracaną wartością, zawsze preferuj klasę bazową od konkretnej implementacji (np. zwracaj List<Foo> zamiast ArrayList<Foo>).

Użyj klasy bazowej, która wyraża odpowiednie ograniczenia interfejsu API. Na przykład użyj symbolu List w przypadku interfejsu API, którego kolekcja musi być uporządkowana, a symbolu Set w przypadku interfejsu API, którego kolekcja musi składać się z unikalnych elementów.

W języku Kotlin preferuj kolekcje niezmienne. Więcej informacji znajdziesz w sekcji Zmiana kolekcji.

Klasy abstrakcyjne a interfejsy

Java 8 dodaje obsługę domyślnych metod interfejsu, co pozwala projektantom interfejsów API dodawać metody do interfejsów przy zachowaniu zgodności binarnej. Kod platformy i wszystkie biblioteki Jetpack powinny być kierowane na Javę 8 lub nowszą.

W przypadku, gdy domyślna implementacja jest bezstanowa, projektanci interfejsów API powinni preferować interfejsy od klas abstrakcyjnych – domyślne metody interfejsu można implementować jako wywołania innych metod interfejsu.

W przypadkach, gdy domyślna implementacja wymaga konstruktora lub stanu wewnętrznego, należy użyć klas abstrakcyjnych.

W obu przypadkach projektanci interfejsu API mogą pozostawić jedną metodę abstrakcyjną, aby uprościć użycie jako wyrażenia lambda:

public interface AnimationEndCallback {
  // Always called, must be implemented.
  public void onFinished(Animation anim);
  // Optional callbacks.
  public default void onStopped(Animation anim) { }
  public default void onCanceled(Animation anim) { }
}

Nazwy klas powinny odzwierciedlać to, co rozszerzają

Na przykład klasy, które rozszerzają Service, powinny mieć nazwę FooService, aby były bardziej czytelne:

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

Przyrostki ogólne

Unikaj używania ogólnych sufiksów nazw klas, takich jak HelperUtil, w przypadku kolekcji metod narzędziowych. Zamiast tego umieść metody bezpośrednio w powiązanych klasach lub w funkcjach rozszerzających Kotlin.

W przypadku metod łączących wiele klas nadaj klasie zawierającej rozpoznawalną nazwę, która wyjaśnia, co robi.

W bardzo rzadkich przypadkach użycie sufiksu Helper może być odpowiednie:

  • Używana do tworzenia domyślnego działania
  • Może obejmować delegowanie istniejącego zachowania do nowych klas.
  • Może wymagać stanu trwałego
  • Zwykle obejmuje View

Jeśli na przykład przenoszenie wsteczne etykietek wymaga zachowania stanu powiązanego z View i wywołania kilku metod w View w celu zainstalowania przeniesienia wstecznego, TooltipHelper będzie odpowiednią nazwą klasy.

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

Kod wygenerowany przez IDL traktuj jako szczegóły implementacji. Dotyczy to protobuf, gniazd, FlatBuffers i innych interfejsów API innych niż Java i NDK. Większość IDL w Androidzie jest w AIDL, więc ta strona skupia się na AIDL.

Wygenerowane klasy AIDL nie spełniają wymagań przewodnika po stylu interfejsu API (np. nie mogą używać przeciążania), a narzędzie AIDL nie zostało zaprojektowane z myślą o zachowaniu zgodności interfejsu API z językiem, więc nie można ich osadzać w publicznym interfejsie API.

Zamiast tego dodaj publiczną warstwę interfejsu API na interfejsie AIDL, nawet jeśli początkowo będzie to tylko powłoka.

Interfejsy Binder

Jeśli interfejs Binder jest szczegółem implementacji, można go w przyszłości dowolnie zmieniać, a warstwa publiczna pozwala zachować wymaganą zgodność wsteczną. Może być na przykład konieczne dodanie nowych argumentów do wywołań wewnętrznych lub zoptymalizowanie ruchu IPC przez użycie przetwarzania wsadowego lub strumieniowego, pamięci współdzielonej itp. Żadnej z tych czynności nie można wykonać, jeśli interfejs AIDL jest też publicznym interfejsem API.

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

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

Zamiast tego umieść interfejs Binder w klasie menedżera lub innej klasie:

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo);
}

public IFooManager {
   public void doFoo(String foo) {
      mFooService.doFoo(foo);
   }
}

Jeśli później do tego wywołania będzie potrzebny nowy argument, interfejs wewnętrzny może być minimalny, a do publicznego interfejsu API można dodać wygodne przeciążenia. Warstwy opakowującej możesz używać do rozwiązywania innych problemów ze zgodnością wsteczną w miarę rozwoju implementacji:

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo, int flags);
}

public IFooManager {
   public void doFoo(String foo) {
      if (mAppTargetSdkLevel < 26) {
         useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
         mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
      } else {
         mFooService.doFoo(foo, 0);
      }
   }

   public void doFoo(String foo, int flags) {
      mFooService.doFoo(foo, flags);
   }
}

W przypadku interfejsów Binder, które nie są częścią platformy Android (np. interfejsu usługi eksportowanego przez usługi Google Play do użytku przez aplikacje), wymóg stabilnego, opublikowanego i wersjonowanego interfejsu IPC oznacza, że znacznie trudniej jest rozwijać sam interfejs. Warto jednak utworzyć warstwę otoki, aby dopasować ją do innych wytycznych dotyczących interfejsów API i ułatwić korzystanie z tego samego publicznego interfejsu API w nowej wersji interfejsu IPC, jeśli kiedykolwiek zajdzie taka potrzeba.

Nie używaj surowych obiektów Binder w publicznym interfejsie API

Obiekt Binder sam w sobie nie ma żadnego znaczenia, dlatego nie należy go używać w publicznym interfejsie API. Jednym z częstych przypadków użycia jest użycie znaku Binder lub IBinder jako tokena, ponieważ ma on semantykę tożsamości. Zamiast używać surowego obiektu Binder, użyj klasy tokena opakowującego.

public final class IdentifiableObject {
  public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
  /**
   * @hide
   */
  public Binder getRawValue() {...}

  /**
   * @hide
   */
  public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}

public final class IdentifiableObject {
  public IdentifiableObjectToken getToken() {...}
}

Klasy menedżera muszą być ostateczne

Klasy menedżera należy deklarować jako final. Klasy menedżera komunikują się z usługami systemowymi i stanowią jedyny punkt interakcji. Nie ma potrzeby dostosowywania, więc zadeklaruj ją jako final.

Nie używaj CompletableFuture ani Future

java.util.concurrent.CompletableFuture ma dużą powierzchnię interfejsu API, która umożliwia dowolną zmianę wartości przyszłej i ma podatne na błędy ustawienia domyślne.

Z kolei w przypadku java.util.concurrent.Future brakuje nasłuchiwania nieblokującego, co utrudnia korzystanie z niego w przypadku kodu asynchronicznego.

W kodzie platformy i interfejsach API bibliotek niskiego poziomu używanych zarówno przez Kotlin, jak i Javę preferuj połączenie wywołania zwrotnego zakończenia, Executor i, jeśli interfejs API obsługuje anulowanie, CancellationSignal.

public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
    Executor callbackExecutor,
    android.os.OutcomeReceiver<FooResult, Throwable> callback);

Jeśli kierujesz reklamy na Kotlin, używaj funkcji suspend.

suspend fun asyncLoadFoo(): Foo

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

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

Nie używaj opcji

Chociaż Optional może mieć zalety w przypadku niektórych interfejsów API, jest niespójny z obecnym obszarem interfejsu API Androida. @Nullable@NonNull zapewniają narzędzia ułatwiające zachowanie null bezpieczeństwa, a Kotlin wymusza umowy dotyczące wartości null na poziomie kompilatora, co sprawia, że Optional jest niepotrzebne.

W przypadku opcjonalnych typów prostych używaj sparowanych metod hasget. Jeśli wartość nie jest ustawiona (has zwraca false), metoda get powinna zgłosić wyjątek IllegalStateException.

public boolean hasAzimuth() { ... }
public int getAzimuth() {
  if (!hasAzimuth()) {
    throw new IllegalStateException("azimuth is not set");
  }
  return azimuth;
}

Używanie prywatnych konstruktorów w przypadku klas, których nie można utworzyć

Klasy, które mogą być tworzone tylko przez Builder, klasy zawierające tylko stałe lub metody statyczne albo klasy, których nie można utworzyć, powinny zawierać co najmniej jeden konstruktor prywatny, aby zapobiec tworzeniu instancji za pomocą domyślnego konstruktora bez argumentów.

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

Singletony

Singletony są odradzane, ponieważ mają następujące wady związane z testowaniem:

  1. Konstrukcja jest zarządzana przez klasę, co zapobiega używaniu podróbek.
  2. Testy nie mogą być hermetyczne ze względu na statyczny charakter pojedynczej instancji
  3. Aby obejść te problemy, deweloperzy muszą znać wewnętrzne szczegóły singletonu lub utworzyć wokół niego otokę.

Zalecamy stosowanie wzorca pojedynczej instancji, który wykorzystuje abstrakcyjną klasę bazową do rozwiązania tych problemów.

Pojedyncza instancja

Klasy z jedną instancją używają abstrakcyjnej klasy bazowej z konstruktorem private lub internal i udostępniają statyczną metodę getInstance() do uzyskiwania instancji. Metoda getInstance() musi zwracać ten sam obiekt w kolejnych wywołaniach.

Obiekt zwracany przez getInstance() powinien być prywatną implementacją abstrakcyjnej klasy bazowej.

class Singleton private constructor(...) {
  companion object {
    private val _instance: Singleton by lazy { Singleton(...) }

    fun getInstance(): Singleton {
      return _instance
    }
  }
}
abstract class SingleInstance private constructor(...) {
  companion object {
    private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
    fun getInstance(): SingleInstance {
      return _instance
    }
  }
}

Pojedyncza instancja różni się od singletonu tym, że deweloperzy mogą utworzyć fałszywą wersję SingleInstance i użyć własnych struktur wstrzykiwania zależności do zarządzania implementacją bez konieczności tworzenia otoki. Biblioteka może też udostępniać własną fałszywą wersję w artefakcie -testing.

Klasy, które zwalniają zasoby, powinny implementować interfejs AutoCloseable

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

Unikaj wprowadzania nowych podklas View w pakiecie android.*

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

Zestaw narzędzi interfejsu Androida jest teraz oparty na Compose. Nowe funkcje interfejsu udostępniane przez platformę powinny być udostępniane jako interfejsy API niższego poziomu, których można używać do implementowania komponentów interfejsu Jetpack Compose i opcjonalnie komponentów interfejsu opartych na widokach dla programistów w bibliotekach Jetpack. Udostępnianie tych komponentów w bibliotekach stwarza możliwości implementacji wstecznych, gdy funkcje platformy są niedostępne.

Fieldsem

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

Nie udostępniaj nieprzetworzonych pól

Klasy Java nie powinny bezpośrednio udostępniać pól. Pola powinny być prywatne i dostępne tylko za pomocą publicznych metod pobierających i ustawiających, niezależnie od tego, czy są one ostateczne.

Rzadkie wyjątki obejmują podstawowe struktury danych, w przypadku których nie ma potrzeby ulepszania sposobu określania lub pobierania pola. W takich przypadkach pola powinny mieć nazwy zgodne ze standardowymi konwencjami nazewnictwa zmiennych, np. Point.xPoint.y.

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

Udostępnione pola powinny być oznaczone jako finalne

Pola surowe są zdecydowanie odradzane (@see Nie udostępniaj pól surowych). Jeśli jednak w rzadkich przypadkach pole jest udostępniane jako publiczne, oznacz je symbolem final.

Pola wewnętrzne nie powinny być udostępniane

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

public int mFlags;

Używanie dostępu publicznego zamiast chronionego

@see Use public instead of protected

Stałe

Są to reguły dotyczące stałych publicznych.

Stałe flagi nie powinny nakładać się na wartości int lub long

Flagi to bity, które można łączyć w pewną wartość sumy. W przeciwnym razie nie nazywaj zmiennej ani stałej flag.

public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;

Więcej informacji o definiowaniu stałych flag publicznych znajdziesz w sekcji @IntDef dotyczącej flag masek bitowych.

stałe statyczne powinny być zgodne z konwencją nazewnictwa z użyciem wielkich liter i podkreślników,

Wszystkie słowa w stałej powinny być pisane wielkimi literami, a poszczególne słowa powinny być oddzielone znakiem _. Na przykład:

public static final int fooThing = 5
public static final int FOO_THING = 5

Używaj standardowych przedrostków dla stałych

Wiele stałych używanych w Androidzie dotyczy standardowych elementów, takich jak flagi, klucze i działania. Stałe te powinny mieć standardowe prefiksy, aby można je było łatwiej zidentyfikować.

Na przykład dodatki do intencji powinny zaczynać się od znaku EXTRA_. Działania związane z intencjami powinny zaczynać się od ACTION_. Stałe używane z Context.bindService() powinny zaczynać się od BIND_.

Nazwy i zakresy kluczowych stałych

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

public static final String FOO_THING = "foo"

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

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

Prefiksy android w stałych ciągach znaków o ograniczonym zakresie są zarezerwowane dla Projektu Android Open Source.

Działania i dodatki intencji oraz wpisy pakietu powinny mieć przestrzeń nazw z nazwą pakietu, w którym są zdefiniowane.

package android.foo.bar {
  public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
  public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}

Używanie dostępu publicznego zamiast chronionego

@see Use public instead of protected

Używanie spójnych prefiksów

Powiązane stałe powinny zaczynać się od tego samego prefiksu. Na przykład w przypadku zbioru stałych do użycia z wartościami flag:

public static final int SOME_VALUE = 0x01;

public static final int SOME_OTHER_VALUE = 0x10;

public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;

public static final int FLAG_SOME_OTHER_VALUE = 0x10;

public static final int FLAG_SOME_THIRD_VALUE = 0x100;

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

Używanie spójnych nazw zasobów

Identyfikatory publiczne, atrybuty i wartości muszą być nazwane zgodnie z konwencją camelCase, np. @id/accessibilityActionPageUp lub @attr/textAppearance, podobnie jak pola publiczne w języku Java.

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

  • 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ć zgodne z hierarchiczną konwencją nazewnictwa PascalCase, np. @style/Theme.Material.Light.DarkActionBar lub @style/Widget.Material.SearchView.ActionBar, podobnie jak klasy zagnieżdżone w języku Java.

Zasoby układu i zasoby rysowalne nie powinny być udostępniane jako publiczne interfejsy API. Jeśli jednak muszą być widoczne, publiczne układy i elementy rysunkowe muszą być nazwane zgodnie z konwencją nazewnictwa under_score, np. layout/simple_list_item_1.xml lub drawable/title_bar_tall.xml.

Gdy stałe mogą się zmienić, ustaw je jako dynamiczne

Kompilator może wstawiać wartości stałe, więc zachowanie tych samych wartości jest uważane za część umowy API. Jeśli wartość stałej MIN_FOO lub MAX_FOO może się w przyszłości zmienić, rozważ użycie zamiast niej metody dynamicznej.

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

Zadbaj o zgodność wywołań zwrotnych z przyszłymi wersjami

Stałe zdefiniowane w przyszłych wersjach interfejsu API nie są znane aplikacjom kierowanym na starsze interfejsy API. Z tego powodu stałe dostarczane do aplikacji powinny uwzględniać docelową wersję interfejsu API aplikacji i mapować nowsze stałe na spójną wartość. Przyjrzyjmy się temu scenariuszowi:

Przykładowe źródło pakietu SDK:

// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;

Hipotetyczna aplikacja z targetSdkVersion="22":

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

W tym przypadku aplikacja została zaprojektowana w ramach ograniczeń poziomu API 22 i przyjęła (dość) rozsądne założenie, że istnieją tylko 2 możliwe stany. Jeśli jednak aplikacja otrzyma nowo dodany kod STATUS_FAILURE_RETRY, zinterpretuje to jako sukces.

Metody zwracające stałe mogą bezpiecznie obsługiwać takie przypadki, ograniczając dane wyjściowe do poziomu interfejsu API, na który jest kierowana aplikacja:

private int mapResultForTargetSdk(Context context, int result) {
  int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
  if (targetSdkVersion < 26) {
    if (result == STATUS_FAILURE_ABORT) {
      return STATUS_FAILURE;
    }
    if (targetSdkVersion < 23) {
      if (result == STATUS_FAILURE_RETRY) {
        return STATUS_FAILURE;
      }
    }
  }
  return result;
}

Deweloperzy nie mogą przewidzieć, czy lista stałych może się w przyszłości zmienić. Jeśli zdefiniujesz interfejs API ze stałą UNKNOWN lub UNSPECIFIED, która wygląda jak stała ogólna, deweloperzy założą, że opublikowane stałe w momencie pisania aplikacji są wyczerpujące. Jeśli nie chcesz tego robić, zastanów się, czy stała obejmująca wszystkie przypadki jest dobrym rozwiązaniem w przypadku Twojego interfejsu API.

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

Stała w postaci liczby całkowitej lub ciągu tekstowego

Używaj stałych całkowitych i @IntDef, jeśli przestrzeń nazw wartości nie jest rozszerzalna poza pakiet. Używaj stałych ciągów znaków, jeśli przestrzeń nazw jest współdzielona lub może być rozszerzana przez kod spoza pakietu.

Klasy danych

Klasy danych reprezentują zestaw niezmiennych właściwości i zapewniają mały i dobrze zdefiniowany zestaw funkcji narzędziowych do interakcji z tymi danymi.

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

Utworzenie instancji

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

W języku Kotlin klasy danych powinny udostępniać konstruktor z argumentami domyślnymi niezależnie od liczby właściwości. Klasy danych zdefiniowane w języku Kotlin mogą również korzystać z udostępniania konstruktora w przypadku klientów Java.

Modyfikowanie i kopiowanie

Jeśli dane wymagają modyfikacji, podaj klasę Builder z konstruktorem kopiującym (Java) lub funkcję copy() (Kotlin), która zwraca nowy obiekt.

Podczas udostępniania funkcji copy() w Kotlinie argumenty muszą być zgodne z konstruktorem klasy, a wartości domyślne muszą być wypełniane przy użyciu bieżących wartości obiektu:

class Typography(
  val labelMedium: TextStyle = TypographyTokens.LabelMedium,
  val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
    fun copy(
      labelMedium: TextStyle = this.labelMedium,
      labelSmall: TextStyle = this.labelSmall
    ): Typography = Typography(
      labelMedium = labelMedium,
      labelSmall = labelSmall
    )
}

Dodatkowe zachowania

Klasy danych powinny implementować zarówno equals(), jak i hashCode(), a każda właściwość musi być uwzględniona w implementacjach tych metod.

Klasy danych mogą implementować toString() w zalecanym formacie pasującym do implementacji klasy danych w Kotlinie, np. User(var1=Alex, var2=42).

Metody

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

Godzina

Określają one, jak w interfejsach API powinny być wyrażane pojęcia związane z czasem, takie jak daty i czas trwania.

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

java.time.Duration, java.time.Instant i wiele innych typów java.time.* jest dostępnych we wszystkich wersjach platformy dzięki odcukrzaniu. Należy ich używać do wyrażania czasu w parametrach interfejsu API lub zwracanych wartościach.

Zalecamy udostępnianie tylko tych wariantów interfejsu API, które akceptują lub zwracają java.time.Duration lub java.time.Instant, i pomijanie wariantów pierwotnych o tym samym zastosowaniu, chyba że domena interfejsu API jest taka, w której alokacja obiektów w zamierzonych wzorcach użycia miałaby nadmierny wpływ na wydajność.

Metody wyrażające czas trwania powinny mieć nazwę duration

Jeśli wartość czasu wyraża czas trwania, nazwij parametr „duration”, a nie „time”.

ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);

Wyjątki:

„timeout” jest odpowiednie, gdy czas trwania dotyczy konkretnie wartości limitu czasu.

Wartość „time” o typie java.time.Instant jest odpowiednia, gdy odnosi się do konkretnego momentu w czasie, a nie do czasu trwania.

Metody wyrażające czas trwania lub czas jako typ prosty powinny mieć w nazwie jednostkę czasu i używać typu long.

Metody, które przyjmują lub zwracają czas trwania jako typ prosty, powinny mieć w nazwie przyrostek z jednostkami czasu (np. Millis, Nanos, Seconds), aby zarezerwować nazwę bez przyrostka do użycia z java.time.Duration. Zobacz Czas.

Metody powinny być też odpowiednio oznaczone jednostką i podstawą czasu:

  • @CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba milisekund od 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba sekund od 1970-01-01T00:00:00Z.
  • @DurationMillisLong: wartość to nieujemny czas trwania w milisekundach.
  • @ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa w SystemClock.elapsedRealtime().
  • @UptimeMillisLong: wartość to nieujemna sygnatura czasowa w SystemClock.uptimeMillis().

Parametry czasu pierwotnego 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 używać nieabbreviowanych skrótów nazw jednostek.

public void setIntervalNs(long intervalNs);

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

public void setTimeoutMicros(long timeoutMicros);

Dodawanie adnotacji do argumentów dotyczących długiego czasu

Platforma zawiera kilka adnotacji, które zapewniają silniejsze typowanie jednostek czasu typu long:

  • @CurrentTimeMillisLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba milisekund od 1970-01-01T00:00:00Z, a więc w System.currentTimeMillis().
  • @CurrentTimeSecondsLong: wartość to nieujemna sygnatura czasowa mierzona jako liczba sekund od 1970-01-01T00:00:00Z.
  • @DurationMillisLong: wartość to nieujemny czas trwania w milisekundach.
  • @ElapsedRealtimeLong: wartość to nieujemna sygnatura czasowa w SystemClock#elapsedRealtime().
  • @UptimeMillisLong: wartość to nieujemna sygnatura czasowa w SystemClock#uptimeMillis().

Jednostki miary

W przypadku wszystkich metod wyrażających jednostkę miary inną niż czas preferuj prefiksy jednostek SI w formacie CamelCase .

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

Umieszczaj parametry opcjonalne na końcu przeciążeń.

Jeśli masz przeciążenia metody z parametrami opcjonalnymi, umieść je na końcu i zachowaj spójną kolejność z innymi parametrami:

public int doFoo(boolean flag);

public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);

public int doFoo(boolean flag, int id);

Podczas dodawania przeciążeń dla argumentów opcjonalnych prostsze metody powinny działać dokładnie tak samo, jak gdyby argumenty domyślne zostały przekazane do bardziej złożonych metod.

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

Metody z parametrami domyślnymi muszą być oznaczone adnotacją @JvmOverloads (tylko w Kotlinie).

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

Więcej informacji znajdziesz w sekcji Function overloads for defaults w oficjalnym przewodniku po współdziałaniu języków Kotlin i Java.

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

Nie usuwaj domyślnych wartości parametrów (tylko Kotlin)

Jeśli metoda została dostarczona z parametrem o wartości domyślnej, usunięcie tej wartości jest zmianą powodującą niezgodność z kodem źródłowym.

Najbardziej charakterystyczne i identyfikujące parametry metody powinny być podane jako pierwsze.

Jeśli metoda ma wiele parametrów, na początku umieść te, które są najbardziej istotne. Parametry określające flagi i inne opcje są mniej ważne niż te, które opisują obiekt, na którym wykonywana jest operacja. Jeśli istnieje wywołanie zwrotne zakończenia, umieść je na końcu.

public void openFile(int flags, String name);

public void openFileAsync(OnFileOpenedListener listener, String name, int flags);

public void setFlags(int mask, int flags);
public void openFile(String name, int flags);

public void openFileAsync(String name, int flags, OnFileOpenedListener listener);

public void setFlags(int flags, int mask);

Zobacz też: Umieszczanie parametrów opcjonalnych na końcu w przypadku przeciążeń

Firmy budowlane

Wzorzec Builder jest zalecany do tworzenia złożonych obiektów Java i jest często używany w Androidzie w przypadkach, gdy:

  • Właściwości wynikowego obiektu powinny być niezmienne.
  • Wymaganych jest wiele właściwości, np. wiele argumentów konstruktora.
  • W momencie tworzenia usług występuje złożona relacja między nimi, np. wymagany jest etap weryfikacji. Pamiętaj, że ten poziom złożoności często wskazuje na problemy z użytecznością interfejsu API.

Zastanów się, czy potrzebujesz kreatora. Obiekty Builder są przydatne w interfejsie API, jeśli służą do:

  • skonfigurować tylko kilka z potencjalnie dużego zbioru opcjonalnych parametrów tworzenia,
  • skonfigurować wiele różnych opcjonalnych lub wymaganych parametrów tworzenia, czasami podobnych lub pasujących do siebie typów, w przypadku których witryny wywołujące mogą być trudne do odczytania lub podatne na błędy podczas pisania;
  • Skonfiguruj tworzenie obiektu przyrostowo, w którym kilka różnych fragmentów kodu konfiguracyjnego może wywoływać konstruktora jako szczegóły implementacji.
  • Zezwalaj na rozwijanie typu przez dodawanie dodatkowych opcjonalnych parametrów tworzenia w przyszłych wersjach interfejsu API.

Jeśli masz typ z maksymalnie 3 wymaganymi parametrami i bez parametrów opcjonalnych, prawie zawsze możesz pominąć narzędzie do tworzenia i użyć zwykłego konstruktora.

W przypadku klas pochodzących z KOTLIN-a należy preferować konstruktory z adnotacją @JvmOverloads z argumentami domyślnymi zamiast konstruktorów, ale można też zwiększyć użyteczność dla klientów Java, udostępniając konstruktory w opisanych wcześniej przypadkach.

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

Klasy konstruktora muszą zwracać konstruktora

Klasy narzędzia do tworzenia muszą umożliwiać łączenie metod przez zwracanie obiektu narzędzia do tworzenia (np. this) z każdej metody z wyjątkiem build(). Dodatkowe wbudowane obiekty należy przekazywać jako argumenty. Nie zwracaj konstruktora innego obiektu. 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 klasa bazowa kreatora musi obsługiwać rozszerzenie, użyj ogólnego typu zwracanego:

public abstract class Builder<T extends Builder<T>> {
  abstract T setValue(int);
}

public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
  T setValue(int);
  T setTypeSpecificValue(long);
}

Klasy narzędzi do tworzenia muszą być tworzone za pomocą konstruktora.

Aby zachować spójność tworzenia narzędzi do tworzenia za pomocą interfejsu Android API, wszystkie narzędzia muszą być tworzone za pomocą konstruktora, a nie statycznej metody tworzenia. W przypadku interfejsów API opartych na Kotlinie pole Builder musi być publiczne, nawet jeśli użytkownicy Kotlina mają niejawnie korzystać z konstruktora za pomocą metody fabrykującej lub mechanizmu tworzenia w stylu DSL. Biblioteki nie mogą używać adnotacji @PublishedApi internal do selektywnego ukrywania konstruktora klasy Builder przed klientami Kotlin.

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

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

Opcjonalne argumenty, np. @Nullable, należy przenieść do metod ustawiających. Konstruktor powinien zgłaszać wyjątek NullPointerException (rozważ użycie Objects.requireNonNull), jeśli nie podano żadnych wymaganych argumentów.

Klasy budujące powinny być ostatecznymi statycznymi klasami wewnętrznymi typów, które tworzą.

Ze względu na logiczną organizację w pakiecie klasy konstruktora powinny być zwykle udostępniane jako wewnętrzne klasy końcowe typów, które tworzą, np. Tone.Builder zamiast ToneBuilder.

Konstruktor może tworzyć nową instancję na podstawie istniejącej instancji.

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

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

  public Builder toBuilder();
}
public class Tone {
  public static class Builder {
    public Builder(Builder original);
    public Builder(Tone original);
  }
}
Metody ustawiające w klasie Builder powinny przyjmować argumenty @Nullable, jeśli klasa Builder ma konstruktor kopiujący

Resetowanie jest niezbędne, jeśli z istniejącej instancji może zostać utworzona nowa instancja narzędzia do tworzenia. Jeśli nie jest dostępny żaden konstruktor kopiujący, konstruktor może mieć argumenty @Nullable lub @NonNullable.

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
Metody ustawiające w klasie Builder mogą przyjmować argumenty @Nullable w przypadku właściwości opcjonalnych.

Często łatwiej jest użyć wartości dopuszczającej wartość null w przypadku danych wejściowych drugiego stopnia, zwłaszcza w języku Kotlin, który zamiast konstruktorów i przeciążeń używa argumentów domyślnych.

Dodatkowo @Nullable ustawiające będą pasować do pobierających, które muszą być @Nullable w przypadku właściwości opcjonalnych.

Value createValue(@Nullable OptionalValue optionalValue) {
  Value.Builder builder = new Value.Builder();
  if (optionalValue != null) {
    builder.setOptionalValue(optionalValue);
  }
  return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
  return new Value.Builder()
    .setOptionalValue(optionalValue);
    .build();
}

// Or in other cases:

Value createValue() {
  return new Value.Builder()
    .setOptionalValue(condition ? new OptionalValue() : null);
    .build();
}

Typowe użycie w języku Kotlin:

fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .apply { optionalValue?.let { setOptionalValue(it) } }
    .build()
fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .setOptionalValue(optionalValue)
    .build()

Wartość domyślna (jeśli funkcja ustawiająca nie jest wywoływana) i znaczenie wartości null muszą być odpowiednio udokumentowane zarówno w funkcji ustawiającej, jak i w funkcji pobierającej.

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

W przypadku właściwości, które można zmieniać, można podać metody ustawiające, jeśli są one dostępne w klasie utworzonej za pomocą konstruktora.

Jeśli Twoja klasa ma właściwości, które można zmieniać, i potrzebuje klasy Builder, najpierw zastanów się, czy Twoja klasa rzeczywiście powinna mieć właściwości, które można zmieniać.

Następnie, jeśli masz pewność, że potrzebujesz właściwości modyfikowalnych, zdecyduj, który z tych scenariuszy lepiej pasuje do Twojego przypadku użycia:

  1. Zbudowany obiekt powinien być od razu gotowy do użycia, dlatego dla wszystkich odpowiednich właściwości, zarówno modyfikowalnych, jak i niemodyfikowalnych, należy podać settery.

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

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

Nie mieszaj tych 2 scenariuszy.

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

Klasy Builder nie powinny mieć metod pobierających

Metoda pobierająca powinna znajdować się w utworzonym obiekcie, a nie w obiekcie budującym.

Ustawiające metody konstruktora muszą mieć odpowiednie metody pobierające w budowanej klasie.

public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }

  public long getDuration();
  public int getFrequency();
  public @NonNull List<DtmfConfig> getDtmfConfigs();
}

Nazewnictwo metod kreatora

Nazwy metod konstruktora powinny być zgodne ze stylem setFoo(), addFoo() lub clearFoo().

Klasy Builder powinny deklarować metodę build()

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

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

Metoda konstruktora build() powinna zwracać instancję utworzonego obiektu, która nie ma wartości null. Jeśli nie można utworzyć obiektu z powodu nieprawidłowych parametrów, weryfikację można odłożyć do metody budowania i należy zgłosić wyjątek IllegalStateException.

Nie udostępniaj wewnętrznych blokad

Metody w publicznym interfejsie API nie powinny używać słowa kluczowego synchronized. Ten słowo kluczowe powoduje, że obiekt lub klasa są używane jako blokada. Ponieważ jest ono udostępniane innym, możesz napotkać nieoczekiwane efekty uboczne, jeśli inny kod poza Twoją klasą zacznie go używać do blokowania.

Zamiast tego wykonaj wymagane blokowanie na wewnętrznym, prywatnym obiekcie.

public synchronized void doThing() { ... }
private final Object mThingLock = new Object();

public void doThing() {
  synchronized (mThingLock) {
    ...
  }
}

Metody w stylu funkcji dostępu powinny być zgodne z wytycznymi dotyczącymi właściwości w Kotlinie.

Metody w stylu akcesora – te, które używają prefiksów get, set lub is – będą też dostępne jako właściwości Kotlin, gdy są wyświetlane ze źródeł Kotlin. Na przykład int getField() zdefiniowany w Javie jest dostępny w Kotlinie jako właściwość val field: Int.

Z tego powodu oraz aby spełnić oczekiwania deweloperów dotyczące zachowania metod dostępu, metody używające prefiksów metod dostępu powinny zachowywać się podobnie do pól w języku Java. Unikaj prefiksów w stylu metod dostępu, gdy:

  • Metoda ma efekty uboczne – wybierz bardziej opisową nazwę metody
  • Metoda wymaga dużych nakładów obliczeniowych – preferuj compute
  • Metoda polega na blokowaniu lub innym długotrwałym działaniu w celu zwrócenia wartości, np. IPC lub innego wejścia/wyjścia – preferuj fetch
  • Metoda blokuje wątek, dopóki nie może zwrócić wartości – preferuj await
  • Metoda zwraca nową instancję obiektu przy każdym wywołaniu – zalecamy używanie metody create.
  • Metoda może nie zwrócić wartości. Zalecamy użycie request

Pamiętaj, że wykonanie kosztownej obliczeniowo pracy raz i zapisanie wartości w pamięci podręcznej na potrzeby kolejnych wywołań nadal jest uznawane za wykonanie kosztownej obliczeniowo pracy. Zacinanie się nie jest rozłożone na klatki.

Używaj prefiksu „is” w przypadku metod dostępu do wartości logicznych

Jest to standardowa konwencja nazewnictwa metod i pól logicznych w języku Java. Zazwyczaj nazwy metod i zmiennych logicznych powinny być zapisywane w formie pytań, na które odpowiada zwracana wartość.

Metody dostępu do wartości logicznych w języku Java powinny być zgodne ze schematem nazewnictwa set/is, a pola powinny preferować is, np.:

// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();

// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();

final boolean isAvailable;

Użycie metod dostępu set/is w przypadku Javy lub is w przypadku pól Javy umożliwi używanie ich jako właściwości w Kotlinie:

obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return

Właściwości i metody dostępu powinny zwykle używać nazw o pozytywnym znaczeniu, np. Enabled zamiast Disabled. Używanie negatywnej terminologii odwraca znaczenie tagów truefalse oraz utrudnia zrozumienie działania.

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

W przypadku, gdy wartość logiczna opisuje włączenie lub własność usługi, możesz użyć słowa has zamiast is. Nie będzie to jednak działać w przypadku składni właściwości Kotlin:

// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();

Niektóre alternatywne prefiksy, które mogą być bardziej odpowiednie, to 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ą zależność od innych zachowań lub funkcji, mogą używać prefiksu is i sufiksu Supported lub Required:

// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()

Zazwyczaj nazwy metod powinny być zapisywane w formie pytań, na które odpowiada wartość zwracana.

Metody właściwości w Kotlinie

W przypadku właściwości klasy var foo: Foo Kotlin wygeneruje metody get/set zgodnie z tą samą regułą: w przypadku gettera dodaj na początku get i zmień pierwszą literę na wielką, a w przypadku settera dodaj na początku set i zmień pierwszą literę na wielką. Deklaracja właściwości wygeneruje odpowiednio metody o nazwach public Foo getFoo()public void setFoo(Foo foo).

Jeśli właściwość jest typu Boolean, w przypadku generowania nazwy obowiązuje dodatkowa reguła: jeśli nazwa właściwości zaczyna się od is, przed nazwą metody pobierającej nie jest dodawany prefiks get, a sama nazwa właściwości jest używana jako metoda pobierająca. Dlatego preferuj nazywanie właściwości Boolean prefiksem is, aby zachować zgodność z wytycznymi dotyczącymi nazewnictwa:

var isVisible: Boolean

Jeśli Twoja usługa należy do jednego z wyjątków wymienionych powyżej i zaczyna się od odpowiedniego prefiksu, użyj w niej adnotacji @get:JvmName, aby ręcznie określić odpowiednią nazwę:

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

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

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

Akcesory do masek bitowych

Więcej informacji o definiowaniu flag bitowych znajdziesz w wytycznych dotyczących interfejsu API w artykule Używanie @IntDef w przypadku flag bitowych.

Settery

Należy udostępnić 2 metody ustawiające: jedną, która przyjmuje pełny ciąg bitów i zastępuje wszystkie istniejące flagi, oraz drugą, która przyjmuje niestandardową maskę bitową, aby zapewnić większą elastyczność.

/**
 * Sets the state of all scroll indicators.
 * <p>
 * See {@link #setScrollIndicators(int, int)} for usage information.
 *
 * @param indicators a bitmask of indicators that should be enabled, or
 *                   {@code 0} to disable all indicators
 * @see #setScrollIndicators(int, int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators);

/**
 * Sets the state of the scroll indicators specified by the mask. To change
 * all scroll indicators at once, see {@link #setScrollIndicators(int)}.
 * <p>
 * When a scroll indicator is enabled, it will be displayed if the view
 * can scroll in the direction of the indicator.
 * <p>
 * Multiple indicator types may be enabled or disabled by passing the
 * logical OR of the specified types. If multiple types are specified, they
 * will all be set to the same enabled state.
 * <p>
 * For example, to enable the top scroll indicator:
 * {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
 * <p>
 * To disable the top scroll indicator:
 * {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
 *
 * @param indicators a bitmask of values to set; may be a single flag,
 *                   the logical OR of multiple flags, or 0 to clear
 * @param mask a bitmask indicating which indicator flags to modify
 * @see #setScrollIndicators(int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);

Gettery

Aby uzyskać pełną maskę bitową, należy podać jeden getter.

/**
 * Returns a bitmask representing the enabled scroll indicators.
 * <p>
 * For example, if the top and left scroll indicators are enabled and all
 * other indicators are disabled, the return value will be
 * {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
 * <p>
 * To check whether the bottom scroll indicator is enabled, use the value
 * of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
 *
 * @return a bitmask representing the enabled scroll indicators
 */
@ScrollIndicators
public int getScrollIndicators();

Używanie dostępu publicznego zamiast chronionego

W publicznym interfejsie API zawsze preferuj public zamiast protected. Długoterminowo dostęp chroniony jest uciążliwy, ponieważ w przypadkach, w których domyślny dostęp zewnętrzny byłby równie dobry, osoby wdrażające muszą zastępować go publicznymi metodami dostępu.

Pamiętaj, że protectedwidocznośćnie uniemożliwia deweloperom wywoływania interfejsu API, tylko sprawia, że jest to nieco bardziej uciążliwe.

Zaimplementuj obie metody equals() i hashCode() lub żadnej z nich.

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

Implementowanie metody toString() w klasach danych

Klasy danych powinny zastępować metodę toString(), aby ułatwić programistom debugowanie kodu.

Dokumentuj, czy dane wyjściowe służą do określania zachowania programu, czy do debugowania.

Zdecyduj, czy działanie programu ma zależeć od Twojej implementacji. Na przykład metody UUID.toString()File.toString() dokumentują swój konkretny format, który mogą wykorzystywać programy. Jeśli udostępniasz informacje tylko na potrzeby debugowania, np. Intent, dziedzicz dokumentację z klasy nadrzędnej.

Nie podawaj dodatkowych informacji

Wszystkie informacje dostępne w toString() powinny być też dostępne w publicznym interfejsie API obiektu. W przeciwnym razie zachęcasz programistów do analizowania i wykorzystywania danych wyjściowych toString(), co uniemożliwi wprowadzanie zmian w przyszłości. Dobrym rozwiązaniem jest implementowanie toString() tylko za pomocą publicznego interfejsu API obiektu.

Zniechęcanie do polegania na danych wyjściowych debugowania

Nie można uniemożliwić deweloperom korzystania z danych wyjściowych debugowania, ale uwzględnienie System.identityHashCode obiektu w jego danych wyjściowych toString() sprawi, że dwa różne obiekty będą miały bardzo małe prawdopodobieństwo uzyskania identycznych danych wyjściowych toString().

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

Może to skutecznie zniechęcić programistów do pisania asercji testowych, takich jak assertThat(a.toString()).isEqualTo(b.toString()), w odniesieniu do Twoich obiektów.

Używaj funkcji createFoo, gdy zwracasz nowo utworzone obiekty

W przypadku metod, które będą tworzyć wartości zwracane, np. przez konstruowanie nowych obiektów, używaj prefiksu create, a nie get ani new.

Jeśli metoda tworzy obiekt do zwrócenia, wyraźnie zaznacz to w nazwie metody.

public FooThing getFooThing() {
  return new FooThing();
}
public FooThing createFooThing() {
  return new FooThing();
}

Metody akceptujące obiekty File powinny też akceptować strumienie

Lokalizacje przechowywania danych na Androidzie nie zawsze są plikami na dysku. Na przykład treści przekazywane między użytkownikami są reprezentowane jako content:// Uri. Aby umożliwić przetwarzanie różnych źródeł danych, interfejsy API, które akceptują obiekty File, powinny też akceptować obiekty InputStream, OutputStream lub oba te typy.

public void setDataSource(File file)
public void setDataSource(InputStream stream)

Przyjmowanie i zwracanie typów prostych zamiast wersji opakowanych

Jeśli chcesz przekazać informacje o brakujących lub zerowych wartościach, użyj -1, Integer.MAX_VALUE lub Integer.MIN_VALUE.

public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)

Unikanie odpowiedników klas typów prostych pozwala uniknąć narzutu pamięci tych klas, dostępu do wartości metod i, co ważniejsze, automatycznego pakowania, które wynika z rzutowania między typami prostymi a obiektowymi. Unikanie tych zachowań oszczędza pamięć i tymczasowe przydziały, które mogą prowadzić do kosztownych i częstszych odśmiecania pamięci.

Używaj adnotacji, aby wyjaśnić prawidłowe parametry i wartości zwracane.

Dodaliśmy adnotacje dla deweloperów, aby wyjaśnić dopuszczalne wartości w różnych sytuacjach. Ułatwia to narzędziom pomaganie deweloperom w przypadku podania nieprawidłowych wartości (np. przekazania dowolnego znaku int, gdy platforma wymaga jednej z określonego zestawu stałych wartości). W razie potrzeby używaj dowolnych z tych adnotacji:

Dopuszczalność wartości null

W przypadku interfejsów Java API wymagane są jawne adnotacje dotyczące możliwości przyjmowania wartości null, ale koncepcja możliwości przyjmowania wartości null jest częścią języka Kotlin i adnotacje dotyczące możliwości przyjmowania wartości null nie powinny być nigdy używane w interfejsach Kotlin API.

@Nullable: oznacza, że dana wartość zwracana, parametr lub pole może mieć wartość null:

@Nullable
public String getName()

public void setName(@Nullable String name)

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

@NonNull
public String getName()

public void setName(@NonNull String name)

W przypadku dokumentacji platformy Android dodanie adnotacji do parametrów metody spowoduje automatyczne wygenerowanie dokumentacji w formie „Ta wartość może mieć wartość null”, chyba że „null” zostanie wyraźnie użyte w innym miejscu dokumentu parametru.

Istniejące metody „nie do końca dopuszczające wartość null”: istniejące metody w interfejsie API bez zadeklarowanej adnotacji @Nullable mogą mieć adnotację @Nullable, jeśli metoda może zwracać wartość null w określonych, oczywistych okolicznościach (np. findViewById()). Dla deweloperów, którzy nie chcą sprawdzać wartości null, należy dodać metody towarzyszące @NotNull requireFoo(), które zgłaszają wyjątek IllegalArgumentException.

Metody interfejsu: nowe interfejsy API powinny dodawać odpowiednią adnotację podczas implementowania metod interfejsu, np. Parcelable.writeToParcel() (tzn. metoda w klasie implementującej powinna być writeToParcel(@NonNull Parcel, int), a nie writeToParcel(Parcel, int)); istniejące interfejsy API, w których brakuje adnotacji, nie muszą być „naprawiane”.

Egzekwowanie dopuszczalności wartości null

W języku Java zaleca się stosowanie metod do sprawdzania poprawności danych wejściowych w przypadku parametrów @NonNull za pomocą Objects.requireNonNull() i zgłaszanie wyjątku NullPointerException, gdy parametry mają wartość null. Jest to automatycznie wykonywane w Kotlinie.

Materiały

Identyfikatory zasobów: parametry całkowite, które oznaczają identyfikatory określonych zasobów, powinny być oznaczone odpowiednią definicją typu zasobu. Adnotacja jest dostępna dla każdego typu zasobu, np. @StringRes, @ColorRes@AnimRes, a także dla typu catch-all @AnyRes. Przykład:

public void setTitle(@StringRes int resId)

@IntDef dla zbiorów stałych

Stałe magiczne: parametry Stringint, które mają przyjmować jedną z skończonego zbioru możliwych wartości oznaczonych stałymi publicznymi, powinny być odpowiednio oznaczone za pomocą @StringDef lub @IntDef. Te adnotacje umożliwiają tworzenie nowych adnotacji, które działają 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);

Metody zalecane do sprawdzania prawidłowości adnotowanych parametrów i zgłaszania wyjątku IllegalArgumentException, jeśli parametr nie jest częścią @IntDef.

@IntDef dla flag bitowych

Adnotacja może też określać, że stałe są flagami, i może być łączona ze znakami & i I:

/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
  FLAG_USE_LOGO,
  FLAG_SHOW_HOME,
  FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

@StringDef dla zestawów stałych ciągów tekstowych

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

@SdkConstant dla stałych pakietu SDK

@SdkConstant Dodaj adnotację do pól publicznych, jeśli mają jedną z tych wartości: SdkConstantACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE.

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

Zapewnianie zgodnej możliwości przyjmowania wartości null w przypadku zastąpień

W przypadku zgodności interfejsu API dopuszczalność wartości null w przypadku zastąpień powinna być zgodna z bieżącą dopuszczalnością wartości null w przypadku elementu nadrzędnego. W tabeli poniżej znajdziesz oczekiwania dotyczące zgodności. Oczywiście zastąpienia powinny być co najmniej tak restrykcyjne jak element, który zastępują.

Typ Rodzic Dziecko
Typ zwracanej wartości Nieoznaczone Nieoznaczone lub niepuste
Typ zwracanej wartości Dopuszczalna wartość null Dopuszczanie wartości null lub nie
Typ zwracanej wartości Nonnull Nonnull
Zabawny argument Nieoznaczone Bez adnotacji lub z możliwością wartości null
Zabawny argument Dopuszczalna wartość null Dopuszczalna wartość null
Zabawny argument Nonnull Dopuszczanie wartości null lub nie

Tam, gdzie to możliwe, używaj argumentów, które nie mogą mieć wartości null (np. @NonNull).

Jeśli metody są przeciążone, preferuj, aby wszystkie argumenty były niepuste.

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

Ta reguła dotyczy też przeciążonych setterów właściwości. Główny argument nie powinien mieć wartości null, a czyszczenie właściwości powinno być realizowane jako osobna metoda. Zapobiega to „bezsensownym” wywołaniom, w przypadku których deweloper musi ustawić parametry końcowe, mimo że nie są one wymagane.

public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)

// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()

W przypadku kontenerów preferuj typy zwracane, które nie dopuszczają wartości null (np. @NonNull).

W przypadku typów kontenerów takich jak Bundle lub Collection zwróć pusty kontener, a w stosownych przypadkach – niezmienny. W przypadkach, w których do rozróżnienia dostępności kontenera używana jest wartość null, rozważ udostępnienie osobnej metody logicznej.

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

Adnotacje dotyczące możliwości przyjmowania wartości null dla par get i set muszą być zgodne

Pary metod pobierania i ustawiania pojedynczej właściwości logicznej powinny zawsze być zgodne w adnotacjach dotyczących możliwości przyjmowania wartości null. Niezastosowanie się do tej wskazówki spowoduje naruszenie składni właściwości w języku Kotlin, a dodanie niezgodnych adnotacji o możliwości przyjmowania wartości null do istniejących metod właściwości spowoduje zmianę powodującą niezgodność kodu źródłowego dla użytkowników języka Kotlin.

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

Wartość zwracana w przypadku niepowodzenia lub błędu

Wszystkie interfejsy API powinny umożliwiać aplikacjom reagowanie na błędy. Zwracanie wartości false, -1, null lub innych ogólnych wartości typu „coś poszło nie tak” nie dostarcza programiście wystarczających informacji o błędzie, aby mógł on spełnić oczekiwania użytkowników lub dokładnie śledzić niezawodność aplikacji w terenie. Podczas projektowania interfejsu API wyobraź sobie, że tworzysz aplikację. Jeśli wystąpi błąd, czy interfejs API dostarczy Ci wystarczająco dużo informacji, aby wyświetlić je użytkownikowi lub odpowiednio zareagować?

  1. W komunikacie o wyjątku można (a nawet należy) umieszczać szczegółowe informacje, ale programiści nie powinni musieć 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 zapewnia elastyczność, która pozwoli Ci w przyszłości wprowadzać nowe typy błędów. W przypadku @IntDef oznacza to uwzględnienie wartości OTHER lub UNKNOWN. Podczas zwracania nowego kodu możesz sprawdzić targetSdkVersion wywołującego, aby uniknąć zwracania kodu błędu, którego aplikacja nie zna. W przypadku wyjątków używaj wspólnej klasy nadrzędnej, którą implementują wyjątki, aby każdy kod obsługujący ten typ mógł też przechwytywać i obsługiwać podtypy.
  3. Deweloper nie powinien mieć możliwości przypadkowego zignorowania błędu. Jeśli błąd jest zgłaszany przez zwrócenie wartości, oznacz metodę adnotacją @CheckResult.

W przypadku osiągnięcia stanu błędu lub niepowodzenia z powodu błędu popełnionego przez programistę, np. zignorowania ograniczeń dotyczących parametrów wejściowych lub niepowodzenia w sprawdzeniu stanu obserwowalnego, zalecamy zgłaszanie wyjątku ? extends RuntimeException.

Metody ustawiające lub działania (np. perform) mogą zwracać kod stanu w postaci liczby całkowitej, jeśli działanie może się nie powieść z powodu stanu aktualizowanego asynchronicznie lub warunków niezależnych od dewelopera.

Kody stanu powinny być zdefiniowane w klasie zawierającej jako pola public static final z prefiksem ERROR_ i wyliczone w adnotacji @hide @IntDef.

Nazwy metod powinny zawsze zaczynać się od czasownika, a nie od podmiotu.

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

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

Preferuj typy Collection zamiast tablic jako typ zwracany lub typ parametru

Interfejsy kolekcji o ogólnym typie mają kilka zalet w porównaniu z tablicami, w tym bardziej rygorystyczne umowy API dotyczące unikalności i kolejności, obsługę typów ogólnych oraz wiele wygodnych metod dla programistów.

Wyjątek w przypadku typów prostych

Jeśli elementy są typami prostymi, używaj raczej tablic, aby uniknąć kosztów automatycznego pakowania. Zobacz Zamiast wersji opakowanych używaj pierwotnych typów danych

Wyjątek dotyczący kodu wrażliwego na wydajność

W określonych scenariuszach, w których interfejs API jest używany w kodzie wrażliwym na wydajność (np. w przypadku grafiki lub innych interfejsów API do pomiarów, układu lub rysowania), dopuszczalne jest używanie tablic zamiast kolekcji w celu zmniejszenia liczby alokacji i zmian w pamięci.

Wyjątek dla języka Kotlin

Tablice w Kotlinie są niezmienne, a język Kotlin udostępnia wiele interfejsów API do obsługi tablic, więc tablice są równoważne z ListCollection w przypadku interfejsów API w Kotlinie, które mają być dostępne z tego języka.

Preferowanie kolekcji @NonNull

W przypadku obiektów kolekcji zawsze preferuj @NonNull. Gdy zwracasz pustą kolekcję, użyj odpowiedniej metody Collections.empty, aby zwrócić niedrogi, prawidłowo określony i niezmienny obiekt kolekcji.

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

W przypadku tablic zamiast kolekcji używaj też @NonNull (patrz poprzedni punkt). Jeśli alokacja obiektów jest problemem, utwórz stałą i przekaż ją dalej – w końcu pusta tablica jest niezmienna. Przykład:

private static final int[] EMPTY_USER_IDS = new int[0];

@NonNull
public int[] getUserIds() {
  int [] userIds = mService.getUserIds();
  return userIds != null ? userIds : EMPTY_USER_IDS;
}

Zmienność kolekcji

Interfejsy API w Kotlinie powinny domyślnie zwracać kolekcje tylko do odczytu (nie Mutable) chyba że umowa interfejsu API wymaga zwracania typu modyfikowalnego.

Interfejsy API w języku Java powinny jednak domyślnie zwracać zmienne typy, ponieważ implementacja interfejsów API w języku Java na platformie Android nie zapewnia jeszcze wygodnej implementacji kolekcji niezmiennych. Wyjątek od tej reguły stanowiąCollections.empty typy zwracane, które są niezmienne. W przypadkach, w których klienci mogą wykorzystać zmienność – celowo lub przez pomyłkę – do naruszenia zamierzonego wzorca użycia interfejsu API, interfejsy API w Javie powinny zdecydowanie rozważyć zwracanie płytkiej kopii kolekcji.

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

Typy zwracane, które można modyfikować

Interfejsy API, które zwracają kolekcje, nie powinny modyfikować zwróconego obiektu kolekcji po jego zwróceniu. Jeśli zwrócona kolekcja musi zostać zmieniona lub w jakiś sposób ponownie użyta – np. dostosowany widok modyfikowalnego zbioru danych – należy wyraźnie udokumentować dokładne zachowanie w momencie, gdy zawartość może ulec zmianie, lub zastosować ustalone konwencje nazewnictwa interfejsu API.

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

Konwencja języka Kotlin .asFoo() jest opisana poniżej i umożliwia zmianę kolekcji zwracanej przez .asList(), jeśli zmieni się oryginalna kolekcja.

Zmienność zwracanych obiektów typu danych

Podobnie jak w przypadku interfejsów API zwracających kolekcje, interfejsy API zwracające obiekty typu danych nie powinny modyfikować właściwości zwróconego obiektu po jego zwróceniu.

val tempResult = DataContainer()

fun add(other: DataContainer): DataContainer {
  tempResult.innerValue = innerValue + other.innerValue
  return tempResult
}
fun add(other: DataContainer): DataContainer {
  return DataContainer(innerValue + other.innerValue)
}

wyjątkowo rzadkich przypadkach niektóre fragmenty kodu, w których wydajność ma kluczowe znaczenie, mogą zyskać na puli obiektów lub ponownym użyciu. Nie pisz własnej struktury danych puli obiektów i nie udostępniaj ponownie używanych obiektów w publicznych interfejsach API. W obu przypadkach zachowaj szczególną ostrożność podczas zarządzania jednoczesnym dostępem.

Używanie typu parametru vararg

Zarówno interfejsy API w języku Kotlin, jak i interfejsy API w języku Java powinny używać adnotacji vararg w przypadkach, gdy deweloper prawdopodobnie utworzy tablicę w miejscu wywołania wyłącznie w celu przekazania wielu powiązanych parametrów tego samego typu.

public void setFeatures(Feature[] features) { ... }

// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }

// Developer code
setFeatures(Features.A, Features.B, Features.C);

Kopie obronne

Implementacje parametrów vararg w Javie i Kotlinie są kompilowane do tego samego kodu bajtowego opartego na tablicy, dlatego można je wywoływać z kodu Java za pomocą tablicy modyfikowalnej. Projektanci interfejsów API są zdecydowanie zachęcani do tworzenia defensywnej, płytkiej kopii parametru tablicy w przypadkach, gdy będzie on zapisywany w polu lub anonimowej klasie wewnętrznej.

public void setValues(SomeObject... values) {
   this.values = Arrays.copyOf(values, values.length);
}

Pamiętaj, że utworzenie kopii obronnej nie zapewnia ochrony przed jednoczesną modyfikacją między początkowym wywołaniem metody a utworzeniem kopii ani nie chroni przed mutacją obiektów zawartych w tablicy.

Podawanie prawidłowej semantyki za pomocą parametrów typu kolekcji lub zwracanych typów

List<Foo> to opcja domyślna, ale rozważ inne typy, aby dodać dodatkowe znaczenie:

  • Użyj Set<Foo>, jeśli kolejność elementów nie ma znaczenia dla interfejsu API i nie zezwala on na duplikaty lub duplikaty nie mają znaczenia.

  • Collection<Foo>, – jeśli interfejs API nie uwzględnia kolejności i umożliwia duplikaty.

Funkcje konwersji w języku Kotlin

W języku Kotlin często używa się operatorów .toFoo().asFoo(), aby uzyskać obiekt innego typu z istniejącego obiektu, gdzie Foo to nazwa typu zwracanego konwersji. Jest to zgodne z dobrze znanym interfejsem JDKObject.toString(). Kotlin idzie o krok dalej i używa go do konwersji typów prostych, takich jak 25.toFloat().

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

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

Podobnie jak w przypadku funkcji .toString(), konwersja „to” zwraca nowy, niezależny obiekt. Jeśli oryginalny obiekt zostanie później zmodyfikowany, nowy obiekt nie będzie odzwierciedlać tych zmian. Podobnie, jeśli nowy obiekt zostanie później zmodyfikowany, stary obiekt nie będzie odzwierciedlać tych zmian.

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

Używaj metody .asFoo() podczas tworzenia opakowania zależnego, udekorowanego obiektu lub rzutowania

Rzutowanie w języku Kotlin odbywa się za pomocą słowa kluczowego as. Odzwierciedla zmianę interfejsu, ale nie tożsamości. Gdy jest używany jako prefiks w funkcji rozszerzającej, .asFoo() dekoruje odbiorcę. Zmiana w obiekcie pierwotnego odbiorcy zostanie odzwierciedlona w obiekcie zwróconym przez asFoo(). Zmiana w nowym obiekcie Foo może zostać odzwierciedlona w obiekcie pierwotnym.

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

Funkcje konwersji powinny być zapisywane jako funkcje rozszerzające.

Zapisywanie funkcji konwersji poza definicjami klasy odbiornika i klasy wyniku zmniejsza powiązanie między typami. Idealna konwersja wymaga tylko dostępu do publicznego interfejsu API oryginalnego obiektu. Pokazuje to na przykładzie, że deweloper może też tworzyć analogiczne konwersje do własnych preferowanych typów.

Zgłaszaj odpowiednie wyjątki

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

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

Detektory i wywołania zwrotne

Oto zasady dotyczące klas i metod używanych w przypadku mechanizmów nasłuchiwania i wywoływania zwrotnego.

Nazwy klas wywołania zwrotnego powinny być w liczbie pojedynczej

Zamiast niego używaj zasady MyObjectCallback.MyObjectCallbacks

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

onFooEvent oznacza, że FooEvent ma miejsce i że wywołanie zwrotne powinno zareagować.

Czas przeszły i teraźniejszy powinny opisywać zachowanie czasowe

Nazwy metod wywołania zwrotnego dotyczących zdarzeń powinny wskazywać, czy zdarzenie już nastąpiło, czy jest w trakcie realizacji.

Jeśli na przykład metoda jest wywoływana po wykonaniu czynności kliknięcia:

public void onClicked()

Jeśli jednak metoda jest odpowiedzialna za wykonanie działania kliknięcia:

public boolean onClick()

Rejestracja wywołania zwrotnego

Gdy do obiektu można dodać lub z niego usunąć odbiornik lub wywołanie zwrotne, powiązane metody powinny mieć nazwy add i remove lub register i unregister. Zachowaj spójność z dotychczasową konwencją stosowaną w klasie lub w innych klasach w tym samym pakiecie. Jeśli nie ma takiego precedensu, preferuj dodawanie i usuwanie.

Metody związane z rejestrowaniem lub wyrejestrowywaniem wywołań zwrotnych powinny określać pełną nazwę typu wywołania zwrotnego.

public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);

Unikaj metod pobierających w przypadku wywołań zwrotnych

Nie dodawaj metod getFooCallback(). Jest to kuszące rozwiązanie w przypadku, gdy deweloperzy chcą połączyć istniejące wywołanie zwrotne z własnym zamiennikiem, ale jest ono niestabilne i utrudnia deweloperom komponentów określenie bieżącego stanu. Na przykład

  • Deweloper A dzwoni do setFooCallback(a)
  • Deweloper B dzwoni do setFooCallback(new B(getFooCallback()))
  • Deweloper A chce usunąć wywołanie zwrotne a, ale nie może tego zrobić, ponieważ nie zna typu B, a B nie zostało zaprojektowane tak, aby umożliwiać takie modyfikacje opakowanego wywołania zwrotnego.

Akceptowanie obiektu Executor do sterowania wysyłaniem wywołań zwrotnych

Podczas rejestrowania wywołań zwrotnych, które nie mają wyraźnych oczekiwań dotyczących wątków (praktycznie wszędzie poza zestawem narzędzi interfejsu), zdecydowanie zaleca się uwzględnienie parametru Executor w ramach rejestracji, aby umożliwić programiście określenie wątku, w którym będą wywoływane wywołania zwrotne.

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

W wyjątku od naszych zwykłych wytycznych dotyczących parametrów opcjonalnych dopuszczalne jest podanie przeciążenia z pominięciem parametru Executor, mimo że nie jest on ostatnim argumentem na liście parametrów. Jeśli nie podano wartości Executor, wywołanie zwrotne powinno być wywoływane w głównym wątku za pomocą Looper.getMainLooper(), a informacja o tym powinna być udokumentowana w powiązanej przeciążonej metodzie.

/**
 * ...
 * Note that the callback will be executed on the main thread using
 * {@link Looper.getMainLooper()}. To specify the execution thread, use
 * {@link registerFooCallback(Executor, FooCallback)}.
 * ...
 */
public void registerFooCallback(
    @NonNull FooCallback callback)

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

Executor Pułapki implementacji: pamiętaj, że poniższy kod jest prawidłowym wykonawcą:

public class SynchronousExecutor implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

Oznacza to, że podczas implementowania interfejsów API, które przyjmują taką formę, implementacja przychodzącego obiektu Binder po stronie procesu aplikacji musi wywołać Binder.clearCallingIdentity() przed wywołaniem wywołania zwrotnego aplikacji w Executor dostarczonym przez aplikację. Dzięki temu każdy kod aplikacji, który używa tożsamości powiązania (np. Binder.getCallingUid()) do sprawdzania uprawnień, prawidłowo przypisuje uruchomiony kod do aplikacji, a nie do procesu systemowego wywołującego aplikację. Jeśli użytkownicy interfejsu API chcą uzyskać informacje o identyfikatorze UID lub PID wywołującego, powinny one być wyraźnie częścią interfejsu API, a nie wynikać z miejsca, w którym uruchomiono dostarczony przez nich kod Executor.

Określanie Executor powinno być obsługiwane przez interfejs API. W przypadku aplikacji, w których wydajność ma kluczowe znaczenie, kod może być wykonywany natychmiast lub synchronicznie z informacjami zwrotnymi z interfejsu API. Zaakceptowanie Executor na to zezwala. Obronne tworzenie dodatkowego HandlerThread lub podobnego do trampoliny z  niweczy ten pożądany przypadek użycia.

Jeśli aplikacja ma uruchamiać kosztowny kod w ramach własnego procesu, pozwól jej na to. Obejścia, które deweloperzy aplikacji znajdą, aby pokonać Twoje ograniczenia, będą znacznie trudniejsze do obsługi w dłuższej perspektywie.

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

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

public void clearFooCallback()

Używanie interfejsu Executor zamiast interfejsu Handler

W przeszłości Handler w Androidzie było używane jako standard przekierowywania wykonania wywołania zwrotnego do konkretnego wątku Looper. Zmieniliśmy ten standard na prefer Executor, ponieważ większość deweloperów aplikacji zarządza własnymi pulami wątków, co sprawia, że główny wątek lub wątek interfejsu użytkownika jest jedynym wątkiem Looper dostępnym dla aplikacji. Użyj Executor, aby dać deweloperom kontrolę potrzebną do ponownego wykorzystania istniejących lub preferowanych kontekstów wykonania.

Nowoczesne biblioteki współbieżności, takie jak kotlinx.coroutines czy RxJava, udostępniają własne mechanizmy planowania, które w razie potrzeby wykonują własne wysyłanie. Dlatego ważne jest, aby umożliwić korzystanie z bezpośredniego wykonawcy (np. Runnable::run), aby uniknąć opóźnień wynikających z podwójnego przeskoku wątków. Na przykład jeden przeskok do opublikowania posta w wątku Looper za pomocą Handler, a następnie kolejny przeskok z ram aplikacji do obsługi współbieżności.

Wyjątki od tej wytycznej są rzadkie. Typowe odwołania dotyczące wyjątku:

Muszę użyć Looper, ponieważ potrzebuję Looper, aby epoll na wydarzenie. Ta prośba o wyjątek została zaakceptowana, ponieważ w tej sytuacji nie można skorzystać z Executor.

Nie chcę, aby kod aplikacji blokował wątek publikujący zdarzenie. Prośba o wyjątek nie jest zwykle rozpatrywana pozytywnie w przypadku kodu, który jest uruchamiany w procesie aplikacji. Aplikacje, które nieprawidłowo korzystają z tej funkcji, szkodzą tylko sobie, a nie ogólnej kondycji systemu. Aplikacje, które działają prawidłowo lub korzystają z popularnych platform współbieżności, nie powinny ponosić dodatkowych kar za opóźnienia.

Handler jest lokalnie spójny z innymi podobnymi interfejsami API w tej samej klasie. Prośba o wyjątek jest rozpatrywana w zależności od sytuacji. Preferowane jest dodawanie przeciążeń opartych na Executor, a wdrożenia Handler są migrowane w celu korzystania z nowej implementacji Executor. (myHandler::post jest prawidłowym znakiem Executor!). W zależności od rozmiaru klasy, liczby istniejących metod Handler i prawdopodobieństwa, że programiści będą musieli używać istniejących metod opartych na Handler wraz z nową metodą, można przyznać wyjątek, aby dodać nową metodę opartą na Handler.

Symetria w rejestracji

Jeśli można coś dodać lub zarejestrować, powinna też istnieć możliwość usunięcia lub wyrejestrowania. Metoda

registerThing(Thing)

powinien mieć pasujący

unregisterThing(Thing)

Podaj identyfikator żądania

Jeśli programista może ponownie wykorzystać wywołanie zwrotne, podaj obiekt identyfikatora, aby powiązać wywołanie zwrotne z żądaniem.

class RequestParameters {
  public int getId() { ... }
}

class RequestExecutor {
  public void executeRequest(
    RequestParameters parameters,
    Consumer<RequestParameters> onRequestCompletedListener) { ... }
}

Obiekty wywołania zwrotnego z wieloma metodami

W przypadku wywołań zwrotnych z wieloma metodami należy preferować metody interface i używać metod default podczas dodawania do wcześniej opublikowanych interfejsów. Wcześniej te wytyczne zalecały używanie abstract class ze względu na brak metod default w języku Java 7.

public interface MostlyOptionalCallback {
  void onImportantAction();
  default void onOptionalInformation() {
    // Empty stub, this method is optional.
  }
}

Używanie android.os.OutcomeReceiver podczas modelowania wywołania funkcji nieblokującej

OutcomeReceiver<R,E> zwraca wartość wyniku R w przypadku powodzenia lub E : Throwable w przeciwnym razie – tak samo jak zwykłe wywołanie metody. Użyj OutcomeReceiver jako typu wywołania zwrotnego podczas przekształcania metody blokującej, która zwraca wynik lub zgłasza wyjątek, w nieblokującą metodę asynchroniczną:

interface FooType {
  // Before:
  public FooResult requestFoo(FooRequest request);

  // After:
  public void requestFooAsync(FooRequest request, Executor executor,
      OutcomeReceiver<FooResult, Throwable> callback);
}

Metody asynchroniczne przekonwertowane w ten sposób zawsze zwracają wartość void. Każdy wynik, któryrequestFoo zostałby zwrócony, jest zamiast tego przekazywany do parametru requestFooAsynccallbackOutcomeReceiver.onResult przez wywołanie go w podanym executor. Każdy wyjątek, który zostałby zgłoszony przez metodę requestFoo, jest zamiast tego zgłaszany do metody OutcomeReceiver.onError w ten sam sposób.

Używanie OutcomeReceiver do raportowania wyników metody asynchronicznej zapewnia też otoczkę Kotlinsuspend fun dla metod asynchronicznych korzystających z rozszerzenia Continuation.asOutcomeReceiverandroidx.core:core-ktx:

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

Takie rozszerzenia umożliwiają klientom Kotlin wywoływanie nieblokujących metod asynchronicznych w wygodny sposób, jak zwykłe wywołanie funkcji, bez blokowania wątku wywołującego. Te rozszerzenia 1:1 interfejsów API platformy mogą być oferowane jako część artefaktu androidx.core:core-ktx w Jetpacku w połączeniu ze standardowymi kontrolami i wskazówkami dotyczącymi zgodności wersji. Więcej informacji, kwestie związane z anulowaniem i przykłady znajdziesz w dokumentacji funkcji asOutcomeReceiver.

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

Preferuj interfejsy funkcyjne zamiast tworzenia nowych typów pojedynczych metod abstrakcyjnych (SAM)

W interfejsie API na poziomie 24 dodano typy java.util.function.* (dokumentacja) oferujące ogólne interfejsy SAM, takie jak Consumer<T>, które można stosować jako wywołania zwrotne lambda. W wielu przypadkach tworzenie nowych interfejsów SAM nie przynosi korzyści w zakresie bezpieczeństwa typów ani przekazywania intencji, a niepotrzebnie zwiększa obszar interfejsu API Androida.

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

Umieszczanie parametrów SAM

Parametry SAM powinny być umieszczane na końcu, aby umożliwić idiomatyczne użycie w języku Kotlin, nawet jeśli metoda jest przeciążona dodatkowymi parametrami.

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Dokumenty

Są to zasady dotyczące publicznej dokumentacji (Javadoc) interfejsów API.

Wszystkie publiczne interfejsy API muszą być udokumentowane

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

Metody

Parametry metody i wartości zwracane muszą być udokumentowane za pomocą adnotacji @param@return. Sformatuj treść Javadoc tak, jakby poprzedzało ją zdanie „This method...”.

W przypadku metod, które nie przyjmują parametrów, nie mają specjalnych wymagań i zwracają to, co sugeruje ich nazwa, 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 dotyczących powiązanych stałych, metod i innych elementów. Używaj tagów Javadoc (np. @see{@link foo}), a nie zwykłych słów.

W przypadku tego przykładu:

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

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

/**
 * Sets value to one of FOO or <code>BAR</code>.
 *
 * @param value the value being set, one of FOO or BAR
 */
public void setValue(int value) { ... }

Zamiast tego używaj linków:

/**
 * Sets value to one of {@link #FOO} or {@link #BAR}.
 *
 * @param value the value being set
 */
public void setValue(@ValueType int value) { ... }

Pamiętaj, że użycie adnotacji IntDef, np. @ValueType, w przypadku parametru automatycznie generuje dokumentację określającą dozwolone typy. Więcej informacji o IntDef znajdziesz w wytycznych dotyczących adnotacji.

Podczas dodawania dokumentacji Javadoc uruchom polecenie update-api lub docs.

Ta reguła jest szczególnie ważna podczas dodawania tagów @link lub @see. Upewnij się, że wynik wygląda zgodnie z oczekiwaniami. Błąd w danych wyjściowych Javadoc jest często spowodowany nieprawidłowymi linkami. Sprawdzanie wykonuje cel update-api lub docs, ale cel docs może być szybszy, jeśli zmieniasz tylko dokumentację Javadoc i nie musisz uruchamiać celu update-api.

Używaj {@code foo}, aby odróżnić wartości w języku Java

Wartości Java, takie jak true, falsenull, należy umieszczać w nawiasach {@code...}, aby odróżnić je od tekstu dokumentacji.

Podczas pisania dokumentacji w kodzie źródłowym Kotlin możesz umieszczać kod w odwrotnych apostrofach, tak jak w przypadku Markdown.

Podsumowania @param i @return powinny być pojedynczym fragmentem zdania

Podsumowania parametrów i wartości zwracanych powinny zaczynać się od małej litery i zawierać tylko fragment jednego zdania. Jeśli masz dodatkowe informacje, które wykraczają poza jedno zdanie, przenieś je do treści dokumentacji Javadoc metody:

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

należy zmienić na:

/**
 * @param e element to be appended to this list, must be non-{@code null}
 * @return {@code true} on success, {@code false} otherwise
 */

Adnotacje w Dokumentach wymagają wyjaśnienia

Wyjaśnij, dlaczego adnotacje @hide@removed są ukryte w publicznym interfejsie API. Dołącz instrukcje dotyczące zastępowania elementów interfejsu API oznaczonych adnotacją @deprecated.

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

Jeśli metoda zgłasza wyjątek kontrolowany, np. IOException, udokumentuj go za pomocą adnotacji @throws. W przypadku interfejsów API pochodzących z języka Kotlin, które są przeznaczone do użycia przez klientów Java, oznacz funkcje adnotacją @Throws.

Jeśli metoda zgłasza nieobsługiwany wyjątek wskazujący na błąd, któremu można zapobiec, np. IllegalArgumentException lub IllegalStateException, udokumentuj wyjątek, wyjaśniając, dlaczego jest zgłaszany. Wyrzucony wyjątek powinien też wskazywać, dlaczego został wyrzucony.

Niektóre przypadki nieobsłużonych wyjątków są uważane za domyślne i nie wymagają dokumentowania, np. NullPointerException lub IllegalArgumentException, gdy argument nie pasuje do adnotacji @IntDef lub podobnej, która osadza kontrakt API w sygnaturze metody:

/**
 * ...
 * @throws IOException If it cannot find the schema for {@code toVersion}
 * @throws IllegalStateException If the schema validation fails
 */
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
    boolean validateDroppedTables, Migration... migrations) throws IOException {
  // ...
  if (!dbPath.exists()) {
    throw new IllegalStateException("Cannot find the database file for " + name
        + ". Before calling runMigrations, you must first create the database "
        + "using createDatabase.");
  }
  // ...

Lub w języku Kotlin:

/**
 * ...
 * @throws IOException If something goes wrong reading the file, such as a bad
 *                     database header or missing permissions
 */
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
  // ...
  val read = input.read(buffer)
    if (read != 4) {
      throw IOException("Bad database header, unable to read 4 bytes at " +
          "offset 60")
    }
  }
  // ...

Jeśli metoda wywołuje kod asynchroniczny, który może zgłaszać wyjątki, zastanów się, w jaki sposób deweloper dowie się o takich wyjątkach i jak na nie zareaguje. Zazwyczaj polega to na przekazaniu wyjątku do wywołania zwrotnego i udokumentowaniu wyjątków zgłoszonych w metodzie, która je odbiera. Wyjątków asynchronicznych nie należy dokumentować za pomocą adnotacji @throws, chyba że są one ponownie zgłaszane z metody z adnotacjami.

Pierwsze zdanie dokumentów kończ kropką

Narzędzie Doclava analizuje dokumenty w prosty sposób, kończąc dokument z podsumowaniem (pierwsze zdanie używane w krótkim opisie u góry dokumentów klasy) po napotkaniu kropki (.) i spacji. Powoduje to 2 problemy:

  • Jeśli krótki dokument nie kończy się kropką, a członek zespołu ma odziedziczone dokumenty, które są wykrywane przez narzędzie, to streszczenie również je uwzględnia. Na przykład w R.attr dokumentach znajdziesz actionBarTabStyle, gdzie do streszczenia dodano opis wymiaru.
  • Z tego samego powodu unikaj w pierwszym zdaniu skrótu „np.”, ponieważ Doclava kończy dokumenty z podsumowaniem po literze „g”. Na przykład zobacz TEXT_ALIGNMENT_CENTERView.java. Pamiętaj, że Metalava automatycznie poprawia ten błąd, wstawiając po kropce nierozdzielającą spację. Nie popełniaj jednak tego błędu.

Formatowanie dokumentów do renderowania w HTML

Dokumentacja Javadoc jest renderowana w HTML-u, więc sformatuj ją odpowiednio:

  • Podziały wierszy powinny używać jawnego tagu <p>. Nie dodawaj zamykającego tagu </p>.

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

  • Listy nieuporządkowane powinny używać znacznika <ul>, a uporządkowane – znacznika <ol>. Każdy element powinien zaczynać się od tagu <li>, ale nie musi mieć tagu zamykającego </li>. Po ostatnim elemencie wymagany jest tag zamykający </ul> lub </ol>.

  • W tabelach należy używać tagów <table><tr> dla wierszy, <th> dla nagłówków i <td> dla komórek. Wszystkie tagi tabeli wymagają pasujących tagów zamykających. Aby oznaczyć tag jako wycofany, możesz użyć adnotacji class="deprecated".

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

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

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

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

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

Postępuj zgodnie z przewodnikiem po stylu dokumentacji API

Aby zachować spójność stylu w przypadku podsumowań klas, opisów metod, opisów parametrów i innych elementów, postępuj zgodnie z zaleceniami zawartymi w oficjalnych wytycznych dotyczących języka Java, które znajdziesz na stronie How to Write Doc Comments for the Javadoc Tool (Jak pisać komentarze do dokumentacji dla narzędzia Javadoc).

Reguły dotyczące platformy Android

Te reguły dotyczą interfejsów API, wzorców i struktur danych, które są specyficzne dla interfejsów API i zachowań wbudowanych w platformę Android (np. Bundle lub Parcelable).

Kreatory intencji powinni używać wzorca create*Intent()

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

Używaj pakietu zamiast tworzyć nowe struktury danych ogólnego przeznaczenia

Unikaj tworzenia nowych struktur danych ogólnego przeznaczenia do reprezentowania dowolnych mapowań klucza na wartość o określonym typie. Zamiast tego możesz użyć interfejsu Bundle.

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

Jeśli platforma odczytuje dane, unikaj używania znaku Bundle i stosuj klasę danych o ściśle określonym typie.

Implementacje Parcelable muszą mieć publiczne pole CREATOR

Inflacja obiektów Parcelable jest udostępniana za pomocą CREATOR, a nie konstruktorów surowych. Jeśli klasa implementuje interfejs Parcelable, jej pole CREATOR musi być publicznym interfejsem API, a konstruktor klasy przyjmujący argument Parcel musi być prywatny.

Używanie CharSequence w ciągach interfejsu

Gdy ciąg znaków jest wyświetlany w interfejsie, użyj CharSequence, aby zezwolić na wystąpienia Spannable.

Jeśli jest to tylko klucz lub inna etykieta bądź wartość, która nie jest widoczna dla użytkowników, wystarczy String.

Unikaj korzystania z wyliczeń

IntDef musi być używany zamiast wyliczeń we wszystkich interfejsach API platformy i należy go rozważyć w przypadku interfejsów API bibliotek, które nie są powiązane z platformą. Wyliczeń używaj tylko wtedy, gdy masz pewność, że nie zostaną dodane nowe wartości.

Korzyści zIntDef:

  • Umożliwia dodawanie wartości z upływem czasu.
  • W czasie działania nie są używane żadne klasy ani obiekty, tylko typy proste.
    • R8 lub minifikacja mogą uniknąć tego kosztu w przypadku interfejsów API biblioteki bez pakietu, ale ta optymalizacja nie ma wpływu na klasy interfejsu API platformy.

Zalety typu wyliczeniowego

  • Idiomatyczna funkcja języka Java, Kotlin
  • Umożliwia wyczerpujące przełączanie, whenużycie instrukcji
    • Uwaga – wartości nie mogą się zmieniać z czasem (patrz poprzednia lista).
  • Jasno określony zakres i łatwa do znalezienia nazwa
  • Włącza weryfikację w czasie kompilacji.
    • Na przykład instrukcja when w języku Kotlin, która zwraca wartość
  • Jest to działająca klasa, która może implementować interfejsy, mieć statyczne funkcje pomocnicze, udostępniać metody członkowskie lub rozszerzające oraz udostępniać pola.

Postępowanie zgodnie z hierarchią warstw pakietu na Androida

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

Unikaj odwoływania się do Google, innych firm i ich produktów

Platforma Android to projekt open source, który ma być niezależny od dostawcy. Interfejs API powinien być ogólny i równie przydatny dla integratorów systemów lub aplikacji z wymaganymi uprawnieniami.

Implementacje Parcelable powinny być ostateczne

Klasy Parcelable zdefiniowane przez platformę są zawsze wczytywane z framework.jar, więc aplikacja nie może zastąpić implementacji Parcelable.

Jeśli aplikacja wysyłająca rozszerzy Parcelable, aplikacja odbierająca nie będzie miała niestandardowej implementacji nadawcy do rozpakowania. Uwaga dotycząca zgodności wstecznej: jeśli Twoja klasa historycznie nie była ostateczna, ale nie miała publicznie dostępnego konstruktora, nadal możesz oznaczyć ją jako final.

Metody wywołujące proces systemowy powinny ponownie zgłaszać wyjątek RemoteException jako RuntimeException

RemoteException jest zwykle zgłaszany przez wewnętrzny interfejs AIDL i wskazuje, że proces systemowy został zakończony lub aplikacja próbuje wysłać zbyt dużo danych. W obu przypadkach publiczny interfejs API powinien ponownie zgłosić wyjątek RuntimeException, aby uniemożliwić aplikacjom utrwalanie decyzji dotyczących bezpieczeństwa lub zasad.

Jeśli wiesz, że po drugiej stronie wywołania Binder znajduje się proces systemowy, ten kod standardowy jest najlepszym rozwiązaniem:

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

Zgłaszanie konkretnych wyjątków w przypadku zmian w interfejsie API

Działanie publicznych interfejsów API może się zmieniać na różnych poziomach interfejsu API i powodować awarie aplikacji (np. w celu egzekwowania nowych zasad bezpieczeństwa).

Gdy interfejs API musi zgłosić błąd w przypadku żądania, które wcześniej było prawidłowe, zgłoś nowy, konkretny wyjątek zamiast ogólnego. Na przykład ExportedFlagRequired zamiast SecurityException (i ExportedFlagRequired może się rozciągać na SecurityException).

Pomoże to deweloperom aplikacji i narzędzi wykrywać zmiany w działaniu interfejsu API.

Zaimplementuj konstruktor kopiujący zamiast metody clone

Używanie metody clone() w języku Java jest zdecydowanie odradzane ze względu na brak umów API udostępnianych przez klasę Object i trudności związane z rozszerzaniem klas, które używają metody clone(). Zamiast tego użyj konstruktora kopiującego, który przyjmuje obiekt tego samego typu.

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

Klasy, które do tworzenia instancji używają klasy Builder, powinny dodać konstruktor kopiujący klasy Builder, aby umożliwić modyfikowanie kopii.

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

Używanie ParcelFileDescriptor zamiast FileDescriptor

Obiekt java.io.FileDescriptor ma słabo zdefiniowaną własność, co może prowadzić do niejasnych błędów typu „use-after-close”. Zamiast tego interfejsy API powinny zwracać lub akceptować instancje ParcelFileDescriptor. Starszy kod może w razie potrzeby konwertować PFD na FD i odwrotnie za pomocą funkcji dup() lub getFileDescriptor().

Unikaj używania wartości liczbowych o nieparzystych rozmiarach.

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

Unikaj używania klasy BitSet

java.util.BitSet świetnie nadaje się do implementacji, ale nie do publicznego interfejsu API. Jest to pole modyfikowalne, które wymaga przydzielenia pamięci w przypadku wywołań metod o wysokiej częstotliwości i nie zawiera informacji o znaczeniu poszczególnych bitów.

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

Preferowanie android.net.Uri

android.net.Uri to preferowany sposób hermetyzacji identyfikatorów URI w interfejsach API Androida.

Unikaj java.net.URI, ponieważ jest zbyt rygorystyczny w parsowaniu identyfikatorów URI, i nigdy nie używaj java.net.URL, ponieważ jego definicja równości jest poważnie wadliwa.

Ukrywanie adnotacji oznaczonych jako @IntDef, @LongDef lub @StringDef

Adnotacje oznaczone jako @IntDef, @LongDef lub @StringDef oznaczają zestaw prawidłowych stałych, które można przekazywać do interfejsu API. Jednak gdy są one eksportowane jako interfejsy API, kompilator wstawia stałe w miejscu wywołania, a w szkielecie interfejsu API adnotacji (na platformie) lub w pliku JAR (w przypadku bibliotek) pozostają tylko (teraz bezużyteczne) wartości.

Dlatego użycie tych adnotacji musi być oznaczone adnotacją @hide docs na platformie lub adnotacją @RestrictTo.Scope.LIBRARY) code w bibliotekach. W obu przypadkach muszą być oznaczone jako @Retention(RetentionPolicy.SOURCE), aby nie pojawiały się w szkieletach interfejsu API ani w plikach JAR.

@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

Podczas tworzenia pakietu SDK platformy i biblioteki AAR narzędzie wyodrębnia adnotacje i pakuje je oddzielnie od skompilowanych źródeł. Android Studio odczytuje ten format pakietu i wymusza definicje typów.

Nie dodawaj nowych kluczy dostawcy ustawień

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

Zamiast tego dodaj odpowiedni interfejs API Java do pobierania i ustawiania wartości w odpowiedniej klasie, która jest zwykle klasą „menedżera”. W razie potrzeby dodaj mechanizm nasłuchiwania lub transmisję, aby powiadamiać klientów o zmianach.

SettingsProvider ustawień ma wiele problemów w porównaniu z metodami pobierającymi i ustawiającymi:

  • Brak bezpieczeństwa typów.
  • Nie ma ujednoliconego sposobu podawania wartości domyślnej.
  • Brak możliwości dostosowania uprawnień.
    • Nie można na przykład chronić ustawienia za pomocą niestandardowego uprawnienia.
  • Brak odpowiedniego sposobu na prawidłowe dodanie niestandardowej logiki.
    • Nie można na przykład zmienić wartości ustawienia A w zależności od wartości ustawienia B.

Przykład: Settings.Secure.LOCATION_MODE istnieje od dawna, ale zespół ds. lokalizacji wycofał go na rzecz odpowiedniego interfejsu API Java LocationManager.isLocationEnabled()MODE_CHANGED_ACTION transmisji, co dało zespołowi znacznie większą elastyczność, a semantyka interfejsów API jest teraz znacznie jaśniejsza.

Nie rozszerzaj klas Activity i AsyncTask

AsyncTask to szczegół implementacji. Zamiast tego udostępnij odbiornik lub w przypadku biblioteki androidx interfejs ListenableFuture API.

Nie można utworzyć podklas Activity. Rozszerzenie aktywności w przypadku funkcji powoduje, że staje się ona niezgodna z innymi funkcjami, które wymagają od użytkowników tego samego działania. Zamiast tego polegaj na kompozycji, używając narzędzi takich jak LifecycleObserver.

Używanie metody getUser() obiektu Context

Klasy powiązane z Context, np. wszystko, co jest zwracane z Context.getSystemService(), powinny używać użytkownika powiązanego z Context zamiast udostępniać elementy, które są kierowane na konkretnych użytkowników.

class FooManager {
  Context mContext;

  void fooBar() {
    mIFooBar.fooBarForUser(mContext.getUser());
  }
}
class FooManager {
  Context mContext;

  Foobar getFoobar() {
    // Bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBarForUser(Process.myUserHandle());
  }

  Foobar getFoobar() {
    // Also bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBar();
  }

  Foobar getFoobarForUser(UserHandle user) {
    mIFooBar.fooBarForUser(user);
  }
}

Wyjątek: metoda może akceptować argument użytkownika, jeśli akceptuje wartości, które nie reprezentują pojedynczego użytkownika, np. UserHandle.ALL.

Używanie UserHandle zamiast zwykłych liczb całkowitych

UserHandle jest preferowane, aby zapewnić bezpieczeństwo typów i uniknąć mylenia identyfikatorów użytkowników z identyfikatorami UID.

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

W przypadku, gdy jest to nieuniknione, element int reprezentujący identyfikator użytkownika musi być oznaczony adnotacją @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

Preferuj detektory lub wywołania zwrotne zamiast intencji rozgłaszania

Intencje rozgłaszania są bardzo przydatne, ale mogą powodować nieoczekiwane zachowania, które negatywnie wpływają na stan systemu. Dlatego nowe intencje rozgłaszania należy dodawać z rozwagą.

Oto niektóre konkretne obawy, które sprawiają, że odradzamy wprowadzanie nowych intencji transmisji:

  • Wysyłanie transmisji bez flagi FLAG_RECEIVER_REGISTERED_ONLY powoduje wymuszone uruchomienie wszystkich aplikacji, które nie są jeszcze uruchomione. Chociaż czasami jest to zamierzony efekt, może prowadzić do jednoczesnego uruchamiania dziesiątek aplikacji, co negatywnie wpływa na stan systemu. Zalecamy stosowanie alternatywnych strategii, takich jak JobScheduler, aby lepiej koordynować spełnianie różnych warunków wstępnych.

  • Podczas wysyłania transmisji nie ma możliwości filtrowania ani dostosowywania treści dostarczanych do aplikacji. Utrudnia to lub uniemożliwia reagowanie na przyszłe problemy związane z prywatnością lub wprowadzanie zmian w zachowaniu na podstawie docelowego pakietu SDK aplikacji odbierającej.

  • Kolejki transmisji są zasobem współdzielonym, więc mogą być przeciążone i nie gwarantują terminowego dostarczenia wydarzenia. Zaobserwowaliśmy kilka kolejek transmisji, których opóźnienie od początku do końca wynosi 10 minut lub więcej.

Z tych powodów zachęcamy do korzystania w nowych funkcjach z odbiorników, wywołań zwrotnych lub innych narzędzi, takich jak JobScheduler, zamiast intencji rozgłaszania.

Jeśli intencje transmisji nadal są idealnym rozwiązaniem, warto wziąć pod uwagę te sprawdzone metody:

  • Jeśli to możliwe, użyj Intent.FLAG_RECEIVER_REGISTERED_ONLY, aby ograniczyć transmisję do aplikacji, które są już uruchomione. Na przykład ACTION_SCREEN_ON używa tego rozwiązania, aby uniknąć wybudzania aplikacji.
  • Jeśli to możliwe, użyj tagu Intent.setPackage() lub Intent.setComponent(), aby kierować transmisję na konkretną aplikację, która Cię interesuje. Na przykład ACTION_MEDIA_BUTTON używa tego projektu, aby skupić się na bieżącej aplikacji obsługującej elementy sterujące odtwarzaniem.
  • Jeśli to możliwe, zdefiniuj transmisję jako <protected-broadcast>, aby uniemożliwić złośliwym aplikacjom podszywanie się pod system operacyjny.

Intencje w usługach dla programistów powiązanych z systemem

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

  1. Zdefiniuj stałą ciągu tekstowego SERVICE_INTERFACE w klasie zawierającej pełną nazwę klasy usługi. Ta stała musi być oznaczona adnotacją @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION).
  2. Dokument dotyczący klasy, do której deweloper musi dodać <intent-filter>, aby otrzymywać intencje z platformy.AndroidManifest.xml
  3. Zdecydowanie rozważ dodanie uprawnień na poziomie systemu, aby zapobiec wysyłaniu przez nieuczciwe aplikacje Intent do usług dla deweloperów.

Współdziałanie Kotlin-Java

Pełną listę wytycznych znajdziesz w oficjalnym przewodniku Androida dotyczącym współdziałania języków Kotlin i Java. Wybrane wytyczne zostały skopiowane do tego przewodnika, aby zwiększyć jego wykrywalność.

Widoczność interfejsu API

Niektóre interfejsy API Kotlin, takie jak suspend funs, nie są przeznaczone do używania przez programistów Java. Nie próbuj jednak kontrolować widoczności specyficznej dla języka za pomocą @JvmSynthetic, ponieważ ma to wpływ na sposób prezentowania interfejsu API w debuggerach, co utrudnia debugowanie.

Szczegółowe wskazówki znajdziesz w przewodniku po współdziałaniu języków Kotlin i Java lub w przewodniku po programowaniu asynchronicznym.

Obiekty towarzyszące

Kotlin używa companion object do udostępniania statycznych elementów. W niektórych przypadkach będą one widoczne w kodzie Javy w klasie wewnętrznej o nazwie Companion, a nie w klasie zawierającej. Companion zajęcia mogą być wyświetlane jako puste w plikach tekstowych interfejsu API – tak ma być.

Aby zmaksymalizować zgodność z Javą, oznacz pola niebędące stałymi obiektów towarzyszących adnotacją @JvmField, a funkcje publiczne adnotacją @JvmStatic, aby udostępnić je bezpośrednio w klasie zawierającej.

companion object {
  @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
  @JvmStatic fun fromPointF(pointf: PointF) {
    /* ... */
  }
}

Ewolucja interfejsów API platformy Android

W tej sekcji opisujemy zasady dotyczące typów zmian, jakie możesz wprowadzać w istniejących interfejsach API na Androida, oraz sposobu ich wdrażania, aby zmaksymalizować zgodność z istniejącymi aplikacjami i bazami kodu.

Zmiany powodujące niezgodność binarną

Unikaj zmian powodujących niezgodność binarną w ukończonych publicznych interfejsach API. Tego typu zmiany zwykle powodują błędy podczas uruchamiania make update-api, ale mogą wystąpić przypadki graniczne, których kontrola interfejsu API Metalava nie wykryje. W razie wątpliwości zapoznaj się z przewodnikiem Evolving Java-based APIs (Rozwijanie interfejsów API opartych na Javie) opublikowanym przez Eclipse Foundation, w którym znajdziesz szczegółowe wyjaśnienie, jakie typy zmian w interfejsie API są zgodne w Javie. Zmiany powodujące niezgodność binarną w ukrytych (np. systemowych) interfejsach API powinny być wprowadzane zgodnie z cyklem wycofania/zastąpienia.

Zmiany powodujące niezgodność na poziomie kodu źródłowego

Odradzamy wprowadzanie zmian powodujących problemy ze źródłem, nawet jeśli nie powodują one problemów z binarnym kodem. Przykładem zmiany, która jest zgodna binarnie, ale powoduje niezgodność kodu źródłowego, jest dodanie typu ogólnego do istniejącej klasy. Jest to zgodne binarnie, ale może powodować błędy kompilacji z powodu dziedziczenia lub niejednoznacznych odwołań. Zmiany powodujące niezgodność na poziomie kodu źródłowego nie powodują błędów podczas uruchamiania make update-api, więc musisz zadbać o to, aby zrozumieć wpływ zmian na istniejące sygnatury interfejsu API.

W niektórych przypadkach zmiany powodujące niezgodność z kodem źródłowym są konieczne, aby poprawić komfort pracy deweloperów lub poprawność kodu. Na przykład dodanie do źródeł w Javie adnotacji o możliwości przyjmowania wartości null poprawia interoperacyjność z kodem w Kotlinie i zmniejsza prawdopodobieństwo wystąpienia błędów, ale często wymaga zmian w kodzie źródłowym – czasami znacznych.

Zmiany w prywatnych interfejsach API

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

Interfejsy API z adnotacją @SystemApi musisz przechowywać przez 3 lata. Musisz usunąć lub zmodyfikować interfejs API systemu zgodnie z tym harmonogramem:

  • API y - Added
  • API y+1 - wycofanie
    • Oznacz kod symbolem @Deprecated.
    • Dodaj zamienniki i utwórz do nich link w dokumentacji Javadoc dla kodu oznaczonego jako przestarzały, używając adnotacji @deprecated docs.
    • W trakcie cyklu rozwoju zgłaszaj błędy użytkownikom wewnętrznym, informując ich o wycofaniu interfejsu API. Pomaga to sprawdzić, czy interfejsy API zastępujące są odpowiednie.
  • API y+2 – łagodne usunięcie
    • Oznacz kod symbolem @removed.
    • Opcjonalnie zgłaszaj wyjątek lub nie rób nic w przypadku aplikacji kierowanych na bieżący poziom pakietu SDK w wersji.
  • API y+3 - trwałe usunięcie
    • Całkowicie usuń kod z drzewa źródłowego.

Wycofanie

Uważamy, że wycofanie to zmiana interfejsu API, która może nastąpić w wersji głównej (np. oznaczonej literą). Podczas wycofywania interfejsów API używaj jednocześnie adnotacji @Deprecated source i adnotacji @deprecated <summary> docs. Podsumowanie musi zawierać strategię migracji. Ta strategia może zawierać link do interfejsu API, który zastępuje wycofany interfejs, lub wyjaśnienie, dlaczego nie należy go używać:

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

Musisz też oznaczyć jako wycofane interfejsy API zdefiniowane w XML i udostępnione w Javie, w tym atrybuty i właściwości, które można dostosowywać, udostępnione w klasie android.R, wraz z podsumowaniem:

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

Kiedy wycofać interfejs API

Wycofanie jest najbardziej przydatne do zniechęcania do używania interfejsu API w nowym kodzie.

Wymagamy też, aby przed @removed oznaczać interfejsy API jako @deprecated, ale nie stanowi to dla deweloperów wystarczającej motywacji do migracji z interfejsu API, którego już używają.

Zanim wycofasz interfejs API, zastanów się, jaki będzie to miało wpływ na deweloperów. Skutki wycofania interfejsu API to:

  • javac emituje ostrzeżenie podczas kompilacji.
    • Ostrzeżeń o wycofaniu nie można wyłączyć globalnie ani ustalić dla nich wartości bazowych, więc programiści korzystający z -Werror muszą indywidualnie naprawić lub wyłączyć każde użycie wycofanego interfejsu API, zanim będą mogli zaktualizować wersję pakietu SDK do kompilacji.
    • Ostrzeżeń o wycofaniu w przypadku importowania wycofanych klas nie można pominąć. W związku z tym deweloperzy muszą wstawić w kodzie w pełni kwalifikowaną nazwę klasy w przypadku każdego użycia wycofanej klasy, zanim będą mogli zaktualizować wersję pakietu SDK do kompilacji.
  • Dokumentacja dotycząca d.android.com zawiera informację o wycofaniu.
  • Środowiska IDE, takie jak Android Studio, wyświetlają ostrzeżenie w miejscu użycia interfejsu API.
  • Środowiska IDE mogą obniżyć ranking interfejsu API lub ukryć go w autouzupełnianiu.

W rezultacie wycofanie interfejsu API może zniechęcić deweloperów, którzy najbardziej dbają o stan kodu (czyli tych, którzy używają -Werror), do wdrażania nowych pakietów SDK. Deweloperzy, którzy nie przejmują się ostrzeżeniami w dotychczasowym kodzie, prawdopodobnie będą całkowicie ignorować wycofania.

Pakiet SDK, który wprowadza dużą liczbę wycofań, pogarsza sytuację w obu tych przypadkach.

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

  • W przyszłej wersji planujemy @remove interfejs API.
  • Użycie interfejsu API prowadzi do nieprawidłowego lub niezdefiniowanego zachowania, którego nie możemy naprawić bez utraty zgodności.

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

Nie zalecamy wycofywania interfejsów API, które działają zgodnie z przeznaczeniem w obecnych i przyszłych wersjach:

/**
 * ...
 * @deprecated Use {@link #doThing(int, Bundle)} instead.
 */
@Deprecated
public void doThing(int action) {
  ...
}

public void doThing(int action, @Nullable Bundle extras) {
  ...
}

Wycofanie jest odpowiednie w przypadku, gdy interfejsy API nie mogą już zachowywać się zgodnie z dokumentacją:

/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

Zmiany w wycofanych interfejsach API

Musisz zachować działanie wycofanych interfejsów API. Oznacza to, że implementacje testów muszą pozostać takie same, a testy muszą nadal przechodzić po wycofaniu interfejsu API. Jeśli interfejs API nie ma testów, dodaj je.

Nie będziemy rozszerzać wycofanych interfejsów API w przyszłych wersjach. Do istniejącego interfejsu API oznaczonego jako wycofany możesz dodać adnotacje dotyczące poprawności (np. @Nullable), ale nie powinieneś 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 (czyli początkowo weszłyby do publicznej powierzchni interfejsu API jako wycofane), musisz je usunąć przed sfinalizowaniem interfejsu API.

Przenoszenie do kosza

Usunięcie miękkie to zmiana powodująca niezgodność ze źródłem, której należy unikać w publicznych interfejsach API, chyba że Rada ds. API wyraźnie na to zezwoli. W przypadku interfejsów API systemu musisz wycofać interfejs API na czas trwania głównej wersji przed jego usunięciem. Usuń wszystkie odniesienia do interfejsów API w dokumentach i użyj adnotacji @removed <summary> w dokumentach, gdy interfejsy API są usuwane w sposób nietrwały. Podsumowanie musi zawierać powód usunięcia i może obejmować strategię migracji, jak wyjaśniliśmy w sekcji Wycofywanie.

Zachowanie interfejsów API usuniętych w sposób nietrwały może pozostać bez zmian, ale co ważniejsze, musi zostać zachowane w taki sposób, aby istniejące wywołania nie powodowały awarii podczas wywoływania interfejsu API. W niektórych przypadkach może to oznaczać zachowanie dotychczasowego działania.

Pokrycie testami musi zostać zachowane, ale zawartość testów może wymagać zmiany, aby uwzględnić zmiany w zachowaniu. Testy muszą nadal sprawdzać, czy istniejące wywołania nie powodują awarii w czasie działania. Możesz zachować dotychczasowe działanie interfejsów API usuniętych w sposób nietrwały, ale co ważniejsze, musisz je zachować w taki sposób, aby istniejące wywołania nie powodowały awarii. W niektórych przypadkach może to oznaczać zachowanie dotychczasowego sposobu działania.

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

Z technicznego punktu widzenia usuwamy interfejs API z pliku JAR z wersją szkieletową pakietu SDK i ze ścieżki klas w czasie kompilacji za pomocą adnotacji @remove Javadoc, ale nadal istnieje on w ścieżce klas w czasie działania – podobnie jak interfejsy API @hide:

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

Z perspektywy dewelopera interfejs API nie będzie już widoczny w funkcji autouzupełniania, a kod źródłowy, który się do niego odwołuje, nie będzie się kompilować, gdy compileSdk będzie równe lub nowsze niż pakiet SDK, w którym interfejs API został usunięty. Kod źródłowy będzie się jednak nadal kompilować w przypadku starszych pakietów SDK, a pliki binarne, które się do niego odwołują, będą nadal działać.

Niektórych kategorii interfejsów API nie wolno usuwać w sposób nietrwały. Nie możesz usuwać niektórych kategorii interfejsów API.

Metody abstrakcyjne

Nie wolno usuwać metod abstrakcyjnych w klasach, które deweloperzy mogą rozszerzać. Uniemożliwia to deweloperom prawidłowe rozszerzenie klasy na wszystkich poziomach pakietu SDK.

W rzadkich przypadkach, gdy nigdynie będzie możliwe rozszerzenie klasy przez deweloperów, nadal możesz usunąć metody abstrakcyjne.

Trwałe usuwanie

Trwałe usunięcie jest zmianą powodującą brak zgodności binarnej i nigdy nie powinno występować w publicznych interfejsach API.

Adnotacja, której nie zalecamy

Adnotacji @Discouraged używamy, aby wskazać, że w większości przypadków (>95%) nie zalecamy korzystania z danego interfejsu API. Od wycofanych interfejsów API różnią się tym, że istnieje wąski, krytyczny przypadek użycia, który uniemożliwia ich wycofanie. Gdy oznaczysz interfejs API jako odradzany, musisz podać wyjaśnienie i alternatywne rozwiązanie:

@Discouraged(message = "Use of this function is discouraged because resource
                        reflection makes it harder to perform build
                        optimizations and compile-time verification of code. It
                        is much more efficient to retrieve resources by
                        identifier (such as `R.foo.bar`) than by name (such as
                        `getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

Nie należy dodawać nowych interfejsów API, które są odradzane.

Zmiany w działaniu istniejących interfejsów API

W niektórych przypadkach możesz chcieć zmienić sposób działania implementacji istniejącego interfejsu API. Na przykład w Androidzie 7.0 ulepszyliśmy DropBoxManager, aby wyraźnie informować, kiedy deweloperzy próbowali publikować zdarzenia, które były zbyt duże, aby można je było przesłać przez Binder.

Aby jednak uniknąć problemów z dotychczasowymi aplikacjami, zdecydowanie zalecamy zachowanie bezpiecznego działania w przypadku starszych aplikacji. W przeszłości chroniliśmy te zmiany w zachowaniu na podstawie ApplicationInfo.targetSdkVersion aplikacji, ale niedawno przeszliśmy na wymaganie korzystania z platformy zgodności aplikacji. Oto przykład, jak wdrożyć zmianę zachowania za pomocą tych nowych ram:

import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;

public class MyClass {
  @ChangeId
  // This means the change will be enabled for target SDK R and higher.
  @EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
  // Use a bug number as the value, provide extra detail in the bug.
  // FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
  static final long FOO_NOW_DOES_X = 123456789L;

  public void doFoo() {
    if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
      // do the new thing
    } else {
      // do the old thing
    }
  }
}

Korzystanie z tej struktury zgodności aplikacji umożliwia deweloperom tymczasowe wyłączanie określonych zmian w zachowaniu aplikacji podczas wersji podglądowych i beta w ramach debugowania aplikacji, zamiast zmuszać ich do jednoczesnego dostosowywania się do dziesiątek zmian w zachowaniu.

Zgodność w przyszłości

Zgodność w przyszłość to cecha konstrukcyjna, która umożliwia systemowi akceptowanie danych wejściowych przeznaczonych dla jego późniejszej wersji. W przypadku projektowania interfejsu API musisz zwrócić szczególną uwagę na początkowy projekt, a także na przyszłe zmiany, ponieważ programiści oczekują, że napiszą kod raz, przetestują go raz i będzie on działać wszędzie bez problemów.

Najczęstsze problemy ze zgodnością z przyszłymi wersjami Androida powodują:

  • Dodawanie nowych stałych do zbioru (np. @IntDef lub enum), który wcześniej był uznawany za kompletny (np. gdy switch ma default, który zgłasza wyjątek).
  • Dodanie obsługi funkcji, która nie jest bezpośrednio uwzględniona w interfejsie API (np. obsługi przypisywania zasobów typu ColorStateList w XML, gdzie wcześniej obsługiwane były tylko zasoby typu <color>).
  • złagodzenie ograniczeń dotyczących sprawdzania w czasie działania, np. usunięcie sprawdzania requireNotNull(), które było obecne w starszych wersjach;

We wszystkich tych przypadkach deweloperzy dowiadują się, że coś jest nie tak, dopiero w czasie działania programu. Co gorsza, mogą się o tym dowiedzieć z raportów o awariach ze starszych urządzeń w terenie.

Dodatkowo wszystkie te przypadki są technicznie prawidłowe. Nie naruszają one zgodności binarnej ani zgodności kodu źródłowego, a narzędzie API Lint nie wykrywa żadnych z tych problemów.

Dlatego projektanci interfejsów API muszą zachować szczególną ostrożność podczas modyfikowania istniejących klas. Zadaj sobie pytanie: „Czy ta zmiana spowoduje, że kod napisany i przetestowany tylko pod kątem najnowszej wersji platformy nie będzie działać w starszych wersjach?”.

Schematy XML

Jeśli schemat XML służy jako stabilny interfejs między komponentami, musi być wyraźnie określony i musi ewoluować w sposób zapewniający zgodność wsteczną, podobnie jak inne interfejsy API Androida. Na przykład struktura elementów i atrybutów XML musi być zachowana podobnie jak metody i zmienne na innych powierzchniach interfejsu Android API.

Wycofanie XML

Jeśli chcesz wycofać element lub atrybut XML, możesz dodać znacznik xs:annotation, ale musisz nadal obsługiwać wszystkie istniejące pliki XML zgodnie ze standardowym cyklem życia @SystemApi.

<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

Typy elementów muszą zostać zachowane

Schematy obsługują elementy sequence, choiceall jako elementy podrzędne elementu complexType. Elementy podrzędne różnią się jednak liczbą i kolejnością elementów podrzędnych, więc zmiana istniejącego typu byłaby niekompatybilna.

Jeśli chcesz zmodyfikować istniejący typ, najlepszym rozwiązaniem jest wycofanie starego typu i wprowadzenie nowego, który go zastąpi.

<!-- Original "sequence" value -->
<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

<!-- New "choice" value -->
<xs:element name="fooChoice">
    <xs:complexType>
        <xs:choice>
            <xs:element name="name" type="xs:string"/>
        </xs:choice>
    </xs:complexType>
</xs:element>

Wzorce specyficzne dla modułów magistrali

Mainline to projekt, który umożliwia aktualizowanie poszczególnych podsystemów („modułów Mainline”) systemu operacyjnego Android zamiast aktualizowania całego obrazu systemu.

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

Moduły główne powinny być zgodne z określonymi wzorcami projektowymi. Opisujemy je w tej sekcji.

Wzorzec <Module>FrameworkInitializer

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

  • Udostępnij klasę <YourModule>FrameworkInitializer z modułu. Ta klasa musi być w domenie $BOOTCLASSPATH. Przykład: StatsFrameworkInitializer

  • Oznacz go symbolem @SystemApi(client = MODULE_LIBRARIES).

  • Dodaj do niego metodę public static void registerServiceWrappers().

  • Użyj SystemServiceRegistry.registerContextAwareService(), aby zarejestrować klasę menedżera usługi, gdy potrzebuje ona odwołania do Context.

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

  • Wywołaj metodę registerServiceWrappers() z inicjatora statycznego SystemServiceRegistry.

Wzorzec <Module>ServiceManager

Zwykle, aby zarejestrować obiekty powiązań usług systemowych lub uzyskać do nich odwołania, używa się funkcji ServiceManager, ale moduły główne nie mogą jej używać, ponieważ jest ukryta. Ta klasa jest ukryta, ponieważ moduły główne nie powinny rejestrować ani odwoływać się do obiektów usługi systemowej Binder udostępnianych przez platformę statyczną lub inne moduły.

Moduły główne mogą zamiast tego używać tego wzorca, aby rejestrować usługi Binder i uzyskiwać do nich odwołania.

  • Utwórz klasę <YourModule>ServiceManager, wzorując się na klasie TelephonyServiceManager.

  • Udostępnij zajęcia jako @SystemApi. Jeśli potrzebujesz dostępu do niego tylko z klas $BOOTCLASSPATH lub klas serwera systemowego, możesz użyć @SystemApi(client = MODULE_LIBRARIES). W innych przypadkach wystarczy @SystemApi(client = PRIVILEGED_APPS).

  • Te zajęcia będą obejmować:

    • Ukryty konstruktor, dzięki czemu tylko statyczny kod platformy może utworzyć jego instancję.
    • Publiczne metody pobierające, które zwracają instancję ServiceRegisterer dla określonej nazwy. Jeśli masz 1 obiekt bindera, potrzebujesz 1 metody pobierającej. Jeśli masz 2 właściwości, potrzebujesz 2 metod pobierających.
    • ActivityThread.initializeMainlineModules() utwórz instancję tej klasy i przekaż ją do metody statycznej udostępnianej przez moduł. Zwykle dodajesz statyczny interfejs API @SystemApi(client = MODULE_LIBRARIES) w klasie FrameworkInitializer, która go przyjmuje.

Ten wzorzec uniemożliwia innym modułom głównym dostęp do tych interfejsów API, ponieważ nie ma możliwości uzyskania przez nie instancji <YourModule>ServiceManager, mimo że interfejsy API get()register() są dla nich widoczne.

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

Jeśli implementujesz obiekt narzędzia do łączenia usług w kodzie natywnym, używasz natywnych interfejsów API AServiceManager. Te interfejsy API odpowiadają interfejsom ServiceManager Java, ale natywne są bezpośrednio udostępniane modułom głównym. Nie używaj ich do rejestrowania ani odwoływania się do obiektów Binder, które nie należą do Twojego modułu. Jeśli udostępniasz obiekt bindera z kodu natywnego, interfejs <YourModule>ServiceManager.ServiceRegisterer nie musi mieć metody register().

Definicje uprawnień w modułach magistrali

Moduły Mainline zawierające pliki APK mogą definiować uprawnienia (niestandardowe) w pliku APKAndroidManifest.xml w taki sam sposób jak zwykły plik APK.

Jeśli zdefiniowane uprawnienie jest używane tylko wewnętrznie w module, jego nazwa powinna mieć prefiks w postaci nazwy pakietu APK, np.:

<permission
    android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
    android:protectionLevel="signature" />

Jeśli zdefiniowane uprawnienie ma być udostępniane innym aplikacjom w ramach interfejsu API platformy, który można aktualizować, jego nazwa powinna zaczynać się od „android.permission.”. (jak w przypadku każdego statycznego uprawnienia platformy) oraz nazwę pakietu modułu, aby sygnalizować, że jest to interfejs API platformy z modułu, unikając przy tym konfliktów nazw, np.:

<permission
    android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
    android:label="@string/active_calories_burned_read_content_description"
    android:protectionLevel="dangerous"
    android:permissionGroup="android.permission-group.HEALTH" />

Moduł może następnie udostępnić nazwę tego uprawnienia jako stałą interfejsu API w swoim interfejsie API, np. HealthPermissions.READ_ACTIVE_CALORIES_BURNED.