Diretrizes da API do Android

Esta página é um guia para que os desenvolvedores entendam os princípios gerais que o Conselho de API aplica nas análises de API.

Além de seguir essas diretrizes ao escrever APIs, os desenvolvedores precisam executar a ferramenta API Lint, que codifica muitas dessas regras em verificações que são executadas em APIs.

Pense nisso como o guia das regras que são obedecidas pela ferramenta Lint, além de conselhos gerais sobre regras que não podem ser codificadas com alta precisão.

Ferramenta API Lint

O API Lint está integrado à ferramenta de análise estática do Metalava e é executado automaticamente durante a validação no CI. É possível executá-lo manualmente em um checkout de plataforma local usando m checkapi ou um checkout local do AndroidX usando ./gradlew :path:to:project:checkApi.

Regras de API

A plataforma Android e muitas bibliotecas do Jetpack existiam antes da criação desse conjunto de diretrizes, e as políticas definidas mais adiante nesta página estão sempre evoluindo para atender às necessidades do ecossistema Android.

Por isso, algumas APIs atuais podem não seguir as diretrizes. Em outros casos, a experiência do usuário pode ser melhor para os desenvolvedores de apps se uma nova API for consistente com as APIs existentes, em vez de aderir estritamente às diretrizes.

Use seu julgamento e entre em contato com o Conselho de API se houver dúvidas difíceis sobre uma API que precisa ser resolvida ou diretrizes que precisam ser atualizadas.

Noções básicas de API

Essa categoria se refere aos aspectos principais de uma API do Android.

Todas as APIs precisam ser implementadas

Independentemente do público-alvo de uma API (por exemplo, público ou @SystemApi), todas as plataformas da API precisam ser implementadas quando mescladas ou expostas como API. Não mescle stubs de API com implementação para ser feita em uma data posterior.

As plataformas de API sem implementações têm vários problemas:

  • Não há garantia de que uma superfície adequada ou completa tenha sido exposta. Até que uma API seja testada ou usada por clientes, não há como verificar se um cliente tem as APIs adequadas para usar o recurso.
  • APIs sem implementação não podem ser testadas nas prévias para desenvolvedores.
  • APIs sem implementação não podem ser testadas no CTS.

Todas as APIs precisam ser testadas

Isso está de acordo com os requisitos do CTS da plataforma, as políticas do AndroidX e, de modo geral, a ideia de que as APIs precisam ser implementadas.

O teste de superfícies de API oferece uma garantia básica de que a superfície de API é utilizável e que abordamos os casos de uso esperados. Testar a existência não é suficiente. O comportamento da própria API precisa ser testado.

Uma mudança que adiciona uma nova API precisa incluir os testes correspondentes no mesmo tópico de CL ou Gerrit.

As APIs também precisam ser testáveis. Você precisa ser capaz de responder à pergunta "Como um desenvolvedor de apps vai testar o código que usa sua API?"

Todas as APIs precisam ser documentadas

A documentação é uma parte importante da usabilidade da API. Embora a sintaxe de uma plataforma de API possa parecer óbvia, novos clientes não vão entender a semântica, o comportamento ou o contexto por trás dela.

Todas as APIs geradas precisam estar em conformidade com as diretrizes

As APIs geradas por ferramentas precisam seguir as mesmas diretrizes da API que o código escrito manualmente.

Ferramentas que não são recomendadas para gerar APIs:

  • AutoValue: viola as diretrizes de várias maneiras, por exemplo, não há como implementar classes de valor final nem builders finais com a forma como o AutoValue funciona.

Estilo de código

Essa categoria se refere ao estilo de código geral que os desenvolvedores precisam usar, principalmente ao criar APIs públicas.

Seguir as convenções de programação padrão, exceto quando indicado

As convenções de programação do Android estão documentadas para colaboradores externos aqui:

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

Em geral, seguimos as convenções de programação padrão do Java e do Kotlin.

Os acrônimos não precisam ser escritos em letras maiúsculas nos nomes dos métodos

Por exemplo: o nome do método precisa ser runCtsTests, e não runCTSTests.

Os nomes não podem terminar com "Impl"

Isso expõe detalhes de implementação. Evite isso.

Classes

Esta seção descreve regras sobre classes, interfaces e herança.

Herdar novas classes públicas da classe base apropriada

A herança expõe elementos da API na subclasse que podem não ser apropriados. Por exemplo, uma nova subclasse pública de FrameLayout se parece com FrameLayout mais os novos comportamentos e elementos da API. Se a API herdada não for adequada para seu caso de uso, herde de uma classe mais acima na árvore, por exemplo, ViewGroup ou View.

Se você tiver a tentação de substituir métodos da classe base para gerar UnsupportedOperationException, reconsidere qual classe base você está usando.

Usar as classes de coleções básicas

Sempre prefira a classe base à implementação específica, seja para usar uma coleção como argumento ou retornar como um valor, como retornar List<Foo> em vez de ArrayList<Foo>.

Use uma classe base que expresse restrições adequadas para a API. Por exemplo, use List para uma API em que a coleção precisa ser ordenada e Set para uma API em que a coleção precisa consistir em elementos exclusivos.

Em Kotlin, prefira coleções imutáveis. Consulte Mutabilidade de coleções para mais detalhes.

Classes abstratas x interfaces

O Java 8 adiciona suporte a métodos de interface padrão, o que permite que designers de API adicionem métodos a interfaces, mantendo a compatibilidade binária. O código da plataforma e todas as bibliotecas do Jetpack precisam ser direcionadas para o Java 8 ou mais recente.

Nos casos em que a implementação padrão não tem estado, os designers de API devem preferir interfaces a classes abstratas. Ou seja, os métodos de interface padrão podem ser implementados como chamadas para outros métodos de interface.

Nos casos em que um construtor ou estado interno é necessário pela implementação padrão, as classes abstratas precisam ser usadas.

Em ambos os casos, os designers de API podem deixar um único método abstrato para simplificar o uso como uma 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) { }
}

Os nomes das classes precisam refletir o que elas estendem

Por exemplo, as classes que estendem Service precisam ser nomeadas FooService para clareza:

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

Sufixos genéricos

Evite usar sufixos de nome de classe genéricos, como Helper e Util, para coleções de métodos utilitários. Em vez disso, coloque os métodos diretamente nas classes associadas ou em funções de extensão do Kotlin.

Nos casos em que os métodos estão conectando várias classes, dê à classe que contém um nome significativo que explique o que ela faz.

Em muitos casos limitados, o uso do sufixo Helper pode ser adequado:

  • Usado para a composição do comportamento padrão
  • Pode envolver a delegação de comportamento atual para novas classes
  • Pode exigir estado persistente
  • Normalmente envolve View

Por exemplo, se a backport de dicas exigir a persistência do estado associado a um View e a chamada de vários métodos no View para instalar a backport, TooltipHelper será um nome de classe aceitável.

Não exponha o código gerado por IDL como APIs públicas diretamente

Mantenha o código gerado pelo IDL como detalhes de implementação. Isso inclui protobuf, sockets, FlatBuffers ou qualquer outra superfície de API que não seja Java ou NDK. No entanto, a maioria dos IDLs no Android está na AIDL. Portanto, esta página se concentra na AIDL.

As classes AIDL geradas não atendem aos requisitos do guia de estilo da API (por exemplo, elas não podem usar sobrecarga) e a ferramenta AIDL não foi projetada explicitamente para manter a compatibilidade da API de linguagem. Portanto, não é possível incorporá-las a uma API pública.

Em vez disso, adicione uma camada de API pública sobre a interface AIDL, mesmo que ela inicialmente seja um wrapper superficial.

Interfaces do Binder

Se a interface Binder for um detalhe de implementação, ela poderá ser alterada livremente no futuro, com a camada pública permitindo que a compatibilidade necessária seja mantida. Por exemplo, talvez seja necessário adicionar novos argumentos às chamadas internas ou otimizar o tráfego de IPC usando batch ou streaming, memória compartilhada ou algo semelhante. Isso não será possível se a interface AIDL também for a API pública.

Por exemplo, não exponha FooService como uma API pública diretamente:

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

Em vez disso, envolva a interface Binder em um gerenciador ou outra classe:

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

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

Se, mais tarde, um novo argumento for necessário para essa chamada, a interface interna poderá ser mínima e sobrecargas convenientes adicionadas à API pública. É possível usar a camada de acondicionamento para processar outras questões de compatibilidade com versões anteriores à medida que a implementação evolui:

/**
 * @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);
   }
}

Para interfaces Binder que não fazem parte da plataforma Android (por exemplo, uma interface de serviço exportada pelo Google Play Services para uso em apps), o requisito de uma interface IPC estável, publicada e com versão significa que é muito mais difícil evoluir a própria interface. No entanto, ainda vale a pena ter uma camada de wrapper ao redor dela para corresponder a outras diretrizes de API e facilitar o uso da mesma API pública para uma nova versão da interface IPC, se isso for necessário.

Não use objetos Binder brutos na API pública

Um objeto Binder não tem nenhum significado por si só e, portanto, não pode ser usado na API pública. Um caso de uso comum é usar um Binder ou IBinder como um token porque ele tem semântica de identidade. Em vez de usar um objeto Binder bruto, use uma classe de token de wrapper.

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

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

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

As classes de adm precisam ser finais

As classes de gerenciador precisam ser declaradas como final. As classes de gerente se comunicam com os serviços do sistema e são o único ponto de interação. Não é necessário fazer personalização, então declare como final.

Não use CompletableFuture ou Future

java.util.concurrent.CompletableFuture tem uma grande plataforma de API que permite a mutação arbitrária do valor do futuro e tem padrões padrão propensos a erros .

Por outro lado, o java.util.concurrent.Future não tem a escuta não bloqueante, o que dificulta o uso com código assíncrono.

No código da plataforma e nas APIs de biblioteca de baixo nível consumidas pelo Kotlin e Java, prefira uma combinação de um callback de conclusão, Executor, e se a API oferece suporte ao cancelamento CancellationSignal.

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

Se você usar o Kotlin, prefira as funções suspend.

suspend fun asyncLoadFoo(): Foo

Em bibliotecas de integração específicas do Java, é possível usar o ListenableFuture do Guava.

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

Não usar "Opcional"

Embora Optional possa ter vantagens em algumas plataformas de API, ele é inconsistente com a área de plataforma de API do Android. @Nullable e @NonNull fornecem assistência de ferramentas para a segurança do null, e o Kotlin aplica contratos de nulidade no nível do compilador, tornando o Optional desnecessário.

Para primitivos opcionais, use métodos has e get pareados. Se o valor não estiver definido (has retorna false), o método get vai gerar uma IllegalStateException.

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

Usar construtores privados para classes não instantiáveis

Classes que só podem ser criadas por Builders, classes que contêm apenas constantes ou métodos estáticos ou classes não instantiáveis precisam incluir pelo menos um construtor privado para evitar a instanciação usando o construtor sem argumentos padrão.

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

Singletons

O uso de singleton é desencorajado porque eles têm as seguintes desvantagens relacionadas a testes:

  1. A construção é gerenciada pela classe, evitando o uso de falsificações
  2. Os testes não podem ser herméticos devido à natureza estática de um singleton
  3. Para contornar esses problemas, os desenvolvedores precisam conhecer os detalhes internos do singleton ou criar um wrapper.

Prefira o padrão de instância única, que depende de uma classe base abstrata para resolver esses problemas.

Instância única

As classes de instância única usam uma classe base abstrata com um construtor private ou internal e fornecem um método getInstance() estático para receber uma instância. O método getInstance() precisa retornar o mesmo objeto em chamadas subsequentes.

O objeto retornado por getInstance() precisa ser uma implementação particular da classe base abstrata.

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

A instância única difere do singleton porque os desenvolvedores podem criar uma versão falsa de SingleInstance e usar o próprio framework de injeção de dependências para gerenciar a implementação sem precisar criar um wrapper. Ou a biblioteca pode fornecer o próprio falso em um artefato -testing.

As classes que liberam recursos precisam implementar a classe AutoCloseable

As classes que liberam recursos por métodos close, release, destroy ou semelhantes precisam implementar java.lang.AutoCloseable para permitir que os desenvolvedores limpem esses recursos automaticamente ao usar um bloco try-with-resources.

Evite introduzir novas subclasses de visualização no Android.*

Não apresente novas classes que herdam diretamente ou indiretamente de android.view.View na API pública da plataforma (ou seja, em android.*).

O kit de ferramentas de interface do Android agora é focado no Compose. Os novos recursos de interface expostos pela plataforma precisam ser expostos como APIs de nível inferior que podem ser usadas para implementar o Jetpack Compose e, opcionalmente, componentes de interface baseados em visualizações para desenvolvedores nas bibliotecas do Jetpack. Oferecer esses componentes em bibliotecas oferece oportunidades para implementações de backport quando os recursos da plataforma não estão disponíveis.

Campos

Essas regras são sobre campos públicos em classes.

Não exponha campos brutos

As classes Java não podem expor campos diretamente. Os campos precisam ser privados e acessíveis apenas usando getters e setters públicos, independentemente de serem finais ou não.

As exceções raras incluem estruturas de dados básicas em que não é necessário melhorar o comportamento de especificar ou recuperar um campo. Nesses casos, os campos precisam ser nomeados usando convenções de nomenclatura de variáveis padrão, por exemplo, Point.x e Point.y.

As classes Kotlin podem expor propriedades.

Os campos expostos precisam ser marcados como finais

Não é recomendável usar campos brutos (@link Não exponha campos brutos). No entanto, em raras situações em que um campo é exposto como público, marque-o como final.

Os campos internos não podem ser expostos

Não faça referência a nomes de campos internos na API pública.

public int mFlags;

Use "Público" em vez de "Protegido"

@consulte Usar "Público" em vez de "Protegido"

Constantes

Estas são regras sobre constantes públicas.

As constantes de flag não podem se sobrepor a valores int ou long.

Flags implica bits que podem ser combinados em algum valor de união. Caso contrário, não chame a variável ou constante 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;

Consulte @IntDef para flags de máscara de bits para mais informações sobre como definir constantes de flag públicas.

As constantes finais estáticas precisam usar a convenção de nomenclatura com todas as letras maiúsculas e sublinhados.

Todas as palavras na constante precisam estar em letras maiúsculas, e várias palavras precisam ser separadas por _. Exemplo:

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

Usar prefixos padrão para constantes

Muitas das constantes usadas no Android são para coisas padrão, como flags, chaves e ações. Essas constantes precisam ter prefixos padrão para que sejam mais identificáveis como essas coisas.

Por exemplo, os extras de intent precisam começar com EXTRA_. As ações de intent precisam começar com ACTION_. As constantes usadas com Context.bindService() precisam começar com BIND_.

Nomes e escopos de constantes de chave

Os valores de constantes de string precisam ser consistentes com o nome da constante e, geralmente, precisam estar no escopo do pacote ou domínio. Exemplo:

public static final String FOO_THING = "foo"

não tem um nome consistente nem um escopo adequado. Em vez disso, considere:

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

Os prefixos de android em constantes de string com escopo são reservados para o Android Open Source Project.

As ações e os extras de intent, assim como as entradas de pacote, precisam ter um namespace usando o nome do pacote em que são definidos.

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

Use "Público" em vez de "Protegido"

@consulte Usar "Público" em vez de "Protegido"

Usar prefixos consistentes

As constantes relacionadas precisam começar com o mesmo prefixo. Por exemplo, para um conjunto de constantes a serem usadas com valores de sinalização:

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 Usar prefixos padrão para constantes

Usar nomes de recursos consistentes

Os identificadores, atributos e valores públicos precisam ser nomeados usando a convenção de nomenclatura camelCase, por exemplo, @id/accessibilityActionPageUp ou @attr/textAppearance, semelhante a campos públicos em Java.

Em alguns casos, um identificador ou atributo público inclui um prefixo comum separado por um sublinhado:

  • Valores de configuração da plataforma, como @string/config_recentsComponentName em config.xml
  • Atributos de visualização específicos do layout, como @attr/layout_marginStart em attrs.xml.

Os temas e estilos públicos precisam seguir a convenção de nomenclatura hierárquica PascalCase, por exemplo, @style/Theme.Material.Light.DarkActionBar ou @style/Widget.Material.SearchView.ActionBar, semelhante a classes aninhadas em Java.

Os recursos de layout e drawable não podem ser expostos como APIs públicas. No entanto, se eles precisarem ser expostos, os layouts e drawables públicos precisam ser nomeados usando a convenção de nomenclatura under_score, por exemplo, layout/simple_list_item_1.xml ou drawable/title_bar_tall.xml.

Quando as constantes podem mudar, torne-as dinâmicas

O compilador pode incorporar valores constantes. Portanto, manter os valores iguais é considerado parte do contrato da API. Se o valor de uma constante MIN_FOO ou MAX_FOO poder mudar no futuro, considere torná-los métodos dinâmicos.

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

Considere a compatibilidade com versões futuras para callbacks

As constantes definidas em versões futuras da API não são conhecidas por apps destinados a APIs mais antigas. Por esse motivo, as constantes enviadas aos apps precisam considerar a versão da API de destino do app e mapear as constantes mais recentes para um valor consistente. Considere o seguinte cenário:

Fonte hipotética do 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;

App hipotético com targetSdkVersion="22":

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

Nesse caso, o app foi projetado dentro das restrições do nível 22 da API e fez uma suposição (um tanto) razoável de que havia apenas dois estados possíveis. No entanto, se o app receber o STATUS_FAILURE_RETRY recém-adicionado, ele vai interpretar isso como sucesso.

Os métodos que retornam constantes podem processar casos como esse com segurança, restringindo a saída para corresponder ao nível da API segmentado pelo app:

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

Os desenvolvedores não podem prever se uma lista de constantes pode mudar no futuro. Se você definir uma API com uma constante UNKNOWN ou UNSPECIFIED que parece ser uma pilha, os desenvolvedores vão presumir que as constantes publicadas quando eles criaram o app são exaustivas. Se você não quiser definir essa expectativa, reconsidere se uma constante catch-all é uma boa ideia para sua API.

Além disso, as bibliotecas não podem especificar o próprio targetSdkVersion separado do app, e o processamento de mudanças de comportamento do targetSdkVersion do código da biblioteca é complicado e propenso a erros.

Constante de número inteiro ou string

Use constantes de números inteiros e @IntDef se o namespace de valores não puder ser estendido fora do pacote. Use constantes de string se o namespace for compartilhado ou puder ser estendido por código fora do pacote.

Classes de dados

As classes de dados representam um conjunto de propriedades imutáveis e fornecem um conjunto pequeno e bem definido de funções utilitárias para interagir com esses dados.

Não use data class em APIs públicas do Kotlin, porque o compilador do Kotlin não garante a API da linguagem ou a compatibilidade binária do código gerado. Em vez disso, implemente manualmente as funções necessárias.

Instanciação

Em Java, as classes de dados precisam fornecer um construtor quando há poucas propriedades ou usar o padrão Builder quando há muitas propriedades.

No Kotlin, as classes de dados precisam fornecer um construtor com argumentos padrão, independente do número de propriedades. As classes de dados definidas em Kotlin também podem se beneficiar de um builder ao segmentar clientes Java.

Modificação e cópia

Nos casos em que os dados precisam ser modificados, forneça uma classe Builder com um construtor de cópia (Java) ou uma função membro copy() (Kotlin) que retorna um novo objeto.

Ao fornecer uma função copy() no Kotlin, os argumentos precisam corresponder ao construtor da classe, e os padrões precisam ser preenchidos usando os valores atuais do objeto:

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

Outros comportamentos

As classes de dados precisam implementar equals() e hashCode(), e todas as propriedades precisam ser consideradas nas implementações desses métodos.

As classes de dados podem implementar toString() com um formato recomendado que corresponde à implementação da classe de dados do Kotlin, por exemplo, User(var1=Alex, var2=42).

Métodos

Essas são regras sobre várias especificações em métodos, em relação a parâmetros, nomes de método, tipos de retorno e especificadores de acesso.

Hora

Essas regras abrangem como conceitos de tempo, como datas e duração, devem ser expressos em APIs.

Prefira os tipos java.time.* sempre que possível.

java.time.Duration, java.time.Instant e muitos outros tipos de java.time.* estão disponíveis em todas as versões da plataforma por meio de simplificação e devem ser preferidos ao expressar o tempo em parâmetros de API ou valores de retorno.

Prefira expor apenas variantes de uma API que aceite ou retorne java.time.Duration ou java.time.Instant e omita variantes primitivas com o mesmo caso de uso, a menos que o domínio da API seja um em que a alocação de objetos em padrões de uso intencionais tenha um impacto de desempenho proibitivo.

Os métodos que expressam durações devem ser nomeados como "duration"

Se um valor de tempo expressar a duração do tempo envolvido, nomeie o parâmetro como "duration", não "time".

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

Exceções:

"timeout" é apropriado quando a duração se aplica especificamente a um valor de tempo limite.

"time" com um tipo de java.time.Instant é apropriado quando se refere a um ponto específico no tempo, não a uma duração.

Os métodos que expressam durações ou tempo como uma primitiva precisam ser nomeados com a unidade de tempo e usar long

Os métodos que aceitam ou retornam durações como uma primitiva precisam ter o sufixo do nome do método com as unidades de tempo associadas (como Millis, Nanos, Seconds) para reservar o nome não decorado para uso com java.time.Duration. Consulte Tempo.

Os métodos também precisam ser anotados adequadamente com a unidade e a base de tempo:

  • @CurrentTimeMillisLong: o valor é um carimbo de data/hora não negativo medido como o número de milissegundos desde 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong: o valor é um carimbo de data/hora não negativo medido como o número de segundos desde 1970-01-01T00:00:00Z.
  • @DurationMillisLong: o valor é uma duração não negativa em milissegundos.
  • @ElapsedRealtimeLong: o valor é um carimbo de data/hora não negativo na base de tempo SystemClock.elapsedRealtime().
  • @UptimeMillisLong: o valor é um carimbo de data/hora não negativo na base de tempo SystemClock.uptimeMillis().

Parâmetros de tempo primitivos ou valores de retorno precisam usar long, não int.

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

Os métodos que expressam unidades de tempo devem preferir abreviações não abreviadas para nomes de unidades

public void setIntervalNs(long intervalNs);

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

public void setTimeoutMicros(long timeoutMicros);

Anotar argumentos de tempo longo

A plataforma inclui várias anotações para fornecer uma tipagem mais forte para unidades de tempo do tipo long:

  • @CurrentTimeMillisLong: o valor é um carimbo de data/hora não negativo medido como o número de milissegundos desde 1970-01-01T00:00:00Z, ou seja, na base de tempo System.currentTimeMillis().
  • @CurrentTimeSecondsLong: o valor é um carimbo de data/hora não negativo medido como o número de segundos desde 1970-01-01T00:00:00Z.
  • @DurationMillisLong: o valor é uma duração não negativa em milissegundos.
  • @ElapsedRealtimeLong: o valor é um carimbo de data/hora não negativo na base de tempo SystemClock#elapsedRealtime().
  • @UptimeMillisLong: o valor é um carimbo de data/hora não negativo na base de tempo SystemClock#uptimeMillis().

Unidades de medida

Para todos os métodos que expressam uma unidade de medida diferente de tempo, prefira prefixos de unidade do SI em letras maiúsculas.

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

Colocar parâmetros opcionais no final das sobrecargas

Se você tiver sobrecargas de um método com parâmetros opcionais, mantenha esses parâmetros no final e mantenha a ordem consistente com os outros parâmetros:

public int doFoo(boolean flag);

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

public int doFoo(boolean flag, int id);

Ao adicionar sobrecargas para argumentos opcionais, o comportamento dos métodos mais simples precisa ser exatamente o mesmo que se argumentos padrão tivessem sido fornecidos aos métodos mais elaborados.

Corolário: não sobrecarregue métodos, exceto para adicionar argumentos opcionais ou aceitar diferentes tipos de argumentos se o método for polimórfico. Se o método sobrecarregado fizer algo fundamentalmente diferente, dê um novo nome a ele.

Os métodos com parâmetros padrão precisam ser anotados com @JvmOverloads (somente Kotlin)

Métodos e construtores com parâmetros padrão precisam ser anotados com @JvmOverloads para manter a compatibilidade binária.

Consulte Sobrecargas de função para padrões no guia oficial de interoperabilidade entre Kotlin e Java para mais detalhes.

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

Não remover valores de parâmetro padrão (somente Kotlin)

Se um método foi enviado com um parâmetro com um valor padrão, a remoção do valor padrão é uma mudança de origem.

Os parâmetros de método mais distintos e de identificação devem ser os primeiros

Se você tiver um método com vários parâmetros, coloque os mais relevantes primeiro. Os parâmetros que especificam flags e outras opções são menos importantes do que aqueles que descrevem o objeto que está sendo acessado. Se houver um callback de conclusão, coloque-o por último.

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);

Consulte também: Colocar parâmetros opcionais no final das sobrecargas

Construtores

O padrão Builder é recomendado para criar objetos Java complexos e é comumente usado no Android em casos em que:

  • As propriedades do objeto resultante precisam ser imutáveis
  • Há um grande número de propriedades obrigatórias, por exemplo, muitos argumentos do construtor
  • Há uma relação complexa entre as propriedades no momento da construção. Por exemplo, uma etapa de verificação é necessária. Esse nível de complexidade muitas vezes indica problemas com a usabilidade da API.

Considere se você precisa de um criador. Os builders são úteis em uma plataforma de API se forem usados para:

  • Configurar apenas alguns de um conjunto potencialmente grande de parâmetros de criação opcionais
  • Configurar muitos parâmetros de criação opcionais ou obrigatórios diferentes, às vezes de tipos semelhantes ou correspondentes, em que os sites de chamada podem se tornar confusos para ler ou propensos a erros para escrever
  • Configure a criação de um objeto de forma incremental, em que vários códigos de configuração diferentes podem fazer chamadas no builder como detalhes de implementação.
  • Permitir que um tipo cresça adicionando outros parâmetros de criação opcionais em versões futuras da API

Se você tiver um tipo com três ou menos parâmetros obrigatórios e nenhum opcional, quase sempre poderá pular um builder e usar um construtor simples.

As classes de origem Kotlin devem preferir construtores com anotação @JvmOverloads com argumentos padrão em vez de builders, mas podem optar por melhorar a usabilidade para clientes Java também fornecendo builders nos casos descritos anteriormente.

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

As classes builder precisam retornar o builder

As classes Builder precisam ativar a vinculação de métodos retornando o objeto Builder (como this) de todos os métodos, exceto build(). Outros objetos criados precisam ser transmitidos como argumentos. Não retorne o builder de um objeto diferente. Exemplo:

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();
  }
}

Em casos raros em que uma classe de builder base precisa oferecer suporte à extensão, use um tipo de retorno genérico:

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

As classes Builder precisam ser criadas por um construtor

Para manter a criação consistente de builders na plataforma da API do Android, todos os builders precisam ser criados usando um construtor, e não um método criador estático. Para APIs baseadas em Kotlin, o Builder precisa ser público, mesmo que os usuários do Kotlin dependam implicitamente do builder por meio de um mecanismo de criação de estilo de método de fábrica/DSL. As bibliotecas não podem usar @PublishedApi internal para ocultar seletivamente o construtor da classe Builder dos clientes do Kotlin.

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

Todos os argumentos para construtores de builder precisam ser obrigatórios (como @NonNull)

Opcional, por exemplo, @Nullable, os argumentos precisam ser movidos para métodos de setter. O construtor do builder precisa gerar uma NullPointerException (use Objects.requireNonNull) se algum argumento obrigatório não for especificado.

As classes do Builder precisam ser classes internas estáticas finais dos tipos de build

Para fins de organização lógica em um pacote, as classes Builder geralmente precisam ser expostas como classes internas finais dos tipos criados, por exemplo, Tone.Builder em vez de ToneBuilder.

Os builders podem incluir um construtor para criar uma nova instância a partir de uma instância existente

Os builders podem incluir um construtor de cópia para criar uma nova instância de builder a partir de um builder ou objeto criado. Eles não devem fornecer métodos alternativos para criar instâncias de builder a partir de builders ou objetos de build atuais.

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);
  }
}
Os setters do builder precisam usar argumentos @Nullable se o builder tiver um construtor de cópia

A redefinição é essencial se uma nova instância de um builder puder ser criada a partir de uma instância existente. Se nenhum construtor de cópia estiver disponível, o builder poderá ter argumentos @Nullable ou @NonNullable.

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
Os setters do Builder podem usar argumentos @Nullable para propriedades opcionais

Muitas vezes, é mais simples usar um valor nullable para entrada de segundo grau, especialmente no Kotlin, que usa argumentos padrão em vez de builders e sobrecargas.

Além disso, os setters @Nullable vão corresponder a eles com os getters, que precisam ser @Nullable para propriedades opcionais.

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();
}

Uso comum em Kotlin:

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

O valor padrão (se o setter não for chamado) e o significado de null precisam ser documentados corretamente no setter e no getter.

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

Os setters do builder podem ser fornecidos para propriedades mutáveis em que os setters estão disponíveis na classe criada

Se a classe tiver propriedades mutáveis e precisar de uma classe Builder, primeiro pergunte a si mesmo se ela realmente precisa ter propriedades mutáveis.

Em seguida, se você tiver certeza de que precisa de propriedades mutáveis, decida qual dos seguintes cenários funciona melhor para seu caso de uso:

  1. O objeto criado precisa ser imediatamente utilizável. Portanto, os setters precisam ser fornecidos para todas as propriedades relevantes, mutáveis ou imutáveis.

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. Talvez seja necessário fazer outras chamadas antes que o objeto criado possa ser útil. Por isso, os setters não devem ser fornecidos para propriedades mutáveis.

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

Não misture os dois cenários.

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

Os formadores não podem ter getters.

O getter precisa estar no objeto criado, não no builder.

Os setters do builder precisam ter getters correspondentes na classe criada

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();
}

Nomeação de métodos do builder

Os nomes dos métodos do builder precisam usar o estilo setFoo(), addFoo() ou clearFoo().

As classes Builder precisam declarar um método build()

As classes Builder precisam declarar um método build() que retorne uma instância do objeto criado.

Os métodos builder build() precisam retornar objetos @NonNull

O método build() de um builder precisa retornar uma instância não nula do objeto construído. Caso o objeto não possa ser criado devido a parâmetros inválidos, a validação pode ser adiada para o método de build, e um IllegalStateException precisa ser gerado.

Não expor bloqueios internos

Os métodos na API pública não podem usar a palavra-chave synchronized. Essa palavra-chave faz com que seu objeto ou classe seja usado como bloqueio. Como ela é exposta a outros, você pode encontrar efeitos colaterais inesperados se outro código fora da sua classe começar a usá-la para bloqueio.

Em vez disso, execute qualquer bloqueio necessário em um objeto interno e particular.

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

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

Os métodos com estilo de accessor precisam seguir as diretrizes de propriedade do Kotlin

Quando visualizados em origens do Kotlin, métodos com estilo de acessador, que usam os prefixos get, set ou is, também ficam disponíveis como propriedades do Kotlin. Por exemplo, int getField() definido em Java está disponível no Kotlin como a propriedade val field: Int.

Por esse motivo, e para atender às expectativas dos desenvolvedores em relação ao comportamento do método de acesso, os métodos que usam prefixos de método de acesso precisam se comportar de maneira semelhante aos campos Java. Evite usar prefixos do tipo de acessório quando:

  • O método tem efeitos colaterais. Use um nome de método mais descritivo.
  • O método envolve trabalho computacionalmente caro. Use compute
  • O método envolve trabalho de bloqueio ou de longa duração para retornar um valor, como IPC ou outra E/S. Use fetch.
  • O método bloqueia a linha de execução até que possa retornar um valor. Use await.
  • O método retorna uma nova instância de objeto em cada chamada. Use create
  • O método pode não retornar um valor. Use request.

A execução de um trabalho computacionalmente caro uma vez e o armazenamento em cache do valor para chamadas subsequentes ainda conta como a execução de um trabalho computacionalmente caro. O jank não é amortizado entre os frames.

Usar o prefixo is para métodos de acesso booleanos

Essa é a convenção de nomenclatura padrão para campos e métodos booleanos em Java. Em geral, os nomes de método e de variável booleanos precisam ser escritos como perguntas que são respondidas pelo valor de retorno.

Os métodos de acessor booleanos Java precisam seguir um esquema de nomenclatura set/is e os campos precisam preferir is, como em:

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

O uso de set/is para métodos de acesso Java ou is para campos Java permite que eles sejam usados como propriedades do Kotlin:

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

Propriedades e métodos de acesso geralmente precisam usar nomes positivos, por exemplo, Enabled em vez de Disabled. O uso de terminologia negativa inverte o significado de true e false e dificulta a compreensão do comportamento.

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

Nos casos em que o booleano descreve a inclusão ou a propriedade de uma propriedade, use has em vez de is. No entanto, isso não funciona com a sintaxe de propriedade do 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();

Alguns prefixos alternativos que podem ser mais adequados incluem can e should:

// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();

// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();

Os métodos que alternam comportamentos ou recursos podem usar o prefixo is e o sufixo 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()

Da mesma forma, métodos que indicam a dependência de outros comportamentos ou recursos podem usar o prefixo is e o sufixo Supported ou 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()

Em geral, os nomes de métodos precisam ser escritos como perguntas que são respondidas pelo valor de retorno.

Métodos de propriedade do Kotlin

Para uma propriedade de classe var foo: Foo, o Kotlin vai gerar métodos get/set usando uma regra consistente: antecipe get e torne o primeiro caractere maiúsculo para o getter e antecipe set e torne o primeiro caractere maiúsculo para o setter. A declaração de propriedade vai produzir métodos com os nomes public Foo getFoo() e public void setFoo(Foo foo), respectivamente.

Se a propriedade for do tipo Boolean, uma regra adicional será aplicada na geração de nomes: se o nome da propriedade começar com is, get não será adicionado ao nome do método getter. O nome da propriedade será usado como o getter. Portanto, prefira nomear propriedades Boolean com um prefixo is para seguir a diretriz de nomenclatura:

var isVisible: Boolean

Se a sua propriedade for uma das exceções mencionadas e começar com um prefixo adequado, use a anotação @get:JvmName na propriedade para especificar manualmente o nome adequado:

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

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

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

Acessórios de máscara de bits

Consulte Usar @IntDef para flags de máscara de bits para ver diretrizes da API sobre como definir flags de máscara de bits.

Setters

Dois métodos de setter precisam ser fornecidos: um que recebe um bitstring completo e substitui todas as flags existentes e outro que recebe uma máscara de bits personalizada para permitir mais flexibilidade.

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

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

Getters

Um getter precisa ser fornecido para conseguir a máscara de bits completa.

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

Use "Público" em vez de "Protegido"

Sempre dê preferência a public em vez de protected na API pública. O acesso protegido acaba sendo doloroso a longo prazo, porque os implementadores precisam substituir para fornecer acessores públicos em casos em que o acesso externo por padrão seria tão bom.

A visibilidade protected não impede que os desenvolvedores chamem uma API, mas apenas a torna um pouco mais irritante.

Não implementar ou implementar ambos equals() e hashCode()

Se você substituir um, terá que substituir o outro.

Implementar toString() para classes de dados

As classes de dados são incentivadas a substituir toString() para ajudar os desenvolvedores a depurar o código.

Documentar se a saída é para comportamento do programa ou depuração

Decida se o comportamento do programa vai depender da sua implementação ou não. Por exemplo, UUID.toString() e File.toString() documentam o formato específico para uso de programas. Se você estiver expondo informações apenas para depuração, como Intent, implique a herança de documentos da superclasse.

Não inclua informações extras

Todas as informações disponíveis em toString() também precisam estar disponíveis na API pública do objeto. Caso contrário, você está incentivando os desenvolvedores a analisar e confiar na saída toString(), o que vai impedir mudanças futuras. Uma boa prática é implementar toString() usando apenas a API pública do objeto.

Desestimular a dependência da saída de depuração

Embora seja impossível impedir que os desenvolvedores dependam da saída de depuração, incluir o System.identityHashCode do objeto na saída toString() vai tornar muito improvável que dois objetos diferentes tenham uma saída toString() igual.

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

Isso pode desencorajar os desenvolvedores a escrever declarações de teste como assertThat(a.toString()).isEqualTo(b.toString()) nos objetos.

Use createFoo ao retornar objetos recém-criados

Use o prefixo create, não get ou new, para métodos que vão criar valores de retorno, por exemplo, construindo novos objetos.

Quando o método vai criar um objeto para retornar, deixe isso claro no nome do método.

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

Os métodos que aceitam objetos File também precisam aceitar streams

Os locais de armazenamento de dados no Android nem sempre são arquivos no disco. Por exemplo, o conteúdo transmitido entre os limites do usuário é representado como Uris content://. Para ativar o processamento de várias fontes de dados, as APIs que aceitam objetos File também precisam aceitar InputStream, OutputStream ou ambos.

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

Receber e retornar primitivos brutos em vez de versões em caixa

Se você precisar comunicar valores ausentes ou nulos, use -1, Integer.MAX_VALUE ou Integer.MIN_VALUE.

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

Evitar equivalentes de classe de tipos primitivos evita a sobrecarga de memória dessas classes, o acesso de método a valores e, mais importante, o autoencaixe que vem do casting entre tipos primitivos e de objetos. Evitar esses comportamentos economizar na memória e em alocações temporárias que podem levar a coletas de lixo caras e mais frequentes.

Usar anotações para esclarecer parâmetros válidos e valores de retorno

As anotações para desenvolvedores foram adicionadas para ajudar a esclarecer os valores permitidos em várias situações. Isso facilita a ajuda de ferramentas aos desenvolvedores quando eles fornecem valores incorretos (por exemplo, transmitindo um int arbitrário quando o framework requer um de um conjunto específico de valores constantes). Use todas as seguintes anotações quando apropriado:

Nulidade

As anotações de nulidade explícitas são necessárias para APIs Java, mas o conceito de nulidade faz parte da linguagem Kotlin, e as anotações de nulidade nunca devem ser usadas em APIs Kotlin.

@Nullable:indica que um determinado valor de retorno, parâmetro ou campo pode ser nulo:

@Nullable
public String getName()

public void setName(@Nullable String name)

@NonNull:indica que um determinado valor de retorno, parâmetro ou campo não pode ser nulo. Marcar elementos como @Nullable é relativamente novo no Android, então a maioria dos métodos de API do Android não são documentados de forma consistente. Portanto, temos um tri-estado de "unknown, @Nullable, @NonNull". Por isso, @NonNull faz parte das diretrizes da API:

@NonNull
public String getName()

public void setName(@NonNull String name)

Para documentos da plataforma Android, a anotação dos parâmetros do método vai gerar automaticamente a documentação no formato "Este valor pode ser nulo", a menos que "null" seja usado explicitamente em outro lugar no documento do parâmetro.

Métodos "não anuláveis" atuais:métodos atuais na API sem uma anotação @Nullable declarada podem ser anotados como @Nullable se o método puder retornar null em circunstâncias específicas e óbvias (como findViewById()). Métodos @NotNull requireFoo() complementares que geram IllegalArgumentException precisam ser adicionados para desenvolvedores que não querem verificar nulos.

Métodos de interface:as novas APIs precisam adicionar a anotação adequada ao implementar métodos de interface, como Parcelable.writeToParcel(). Ou seja, esse método na classe de implementação precisa ser writeToParcel(@NonNull Parcel, int), não writeToParcel(Parcel, int). No entanto, as APIs que não têm as anotações não precisam ser "corrigidas".

Aplicação de nulidade

Em Java, os métodos são recomendados para realizar a validação de entrada de parâmetros @NonNull usando Objects.requireNonNull() e gerar uma NullPointerException quando os parâmetros são nulos. Isso é realizado automaticamente no Kotlin.

Recursos

Identificadores de recursos:os parâmetros inteiros que denotam IDs para recursos específicos precisam ser anotados com a definição adequada do tipo de recurso. Há uma anotação para cada tipo de recurso, como @StringRes, @ColorRes e @AnimRes, além do @AnyRes de uso geral. Por exemplo:

public void setTitle(@StringRes int resId)

@IntDef para conjuntos de constantes

Constantes mágicas:os parâmetros String e int que devem receber um de um conjunto finito de valores possíveis indicados por constantes públicas precisam ser anexados adequadamente com @StringDef ou @IntDef. Essas anotações permitem criar uma nova anotação que funciona como um typedef para parâmetros permitidos. Exemplo:

/** @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);

Os métodos são recomendados para verificar a validade dos parâmetros anotados e lançar um IllegalArgumentException se o parâmetro não fizer parte do @IntDef.

@IntDef para flags de máscara de bits

A anotação também pode especificar que as constantes são flags e podem ser combinadas com & e I:

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

@StringDef para conjuntos de constantes de string

Há também a anotação @StringDef, que é exatamente como @IntDef na seção anterior, mas para constantes String. É possível incluir vários valores de "prefixo", que são usados para emitir automaticamente a documentação de todos os valores.

@SdkConstant para constantes do SDK

@SdkConstant Anotação de campos públicos quando eles são um destes valores SdkConstant: ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE.

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

Fornecer nulidade compatível para substituições

Para compatibilidade com a API, a nulidade das substituições precisa ser compatível com a nulidade atual do elemento pai. A tabela a seguir representa as expectativas de compatibilidade. As substituições precisam ser tão restritivas ou mais restritivas do que o elemento que elas substituem.

Tipo Pai/mãe filho(a);
Tipo de retorno Sem anotações Não anotado ou não nulo
Tipo de retorno Nullable Nullable ou não nulo
Tipo de retorno Nonnull Nonnull
Argumento divertido Sem anotações Não anotado ou anulável
Argumento divertido Nullable Nullable
Argumento divertido Nonnull Nullable ou não nulo

Sempre que possível, prefira argumentos não nulos (como @NonNull).

Quando os métodos são sobrecarregados, é preferível que todos os argumentos sejam não nulos.

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

Essa regra também se aplica a setters de propriedade sobrecarregados. O argumento principal precisa ser não nulo, e a limpeza da propriedade precisa ser implementada como um método separado. Isso evita chamadas "nonsense" em que o desenvolvedor precisa definir parâmetros de final de linha, mesmo que eles não sejam necessários.

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()

Prefira tipos de retorno não anuláveis (como @NonNull) para contêineres.

Para tipos de contêiner, como Bundle ou Collection, retorne um contêiner vazio e imutável, quando aplicável. Nos casos em que null seria usado para distinguir a disponibilidade de um contêiner, considere fornecer um método booleano separado.

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

As anotações de nulidade para pares de get e set precisam ser iguais

Os pares de métodos de acesso e definição para uma única propriedade lógica sempre precisam concordar nas anotações de nulidade. Não seguir essa diretriz vai anular a sintaxe de propriedades do Kotlin. Portanto, adicionar anotações de nulidade conflitantes a métodos de propriedade existentes é uma mudança de origem para usuários do Kotlin.

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

Valor de retorno em condições de falha ou erro

Todas as APIs precisam permitir que os apps reajam a erros. O retorno de false, -1, null ou outros valores "pega-tudo" de "algo deu errado" não informa ao desenvolvedor o suficiente sobre a falha em definir as expectativas do usuário ou rastrear com precisão a confiabilidade do app no campo. Ao projetar uma API, imagine que você está criando um app. Se você encontrar um erro, a API vai fornecer informações suficientes para apresentá-lo ao usuário ou reagir de maneira adequada?

  1. É permitido (e incentivado) incluir informações detalhadas em uma mensagem de exceção, mas os desenvolvedores não precisam analisá-la para processar o erro adequadamente. Códigos de erro detalhados ou outras informações precisam ser expostos como métodos.
  2. Verifique se a opção de tratamento de erros escolhida oferece flexibilidade para introduzir novos tipos de erros no futuro. Para @IntDef, isso significa incluir um valor OTHER ou UNKNOWN. Ao retornar um novo código, você pode verificar o targetSdkVersion do autor da chamada para evitar o retorno de um código de erro que o app não conhece. Para exceções, tenha uma superclasse comum que suas exceções implementem, para que qualquer código que processe esse tipo também capture e processe os subtipos.
  3. Deve ser difícil ou impossível para um desenvolvedor ignorar acidentalmente um erro. Se o erro for comunicado retornando um valor, anexe uma anotação ao método com @CheckResult.

Prefira gerar uma ? extends RuntimeException quando uma condição de falha ou erro for alcançada devido a algo que o desenvolvedor fez errado, por exemplo, ignorando restrições em parâmetros de entrada ou deixando de verificar o estado observável.

Os métodos de ação ou setter (por exemplo, perform) podem retornar um código de status inteiro se a ação falhar devido a um estado atualizado de forma assíncrona ou condições fora do controle do desenvolvedor.

Os códigos de status precisam ser definidos na classe que contém como campos public static final, com prefixo ERROR_ e enumerados em uma anotação @IntDef @hide.

Os nomes dos métodos sempre devem começar com o verbo, não com o sujeito

O nome do método sempre deve começar com o verbo (como get, create, reload etc.), não com o objeto em que você está agindo.

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

Prefira tipos de coleção em vez de matrizes como tipo de retorno ou parâmetro

As interfaces de coleção com tipo genérico oferecem várias vantagens em relação a matrizes, incluindo contratos de API mais fortes em relação à exclusividade e ordenação, suporte a genéricos e vários métodos de conveniência para desenvolvedores.

Exceção para primitivos

Se os elementos forem primitivos, use matrizes para evitar o custo do autoboxing. Consulte Receber e retornar primitivos brutos em vez de versões encapsuladas.

Exceção para código sensível ao desempenho

Em alguns casos, quando a API é usada em código sensível à performance (como gráficos ou outras APIs de medição/layout/desenho), é aceitável usar matrizes em vez de coleções para reduzir alocações e a rotatividade da memória.

Exceção para Kotlin

As matrizes do Kotlin são invariáveis, e a linguagem Kotlin oferece APIs de utilitário amplas em matrizes. Assim, as matrizes são equivalentes a List e Collection para APIs do Kotlin que devem ser acessadas pelo Kotlin.

Preferir coleções @NonNull

Sempre prefira @NonNull para objetos de coleção. Ao retornar uma coleção vazia, use o método Collections.empty apropriado para retornar um objeto de coleção de baixo custo, com tipo correto e imutável.

Sempre prefira @NonNull para elementos de coleção quando houver suporte a anotações de tipo.

Também é recomendável usar @NonNull ao usar matrizes em vez de coleções (consulte o item anterior). Se a alocação de objetos for um problema, crie uma constante e transmita-a. Afinal, uma matriz vazia é imutável. Exemplo:

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

Mutabilidade da coleção

As APIs do Kotlin precisam preferir tipos de retorno somente leitura (não Mutable) para coleções por padrão, a menos que o contrato da API exija especificamente um tipo de retorno mutável.

No entanto, as APIs Java devem preferir tipos de retorno mutáveis por padrão, porque a implementação de APIs Java na plataforma Android ainda não oferece uma implementação conveniente de coleções imutáveis. A exceção a essa regra são os tipos de retorno Collections.empty, que são imutáveis. Nos casos em que a mutabilidade pode ser explorada por clientes, intencionalmente ou por engano, para quebrar o padrão de uso previsto da API, as APIs Java devem considerar retornar uma cópia superficial da coleção.

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

Tipos de retorno explicitamente mutáveis

As APIs que retornam coleções não devem modificar o objeto de coleção retornado após o retorno. Se a coleção retornada precisar mudar ou ser reutilizada de alguma forma, por exemplo, uma visualização adaptada de um conjunto de dados mutável, o comportamento preciso de quando o conteúdo pode mudar precisa ser documentado explicitamente ou seguir as convenções de nomenclatura da API estabelecidas.

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

A convenção .asFoo() do Kotlin é descrita abaixo e permite que a coleção retornada por .asList() mude se a coleção original mudar.

Mutabilidade dos objetos de tipo de dados retornados

Assim como as APIs que retornam coleções, as APIs que retornam objetos do tipo de dados não devem modificar as propriedades do objeto retornado após o retorno.

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

Em casos extremamente limitados, alguns códigos sensíveis ao desempenho podem se beneficiar do pool de objetos ou da reutilização. Não escreva sua própria estrutura de dados do pool de objetos e não exponha objetos reutilizados em APIs públicas. Em ambos os casos, tenha muito cuidado ao gerenciar o acesso simultâneo.

Uso do tipo de parâmetro vararg

As APIs Kotlin e Java são incentivadas a usar vararg nos casos em que o desenvolvedor provavelmente criaria uma matriz no local da chamada com o único propósito de transmitir vários parâmetros relacionados do mesmo tipo.

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

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

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

Cópias defensivas

As implementações de parâmetros vararg do Java e do Kotlin são compiladas para o mesmo bytecode com suporte a matriz e, como resultado, podem ser chamadas de código Java com uma matriz mutável. Os designers de API são fortemente incentivados a criar uma cópia superficial defensiva do parâmetro de matriz nos casos em que ele será mantido em um campo ou classe interna anônima.

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

Criar uma cópia defensiva não oferece proteção contra modificações simultâneas entre a chamada de método inicial e a criação da cópia, nem contra a mutação dos objetos contidos no vetor.

Fornecer semântica correta com parâmetros de tipo de coleção ou tipos retornados

List<Foo> é a opção padrão, mas considere outros tipos para fornecer mais significado:

  • Use Set<Foo> se a ordem dos elementos não for importante para sua API e ela não permitir ou se duplicações não tiverem sentido.

  • Collection<Foo>, se a API for indiferente à ordem e permitir duplicados.

Funções de conversão do Kotlin

O Kotlin usa com frequência .toFoo() e .asFoo() para extrair um objeto de um tipo diferente de um objeto existente, em que Foo é o nome do tipo de retorno da conversão. Isso é consistente com o JDK Object.toString(). O Kotlin leva isso adiante usando-o para conversões primitivas, como 25.toFloat().

A distinção entre as conversões nomeadas .toFoo() e .asFoo() é significativa:

Use .toFoo() ao criar um objeto novo e independente

Assim como .toString(), uma conversão "para" retorna um objeto novo e independente. Se o objeto original for modificado mais tarde, o novo objeto não vai refletir essas mudanças. Da mesma forma, se o objeto novo for modificado mais tarde, o objeto antigo não vai refletir essas mudanças.

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

Use .asFoo() ao criar um wrapper dependente, objeto decorado ou cast

A atribuição em Kotlin é realizada usando a palavra-chave as. Ela reflete uma mudança na interface, mas não uma mudança na identidade. Quando usado como um prefixo em uma função de extensão, .asFoo() decora o receptor. Uma mutação no objeto receptor original será refletida no objeto retornado por asFoo(). Uma mutação no novo objeto Foo pode ser refletida no objeto original.

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

As funções de conversão precisam ser escritas como funções de extensão

Escrever funções de conversão fora das definições da classe receptora e da classe de resultado reduz o acoplamento entre os tipos. Uma conversão ideal precisa apenas de acesso à API pública ao objeto original. Isso prova por exemplo que um desenvolvedor também pode gravar conversões análogas aos próprios tipos preferidos.

Gerar exceções específicas adequadas

Os métodos não podem gerar exceções genéricas, como java.lang.Exception ou java.lang.Throwable. Em vez disso, uma exceção específica adequada precisa ser usada, como java.lang.NullPointerException, para permitir que os desenvolvedores processem exceções sem serem muito amplos.

Erros não relacionados aos argumentos fornecidos diretamente ao método invocado publicamente devem gerar java.lang.IllegalStateException em vez de java.lang.IllegalArgumentException ou java.lang.NullPointerException.

Listeners e callbacks

Estas são as regras sobre as classes e os métodos usados para mecanismos de listener e callback.

Os nomes de classes de callback precisam ser singulares

Use MyObjectCallback em vez de MyObjectCallbacks

Os nomes dos métodos de callback precisam estar no formato on

onFooEvent significa que FooEvent está acontecendo e que o callback precisa agir em resposta.

O pretérito e o presente devem descrever o comportamento de tempo.

Os métodos de callback relacionados a eventos precisam ser nomeados para indicar se o evento já aconteceu ou está em andamento.

Por exemplo, se o método for chamado depois que uma ação de clique for realizada:

public void onClicked()

No entanto, se o método for responsável por executar a ação de clique:

public boolean onClick()

Registro de callback

Quando um listener ou callback pode ser adicionado ou removido de um objeto, os métodos associados precisam ser nomeados como add e remove ou register e unregister. Seja consistente com a convenção usada pela classe ou por outras classes no mesmo pacote. Quando não houver esse precedente, prefira adicionar e remover.

Os métodos que envolvem o registro ou cancelamento do registro de callbacks precisam especificar o nome completo do tipo de callback.

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

Evite getters para callbacks

Não adicione métodos getFooCallback(). Essa é uma saída de emergência tentadora para casos em que os desenvolvedores podem querer encadear um callback existente com a própria substituição, mas ela é frágil e dificulta a análise do estado atual para desenvolvedores de componentes. Por exemplo:

  • O Desenvolvedor A chama setFooCallback(a)
  • O desenvolvedor B chama setFooCallback(new B(getFooCallback()))
  • O desenvolvedor A quer remover o callback a e não tem como fazer isso sem conhecer o tipo de B, e B foi criado para permitir essas modificações do callback encapsulado.

Aceitar o executor para controlar o envio de callbacks

Ao registrar callbacks que não têm expectativas de linha de execução explícitas (quase em qualquer lugar fora do kit de ferramentas da interface), é altamente recomendável incluir um parâmetro Executor como parte do registro para permitir que o desenvolvedor especifique a linha de execução em que os callbacks serão invocados.

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

Como exceção às nossas orientações usuais sobre parâmetros opcionais, é aceitável fornecer uma sobrecarga omitindo o Executor, mesmo que ele não seja o argumento final na lista de parâmetros. Se o Executor não for fornecido, o callback precisa ser invocado na linha de execução principal usando Looper.getMainLooper() e precisa ser documentado no método sobrecarregado associado.

/**
 * ...
 * 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)

Problemas de implementação de Executor:confira o exemplo a seguir de um executor válido.

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

Isso significa que, ao implementar APIs que usam esse formulário, a implementação do objeto de vinculação de entrada no lado do processo do app precisa chamar Binder.clearCallingIdentity() antes de invocar o callback do app no Executor fornecido pelo app. Dessa forma, qualquer código de app que use a identidade do binder (como Binder.getCallingUid()) para verificações de permissão atribui corretamente o código em execução ao app e não ao processo do sistema que chama o app. Se os usuários da API quiserem as informações de UID ou PID do autor da chamada, isso precisará ser uma parte explícita da superfície da API, em vez de implícita, com base em onde o Executor fornecido foi executado.

A API precisa oferecer suporte à especificação de um Executor. Em casos de desempenho crítico, os apps podem precisar executar o código imediatamente ou simultaneamente com o feedback da API. Aceitar um Executor permite isso. Criar defensivamente um HandlerThread adicional ou semelhante a um trampoline de derrota esse caso de uso desejável.

Se um app for executar um código caro em algum lugar do próprio processo, avise o usuário. As soluções alternativas que os desenvolvedores de apps vão encontrar para superar suas restrições serão muito mais difíceis de oferecer suporte a longo prazo.

Exceção para callback único:quando a natureza dos eventos que estão sendo informados exige suporte a apenas uma instância de callback, use o seguinte estilo:

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

public void clearFooCallback()

Usar o executor em vez do gerenciador

O Handler do Android era usado como um padrão para redirecionar a execução de callbacks para uma linha de execução Looper específica no passado. Esse padrão foi alterado para preferir Executor, já que a maioria dos desenvolvedores de apps gerencia os próprios pools de linhas de execução, tornando a linha de execução principal ou de interface a única linha de execução Looper disponível para o app. Use Executor para dar aos desenvolvedores o controle necessário para reutilizar os contextos de execução existentes/preferidos.

Bibliotecas de simultaneidade modernas, como kotlinx.coroutines ou RxJava, fornecem mecanismos de programação próprios que executam o próprio envio quando necessário. Isso torna importante oferecer a capacidade de usar um executor direto (como Runnable::run) para evitar a latência de saltos de linha dupla. Por exemplo, um salto para postar em uma linha de execução Looper usando um Handler seguido por outro salto do framework de simultaneidade do app.

As exceções a essa diretriz são raras. As solicitações de exceção comuns incluem:

Preciso usar um Looper porque preciso de um Looper para epoll no evento. Esse pedido de exceção foi concedido porque os benefícios de Executor não podem ser aproveitados nessa situação.

Não quero que o código do app bloqueie minha linha de execução para publicar o evento. Essa solicitação de exceção normalmente não é concedida para códigos executados em um processo de app. Os apps que erram nisso só se prejudicam, sem afetar a integridade geral do sistema. Os apps que fazem isso corretamente ou usam um framework de concorrência comum não pagam penalidades de latência adicionais.

Handler é consistente localmente com outras APIs semelhantes na mesma classe. Esse pedido de exceção é concedido de acordo com a situação. A preferência é que as sobrecargas baseadas em Executor sejam adicionadas, migrando as implementações de Handler para usar a nova implementação de Executor. (myHandler::post é um Executor válido). Dependendo do tamanho da classe, do número de métodos Handler existentes e da probabilidade de os desenvolvedores precisarem usar métodos baseados em Handler com o novo método, uma exceção pode ser concedida para adicionar um novo método baseado em Handler.

Simetria no registro

Se houver uma maneira de adicionar ou registrar algo, também deve haver uma maneira de remover/cancelar o registro. Método

registerThing(Thing)

precisa ter uma correspondência

unregisterThing(Thing)

Fornecer um identificador de solicitação

Se for razoável que um desenvolvedor reutilize um callback, forneça um objeto identificador para vincular o callback à solicitação.

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

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

Objetos de callback de vários métodos

Os callbacks de vários métodos devem preferir interface e usar métodos default ao adicionar a interfaces lançadas anteriormente. Anteriormente, esta diretriz recomendava abstract class devido à falta de métodos default no Java 7.

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

Use android.os.OutcomeReceiver ao modelar uma chamada de função não bloqueante

OutcomeReceiver<R,E> informa um valor de resultado R quando bem-sucedido ou E : Throwable, caso contrário, as mesmas coisas que uma chamada de método simples pode fazer. Use OutcomeReceiver como o tipo de callback ao converter um método de bloqueio que retorna um resultado ou gera uma exceção para um método assíncrono não de bloqueio:

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

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

Os métodos assíncronos convertidos dessa maneira sempre retornam void. Qualquer resultado que requestFoo retornaria é informado ao OutcomeReceiver.onResult do parâmetro callback de requestFooAsync chamando-o no executor fornecido. Qualquer exceção que o requestFoo gerar será informada ao método OutcomeReceiver.onError da mesma maneira.

O uso de OutcomeReceiver para informar os resultados de métodos assíncronos também permite um wrapper suspend fun do Kotlin para métodos assíncronos usando a extensão Continuation.asOutcomeReceiver do androidx.core:core-ktx:

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

Extensões como essa permitem que os clientes do Kotlin chamem métodos assíncronos não bloqueadores com a conveniência de uma chamada de função simples sem bloquear a linha de execução de chamada. Essas extensões 1 para 1 para APIs da plataforma podem ser oferecidas como parte do artefato androidx.core:core-ktx no Jetpack quando combinados com verificações e considerações de compatibilidade de versão padrão. Consulte a documentação de asOutcomeReceiver para mais informações, considerações de cancelamento e exemplos.

Métodos assíncronos que não correspondem à semântica de um método que retorna um resultado ou gera uma exceção quando o trabalho é concluído não devem usar OutcomeReceiver como um tipo de callback. Em vez disso, considere uma das outras opções listadas na seção a seguir.

Prefira interfaces funcionais em vez de criar novos tipos de método abstrato simples (SAM, na sigla em inglês).

O nível 24 da API adicionou os tipos java.util.function.* (documentos de referência) que oferecem interfaces genéricas de SAM, como Consumer<T>, que são adequadas para uso como lambdas de callback. Em muitos casos, a criação de novas interfaces SAM não oferece muito valor em termos de segurança de tipo ou comunicação de intent, expandindo desnecessariamente a área da API do Android.

Considere usar estas interfaces genéricas em vez de criar outras:

Posicionamento dos parâmetros SAM

Os parâmetros SAM precisam ser colocados por último para permitir o uso idiomático do Kotlin, mesmo se o método estiver sendo sobrecarregado com parâmetros adicionais.

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Documentos

Estas são regras sobre os documentos públicos (Javadoc) para APIs.

Todas as APIs públicas precisam ser documentadas

Todas as APIs públicas precisam ter documentação suficiente para explicar como um desenvolvedor usaria a API. Suponha que o desenvolvedor tenha encontrado o método usando o preenchimento automático ou enquanto navegava pelos documentos de referência da API e tenha uma quantidade mínima de contexto da superfície da API adjacente (por exemplo, a mesma classe).

Métodos

Os parâmetros do método e os valores de retorno precisam ser documentados usando as anotações @param e @return, respectivamente. Formate o corpo do Javadoc como se ele fosse precedido por "Este método...".

Nos casos em que um método não usa parâmetros, não tem considerações especiais e retorna o que o nome do método diz, você pode omitir o @return e escrever documentos semelhantes a este:

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

Os documentos precisam ter links para outros documentos de constantes, métodos e outros elementos relacionados. Use tags Javadoc (por exemplo, @see e {@link foo}), não apenas palavras de texto simples.

No exemplo de origem a seguir:

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

Não use texto bruto ou fonte de código:

/**
 * 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) { ... }

Em vez disso, use links:

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

O uso de uma anotação IntDef, como @ValueType, em um parâmetro gera automaticamente a documentação que especifica os tipos permitidos. Consulte as orientações sobre anotações para mais informações sobre IntDef.

Execute o destino "update-api" ou "docs" ao adicionar Javadoc.

Essa regra é particularmente importante ao adicionar tags @link ou @see e garantir que a saída esteja como esperado. A saída ERROR no Javadoc geralmente é causada por links inválidos. O destino de make update-api ou docs executa essa verificação, mas o destino docs pode ser mais rápido se você estiver apenas mudando o Javadoc e não precisando executar o destino update-api.

Use {@code foo} para distinguir valores Java

Encapsule valores Java como true, false e null com {@code...} para distinguir o texto da documentação.

Ao escrever a documentação em origens Kotlin, você pode agrupar o código com chaves como faria para Markdown.

Os resumos de @param e @return precisam ser um único fragmento de frase

Os resumos de parâmetros e valores de retorno precisam começar com um caractere minúsculo e conter apenas um único fragmento de frase. Se você tiver outras informações que se estendem além de uma única frase, mova-as para o corpo do Javadoc do método:

/**
 * @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.
 */

Precisa ser alterado para:

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

As anotações nos documentos precisam de explicações

Documente por que as anotações @hide e @removed estão ocultas da API pública. Inclua instruções sobre como substituir elementos da API marcados com a anotação @deprecated.

Usar @throws para documentar exceções

Se um método gerar uma exceção verificada, por exemplo, IOException, documente a exceção com @throws. Para APIs de origem Kotlin destinadas a serem usadas por clientes Java, anote as funções com @Throws.

Se um método gerar uma exceção não verificada indicando um erro evitável, por exemplo IllegalArgumentException ou IllegalStateException, documente a exceção com uma explicação do motivo dela. A exceção gerada também precisa indicar por que ela foi gerada.

Alguns casos de exceção não verificada são considerados implícitos e não precisam ser documentados, como NullPointerException ou IllegalArgumentException, em que um argumento não corresponde a uma @IntDef ou a uma anotação semelhante que incorpora o contrato da API à assinatura do método:

/**
 * ...
 * @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.");
  }
  // ...

Ou, em Kotlin:

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

Se o método invocar um código assíncrono que pode gerar exceções, considere como o desenvolvedor descobre e responde a essas exceções. Normalmente, isso envolve encaminhar a exceção para um callback e documentar as exceções geradas no método que as recebe. As exceções assíncronas não precisam ser documentadas com @throws, a menos que sejam realmente lançadas novamente do método anotado.

Termine a primeira frase dos documentos com um ponto

A ferramenta Doclava analisa os documentos de forma simplificada, encerrando o documento de sinopse (a primeira frase, usada na descrição rápida na parte de cima dos documentos da classe) assim que encontra um ponto (.) seguido de um espaço. Isso causa dois problemas:

  • Se um documento curto não terminar com um período e se esse membro tiver documentos herdados que são coletados pela ferramenta, a sinopse também vai coletar esses documentos. Por exemplo, consulte actionBarTabStyle nos documentos R.attr, que têm a descrição da dimensão adicionada à sinopse.
  • Evite "por exemplo" na primeira frase pelo mesmo motivo, porque o Doclava termina os documentos de sinopse após "g". Por exemplo, consulte TEXT_ALIGNMENT_CENTER em View.java. O Metalava corrige automaticamente esse erro inserindo um espaço não quebrável após o período. No entanto, não cometa esse erro.

Formatar documentos para renderização em HTML

O Javadoc é renderizado em HTML. Formate os documentos da seguinte maneira:

  • As quebras de linha precisam usar uma tag <p> explícita. Não adicione uma tag </p> de fechamento.

  • Não use ASCII para renderizar listas ou tabelas.

  • As listas devem usar <ul> ou <ol> para não ordenadas e ordenadas, respectivamente. Cada item precisa começar com uma tag <li>, mas não precisa de uma tag </li> de fechamento. Uma tag </ul> ou </ol> de fechamento é necessária após o último item.

  • As tabelas devem usar <table>, <tr> para linhas, <th> para cabeçalhos e <td> para células. Todas as tags de tabela precisam de tags de fechamento correspondentes. É possível usar class="deprecated" em qualquer tag para indicar a descontinuação.

  • Para criar uma fonte de código inline, use {@code foo}.

  • Para criar blocos de código, use <pre>.

  • Todo o texto dentro de um bloco <pre> é analisado pelo navegador. Portanto, tenha cuidado com colchetes <>. É possível usar o escape com entidades HTML &lt; e &gt;.

  • Como alternativa, você pode deixar colchetes brutos <> no snippet de código se envolver as seções com {@code foo}. Exemplo:

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

Seguir o guia de estilo de referência da API

Para manter a consistência no estilo de resumos de classes, descrições de métodos e de parâmetros e outros itens, siga as recomendações nas diretrizes oficiais da linguagem Java em Como escrever comentários em documentos na ferramenta Javadoc (link em inglês).

Regras específicas do framework do Android

Essas regras são sobre APIs, padrões e estruturas de dados específicos para APIs e comportamentos integrados ao framework do Android (por exemplo, Bundle ou Parcelable).

Os criadores de intent precisam usar o padrão create*Intent()

Os criadores de intents precisam usar métodos com o nome createFooIntent().

Use o pacote em vez de criar novas estruturas de dados de uso geral

Evite criar novas estruturas de dados de uso geral para representar mapeamentos de valor tipado para chave arbitrária. Em vez disso, considere usar Bundle.

Isso geralmente aparece ao escrever APIs de plataforma que servem como canais de comunicação entre apps e serviços que não são da plataforma, em que a plataforma não lê os dados enviados pelo canal, e o contrato da API pode ser parcialmente definido fora da plataforma (por exemplo, em uma biblioteca do Jetpack).

Nos casos em que a plataforma os dados, evite usar Bundle e prefira uma classe de dados fortemente tipada.

As implementações parceláveis precisam ter um campo CREATOR público

A inflação parcelável é exposta por CREATOR, não por construtores brutos. Se uma classe implementar Parcelable, o campo CREATOR dela também precisa ser uma API pública, e o construtor de classe que recebe um argumento Parcel precisa ser privado.

Usar CharSequence para strings de interface

Quando uma string for apresentada em uma interface do usuário, use CharSequence para permitir instâncias de Spannable.

Se for apenas uma chave ou algum outro rótulo ou valor que não está visível para os usuários, String é aceitável.

Evite usar enumerações

O IntDef precisa ser usado em vez de enumerações em todas as APIs da plataforma e precisa ser considerado em APIs de biblioteca não agrupadas. Use tipos enumerados apenas quando tiver certeza de que novos valores não serão adicionados.

Benefícios doIntDef:

  • Permite adicionar valores ao longo do tempo
    • As instruções when do Kotlin podem falhar no momento da execução se não forem mais exaustivas devido a um valor de tipo enumerado adicionado na plataforma.
  • Nenhuma classe ou objeto usado no momento da execução, apenas primitivos
    • Embora o R8 ou a minificação possam evitar esse custo para APIs de biblioteca não agrupadas, essa otimização não pode afetar as classes de API da plataforma.

Benefícios do tipo enumerado

  • Recurso idiomático da linguagem Java, Kotlin
  • Ativa a chave exaustiva, uso da instrução when
    • Observação: os valores não podem mudar com o tempo. Consulte a lista anterior.
  • Nomeação com escopo claro e detectável
  • Ativa a verificação no tempo de compilação
    • Por exemplo, uma instrução when em Kotlin que retorna um valor
  • É uma classe funcional que pode implementar interfaces, ter auxiliares estáticos, exibir métodos de membro ou extensão e expor campos.

Seguir a hierarquia de camadas de pacotes do Android

A hierarquia de pacotes android.* tem uma ordem implícita, em que pacotes de nível inferior não podem depender de pacotes de nível superior.

Evite se referir ao Google, a outras empresas e aos produtos delas

A plataforma Android é um projeto de código aberto e tem como objetivo ser neutra em relação ao fornecedor. A API precisa ser genérica e igualmente utilizável por integradores de sistemas ou apps com as permissões necessárias.

As implementações parceláveis precisam ser finais

As classes parceláveis definidas pela plataforma são sempre carregadas do framework.jar. Portanto, é inválido que um app tente substituir uma implementação Parcelable.

Se o app de envio estender um Parcelable, o app de recebimento não terá a implementação personalizada do remetente para descompactar. Observação sobre a compatibilidade anterior: se a classe não era final, mas não tinha um construtor disponível publicamente, ainda é possível marcá-la como final.

Os métodos que chamam o processo do sistema precisam gerar novamente a RemoteException como RuntimeException.

O RemoteException normalmente é gerado pela AIDL interna e indica que o processo do sistema foi encerrado ou que o app está tentando enviar muitos dados. Em ambos os casos, a API pública precisa ser reenviada como RuntimeException para evitar que os apps persistam decisões de segurança ou políticas.

Se você souber que o outro lado de uma chamada Binder é o processo do sistema, esta prática recomendada é o código boilerplate:

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

Gerar exceções específicas para mudanças na API

Os comportamentos das APIs públicas podem mudar entre os níveis de API e causar falhas no app, por exemplo, para aplicar novas políticas de segurança.

Quando a API precisar gerar uma exceção para uma solicitação que era válida anteriormente, gere uma nova exceção específica em vez de uma genérica. Por exemplo, ExportedFlagRequired em vez de SecurityException (e ExportedFlagRequired pode estender SecurityException).

Isso vai ajudar os desenvolvedores de apps e as ferramentas a detectar mudanças no comportamento da API.

Implementar o construtor de cópia em vez de clone

O uso do método clone() do Java é desencorajado devido à falta de contratos de API fornecidos pela classe Object e às dificuldades inerentes à extensão de classes que usam clone(). Em vez disso, use um construtor de cópia que recebe um objeto do mesmo tipo.

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

As classes que dependem de um Builder para construção devem considerar a adição de um construtor de cópia do Builder para permitir modificações na cópia.

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

Usar ParcelFileDescriptor em vez de FileDescriptor

O objeto java.io.FileDescriptor tem uma definição ruim de propriedade, o que pode resultar em bugs obscuros de uso após o fechamento. Em vez disso, as APIs precisam retornar ou aceitar instâncias ParcelFileDescriptor. O código herdado pode converter entre PFD e FD, se necessário, usando dup() ou getFileDescriptor().

Evite usar valores numéricos de tamanho ímpar

Evite usar os valores short ou byte diretamente, porque eles geralmente limitam como você pode evoluir a API no futuro.

Evite usar o BitSet

O java.util.BitSet é ótimo para implementação, mas não para API pública. Ele é mutável, requer uma alocação para chamadas de método de alta frequência e não oferece significado semântico para o que cada bit representa.

Para cenários de alto desempenho, use um int ou long com @IntDef. Para cenários de baixo desempenho, considere uma Set<EnumType>. Para dados binários brutos, use byte[].

Preferir android.net.Uri

android.net.Uri é a encapsulação preferencial para URIs em APIs do Android.

Evite java.net.URI, porque ele é muito rigoroso na análise de URIs, e nunca use java.net.URL, porque a definição de igualdade está muito quebrada.

Ocultar anotações marcadas como @IntDef, @LongDef ou @StringDef

As anotações marcadas como @IntDef, @LongDef ou @StringDef indicam um conjunto de constantes válidas que podem ser transmitidas a uma API. No entanto, quando elas são exportadas como APIs, o compilador inline as constantes, e apenas os valores (agora inúteis) permanecem no stub da API da anotação (para a plataforma) ou JAR (para bibliotecas).

Portanto, o uso dessas anotações precisa ser marcado com a anotação @hide na plataforma ou a anotação de código @RestrictTo.Scope.LIBRARY) nas bibliotecas. Elas precisam ser marcadas como @Retention(RetentionPolicy.SOURCE) em ambos os casos para evitar que apareçam em stubs de API ou JARs.

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

Ao criar os AARs da biblioteca e do SDK da plataforma, uma ferramenta extrai as anotações e as agrupa separadamente das origens compiladas. O Android Studio lê esse formato agrupado e aplica as definições de tipo.

Não adicionar novas chaves de provedor de configuração

Não exponha novas chaves de Settings.Global, Settings.System ou Settings.Secure.

Em vez disso, adicione uma API Java de getter e setter adequada em uma classe relevante, que geralmente é uma classe "manager". Adicione um mecanismo de listener ou uma transmissão para notificar os clientes sobre as mudanças conforme necessário.

As configurações de SettingsProvider têm vários problemas em comparação com getters/setters:

  • Sem segurança de tipo.
  • Não há uma maneira unificada de fornecer um valor padrão.
  • Não há uma maneira adequada de personalizar as permissões.
    • Por exemplo, não é possível proteger a configuração com uma permissão personalizada.
  • Não há uma maneira adequada de adicionar a lógica personalizada.
    • Por exemplo, não é possível mudar o valor da configuração A dependendo do valor da configuração B.

Exemplo: Settings.Secure.LOCATION_MODE já existe há muito tempo, mas a equipe de localização o desativou para uma API Java adequada LocationManager.isLocationEnabled() e a transmissão MODE_CHANGED_ACTION, o que deu à equipe muito mais flexibilidade, e a semântica das APIs está muito mais clara agora.

Não estender Activity e AsyncTask

AsyncTask é um detalhe de implementação. Em vez disso, exponha um listener ou, no androidx, uma API ListenableFuture.

Não é possível compor subclasses Activity. A extensão da atividade para seu recurso o torna incompatível com outros recursos que exigem que os usuários façam o mesmo. Em vez disso, use ferramentas como LifecycleObserver para depender da composição.

Usar a função getUser() do contexto

Classes vinculadas a um Context, como qualquer coisa retornada de Context.getSystemService(), devem usar o usuário vinculado ao Context em vez de expor membros que segmentam usuários específicos.

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

Exceção: um método pode aceitar um argumento do usuário se aceitar valores que não representem um único usuário, como UserHandle.ALL.

Use UserHandle em vez de ints simples

O UserHandle é preferido para fornecer segurança de tipo e evitar a confusão entre IDs de usuário e UIDs.

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

Quando inevitável, um int que representa um ID do usuário precisa ser anotado com @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

Preferir listeners ou callbacks para intents de transmissão

As intents de transmissão são muito poderosas, mas resultaram em comportamentos emergentes que podem afetar negativamente a integridade do sistema. Portanto, novas intents de transmissão precisam ser adicionadas com cuidado.

Confira algumas preocupações específicas que nos levam a desencorajar a introdução de novas intents de transmissão:

  • Ao enviar transmissões sem a flag FLAG_RECEIVER_REGISTERED_ONLY, eles iniciam à força todos os apps que ainda não estão em execução. Embora isso às vezes seja um resultado esperado, pode resultar na interrupção de dezenas de apps, afetando negativamente a integridade do sistema. Recomendamos usar estratégias alternativas, como JobScheduler, para coordenar melhor quando várias pré-condições forem atendidas.

  • Ao enviar transmissões, há pouca capacidade de filtrar ou ajustar o conteúdo enviado aos apps. Isso dificulta ou impossibilita responder a preocupações de privacidade futuras ou introduzir mudanças de comportamento com base no SDK de destino do app receptor.

  • Como as filas de transmissão são um recurso compartilhado, elas podem ficar sobrecarregadas e não resultar na entrega oportuna do evento. Observamos várias filas de transmissão em uso que têm uma latência de ponta a ponta de 10 minutos ou mais.

Por esses motivos, recomendamos que novos recursos considerem usar listeners ou callbacks ou outras ferramentas, como JobScheduler, em vez de intents de transmissão.

Nos casos em que as intents de transmissão ainda são o design ideal, considere estas práticas recomendadas:

  • Se possível, use Intent.FLAG_RECEIVER_REGISTERED_ONLY para limitar a transmissão a apps que já estão em execução. Por exemplo, ACTION_SCREEN_ON usa esse design para evitar ativar apps.
  • Se possível, use Intent.setPackage() ou Intent.setComponent() para segmentar a transmissão em um app específico de interesse. Por exemplo, ACTION_MEDIA_BUTTON usa esse design para se concentrar no processamento atual de controles de reprodução do app.
  • Se possível, defina a transmissão como uma <protected-broadcast> para evitar que apps maliciosos imitem o SO.

Intents em serviços de desenvolvedor vinculados ao sistema

Serviços que são destinados a serem estendidos pelo desenvolvedor e vinculados pelo sistema, por exemplo, serviços abstratos como NotificationListenerService, podem responder a uma ação Intent do sistema. Esses serviços precisam atender aos seguintes critérios:

  1. Defina uma constante de string SERVICE_INTERFACE na classe que contém o nome totalmente qualificado da classe do serviço. Essa constante precisa ser anotada com @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION).
  2. Documente na classe que um desenvolvedor precisa adicionar um <intent-filter> à AndroidManifest.xml para receber intents da plataforma.
  3. Considere adicionar uma permissão no nível do sistema para impedir que apps maliciosos enviem Intents aos serviços do desenvolvedor.

Interoperabilidade entre Kotlin e Java

Consulte o Guia de interoperabilidade entre Kotlin e Java (link em inglês) para desenvolvedores Android para conferir uma lista completa de diretrizes. Algumas diretrizes foram copiadas para este guia para melhorar a facilidade de descoberta.

Visibilidade da API

Algumas APIs do Kotlin, como suspend funs, não são destinadas a desenvolvedores Java. No entanto, não tente controlar a visibilidade específica da linguagem usando @JvmSynthetic, porque isso tem efeitos colaterais na forma como a API é apresentada em depurar que dificultam a depuração.

Consulte o Guia de interoperabilidade entre Kotlin e Java ou o Guia de assincronia para orientações específicas.

Objetos complementares

O Kotlin usa companion object para expor membros estáticos. Em alguns casos, elas vão aparecer em Java em uma classe interna chamada Companion, e não na classe que contém. As classes Companion podem aparecer como classes vazias em arquivos de texto de API. Isso está funcionando como esperado.

Para maximizar a compatibilidade com Java, anote os campos não constantes dos objetos complementares com @JvmField e as funções públicas com @JvmStatic para que eles sejam expostos diretamente na classe que contém.

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

Evolução das APIs da plataforma Android

Esta seção descreve políticas sobre os tipos de mudanças que você pode fazer nas APIs do Android e como implementar essas mudanças para maximizar a compatibilidade com apps e bases de código atuais.

Alterações interruptivas binárias

Evite mudanças de interrupção binária em plataformas de API públicas finalizadas. Esses tipos de mudanças geralmente geram erros ao executar make update-api, mas pode haver casos extremos que a verificação de API do Metalava não detecta. Em caso de dúvida, consulte o guia Evolving Java-based APIs da Eclipse Foundation para uma explicação detalhada sobre quais tipos de mudanças de API são compatíveis com Java. As mudanças interruptivas binárias em APIs ocultas (por exemplo, do sistema) precisam seguir o ciclo de descontinuação/substituição.

Mudanças que quebram a origem

Não recomendamos mudanças que quebrem a origem, mesmo que não sejam binárias. Um exemplo de mudança binária compatível, mas que quebra a origem, é adicionar um tipo genérico a uma classe existente, que é compatível com binários, mas pode introduzir erros de compilação devido à herança ou referências ambíguas. As mudanças que quebram a origem não geram erros ao executar make update-api. Portanto, é necessário entender o impacto das mudanças nas assinaturas de API existentes.

Em alguns casos, mudanças que quebram a origem se tornam necessárias para melhorar a experiência do desenvolvedor ou a correção do código. Por exemplo, adicionar anotações de nulidade a origens Java melhora a interoperabilidade com o código Kotlin e reduz a probabilidade de erros, mas geralmente requer mudanças, às vezes significativas, no código-fonte.

Mudanças nas APIs privadas

É possível mudar as APIs anotadas com @TestApi a qualquer momento.

Você precisa preservar as APIs anotadas com @SystemApi por três anos. É necessário remover ou refatorar uma API do sistema na seguinte programação:

  • API y: adição
  • API y+1: Descontinuação
    • Marque o código com @Deprecated.
    • Adicione substituições e vincule à substituição no Javadoc para o código descontinuado usando a anotação de documentos @deprecated.
    • Durante o ciclo de desenvolvimento, registre bugs contra usuários internos informando que a API está sendo descontinuada. Isso ajuda a validar se as APIs de substituição são adequadas.
  • API y+2: Remoção suave
    • Marque o código com @removed.
    • Opcionalmente, lance ou não execute operações para apps destinados ao nível atual do SDK para a versão.
  • API y+3: Remoção difícil
    • Remova completamente o código da árvore de origem.

Previsão de remoção

Consideramos a descontinuação uma mudança na API, e ela pode ocorrer em uma versão principal (como a versão com a letra). Use a anotação de origem @Deprecated e a anotação de documentos @deprecated <summary> juntas ao descontinuar as APIs. O resumo precisa incluir uma estratégia de migração. Essa estratégia pode vincular a uma API de substituição ou explicar por que você não deve usar a API:

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

Você precisa descontinuar as APIs definidas em XML e expostas em Java, incluindo atributos e propriedades estilizáveis expostas na classe android.R, com um resumo:

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

Quando descontinuar uma API

A descontinuação é mais útil para desencorajar a adoção de uma API em novo código.

Também exigimos que você marque as APIs como @deprecated antes que elas sejam @removed, mas isso não motiva muito os desenvolvedores a migrar de uma API que já estão usando.

Antes de descontinuar uma API, considere o impacto nos desenvolvedores. Os efeitos da descontinuação de uma API incluem:

  • javac emite um aviso durante a compilação.
    • Os avisos de descontinuação não podem ser suprimidos globalmente ou como padrão, portanto, os desenvolvedores que usam -Werror precisam corrigir ou suprimir individualmente cada uso de uma API descontinuada antes de atualizar a versão do SDK de compilação.
    • Os avisos de descontinuação nas importações de classes descontinuadas não podem ser suprimidos. Como resultado, os desenvolvedores precisam inserir o nome da classe totalmente qualificado em cada uso de uma classe descontinuada antes de atualizar a versão do SDK de compilação.
  • A documentação sobre d.android.com mostra um aviso de descontinuação.
  • Ambientes de desenvolvimento integrados (IDEs, na sigla em inglês) como o Android Studio mostram um aviso no site de uso da API.
  • Os IDEs podem rebaixar a classificação ou ocultar a API do preenchimento automático.

Como resultado, a descontinuação de uma API pode desencorajar os desenvolvedores mais preocupados com a integridade do código (aqueles que usam -Werror) a adotar novos SDKs. Os desenvolvedores que não se preocupam com avisos no código atual provavelmente vão ignorar as descontinuações.

Um SDK que introduz um grande número de descontinuações piora esses dois casos.

Por esse motivo, recomendamos a descontinuação de APIs apenas nos casos em que:

  • Planejamos @remove a API em uma versão futura.
  • O uso da API leva a um comportamento incorreto ou indefinido que não podemos corrigir sem interromper a compatibilidade.

Quando você descontinuar uma API e substituí-la por uma nova, recomendamos adicionar uma API de compatibilidade correspondente a uma biblioteca do Jetpack, como androidx.core, para simplificar o suporte a dispositivos antigos e novos.

Não recomendamos a descontinuação de APIs que funcionam conforme o esperado nas versões atuais e futuras:

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

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

A descontinuação é adequada nos casos em que as APIs não podem mais manter os comportamentos documentados:

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

Mudanças em APIs descontinuadas

É necessário manter o comportamento das APIs descontinuadas. Isso significa que as implementações de teste precisam permanecer as mesmas, e os testes precisam continuar sendo aprovados depois que a API for descontinuada. Se a API não tiver testes, adicione-os.

Não expanda as plataformas de API descontinuadas em versões futuras. É possível adicionar anotações de correção de lint (por exemplo, @Nullable) a uma API descontinuada existente, mas não é possível adicionar novas APIs.

Não adicione novas APIs como descontinuadas. Se alguma API foi adicionada e posteriormente descontinuada em um ciclo de pré-lançamento (ou seja, inicialmente entraria na API pública como descontinuada), você precisa removê-las antes de finalizar a API.

Exclusão reversível

A remoção parcial é uma mudança que quebra a origem, e você deve evitá-la em APIs públicas, a menos que o Conselho de API a aprove explicitamente. Para APIs do sistema, é necessário descontinuar a API durante a duração de uma versão principal antes de uma remoção suave. Remova todas as referências de documentos para as APIs e use a anotação de documentos @removed <summary> ao remover as APIs. O resumo precisa incluir o motivo da remoção e pode incluir uma estratégia de migração, conforme explicado em Descontinuação.

O comportamento das APIs removidas parcialmente pode ser mantido como está, mas o mais importante é que elas precisam ser preservadas para que os autores de chamadas atuais não falhem ao chamar a API. Em alguns casos, isso pode significar preservar o comportamento.

A cobertura de teste precisa ser mantida, mas o conteúdo dos testes pode precisar mudar para acomodar mudanças comportamentais. Os testes ainda precisam validar que os autores de chamadas atuais não falham no momento da execução. Você pode manter o comportamento das APIs removidas parcialmente, mas o mais importante é que elas precisam ser preservadas para que os autores de chamadas atuais não falhem ao chamar a API. Em alguns casos, isso pode significar preservar o comportamento.

Você precisa manter a cobertura de teste, mas o conteúdo dos testes pode precisar mudar para acomodar mudanças comportamentais. Os testes ainda precisam validar que os autores de chamadas atuais não falham no momento da execução.

Em um nível técnico, removemos a API do JAR do stub do SDK e do caminho de classe no tempo de compilação usando a anotação Javadoc @remove, mas ela ainda existe no caminho de classe no tempo de execução, semelhante às APIs @hide:

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

Do ponto de vista do desenvolvedor de apps, a API não aparece mais no preenchimento automático e o código-fonte que faz referência a ela não é compilado quando o compileSdk é igual ou mais recente do que o SDK em que a API foi removida. No entanto, o código-fonte continua sendo compilado com sucesso em SDKs anteriores e binários que fazem referência à API.

Algumas categorias de API não podem ser removidas de forma suave. Não é possível remover soft algumas categorias de API.

Métodos abstratos

Não é possível remover métodos abstratos de forma suave em classes que os desenvolvedores podem estender. Isso torna impossível para os desenvolvedores estenderem a classe em todos os níveis do SDK.

Em casos raros em que nunca e nunca será possível para os desenvolvedores estenderem uma classe, ainda é possível remover métodos abstratos de forma suave.

Remoção forçada

A remoção permanente é uma mudança binária e nunca deve ocorrer em APIs públicas.

Anotação desencorajada

Usamos a anotação @Discouraged para indicar que não recomendamos uma API na maioria dos casos (mais de 95%). As APIs desaconselhadas são diferentes das APIs descontinuadas porque existe um caso de uso crítico restrito que impede a descontinuação. Ao marcar uma API como desaconselhada, você precisa fornecer uma explicação e uma solução alternativa:

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

Não é recomendável adicionar novas APIs como desaconselhadas.

Mudanças no comportamento de APIs atuais

Em alguns casos, talvez seja necessário mudar o comportamento de implementação de uma API existente. Por exemplo, no Android 7.0, melhoramos o DropBoxManager para comunicar claramente quando os desenvolvedores tentavam postar eventos muito grandes para enviar pelo Binder.

No entanto, para evitar problemas com apps atuais, recomendamos preservar um comportamento seguro para apps mais antigos. Historicamente, protegemos essas mudanças de comportamento com base na ApplicationInfo.targetSdkVersion do app, mas recentemente migramos para exigir o uso do framework de compatibilidade do app. Confira um exemplo de como implementar uma mudança de comportamento usando esse novo framework:

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

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

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

O uso desse design do framework de compatibilidade de apps permite que os desenvolvedores desativem temporariamente mudanças de comportamento específicas durante as versões Beta e de prévia como parte da depuração dos apps, em vez de forçar a adaptação a dezenas de mudanças de comportamento simultaneamente.

Compatibilidade com versões futuras

A compatibilidade futura é uma característica de design que permite que um sistema aceite entradas destinadas a uma versão posterior dele mesmo. No caso do design da API, é preciso prestar atenção especial ao design inicial e às mudanças futuras porque os desenvolvedores esperam programar o código uma vez, testá-lo uma vez e executá-lo em todos os lugares sem problemas.

Os seguintes itens causam os problemas de compatibilidade futura mais comuns no Android:

  • Adição de novas constantes a um conjunto (como @IntDef ou enum) que antes era considerado completo (por exemplo, onde switch tem um default que gera uma exceção).
  • Foi adicionado suporte a um recurso que não é capturado diretamente na plataforma da API, por exemplo, suporte para a atribuição de recursos do tipo ColorStateList em XML, em que anteriormente só eram aceitos recursos <color>.
  • Restrições mais flexíveis nas verificações de tempo de execução, por exemplo, a remoção de uma verificação requireNotNull() que estava presente em versões anteriores.

Em todos esses casos, os desenvolvedores descobrem que algo está errado apenas no momento de execução. Pior ainda, eles podem descobrir isso como resultado de relatórios de erros de dispositivos mais antigos no campo.

Além disso, esses casos são mudanças de API tecnicamente válidas. Elas não interrompem a compatibilidade binária ou de origem, e o lint da API não detecta nenhum desses problemas.

Como resultado, os designers de API precisam prestar atenção ao modificar classes existentes. Faça a pergunta: "Essa mudança vai fazer com que o código escrito e testado somente na versão mais recente da plataforma falhe em versões anteriores?"

Esquemas XML

Se um esquema XML servir como uma interface estável entre os componentes, esse esquema precisa ser especificado explicitamente e evoluir de uma maneira compatível com versões anteriores, semelhante a outras APIs do Android. Por exemplo, a estrutura de elementos e atributos XML precisa ser preservada de forma semelhante à maneira como os métodos e variáveis são mantidos em outras plataformas da API do Android.

Descontinuação do XML

Se você quiser descontinuar um elemento ou atributo XML, adicione o marcador xs:annotation, mas continue oferecendo suporte a todos os arquivos XML seguindo o ciclo de vida de evolução @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>

Os tipos de elementos precisam ser preservados

Os esquemas oferecem suporte aos elementos sequence, choice e all como elementos filhos do elemento complexType. No entanto, esses elementos filhos diferem no número e na ordem dos elementos filhos. Portanto, modificar um tipo existente seria uma mudança incompatível.

Se você quiser modificar um tipo existente, a prática recomendada é descontinuar o tipo antigo e introduzir um novo tipo para substituí-lo.

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

Padrões específicos da linha principal

O Mainline é um projeto que permite atualizar subsistemas ("mainline modules") do SO Android individualmente, em vez de atualizar toda a imagem do sistema.

Os módulos principais precisam ser "desempacotados" da plataforma principal, o que significa que todas as interações entre cada módulo e o restante do mundo precisam ser feitas usando APIs formais (públicas ou do sistema).

Há certos padrões de design que os módulos principais precisam seguir. Esta seção os descreve.

O padrão <Module>FrameworkInitializer

Se um módulo principal precisar expor classes @SystemService (por exemplo, JobScheduler), use o seguinte padrão:

  • Exponha uma classe <YourModule>FrameworkInitializer do seu módulo. Essa classe precisa estar em $BOOTCLASSPATH. Exemplo: StatsFrameworkInitializer

  • Marque-o com @SystemApi(client = MODULE_LIBRARIES).

  • Adicione um método public static void registerServiceWrappers() a ele.

  • Use SystemServiceRegistry.registerContextAwareService() para registrar uma classe de gerenciador de serviços quando ela precisar de uma referência a um Context.

  • Use SystemServiceRegistry.registerStaticService() para registrar uma classe de gerenciador de serviços quando ela não precisar de uma referência a um Context.

  • Chame o método registerServiceWrappers() do inicializador estático de SystemServiceRegistry.

O padrão <Module>ServiceManager

Normalmente, para registrar objetos de vinculação de serviço do sistema ou receber referências a eles, é necessário usar ServiceManager, mas os módulos principais não podem usá-lo porque ele está oculto. Essa classe está oculta porque os módulos principais não podem registrar ou se referir a objetos de vinculação de serviço do sistema expostos pela plataforma estática ou por outros módulos.

Os módulos principais podem usar o padrão a seguir para registrar e receber referências a serviços de vinculação implementados no módulo.

  • Crie uma classe <YourModule>ServiceManager, seguindo o design de TelephonyServiceManager.

  • Exponha a classe como @SystemApi. Se você só precisar acessar o recurso de classes $BOOTCLASSPATH ou classes do servidor do sistema, use @SystemApi(client = MODULE_LIBRARIES). Caso contrário, @SystemApi(client = PRIVILEGED_APPS) funcionará.

  • Essa classe consiste em:

    • Um construtor oculto, para que apenas o código estático da plataforma possa ser instanciado.
    • Métodos getter públicos que retornam uma instância ServiceRegisterer para um nome específico. Se você tiver um objeto de vinculação, precisará de um método getter. Se você tiver dois, precisará de dois getters.
    • Em ActivityThread.initializeMainlineModules(), instancie essa classe e transmita-a a um método estático exposto pelo módulo. Normalmente, você adiciona uma API @SystemApi(client = MODULE_LIBRARIES) estática na classe FrameworkInitializer que a usa.

Esse padrão impediria que outros módulos principais acessassem essas APIs, porque não há como outros módulos conseguirem uma instância de <YourModule>ServiceManager, mesmo que as APIs get() e register() sejam visíveis para eles.

Confira como a telefonia recebe uma referência ao serviço de telefonia: link de pesquisa de código.

Se você implementar um objeto de vinculação de serviço em código nativo, use as APIs nativas AServiceManager. Essas APIs correspondem às APIs Java ServiceManager, mas as nativas são expostas diretamente aos módulos principais. Não use-as para registrar ou se referir a objetos de vinculação que não são de propriedade do módulo. Se você expor um objeto de vinculação de forma nativa, o <YourModule>ServiceManager.ServiceRegisterer não precisará de um método register().

Definições de permissões em módulos principais

Os módulos principais que contêm APKs podem definir permissões (personalizadas) no APK AndroidManifest.xml da mesma forma que um APK normal.

Se a permissão definida for usada apenas internamente em um módulo, o nome da permissão vai precisar ter o nome do pacote do APK como prefixo, por exemplo:

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

Se a permissão definida for fornecida como parte de uma API de plataforma atualizável para outros apps, o nome da permissão precisa ter o prefixo "android.permission". (como qualquer permissão estática da plataforma) mais o nome do pacote do módulo, para indicar que é uma API de plataforma de um módulo, evitando conflitos de nomenclatura, por exemplo:

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

Em seguida, o módulo pode expor esse nome de permissão como uma constante de API na superfície da API, por exemplo, HealthPermissions.READ_ACTIVE_CALORIES_BURNED.