이 페이지는 개발자가 API 위원회에서 API 검토 시 적용하는 일반적인 원칙을 이해할 수 있도록 안내하기 위한 것입니다.
개발자는 API를 작성할 때 이러한 가이드라인을 따르는 것 외에도 이러한 규칙의 다수를 API에 대해 실행하는 검사에 인코딩하는 API 린트 도구를 실행해야 합니다.
이 가이드는 린트 도구에서 준수하는 규칙에 관한 가이드와 해당 도구에 높은 정확도로 코딩할 수 없는 규칙에 관한 일반적인 조언으로 구성되어 있습니다.
API 린트 도구
API 린트는 Metalava 정적 분석 도구에 통합되어 있으며 CI의 유효성 검사 중에 자동으로 실행됩니다. m
checkapi
를 사용하여 로컬 플랫폼 체크아웃에서 수동으로 실행하거나 ./gradlew :path:to:project:checkApi
를 사용하여 로컬 AndroidX 체크아웃에서 수동으로 실행할 수 있습니다.
API 규칙
이 가이드라인이 작성되기 전에 Android 플랫폼과 많은 Jetpack 라이브러리가 존재했으며 이 페이지 뒷부분에 설명된 정책은 Android 생태계의 요구사항을 충족하기 위해 지속적으로 발전하고 있습니다.
따라서 일부 기존 API는 가이드라인을 따르지 않을 수 있습니다. 반대로 새 API가 가이드라인을 엄격하게 준수하기보다는 기존 API와 일관성을 유지하는 것이 앱 개발자에게 더 나은 사용자 환경을 제공할 수 있습니다.
해결해야 하는 API 또는 업데이트해야 하는 가이드라인에 관한 어려운 질문이 있는 경우 판단에 따라 API 위원회에 문의하세요.
API 기본사항
이 카테고리는 Android API의 핵심 측면과 관련이 있습니다.
모든 API를 구현해야 합니다.
API의 대상 (예: 공개 또는 @SystemApi
)과 관계없이 모든 API 노출 영역은 API로 병합되거나 노출될 때 구현되어야 합니다. 나중에 구현할 API 스텁을 나중에 구현할 구현과 병합하지 마세요.
구현이 없는 API 노출 영역에는 여러 문제가 있습니다.
- 적절하거나 완전한 노출 영역이 보장되지는 않습니다. API가 클라이언트에서 테스트되거나 사용될 때까지는 클라이언트에 기능을 사용할 수 있는 적절한 API가 있는지 확인할 방법이 없습니다.
- 구현이 없는 API는 개발자 프리뷰에서 테스트할 수 없습니다.
- 구현이 없는 API는 CTS에서 테스트할 수 없습니다.
모든 API를 테스트해야 합니다.
이는 플랫폼 CTS 요구사항, AndroidX 정책, 일반적으로 API를 구현해야 한다는 생각에 부합합니다.
API 노출 영역을 테스트하면 API 노출 영역을 사용할 수 있고 예상되는 사용 사례가 해결되었다는 기본 보장이 제공됩니다. 존재 여부를 테스트하는 것만으로는 충분하지 않습니다. API 자체의 동작을 테스트해야 합니다.
새 API를 추가하는 변경사항에는 동일한 CL 또는 Gerrit 주제에 상응하는 테스트가 포함되어야 합니다.
API는 테스트 가능해야 합니다. '앱 개발자는 내 API를 사용하는 코드를 어떻게 테스트하나요?'라는 질문에 답변할 수 있어야 합니다.
모든 API에 문서가 있어야 합니다.
문서는 API 사용성의 핵심 부분입니다. API 노출 영역의 문법은 명확해 보이지만, 새 클라이언트는 API의 시맨틱, 동작 또는 컨텍스트를 이해하지 못합니다.
생성된 모든 API는 가이드라인을 준수해야 합니다.
도구에서 생성된 API는 직접 작성한 코드와 동일한 API 가이드라인을 따라야 합니다.
API 생성에 권장되지 않는 도구:
AutoValue
: 다양한 방식으로 가이드라인을 위반합니다. 예를 들어 AutoValue의 작동 방식으로 최종 값 클래스나 최종 빌더를 구현할 방법이 없습니다.
코드 스타일
이 카테고리는 특히 공개 API를 작성할 때 개발자가 사용해야 하는 일반적인 코드 스타일과 관련이 있습니다.
별도로 명시되어 있는 경우를 제외하고 표준 코딩 규칙을 따르세요.
Android 코딩 규칙은 외부 참여자를 위해 다음과 같이 문서화되어 있습니다.
https://source.android.com/source/code-style.html
전반적으로 표준 Java 및 Kotlin 코딩 규칙을 따르는 경향이 있습니다.
메서드 이름에서 약어는 대문자로 표기해서는 안 됩니다.
예를 들어 메서드 이름은 runCTSTests
가 아니라 runCtsTests
여야 합니다.
이름이 Impl로 끝나서는 안 됩니다.
이렇게 하면 구현 세부정보가 노출되므로 피해야 합니다.
클래스
이 섹션에서는 클래스, 인터페이스, 상속과 관련된 규칙을 설명합니다.
적절한 기본 클래스에서 새 공개 클래스 상속
상속은 적절하지 않을 수 있는 API 요소를 서브클래스에 노출합니다.
예를 들어 FrameLayout
의 새로운 공개 서브클래스는 FrameLayout
에 새로운 동작과 API 요소가 추가된 것처럼 보입니다. 상속된 API가 사용 사례에 적합하지 않은 경우 트리 위쪽의 클래스(예: ViewGroup
또는 View
)에서 상속합니다.
UnsupportedOperationException
를 발생시키기 위해 기본 클래스의 메서드를 재정의하고 싶은 경우 사용 중인 기본 클래스를 다시 생각해 보세요.
기본 컬렉션 클래스 사용
컬렉션을 인수로 사용하거나 값으로 반환할 때 항상 특정 구현보다 기본 클래스를 사용하는 것이 좋습니다 (예: ArrayList<Foo>
대신 List<Foo>
반환).
API에 적절한 제약조건을 표현하는 기본 클래스를 사용합니다. 예를 들어 컬렉션의 순서가 지정되어야 하는 API에는 List
를 사용하고 컬렉션이 고유한 요소로 구성되어야 하는 API에는 Set
를 사용합니다.
Kotlin에서는 변경 불가능한 컬렉션을 사용하는 것이 좋습니다. 자세한 내용은 컬렉션 변경 가능성을 참고하세요.
추상 클래스와 인터페이스 비교
Java 8에는 기본 인터페이스 메서드에 대한 지원이 추가되어 API 설계자가 바이너리 호환성을 유지하면서 인터페이스에 메서드를 추가할 수 있습니다. 플랫폼 코드와 모든 Jetpack 라이브러리는 Java 8 이상을 타겟팅해야 합니다.
기본 구현이 스테이트리스인 경우 API 디자이너는 추상 클래스보다 인터페이스를 선호해야 합니다. 즉, 기본 인터페이스 메서드는 다른 인터페이스 메서드 호출로 구현할 수 있습니다.
기본 구현에 생성자 또는 내부 상태가 필요한 경우 추상 클래스를 사용해야 합니다.
두 경우 모두 API 디자이너는 람다로의 사용을 간소화하기 위해 단일 메서드 추상화를 남겨 둘 수 있습니다.
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) { }
}
클래스 이름은 확장하는 항목을 반영해야 함
예를 들어 Service
를 확장하는 클래스의 이름은 명확성을 위해 FooService
여야 합니다.
public class IntentHelper extends Service {}
public class IntentService extends Service {}
일반 접미사
유틸리티 메서드 모음에 Helper
및 Util
와 같은 일반 클래스 이름 접미사를 사용하지 마세요. 대신 메서드를 연결된 클래스 또는 Kotlin 확장 함수에 직접 배치합니다.
메서드가 여러 클래스를 연결하는 경우 포함된 클래스에 역할을 설명하는 의미 있는 이름을 지정합니다.
매우 제한된 경우에 Helper
접미사를 사용하는 것이 적절할 수 있습니다.
- 기본 동작 구성에 사용됩니다.
- 기존 동작을 새 클래스에 위임해야 할 수 있습니다.
- 영구 저장 상태가 필요할 수 있음
- 일반적으로
View
가 포함됩니다.
예를 들어 도움말 텍스트를 백포팅하려면 View
와 연결된 상태를 유지하고 View
에서 여러 메서드를 호출하여 백포트를 설치해야 하는 경우 TooltipHelper
가 허용되는 클래스 이름입니다.
IDL 생성 코드를 공개 API로 직접 노출하지 않음
IDL 생성 코드를 구현 세부정보로 유지합니다. 여기에는 protobuf, 소켓, FlatBuffers 또는 기타 Java 및 NDK가 아닌 API 노출 영역이 포함됩니다. 하지만 Android의 대부분의 IDL은 AIDL에 있으므로 이 페이지에서는 AIDL에 중점을 둡니다.
생성된 AIDL 클래스는 API 스타일 가이드 요구사항을 충족하지 않으며 (예: 오버로드를 사용할 수 없음) AIDL 도구는 언어 API 호환성을 유지하도록 명시적으로 설계되지 않았으므로 공개 API에 삽입할 수 없습니다.
대신 AIDL 인터페이스가 처음에는 얕은 래퍼이더라도 AIDL 인터페이스 위에 공개 API 레이어를 추가합니다.
바인더 인터페이스
Binder
인터페이스가 구현 세부정보인 경우 향후 공개 레이어를 통해 필요한 하위 호환성을 유지하면서 자유롭게 변경할 수 있습니다. 예를 들어 내부 호출에 새 인수를 추가하거나 일괄 처리 또는 스트리밍을 사용하거나 공유 메모리를 사용하여 IPC 트래픽을 최적화해야 할 수 있습니다. AIDL 인터페이스가 공개 API이기도 한 경우에는 이 중 아무것도 할 수 없습니다.
예를 들어 FooService
를 공개 API로 직접 노출하지 마세요.
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
대신 관리자 또는 다른 클래스 내에 Binder
인터페이스를 래핑합니다.
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
나중에 이 호출에 새 인수가 필요한 경우 내부 인터페이스는 공개 API에 추가된 최소한의 편리한 오버로드가 될 수 있습니다. 래핑 레이어를 사용하여 구현이 발전함에 따라 다른 이전 버전과의 호환성 문제를 처리할 수 있습니다.
/**
* @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);
}
}
Android 플랫폼의 일부가 아닌 Binder
인터페이스 (예: 앱에서 사용할 수 있도록 Google Play 서비스에서 내보낸 서비스 인터페이스)의 경우 안정적이고 게시되었으며 버전이 지정된 IPC 인터페이스가 필요하므로 인터페이스 자체를 발전시키는 것이 훨씬 더 어렵습니다. 하지만 다른 API 가이드라인에 맞추고 필요한 경우 새 버전의 IPC 인터페이스에 동일한 공개 API를 더 쉽게 사용할 수 있도록 래퍼 레이어를 사용하는 것이 좋습니다.
공개 API에서 원시 바인더 객체를 사용하지 않음
Binder
객체는 그 자체로 의미가 없으므로 공개 API에서 사용해서는 안 됩니다. 일반적인 사용 사례 중 하나는 ID 시맨틱이 있으므로 Binder
또는 IBinder
를 토큰으로 사용하는 것입니다. 원시 Binder
객체를 사용하는 대신 래퍼 토큰 클래스를 사용하세요.
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() {...}
}
관리자 클래스는 최종 클래스여야 합니다.
관리자 클래스는 final
로 선언해야 합니다. 관리자 클래스는 시스템 서비스와 통신하며 단일 상호작용 지점입니다. 맞춤설정할 필요가 없으므로 final
로 선언합니다.
CompletableFuture 또는 Future를 사용하지 않음
java.util.concurrent.CompletableFuture
에는 future 값의 임의 변이를 허용하고 오류가 발생하기 쉬운 기본값이 있는 대규모 API 노출 영역이 있습니다.
반대로 java.util.concurrent.Future
에는 비차단 수신이 없으므로 비동기 코드에서 사용하기 어렵습니다.
플랫폼 코드 및 Kotlin과 Java에서 모두 사용하는 하위 수준 라이브러리 API에서는 완료 콜백 Executor
과 API가 취소를 지원하는 경우 CancellationSignal
의 조합을 사용하는 것이 좋습니다.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
Kotlin을 타겟팅하는 경우 suspend
함수를 사용하는 것이 좋습니다.
suspend fun asyncLoadFoo(): Foo
Java별 통합 라이브러리에서는 Guava의 ListenableFuture
를 사용할 수 있습니다.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
선택사항을 사용하지 않음
Optional
는 일부 API 노출 영역에서 유리할 수 있지만 기존 Android API 노출 영역과 일치하지 않습니다. @Nullable
및 @NonNull
는 null
안전을 위한 도구 지원을 제공하며 Kotlin은 컴파일러 수준에서 null 허용 여부 계약을 시행하므로 Optional
가 필요하지 않습니다.
선택적 원시 유형의 경우 페어링된 has
및 get
메서드를 사용합니다. 값이 설정되지 않은 경우 (has
가 false
를 반환함) get
메서드는 IllegalStateException
을 발생시켜야 합니다.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
인스턴스화 불가능한 클래스에 비공개 생성자 사용
Builder
로만 만들 수 있는 클래스, 상수 또는 정적 메서드만 포함하는 클래스 또는 인스턴스화할 수 없는 다른 클래스는 기본 인수가 없는 생성자를 사용하여 인스턴스화하는 것을 방지하기 위해 비공개 생성자를 하나 이상 포함해야 합니다.
public final class Log {
// Not instantiable.
private Log() {}
}
싱글톤
싱글톤은 다음과 같은 테스트 관련 단점이 있으므로 권장하지 않습니다.
- 생성은 클래스에서 관리하므로 가짜를 사용할 수 없습니다.
- 싱글톤의 정적 특성으로 인해 테스트를 밀폐형으로 만들 수 없음
- 이러한 문제를 해결하려면 개발자가 싱글톤의 내부 세부정보를 알고 있거나 싱글톤을 둘러싸는 래퍼를 만들어야 합니다.
이러한 문제를 해결하기 위해 추상 기본 클래스를 사용하는 단일 인스턴스 패턴을 사용하는 것이 좋습니다.
단일 인스턴스
단일 인스턴스 클래스는 private
또는 internal
생성자가 있는 추상 기본 클래스를 사용하고 인스턴스를 가져오는 정적 getInstance()
메서드를 제공합니다. getInstance()
메서드는 후속 호출에서 동일한 객체를 반환해야 합니다.
getInstance()
에서 반환된 객체는 추상 기본 클래스의 비공개 구현이어야 합니다.
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
}
}
}
단일 인스턴스는 개발자가 가짜 버전의 SingleInstance
를 만들고 자체 종속 항목 주입 프레임워크를 사용하여 래퍼를 만들지 않고도 구현을 관리할 수 있거나 라이브러리가 -testing
아티팩트에 자체 가짜를 제공할 수 있다는 점에서 싱글톤과 다릅니다.
리소스를 해제하는 클래스는 AutoCloseable을 구현해야 함
close
, release
, destroy
또는 유사한 메서드를 통해 리소스를 해제하는 클래스는 개발자가 try-with-resources
블록을 사용할 때 이러한 리소스를 자동으로 정리할 수 있도록 java.lang.AutoCloseable
를 구현해야 합니다.
android.*에 새 View 서브클래스를 도입하지 마세요.
플랫폼 공개 API (android.*
)에서 android.view.View
에서 직접 또는 간접적으로 상속받는 새 클래스를 도입하지 마세요.
이제 Android의 UI 도구 키트는 Compose 우선입니다. 플랫폼에서 노출하는 새로운 UI 기능은 Jetpack 라이브러리에서 개발자를 위해 Jetpack Compose 및 선택적으로 뷰 기반 UI 구성요소를 구현하는 데 사용할 수 있는 하위 수준 API로 노출되어야 합니다. 라이브러리에서 이러한 구성요소를 제공하면 플랫폼 기능을 사용할 수 없는 경우 백포팅된 구현을 사용할 수 있습니다.
필드
이 규칙은 클래스의 공개 필드에 관한 규칙입니다.
원시 필드를 노출하지 않음
Java 클래스는 필드를 직접 노출해서는 안 됩니다. 필드는 최종인지 여부와 관계없이 비공개여야 하며 공개 getter 및 setter를 통해서만 액세스할 수 있어야 합니다.
드물지만 필드를 지정하거나 검색하는 동작을 개선할 필요가 없는 기본 데이터 구조가 예외가 될 수 있습니다. 이 경우 필드의 이름은 표준 변수 이름 지정 규칙(예: Point.x
및 Point.y
)을 사용하여 지정해야 합니다.
Kotlin 클래스는 속성을 노출할 수 있습니다.
노출된 필드는 final로 표시해야 합니다.
원시 필드는 사용하지 않는 것이 좋습니다 (@see
원시 필드 노출 금지). 하지만 필드가 공개 필드로 노출되는 드문 경우 해당 필드를 final
로 표시합니다.
내부 필드는 노출되어서는 안 됩니다.
공개 API에서 내부 필드 이름을 참조하지 마세요.
public int mFlags;
protected 대신 public 사용
상수
다음은 공개 상수에 관한 규칙입니다.
플래그 상수가 int 또는 long 값과 겹쳐서는 안 됩니다.
플래그는 일부 유니언 값으로 결합할 수 있는 비트를 의미합니다. 그렇지 않은 경우 변수 또는 상수 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;
공개 플래그 상수를 정의하는 방법에 관한 자세한 내용은 비트 마스크 플래그용 @IntDef
를 참고하세요.
static final 상수는 모두 대문자이고 밑줄로 구분된 이름 지정 규칙을 사용해야 합니다.
상수의 모든 단어는 대문자로 표기해야 하며 여러 단어는 _
로 구분해야 합니다. 예를 들면 다음과 같습니다.
public static final int fooThing = 5
public static final int FOO_THING = 5
상수에 표준 접두사 사용
Android에서 사용되는 상수의 대부분은 플래그, 키, 작업과 같은 표준 항목을 위한 것입니다. 이러한 상수는 이러한 항목으로 더 쉽게 식별할 수 있도록 표준 접두사를 사용해야 합니다.
예를 들어 인텐트 추가 항목은 EXTRA_
로 시작해야 합니다. 인텐트 작업은 ACTION_
로 시작해야 합니다. Context.bindService()
와 함께 사용되는 상수는 BIND_
로 시작해야 합니다.
주요 상수 이름 및 범위
문자열 상수 값은 상수 이름 자체와 일치해야 하며 일반적으로 패키지 또는 도메인으로 범위가 지정되어야 합니다. 예를 들면 다음과 같습니다.
public static final String FOO_THING = "foo"
일관되게 이름이 지정되지 않았거나 적절한 범위가 지정되지 않았습니다. 대신 다음을 고려하세요.
public static final String FOO_THING = "android.fooservice.FOO_THING"
범위가 지정된 문자열 상수의 접두사 android
는 Android 오픈소스 프로젝트용으로 예약되어 있습니다.
인텐트 작업 및 부가 항목과 번들 항목은 정의된 패키지 이름을 사용하여 네임스페이스화해야 합니다.
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"
}
protected 대신 public 사용
일관된 접두사 사용
관련 상수는 모두 동일한 접두사로 시작해야 합니다. 예를 들어 플래그 값과 함께 사용할 상수 집합의 경우 다음과 같습니다.
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 상수에 표준 접두사 사용
일관된 리소스 이름 사용
공개 식별자, 속성, 값은 Java의 공개 필드와 마찬가지로 camelCase 이름 지정 규칙(예: @id/accessibilityActionPageUp
또는 @attr/textAppearance
)을 사용하여 이름을 지정해야 합니다.
공개 식별자 또는 속성에 밑줄로 구분된 공통 접두사가 포함되는 경우도 있습니다.
- config.xml의
@string/config_recentsComponentName
와 같은 플랫폼 구성 값 - attrs.xml의
@attr/layout_marginStart
와 같은 레이아웃별 뷰 속성
공개 테마 및 스타일은 Java의 중첩 클래스와 마찬가지로 계층적 PascalCase 이름 지정 규칙(예: @style/Theme.Material.Light.DarkActionBar
또는 @style/Widget.Material.SearchView.ActionBar
)을 따라야 합니다.
레이아웃 및 드로어블 리소스는 공개 API로 노출되어서는 안 됩니다. 그러나 노출해야 하는 경우 공개 레이아웃과 드로어블은 under_score 이름 지정 규칙(예: layout/simple_list_item_1.xml
또는 drawable/title_bar_tall.xml
)을 사용하여 이름을 지정해야 합니다.
상수가 변경될 수 있는 경우 동적으로 만듭니다.
컴파일러는 상수 값을 인라인할 수 있으므로 값을 동일하게 유지하는 것이 API 계약의 일부로 간주됩니다. MIN_FOO
또는 MAX_FOO
상수의 값이 향후 변경될 수 있는 경우 대신 동적 메서드로 만드는 것이 좋습니다.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
콜백의 향후 버전과의 호환성 고려
향후 API 버전에서 정의된 상수는 이전 API를 타겟팅하는 앱에 알려지지 않습니다. 따라서 앱에 전송되는 상수는 앱의 대상 API 버전을 고려하고 최신 상수를 일관된 값에 매핑해야 합니다. 다음 시나리오를 고려해 보세요.
가상의 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;
targetSdkVersion="22"
가 있는 가상의 앱:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
이 경우 앱은 API 수준 22의 제약 조건 내에서 설계되었으며 가능한 상태가 두 개뿐이라고 (다소) 합리적으로 가정했습니다. 그러나 앱이 새로 추가된 STATUS_FAILURE_RETRY
를 수신하면 이를 성공으로 해석합니다.
상수를 반환하는 메서드는 앱에서 타겟팅하는 API 수준과 일치하도록 출력을 제한하여 이와 같은 사례를 안전하게 처리할 수 있습니다.
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;
}
개발자는 상수 목록이 향후 변경될지 예측할 수 없습니다. 포괄적으로 보이는 UNKNOWN
또는 UNSPECIFIED
상수로 API를 정의하면 개발자는 앱을 작성할 때 게시된 상수가 완전하다고 가정합니다. 이 기대치를 설정하고 싶지 않다면 API에 포괄적 상수가 적합한지 다시 생각해 보세요.
또한 라이브러리는 앱과 별개로 자체 targetSdkVersion
를 지정할 수 없으며 라이브러리 코드에서 targetSdkVersion
동작 변경사항을 처리하는 것은 복잡하고 오류가 발생하기 쉽습니다.
정수 또는 문자열 상수
값의 네임스페이스가 패키지 외부에서 확장 가능하지 않은 경우 정수 상수와 @IntDef
를 사용하세요. 네임스페이스가 공유되거나 패키지 외부의 코드로 확장될 수 있는 경우 문자열 상수를 사용하세요.
data 클래스
데이터 클래스는 변경 불가능한 속성 집합을 나타내며, 해당 데이터와 상호작용하는 작고 잘 정의된 유틸리티 함수 집합을 제공합니다.
Kotlin 컴파일러가 생성된 코드의 언어 API 또는 바이너리 호환성을 보장하지 않으므로 공개 Kotlin API에서 data class
를 사용하지 마세요. 대신 필요한 함수를 수동으로 구현합니다.
인스턴스화
Java에서 데이터 클래스는 속성이 적은 경우 생성자를 제공하거나 속성이 많은 경우 Builder
패턴을 사용해야 합니다.
Kotlin에서 데이터 클래스는 속성 수와 관계없이 기본 인수가 있는 생성자를 제공해야 합니다. Kotlin에 정의된 데이터 클래스는 Java 클라이언트를 타겟팅할 때 빌더를 제공하는 것도 유용할 수 있습니다.
수정 및 복사
데이터를 수정해야 하는 경우 사본 생성자가 있는 Builder
클래스 (Java) 또는 새 객체를 반환하는 copy()
구성원 함수 (Kotlin)를 제공합니다.
Kotlin에서 copy()
함수를 제공할 때 인수는 클래스의 생성자와 일치해야 하며 기본값은 객체의 현재 값을 사용하여 채워야 합니다.
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
)
}
추가 동작
데이터 클래스는 equals()
및 hashCode()
를 모두 구현해야 하며, 이러한 메서드의 구현에서 모든 속성이 고려되어야 합니다.
데이터 클래스는 Kotlin의 데이터 클래스 구현과 일치하는 권장 형식(예: User(var1=Alex, var2=42)
)으로 toString()
를 구현할 수 있습니다.
메서드
매개변수, 메서드 이름, 반환 유형, 액세스 지정자와 관련된 메서드의 다양한 세부사항에 관한 규칙입니다.
시간
이 규칙에서는 날짜 및 기간과 같은 시간 개념을 API에서 표현하는 방법을 다룹니다.
가능하면 java.time.* 유형을 사용합니다.
java.time.Duration
, java.time.Instant
및 기타 여러 java.time.*
유형은 디슈가링을 통해 모든 플랫폼 버전에서 사용할 수 있으며 API 매개변수 또는 반환 값으로 시간을 표현할 때 선호해야 합니다.
의도된 사용 패턴에서 객체 할당이 심각한 성능 영향을 미치는 API 도메인이 아닌 한 java.time.Duration
또는 java.time.Instant
를 수락하거나 반환하는 API의 변형만 노출하고 동일한 사용 사례가 있는 원시 변형은 생략하는 것이 좋습니다.
시간을 나타내는 메서드의 이름은 duration여야 합니다.
시간 값이 관련 시간의 길이를 나타내는 경우 매개변수 이름을 '시간'이 아닌 '시간 지속'으로 지정합니다.
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
예외:
'timeout'은 기간이 특히 시간 제한 값에 적용되는 경우에 적합합니다.
유형이 java.time.Instant
인 '시간'은 지속 시간이 아닌 특정 시점을 참조할 때 적절합니다.
기간 또는 시간을 원시값으로 표현하는 메서드는 시간 단위로 이름을 지정하고 long
기간을 원시로 허용하거나 반환하는 메서드는 java.time.Duration
와 함께 사용하기 위해 장식되지 않은 이름을 예약할 수 있도록 메서드 이름 뒤에 연결된 시간 단위 (예: Millis
, Nanos
, Seconds
)를 접미사로 추가해야 합니다. 시간을 참고하세요.
메서드는 단위 및 시간 기준으로 적절하게 주석 처리되어야 합니다.
@CurrentTimeMillisLong
: 값은 1970-01-01T00:00:00Z 이후의 밀리초 수로 측정된 비양수 타임스탬프입니다.@CurrentTimeSecondsLong
: 값은 1970-01-01T00:00:00Z 이후 경과된 초 수로 측정된 비양수 타임스탬프입니다.@DurationMillisLong
: 값은 밀리초 단위의 비양수 시간입니다.@ElapsedRealtimeLong
: 값은SystemClock.elapsedRealtime()
시간 기준의 비음수 타임스탬프입니다.@UptimeMillisLong
: 값은SystemClock.uptimeMillis()
시간 기준에서 0보다 크고 0보다 작은 타임스탬프입니다.
원시 시간 매개변수 또는 반환 값은 int
가 아닌 long
를 사용해야 합니다.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
시간 단위를 표현하는 메서드는 단위 이름의 축약되지 않은 약어를 사용해야 합니다.
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
긴 시간 인수에 주석 추가
플랫폼에는 long
유형 시간 단위에 더 강력한 유형 지정을 제공하는 여러 주석이 포함되어 있습니다.
@CurrentTimeMillisLong
: 값은1970-01-01T00:00:00Z
이후의 밀리초 수로 측정된 비양수 타임스탬프이므로System.currentTimeMillis()
시간 기준을 사용합니다.@CurrentTimeSecondsLong
: 값은1970-01-01T00:00:00Z
이후 경과된 초 수로 측정된 비양수 타임스탬프입니다.@DurationMillisLong
: 값은 밀리초 단위의 비양수 시간입니다.@ElapsedRealtimeLong
: 값은SystemClock#elapsedRealtime()
시간 기준의 비음수 타임스탬프입니다.@UptimeMillisLong
: 값은SystemClock#uptimeMillis()
시간 기준의 비음수 타임스탬프입니다.
측정 단위
시간 이외의 측정 단위를 나타내는 모든 메서드의 경우 CamelCase 형식의 SI 단위 접두사를 사용하는 것이 좋습니다.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
오버로드 끝에 선택적 매개변수 배치
선택적 매개변수가 있는 메서드의 오버로드가 있는 경우 이러한 매개변수를 끝에 유지하고 다른 매개변수와 일관된 순서를 유지합니다.
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
선택적 인수의 오버로드를 추가할 때 더 간단한 메서드의 동작은 더 정교한 메서드에 기본 인수가 제공된 것과 정확히 동일한 방식으로 동작해야 합니다.
추론: 메서드가 다형식인 경우 선택적 인수를 추가하거나 다른 유형의 인수를 허용하는 것 외에는 메서드를 오버로드하지 마세요. 오버로드된 메서드가 근본적으로 다른 작업을 실행하는 경우 새 이름을 지정합니다.
기본 매개변수가 있는 메서드는 @JvmOverloads로 주석 처리되어야 합니다 (Kotlin만 해당).
기본 매개변수가 있는 메서드와 생성자는 바이너리 호환성을 유지하기 위해 @JvmOverloads
주석이 있어야 합니다.
자세한 내용은 공식 Kotlin-Java 상호 운용성 가이드의 기본값의 함수 오버로드를 참고하세요.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
기본 매개변수 값 삭제 안 함 (Kotlin만 해당)
메서드가 기본값이 있는 매개변수와 함께 제공된 경우 기본값을 삭제하면 소스 변경이 발생합니다.
가장 고유하고 식별 가능한 메서드 매개변수가 먼저 나와야 합니다.
매개변수가 여러 개인 메서드가 있는 경우 가장 관련성이 높은 매개변수를 먼저 배치합니다. 플래그 및 기타 옵션을 지정하는 매개변수는 작업 대상 객체를 설명하는 매개변수보다 중요도가 낮습니다. 완료 콜백이 있는 경우 마지막에 배치합니다.
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);
빌더
빌더 패턴은 복잡한 Java 객체를 만드는 데 권장되며 다음과 같은 경우에 Android에서 일반적으로 사용됩니다.
- 결과 객체의 속성은 변경할 수 없어야 합니다.
- 필수 속성이 많습니다(예: 많은 생성자 인수).
- 구성 시 속성 간에 복잡한 관계가 있습니다(예: 확인 단계가 필요함). 이러한 수준의 복잡성은 API의 사용성에 문제가 있음을 나타내는 경우가 많습니다.
빌더가 필요한지 고려합니다. 빌더는 다음과 같은 용도로 사용되는 경우 API 노출 영역에서 유용합니다.
- 잠재적으로 많은 수의 선택적 생성 매개변수 중 일부만 구성
- 호출 사이트의 읽기가 혼란스러워지거나 쓰기에 오류가 발생할 수 있는 경우 유사하거나 일치하는 유형의 다양한 선택적 또는 필수 생성 매개변수를 구성합니다.
- 여러 구성 코드가 각각 구현 세부정보로 빌더를 호출할 수 있는 객체 생성을 점진적으로 구성합니다.
- 향후 API 버전에서 선택적 생성 매개변수를 추가하여 유형을 확장할 수 있도록 허용
필수 매개변수가 3개 이하이고 선택적 매개변수가 없는 유형이 있는 경우 거의 항상 빌더를 건너뛰고 일반 생성자를 사용할 수 있습니다.
Kotlin 소스 클래스는 빌더보다 기본 인수가 있는 @JvmOverloads
주석 생성자를 선호해야 하지만, 앞서 설명한 경우에 빌더도 제공하여 Java 클라이언트의 사용성을 개선할 수 있습니다.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
빌더 클래스는 빌더를 반환해야 합니다.
빌더 클래스는 build()
를 제외한 모든 메서드에서 빌더 객체(예: this
)를 반환하여 메서드 체이닝을 사용 설정해야 합니다. 추가로 빌드된 객체는 인수로 전달해야 합니다. 다른 객체의 빌더를 반환하면 안 됩니다.
예를 들면 다음과 같습니다.
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();
}
}
기본 빌더 클래스가 확장 프로그램을 지원해야 하는 드문 경우는 제네릭 반환 유형을 사용합니다.
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);
}
빌더 클래스는 생성자를 통해 만들어야 합니다.
Android API 노출 영역을 통해 일관된 빌더 생성을 유지하려면 모든 빌더를 정적 생성자 메서드가 아닌 생성자를 통해 만들어야 합니다. Kotlin 기반 API의 경우 Kotlin 사용자가 팩토리 메서드/DSL 스타일 생성 메커니즘을 통해 빌더를 암시적으로 사용해야 하는 경우에도 Builder
는 공개되어야 합니다. 라이브러리는 @PublishedApi internal
를 사용하여 Kotlin 클라이언트에서 Builder
클래스 생성자를 선택적으로 숨겨서는 안 됩니다.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
빌더 생성자의 모든 인수는 필수여야 합니다 (예: @NonNull).
선택사항(예: @Nullable
) 인수는 setter 메서드로 이동해야 합니다.
필수 인수가 지정되지 않은 경우 빌더 생성자는 NullPointerException
를 발생시켜야 합니다 (Objects.requireNonNull
사용 고려).
빌더 클래스는 빌드된 유형의 최종 정적 내부 클래스여야 합니다.
패키지 내의 논리적 구성을 위해 빌더 클래스는 일반적으로 빌드된 유형의 최종 내부 클래스로 노출되어야 합니다(예: ToneBuilder
대신 Tone.Builder
).
빌더는 기존 인스턴스에서 새 인스턴스를 만드는 생성자를 포함할 수 있습니다.
빌더는 기존 빌더 또는 빌드된 객체에서 새 빌더 인스턴스를 만드는 복사 생성자를 포함할 수 있습니다. 기존 빌더 또는 빌드 객체에서 빌더 인스턴스를 만드는 대체 메서드를 제공해서는 안 됩니다.
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);
}
}
빌더에 복사 생성자가 있는 경우 빌더 setter는 @Nullable 인수를 사용해야 합니다.
기존 인스턴스에서 빌더의 새 인스턴스가 생성될 수 있는 경우 재설정이 필요합니다. 사용할 수 있는 복사 생성자가 없는 경우 빌더에 @Nullable
또는 @NonNullable
인수가 있을 수 있습니다.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
빌더 setter가 선택적 속성에 @Nullable 인수를 사용할 수 있음
특히 빌더와 오버로드 대신 기본 인수를 사용하는 Kotlin에서는 2차 입력에 nullable 값을 사용하는 것이 더 간단합니다.
또한 @Nullable
setter는 getter와 일치하며, 이는 선택적 속성의 경우 @Nullable
여야 합니다.
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();
}
Kotlin의 일반적인 사용법:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
기본값 (setter가 호출되지 않은 경우) 및 null
의 의미는 setter와 getter 모두에 적절하게 문서화되어야 합니다.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
빌드된 클래스에서 setter를 사용할 수 있는 변경 가능한 속성에 대해 빌더 setter를 제공할 수 있습니다.
클래스에 변경 가능한 속성이 있고 Builder
클래스가 필요한 경우 먼저 클래스에 변경 가능한 속성이 실제로 있어야 하는지 자문해 보세요.
다음으로, 변경 가능한 속성이 필요하다고 확신하는 경우 예상되는 사용 사례에 더 적합한 다음 시나리오 중에서 선택합니다.
빌드된 객체는 즉시 사용할 수 있어야 하므로 변경 가능 여부와 관계없이 관련 속성 전체에 setter를 제공해야 합니다.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
빌드된 객체를 유용하게 사용하려면 몇 가지 추가 호출이 필요할 수 있으므로 변경 가능한 속성에 setter를 제공해서는 안 됩니다.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
두 시나리오를 혼합하지 마세요.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
빌더에 getter가 없어야 함
getter는 빌더가 아닌 빌드된 객체에 있어야 합니다.
빌더 setter에는 빌드된 클래스에 상응하는 getter가 있어야 합니다.
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();
}
빌더 메서드 이름 지정
빌더 메서드 이름은 setFoo()
, addFoo()
또는 clearFoo()
스타일을 사용해야 합니다.
빌더 클래스는 build() 메서드를 선언해야 합니다.
빌더 클래스는 생성된 객체의 인스턴스를 반환하는 build()
메서드를 선언해야 합니다.
빌더 build() 메서드는 @NonNull 객체를 반환해야 함
빌더의 build()
메서드는 생성된 객체의 null이 아닌 인스턴스를 반환해야 합니다. 잘못된 매개변수로 인해 객체를 만들 수 없는 경우 유효성 검사를 빌드 메서드로 연기할 수 있으며 IllegalStateException
를 발생시켜야 합니다.
내부 잠금 노출하지 않음
공개 API의 메서드는 synchronized
키워드를 사용해서는 안 됩니다. 이 키워드를 사용하면 객체나 클래스가 잠금으로 사용되며 다른 코드에 노출되므로 클래스 외부의 다른 코드가 잠금 목적으로 이를 사용하기 시작하면 예상치 못한 부작용이 발생할 수 있습니다.
대신 내부 비공개 객체에 대해 필요한 잠금을 실행합니다.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
액세서리 스타일 메서드는 Kotlin 속성 가이드라인을 따라야 함
Kotlin 소스에서 보면 get
, set
또는 is
접두사를 사용하는 접근자 스타일 메서드도 Kotlin 속성으로 사용할 수 있습니다.
예를 들어 Java에서 정의된 int getField()
는 Kotlin에서 val field: Int
속성으로 사용할 수 있습니다.
따라서 액세스 메서드 동작에 관한 일반적인 개발자 기대치를 충족하려면 액세스 메서드 접두사를 사용하는 메서드가 Java 필드와 유사하게 동작해야 합니다. 다음과 같은 경우에는 접근자 스타일 접두사를 사용하지 마세요.
- 메서드에 부작용이 있음 - 더 설명적인 메서드 이름을 사용하는 것이 좋습니다.
- 계산이 비용이 많이 드는 작업이 포함된 메서드입니다.
compute
를 사용하는 것이 좋습니다. - 이 메서드는 IPC 또는 기타 I/O와 같이 값을 반환하기 위해 차단하거나 장기 실행 작업을 포함합니다.
fetch
를 사용하는 것이 좋습니다. - 이 메서드는 값을 반환할 수 있을 때까지 스레드를 차단합니다.
await
를 사용하는 것이 좋습니다. - 이 메서드는 호출할 때마다 새 객체 인스턴스를 반환합니다.
create
를 사용하는 것이 좋습니다. - 메서드가 값을 제대로 반환하지 못할 수 있습니다.
request
를 사용하는 것이 좋습니다.
계산이 비용이 많이 드는 작업을 한 번 실행하고 후속 호출을 위해 값을 캐시하는 것도 계산이 비용이 많이 드는 작업을 실행하는 것으로 간주됩니다. 버벅거림은 프레임 간에 분산되지 않습니다.
불리언 접근자 메서드에 is 접두사 사용
이는 Java의 불리언 메서드 및 필드에 대한 표준 이름 지정 규칙입니다. 일반적으로 불리언 메서드 및 변수 이름은 반환 값으로 답변되는 질문으로 작성해야 합니다.
Java 불리언 접근자 메서드는 set
/is
이름 지정 스키마를 따라야 하며 필드는 다음과 같이 is
를 선호해야 합니다.
// 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;
Java 접근자 메서드에 set
/is
를 사용하거나 Java 필드에 is
를 사용하면 Kotlin에서 속성으로 사용할 수 있습니다.
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
속성과 접근자 메서드는 일반적으로 긍정적인 이름 지정을 사용해야 합니다(예: Disabled
대신 Enabled
). 부정적인 용어를 사용하면 true
및 false
의 의미가 반전되고 동작을 추론하기가 더 어려워집니다.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
불리언이 속성의 포함 또는 소유권을 설명하는 경우 is 대신 has를 사용할 수 있습니다. 그러나 이는 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();
더 적합한 대체 접두사로는 can 및 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();
동작이나 기능을 전환하는 메서드는 is 접두사와 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()
마찬가지로 다른 동작이나 기능에 대한 종속 항목을 나타내는 메서드는 is 접두사와 Supported 또는 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()
일반적으로 메서드 이름은 반환 값으로 답변되는 질문으로 작성해야 합니다.
Kotlin 속성 메서드
클래스 속성 var foo: Foo
의 경우 Kotlin은 일관된 규칙을 사용하여 get
/set
메서드를 생성합니다. 즉, getter의 경우 get
을 접두사로 추가하고 첫 번째 문자를 대문자로, setter의 경우 set
을 접두사로 추가하고 첫 번째 문자를 대문자로 표시합니다. 속성 선언은 각각 public Foo getFoo()
및 public void setFoo(Foo foo)
라는 이름의 메서드를 생성합니다.
속성이 Boolean
유형인 경우 이름 생성에 추가 규칙이 적용됩니다. 속성 이름이 is
로 시작하면 getter 메서드 이름 앞에 get
가 추가되지 않으며 속성 이름 자체가 getter로 사용됩니다.
따라서 이름 지정 가이드라인을 따르려면 Boolean
속성의 이름을 is
접두사로 지정하는 것이 좋습니다.
var isVisible: Boolean
속성이 앞서 언급된 예외 중 하나이고 적절한 접두사로 시작하는 경우 속성의 @get:JvmName
주석을 사용하여 적절한 이름을 수동으로 지정합니다.
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
비트 마스크 접근자
비트마스크 플래그 정의와 관련된 API 가이드라인은 비트마스크 플래그에 @IntDef
사용을 참고하세요.
setter
전체 비트 문자열을 사용하여 모든 기존 플래그를 덮어쓰는 setter 메서드와 더 유연성을 제공하기 위해 맞춤 비트 마스크를 사용하는 setter 메서드 두 가지를 제공해야 합니다.
/**
* 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);
getter
전체 비트 마스크를 가져오려면 하나의 getter를 제공해야 합니다.
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
protected 대신 public 사용
공개 API에서는 항상 public
를 protected
보다 선호합니다. 보호된 액세스는 장기적으로 불편해집니다. 기본적으로 외부 액세스가 더 나은 경우에도 구현자가 재정의하여 공개 액세스자를 제공해야 하기 때문입니다.
protected
공개 상태는 개발자가 API를 호출하는 것을 방지하지는 않으며, 다만 약간 더 불편하게 만들 뿐입니다.
equals() 또는 hashCode()를 구현하지 않거나 둘 다 구현
하나를 재정의하면 다른 하나도 재정의해야 합니다.
데이터 클래스에 toString() 구현
개발자가 코드를 디버그하는 데 도움이 되도록 데이터 클래스는 toString()
를 재정의하는 것이 좋습니다.
출력이 프로그램 동작용인지 디버깅용인지 문서화합니다.
프로그램 동작이 구현에 의존할지 여부를 결정합니다. 예를 들어 UUID.toString() 및 File.toString()은 프로그램에서 사용할 수 있는 특정 형식을 문서화합니다. Intent와 같이 디버깅 전용으로 정보를 노출하는 경우 상위 클래스에서 문서를 상속한다고 암시합니다.
추가 정보를 포함하지 마세요.
toString()
에서 사용할 수 있는 모든 정보는 객체의 공개 API를 통해서도 사용할 수 있어야 합니다. 그러지 않으면 개발자가 toString()
출력을 파싱하고 이를 사용하는 것이 권장되므로 향후 변경사항이 방지됩니다. 객체의 공개 API만 사용하여 toString()
를 구현하는 것이 좋습니다.
디버그 출력에 의존하지 않도록 권장
개발자가 디버그 출력에 의존하는 것을 방지할 수는 없지만 toString()
출력에 객체의 System.identityHashCode
를 포함하면 두 개의 서로 다른 객체가 동일한 toString()
출력을 갖는 가능성은 매우 낮아집니다.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
이렇게 하면 개발자가 객체에 assertThat(a.toString()).isEqualTo(b.toString())
와 같은 테스트 어설션을 작성하지 못하도록 효과적으로 방지할 수 있습니다.
새로 생성된 객체를 반환할 때 createFoo 사용
새 객체를 생성하는 등 반환 값을 만드는 메서드에는 get
또는 new
가 아닌 접두사 create
를 사용합니다.
메서드가 반환할 객체를 만드는 경우 메서드 이름에 이를 명확하게 표시합니다.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
File 객체를 허용하는 메서드는 스트림도 허용해야 함
Android의 데이터 저장소 위치는 항상 디스크의 파일이 아닙니다. 예를 들어 사용자 경계를 넘어 전달된 콘텐츠는 content://
Uri
로 표시됩니다. 다양한 데이터 소스의 처리를 사용 설정하려면 File
객체를 허용하는 API가 InputStream
, OutputStream
또는 둘 다를 허용해야 합니다.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
박스 버전 대신 원시 프리미티브를 가져와서 반환
누락되거나 null인 값을 전달해야 하는 경우 -1
, Integer.MAX_VALUE
또는 Integer.MIN_VALUE
을 사용하는 것이 좋습니다.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
원시 유형의 클래스 등가를 피하면 이러한 클래스의 메모리 오버헤드, 값에 대한 메서드 액세스, 그리고 더 중요한 것은 원시 유형과 객체 유형 간에 전송할 때 발생하는 오토박싱을 방지할 수 있습니다. 이러한 동작을 피하면 메모리와 임시 할당이 절약되므로 비용이 많이 들고 빈번한 가비지 컬렉션이 발생할 수 있습니다.
주석을 사용하여 유효한 매개변수 및 반환 값 명확히 하기
다양한 상황에서 허용되는 값을 명확히 하는 데 도움이 되도록 개발자 주석이 추가되었습니다. 이렇게 하면 개발자가 잘못된 값을 제공할 때 도구가 더 쉽게 개발자를 지원할 수 있습니다 (예: 프레임워크에 특정 상수 값 집합 중 하나가 필요한 경우 임의의 int
전달). 적절한 경우 다음 주석을 모두 사용하세요.
null 허용 여부
Java API에는 명시적 null 허용 여부 주석이 필요하지만 null 허용 여부 개념은 Kotlin 언어의 일부이므로 Kotlin API에서는 null 허용 여부 주석을 사용해서는 안 됩니다.
@Nullable
: 지정된 반환 값, 매개변수 또는 필드가 null이 될 수 있음을 나타냅니다.
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: 지정된 반환 값, 매개변수 또는 필드가 null이 될 수 없음을 나타냅니다. 항목을 @Nullable
로 표시하는 것은 Android에서 비교적 새로운 기능이므로 대부분의 Android API 메서드는 일관되게 문서화되어 있지 않습니다. 따라서 'unknown, @Nullable
, @NonNull
'의 3가지 상태가 있으며, 이 때문에 @NonNull
가 API 가이드라인의 일부입니다.
@NonNull
public String getName()
public void setName(@NonNull String name)
Android 플랫폼 문서의 경우 메서드 매개변수에 주석을 추가하면 '이 값은 null일 수 있습니다.' 형식의 문서가 자동으로 생성됩니다. 단, 'null'이 매개변수 문서의 다른 곳에서 명시적으로 사용되지 않는 경우는 예외입니다.
기존의 '실제로는 null이 허용되지 않는' 메서드: 선언된 @Nullable
주석이 없는 API의 기존 메서드는 메서드가 특정 명확한 상황에서 (예: findViewById()
) null
를 반환할 수 있는 경우 @Nullable
주석을 추가할 수 있습니다. null 검사를 원하지 않는 개발자를 위해 IllegalArgumentException
을 발생시키는 컴패니언 @NotNull requireFoo()
메서드를 추가해야 합니다.
인터페이스 메서드: 새 API는 Parcelable.writeToParcel()
와 같은 인터페이스 메서드를 구현할 때 적절한 주석을 추가해야 합니다 (즉, 구현 클래스의 메서드는 writeToParcel(Parcel, int)
가 아닌 writeToParcel(@NonNull Parcel,
int)
여야 함). 주석이 없는 기존 API는 '수정'할 필요가 없습니다.
Null 허용 여부 적용
Java에서는 메서드가 Objects.requireNonNull()
를 사용하여 @NonNull
매개변수의 입력 유효성 검사를 실행하고 매개변수가 null인 경우 NullPointerException
를 발생시키는 것이 권장됩니다. 이는 Kotlin에서 자동으로 실행됩니다.
리소스
리소스 식별자: 특정 리소스의 ID를 나타내는 정수 매개변수에는 적절한 리소스 유형 정의가 주석으로 추가되어야 합니다.
포괄적인 @AnyRes
외에도 @StringRes
, @ColorRes
, @AnimRes
와 같은 모든 유형의 리소스에 관한 주석이 있습니다. 예를 들면 다음과 같습니다.
public void setTitle(@StringRes int resId)
상수 집합의 @IntDef
매직 상수: 공개 상수로 표시된 가능한 값의 유한 집합 중 하나를 수신하도록 설계된 String
및 int
매개변수는 @StringDef
또는 @IntDef
로 적절하게 주석 처리해야 합니다. 이러한 주석을 사용하면 허용되는 매개변수의 typedef처럼 작동하는 새 주석을 만들 수 있습니다. 예를 들면 다음과 같습니다.
/** @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);
메서드는 주석이 달린 매개변수의 유효성을 확인하고 매개변수가 @IntDef
에 포함되지 않은 경우 IllegalArgumentException
을 발생시키는 것이 권장됩니다.
비트마스크 플래그용 @IntDef
주석은 상수가 플래그임을 지정할 수도 있으며 & 및 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
@StringDef
주석도 있습니다. 이 주석은 이전 섹션의 @IntDef
와 정확히 동일하지만 String
상수에 적용됩니다. 모든 값에 관한 문서를 자동으로 내보내는 데 사용되는 여러 '접두사' 값을 포함할 수 있습니다.
SDK 상수의 @SdkConstant
@SdkConstant 공개 필드가 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";
재정의에 호환되는 null 허용 여부 제공
API 호환성을 위해 재정의의 null 허용 여부는 상위 요소의 현재 null 허용 여부와 호환되어야 합니다. 다음 표는 호환성 기대치를 나타냅니다. 재정의는 재정의하는 요소만큼 또는 그보다 더 제한적일 수 있습니다.
유형 | 부모 | 자녀용 |
---|---|---|
반환 유형 | 주석 없음 | 주석이 없거나 null이 아님 |
반환 유형 | Null 허용됨 | null 허용 여부 |
반환 유형 | Nonnull | Nonnull |
재미있는 논쟁 | 주석 없음 | 주석이 없거나 nullable |
재미있는 논쟁 | Null 허용됨 | Null 허용됨 |
재미있는 논쟁 | Nonnull | null 허용 여부 |
가능하면 null이 허용되지 않는 인수(@NonNull 등)를 사용하는 것이 좋습니다.
메서드가 오버로드된 경우 모든 인수가 null이 아닌 것이 좋습니다.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
이 규칙은 오버로드된 속성 setter에도 적용됩니다. 기본 인수는 null이 아니어야 하며 속성을 지우는 작업은 별도의 메서드로 구현해야 합니다. 이렇게 하면 개발자가 후행 매개변수가 필요하지 않더라도 후행 매개변수를 설정해야 하는 '무의미한' 호출이 방지됩니다.
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()
컨테이너에 null이 허용되지 않는 반환 유형(@NonNull 등)을 사용하는 것이 좋습니다.
Bundle
또는 Collection
와 같은 컨테이너 유형의 경우 빈 컨테이너(해당하는 경우 변경 불가)를 반환합니다. null
가 컨테이너의 가용성을 구별하는 데 사용되는 경우 별도의 불리언 메서드를 제공하는 것이 좋습니다.
@NonNull
public Bundle getExtras() { ... }
get 및 set 쌍의 null 허용 여부 주석이 일치해야 함
단일 논리 속성의 get 및 set 메서드 쌍은 항상 null 허용 여부 주석에서 일치해야 합니다. 이 가이드라인을 따르지 않으면 Kotlin의 속성 문법이 무효화되며, 기존 속성 메서드에 일치하지 않는 null 허용 여부 주석을 추가하면 Kotlin 사용자에게 소스 중단 변경사항이 됩니다.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
실패 또는 오류 조건에서 값 반환
모든 API는 앱이 오류에 반응하도록 허용해야 합니다. false
, -1
, null
또는 기타 포괄적인 '오류 발생' 값을 반환하면 개발자가 사용자 기대치를 설정하지 못하거나 현장에서 앱의 안정성을 정확하게 추적하지 못하는 이유를 충분히 파악할 수 없습니다. API를 설계할 때 앱을 빌드한다고 가정해 보세요. 오류가 발생하면 API가 사용자에게 표시하거나 적절하게 반응하기에 충분한 정보를 제공하나요?
- 예외 메시지에 자세한 정보를 포함하는 것은 좋으며 권장되지만 개발자가 오류를 적절하게 처리하기 위해 이를 파싱할 필요는 없습니다. 상세 오류 코드 또는 기타 정보는 메서드로 노출되어야 합니다.
- 선택한 오류 처리 옵션을 통해 향후 새로운 오류 유형을 유연하게 도입할 수 있는지 확인합니다.
@IntDef
의 경우OTHER
또는UNKNOWN
값을 포함하는 것을 의미합니다. 새 코드를 반환할 때 호출자의targetSdkVersion
를 확인하여 앱이 알지 못하는 오류 코드를 반환하지 않을 수 있습니다. 예외의 경우 예외가 구현하는 공통 슈퍼클래스를 보유하세요. 그러면 해당 유형을 처리하는 모든 코드가 하위유형도 포착하고 처리합니다. - 개발자가 실수로 오류를 무시하기가 어렵거나 불가능해야 합니다. 값을 반환하여 오류를 전달하는 경우 메서드에
@CheckResult
주석을 추가합니다.
입력 매개변수의 제약 조건을 무시하거나 관찰 가능한 상태를 확인하지 않는 등 개발자가 잘못한 작업으로 인해 실패 또는 오류 조건에 도달한 경우 ? extends RuntimeException
를 발생시키는 것이 좋습니다.
비동기식으로 업데이트된 상태 또는 개발자가 제어할 수 없는 조건으로 인해 작업이 실패할 수 있는 경우 setter 또는 작업 (예: perform
) 메서드는 정수 상태 코드를 반환할 수 있습니다.
상태 코드는 포함 클래스에서 public static final
필드로 정의하고 접두어로 ERROR_
를 사용하며 @hide
@IntDef
주석에서 열거해야 합니다.
메서드 이름은 항상 주제가 아닌 동사로 시작해야 합니다.
메서드 이름은 항상 작업할 객체가 아닌 동사 (예: get
, create
, reload
등)로 시작해야 합니다.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
반환 유형 또는 매개변수 유형으로 배열보다 컬렉션 유형을 사용하는 것이 좋습니다 .
일반 유형의 컬렉션 인터페이스는 고유성과 순서에 관한 더 강력한 API 계약, 제네릭 지원, 개발자 친화적인 여러 편의 메서드 등 배열에 비해 여러 이점을 제공합니다.
프리미티브 예외
요소가 프리미티브인 경우 자동 박스의 비용을 피하기 위해 대신 배열을 사용하는 것이 좋습니다. 박스 버전 대신 원시 프리미티브를 가져와서 반환을 참고하세요.
성능에 민감한 코드의 예외
API가 성능에 민감한 코드 (예: 그래픽 또는 기타 측정/레이아웃/그리기 API)에서 사용되는 특정 시나리오에서는 할당과 메모리 변경을 줄이기 위해 컬렉션 대신 배열을 사용하는 것이 허용됩니다.
Kotlin 예외
Kotlin 배열은 불변이며 Kotlin 언어는 배열과 관련된 충분한 유틸리티 API를 제공하므로 Kotlin에서 액세스하려는 Kotlin API의 경우 배열은 List
및 Collection
와 동일합니다.
@NonNull 컬렉션 선호
항상 컬렉션 객체에는 @NonNull
를 사용하세요. 빈 컬렉션을 반환할 때는 적절한 Collections.empty
메서드를 사용하여 비용이 적고 유형이 올바르며 변경 불가능한 컬렉션 객체를 반환합니다.
유형 주석이 지원되는 경우 항상 컬렉션 요소에 @NonNull
을 사용하세요.
컬렉션 대신 배열을 사용할 때도 @NonNull
를 사용하는 것이 좋습니다 (이전 항목 참고). 객체 할당이 우려되는 경우 상수를 만들어 전달합니다. 결국 빈 배열은 변경 불가능하니까요. 예:
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;
}
컬렉션 변경 가능 여부
Kotlin API는 API 계약에서 명시적으로 변경 가능한 반환 유형을 요구하지 않는 한 기본적으로 컬렉션에 읽기 전용 (Mutable
아님) 반환 유형을 사용해야 합니다.
그러나 Java API의 Android 플랫폼 구현은 아직 변경 불가능한 컬렉션의 편리한 구현을 제공하지 않으므로 Java API는 기본적으로 변경 가능한 반환 유형을 선호해야 합니다. 이 규칙의 예외는 변경할 수 없는 Collections.empty
반환 유형입니다. 클라이언트가 의도적으로 또는 실수로 변경성을 악용하여 API의 의도된 사용 패턴을 중단할 수 있는 경우 Java API는 컬렉션의 얕은 사본을 반환하는 것이 좋습니다.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
명시적으로 변경 가능한 반환 유형
컬렉션을 반환하는 API는 반환 후 반환된 컬렉션 객체를 수정하지 않는 것이 좋습니다. 반환된 컬렉션을 어떤 방식으로든 변경하거나 재사용해야 하는 경우(예: 변경 가능한 데이터 세트의 조정된 뷰) 콘텐츠가 변경될 수 있는 시점의 정확한 동작을 명시적으로 문서화하거나 기존 API 명명 규칙을 따라야 합니다.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
Kotlin .asFoo()
규칙은 아래에 설명되어 있으며 원래 컬렉션이 변경되는 경우 .asList()
에서 반환된 컬렉션이 변경되도록 허용합니다.
반환된 데이터 유형 객체의 변경 가능성
컬렉션을 반환하는 API와 마찬가지로 데이터 유형 객체를 반환하는 API는 반환 후 반환된 객체의 속성을 수정하지 않는 것이 좋습니다.
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)
}
매우 드물지만 성능에 민감한 일부 코드에서는 객체 풀링이나 재사용이 유용할 수 있습니다. 자체 객체 풀 데이터 구조를 작성하지 마세요. 또한 재사용된 객체를 공개 API에 노출하지 마세요. 두 경우 모두 동시 액세스 관리에 매우 주의해야 합니다.
vararg 매개변수 유형 사용
Kotlin 및 Java API 모두 개발자가 동일한 유형의 관련 매개변수를 여러 개 전달하기 위한 목적으로만 호출 사이트에서 배열을 만들 가능성이 있는 경우 vararg
를 사용하는 것이 좋습니다.
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);
방어적 복사
vararg
매개변수의 Java 및 Kotlin 구현은 모두 동일한 배열 지원 바이트 코드로 컴파일되므로 변경 가능한 배열이 있는 Java 코드에서 호출될 수 있습니다. API 설계자는 배열 매개변수가 필드나 익명의 내부 클래스에 유지되는 경우 배열 매개변수의 방어적 얕은 사본을 만드는 것이 좋습니다.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
방어용 사본을 만들더라도 초기 메서드 호출과 사본 생성 간에 동시 수정으로 인한 손상을 방지하지 못하며 배열에 포함된 객체의 변이와도 무관합니다.
컬렉션 유형 매개변수 또는 반환 유형으로 올바른 시맨틱 제공
List<Foo>
는 기본 옵션이지만 추가 의미를 제공하기 위해 다른 유형을 고려하세요.
API가 요소의 순서에 관계없고 중복을 허용하지 않거나 중복이 의미가 없는 경우
Set<Foo>
를 사용합니다.Collection<Foo>,
: API가 순서에 관계없고 중복을 허용하는 경우
Kotlin 변환 함수
Kotlin은 .toFoo()
및 .asFoo()
를 사용하여 기존 객체에서 다른 유형의 객체를 가져오는 경우가 많으며 여기서 Foo
는 변환의 반환 유형 이름입니다. 이는 익숙한 JDK Object.toString()
와 일치합니다. Kotlin은 이를 25.toFloat()
와 같은 원시 변환에 사용하여 한 단계 더 나아갑니다.
.toFoo()
와 .asFoo()
라는 이름의 전환은 차이가 큽니다.
독립적인 새 객체를 만들 때 .toFoo() 사용
.toString()
와 마찬가지로 'to' 변환은 새로운 독립 객체를 반환합니다. 나중에 원본 객체가 수정되면 새 객체에는 이러한 변경사항이 반영되지 않습니다.
마찬가지로 새 객체가 나중에 수정되면 기존 객체에는 이러한 변경사항이 반영되지 않습니다.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
종속 래퍼, 장식된 객체 또는 변환을 만들 때 .asFoo() 사용
Kotlin에서의 변환은 as
키워드를 사용하여 실행됩니다. 인터페이스의 변경사항은 반영하지만 ID의 변경사항은 반영하지 않습니다. 확장 함수에서 접두사로 사용하면 .asFoo()
이 수신자를 장식합니다. 원래 수신기 객체의 변형은 asFoo()
에서 반환된 객체에 반영됩니다.
새 Foo
객체의 변형은 원래 객체에 반영될 수 있습니다.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
변환 함수는 확장 함수로 작성해야 합니다.
수신기와 결과 클래스 정의 외부에 변환 함수를 작성하면 유형 간의 결합이 줄어듭니다. 이상적인 변환은 원본 객체에 대한 공개 API 액세스만 필요합니다. 이는 개발자가 원하는 유형에 상응하는 변환을 작성할 수도 있다는 것을 보여주는 예입니다.
적절한 특정 예외 발생
메서드는 java.lang.Exception
또는 java.lang.Throwable
와 같은 일반 예외를 발생시켜서는 안 됩니다. 대신 개발자가 지나치게 광범위하지 않게 예외를 처리할 수 있도록 java.lang.NullPointerException
와 같이 적절한 구체적인 예외를 사용해야 합니다.
공개적으로 호출된 메서드에 직접 제공된 인수와 관련 없는 오류는 java.lang.IllegalArgumentException
또는 java.lang.NullPointerException
대신 java.lang.IllegalStateException
를 발생시켜야 합니다.
리스너 및 콜백
리스너 및 콜백 메커니즘에 사용되는 클래스 및 메서드에 관한 규칙입니다.
콜백 클래스 이름은 단수여야 합니다.
MyObjectCallbacks
대신 MyObjectCallback
를 사용합니다.
콜백 메서드 이름은 다음 형식이어야 합니다.
onFooEvent
는 FooEvent
가 발생하고 콜백이 응답으로 작동해야 함을 나타냅니다.
과거형과 현재형은 타이밍 동작을 설명해야 합니다.
이벤트와 관련된 콜백 메서드는 이벤트가 이미 발생했는지 또는 발생 중인지를 나타내는 이름을 지정해야 합니다.
예를 들어 클릭 작업이 실행된 후에 메서드가 호출되는 경우:
public void onClicked()
그러나 메서드가 클릭 작업을 실행하는 경우:
public boolean onClick()
콜백 등록
리스너 또는 콜백을 객체에 추가하거나 삭제할 수 있는 경우 연결된 메서드의 이름은 add 및 remove 또는 register 및 unregister여야 합니다. 클래스 또는 동일한 패키지의 다른 클래스에서 사용하는 기존 규칙과 일관되게 합니다. 이러한 선례가 없는 경우 추가 및 삭제를 사용하는 것이 좋습니다.
콜백 등록 또는 등록 취소와 관련된 메서드는 콜백 유형의 전체 이름을 지정해야 합니다.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
콜백의 getter 피하기
getFooCallback()
메서드를 추가하지 마세요. 이는 개발자가 기존 콜백을 자체 대체 콜백과 함께 체이닝하려는 경우에 유용한 탈출구이지만 취약하며 구성요소 개발자가 현재 상태를 추론하기 어렵게 만듭니다. 예를 들면 다음과 같습니다.
- 개발자 A가
setFooCallback(a)
를 호출합니다. - 개발자 B가
setFooCallback(new B(getFooCallback()))
를 호출합니다. - 개발자 A는 콜백
a
를 삭제하려고 하지만B
의 유형을 알지 못하고B
가 래핑된 콜백의 이러한 수정을 허용하도록 빌드되었기 때문에 이를 삭제할 방법이 없습니다.
실행자를 수락하여 콜백 전달 제어
명시적인 스레딩 기대치가 없는 콜백을 등록할 때 (UI 도구 키트 외부의 거의 모든 위치) 개발자가 콜백이 호출될 스레드를 지정할 수 있도록 등록의 일부로 Executor
매개변수를 포함하는 것이 좋습니다.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
일반적인 선택적 매개변수에 관한 가이드라인의 예외로, Executor
가 매개변수 목록의 마지막 인수가 아니더라도 Executor
를 생략하는 오버로드를 제공하는 것이 허용됩니다. Executor
가 제공되지 않으면 콜백은 Looper.getMainLooper()
를 사용하여 기본 스레드에서 호출되어야 하며 이는 연결된 오버로드된 메서드에 문서화되어야 합니다.
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Executor
구현 문제: 다음은 유효한 실행자입니다.
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
즉, 이 형식을 사용하는 API를 구현할 때 앱 프로세스 측의 수신 바인더 객체 구현은 앱에서 제공한 Executor
에서 앱의 콜백을 호출하기 전에 Binder.clearCallingIdentity()
를 호출해야 합니다. 이렇게 하면 권한 확인을 위해 바인더 ID (예: Binder.getCallingUid()
)를 사용하는 모든 앱 코드가 앱을 호출하는 시스템 프로세스가 아닌 앱에 실행되는 코드에 올바르게 기여도를 부여합니다. API 사용자가 호출자의 UID 또는 PID 정보를 원하는 경우 이는 제공된 Executor
가 실행된 위치를 기반으로 하는 암시적 부분이 아니라 API 노출 영역의 명시적 부분이어야 합니다.
API에서 Executor
지정을 지원해야 합니다. 성능이 중요한 경우에는 앱이 코드를 즉시 실행하거나 API의 의견과 동시에 코드를 실행해야 할 수 있습니다. Executor
를 수락하면 이를 허용할 수 있습니다.
방어적으로 trampoline과 유사한 추가 HandlerThread
를 만드는 것은 바람직한 사용 사례를 무력화합니다.
앱이 자체 프로세스의 어딘가에서 리소스를 많이 소모하는 코드를 실행하려는 경우 허용합니다. 앱 개발자가 제한사항을 극복하기 위해 찾은 해결 방법은 장기적으로 지원하기가 훨씬 더 어렵습니다.
단일 콜백 예외: 보고되는 이벤트의 특성상 단일 콜백 인스턴스만 지원해야 하는 경우 다음 스타일을 사용하세요.
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
핸들러 대신 실행자 사용
Android의 Handler
는 이전에 콜백 실행을 특정 Looper
스레드로 리디렉션하는 표준으로 사용되었습니다. 대부분의 앱 개발자가 자체 스레드 풀을 관리하므로 이 표준은 Executor
를 우선하도록 변경되었습니다. 따라서 기본 스레드 또는 UI 스레드가 앱에서 사용할 수 있는 유일한 Looper
스레드가 됩니다. Executor
를 사용하여 개발자가 기존/선호하는 실행 컨텍스트를 재사용하는 데 필요한 제어 기능을 제공하세요.
kotlinx.coroutines 또는 RxJava와 같은 최신 동시 실행 라이브러리는 필요할 때 자체 전달을 실행하는 자체 예약 메커니즘을 제공하므로 이중 스레드 호핑으로 인한 지연을 방지하기 위해 직접 실행자 (예: Runnable::run
)를 사용할 수 있는 기능을 제공하는 것이 중요합니다. 예를 들어 Handler
를 사용하여 Looper
스레드에 게시하는 홉 하나와 앱의 동시 실행 프레임워크에서 발생하는 다른 홉이 있습니다.
이 가이드라인에 대한 예외는 드뭅니다. 예외를 요청하는 일반적인 사유는 다음과 같습니다.
이벤트를 epoll
하려면 Looper
가 필요하므로 Looper
를 사용해야 합니다.
이 상황에서는 Executor
의 이점을 실현할 수 없으므로 이 예외 요청이 승인됩니다.
앱 코드가 이벤트를 게시하는 스레드를 차단하지 않도록 하려면 어떻게 해야 하나요? 이 예외 요청은 일반적으로 앱 프로세스에서 실행되는 코드에는 부여되지 않습니다. 이를 잘못 이해하는 앱은 전반적인 시스템 상태에 영향을 미치지 않고 자신만 해치는 것입니다. 올바르게 실행되거나 공통 동시 실행 프레임워크를 사용하는 앱은 추가 지연 시간 패널티를 지불해서는 안 됩니다.
Handler
는 동일한 클래스의 다른 유사한 API와 로컬에서 일관됩니다.
이 예외 요청은 상황에 따라 부여됩니다. Executor
기반 오버로드를 추가하고 Handler
구현을 이전하여 새 Executor
구현을 사용하는 것이 좋습니다. (myHandler::post
는 유효한 Executor
입니다.) 클래스의 크기, 기존 Handler
메서드의 수, 개발자가 새 메서드와 함께 기존 Handler
기반 메서드를 사용해야 할 가능성에 따라 새 Handler
기반 메서드를 추가하는 예외가 허용될 수 있습니다.
등록의 대칭
항목을 추가하거나 등록하는 방법이 있다면 삭제/등록 취소하는 방법도 있어야 합니다. 메서드
registerThing(Thing)
일치하는
unregisterThing(Thing)
요청 식별자 제공
개발자가 콜백을 재사용하는 것이 합리적인 경우 콜백을 요청에 연결하는 식별자 객체를 제공합니다.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
다중 메서드 콜백 객체
다중 메서드 콜백은 이전에 출시된 인터페이스에 추가할 때 interface
를 선호하고 default
메서드를 사용해야 합니다. 이전에는 Java 7에 default
메서드가 없으므로 이 가이드라인에서는 abstract class
를 권장했습니다.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
비차단 함수 호출을 모델링할 때 android.os.OutcomeReceiver 사용
OutcomeReceiver<R,E>
는 성공하면 결과 값 R
을, 그렇지 않으면 E : Throwable
를 보고합니다. 이는 일반 메서드 호출에서 할 수 있는 것과 동일합니다. 결과를 반환하거나 예외를 발생시키는 차단 메서드를 비차단 비동기 메서드로 변환할 때 OutcomeReceiver
를 콜백 유형으로 사용합니다.
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
이 방식으로 변환된 비동기 메서드는 항상 void
를 반환합니다. requestFoo
가 반환할 모든 결과는 대신 제공된 executor
에서 호출하여 requestFooAsync
의 callback
매개변수의 OutcomeReceiver.onResult
에 보고됩니다.
requestFoo
에서 발생하는 예외는 대신 동일한 방식으로 OutcomeReceiver.onError
메서드에 보고됩니다.
비동기 메서드 결과를 보고하는 데 OutcomeReceiver
를 사용하면 androidx.core:core-ktx
의 Continuation.asOutcomeReceiver
확장 프로그램을 사용하여 비동기 메서드의 Kotlin suspend fun
래퍼도 사용할 수 있습니다.
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
이러한 확장 프로그램을 사용하면 Kotlin 클라이언트가 호출 스레드를 차단하지 않고도 일반 함수 호출의 편리함으로 비차단 비동기 메서드를 호출할 수 있습니다. 플랫폼 API의 이러한 1:1 확장 프로그램은 표준 버전 호환성 확인 및 고려사항과 결합될 때 Jetpack의 androidx.core:core-ktx
아티팩트의 일부로 제공될 수 있습니다. 자세한 내용, 취소 고려사항, 샘플은 asOutcomeReceiver 문서를 참고하세요.
결과를 반환하거나 작업이 완료될 때 예외를 발생시키는 메서드의 시맨틱스와 일치하지 않는 비동기 메서드는 OutcomeReceiver
를 콜백 유형으로 사용하면 안 됩니다. 대신 다음 섹션에 나열된 다른 옵션 중 하나를 고려해 보세요.
새 단일 추상 메서드 (SAM) 유형을 만드는 대신 기능적 인터페이스를 선호합니다.
API 수준 24에서는 콜백 람다로 사용하기에 적합한 Consumer<T>
와 같은 일반 SAM 인터페이스를 제공하는 java.util.function.*
(참조 문서) 유형을 추가했습니다. 대부분의 경우 새 SAM 인터페이스를 만들면 유형 안전성이나 인텐트 전달 측면에서 거의 가치가 없으며 Android API 노출 영역을 불필요하게 확장합니다.
새 인터페이스를 만드는 대신 다음과 같은 일반 인터페이스를 사용하는 것이 좋습니다.
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- 참조 문서에서 더 많은 내용 확인 가능
SAM 매개변수의 게재위치
메서드가 추가 매개변수로 오버로드되더라도 Kotlin에서 관용적인 사용을 사용 설정하려면 SAM 매개변수를 마지막에 배치해야 합니다.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
문서
다음은 API의 공개 문서 (Javadoc)에 관한 규칙입니다.
모든 공개 API는 문서화되어야 합니다.
모든 공개 API에는 개발자가 API를 사용하는 방법을 설명하기에 충분한 문서가 있어야 합니다. 개발자가 자동 완성 기능을 사용하거나 API 참조 문서를 탐색하는 중에 메서드를 찾았으며 인접한 API 노출 영역 (예: 동일한 클래스)의 컨텍스트가 최소라고 가정해 보겠습니다.
메서드
메서드 매개변수와 반환 값은 각각 @param
및 @return
문서 주석을 사용하여 문서화해야 합니다. '이 메서드는...'이 앞에 오는 것처럼 Javadoc 본문의 형식을 지정합니다.
메서드가 매개변수를 사용하지 않고, 특별한 고려사항이 없으며, 메서드 이름에 설명된 대로 반환하는 경우 @return
를 생략하고 다음과 유사한 문서를 작성할 수 있습니다.
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
Javadoc에서 항상 링크 사용
문서는 관련 상수, 메서드, 기타 요소의 다른 문서로 연결되어야 합니다. 일반 텍스트 단어뿐만 아니라 Javadoc 태그 (예: @see
및 {@link foo}
)를 사용하세요.
다음 소스 예시를 참고하세요.
public static final int FOO = 0;
public static final int BAR = 1;
원시 텍스트 또는 코드 글꼴을 사용하지 마세요.
/**
* 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) { ... }
대신 링크를 사용하세요.
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
매개변수에 @ValueType
와 같은 IntDef
주석을 사용하면 허용되는 유형을 지정하는 문서가 자동으로 생성됩니다. IntDef
에 관한 자세한 내용은 주석 가이드를 참고하세요.
Javadoc을 추가할 때 update-api 또는 docs 타겟 실행
이 규칙은 @link
또는 @see
태그를 추가할 때 특히 중요하며 출력이 예상대로 표시되는지 확인합니다. Javadoc의 ERROR 출력은 잘못된 링크로 인해 발생하는 경우가 많습니다. update-api
또는 docs
Make 타겟이 이 검사를 실행하지만 Javadoc만 변경하고 update-api
타겟을 실행할 필요가 없는 경우 docs
타겟이 더 빠를 수 있습니다.
{@code foo}를 사용하여 Java 값 구분
true
, false
, null
와 같은 Java 값을 {@code...}
로 래핑하여 문서 텍스트와 구분합니다.
Kotlin 소스에서 문서를 작성할 때는 마크다운에서와 같이 백틱으로 코드를 래핑할 수 있습니다.
@param 및 @return 요약은 단일 문장으로 작성해야 합니다.
매개변수 및 반환 값 요약은 소문자로 시작하고 단일 문장으로만 구성해야 합니다. 한 문장 이상으로 확장되는 추가 정보가 있는 경우 메서드 Javadoc 본문으로 이동합니다.
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
다음으로 변경해야 합니다.
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Docs 주석에 설명 필요
주석 @hide
및 @removed
가 공개 API에서 숨겨진 이유를 문서화합니다.
@deprecated
주석으로 표시된 API 요소를 대체하는 방법에 관한 안내를 포함합니다.
@throws를 사용하여 예외 문서화
메서드가 확인된 예외(예: IOException
)를 발생시키는 경우 @throws
로 예외를 문서화합니다. Java 클라이언트에서 사용하도록 설계된 Kotlin 소스 API의 경우 함수에 @Throws
주석을 추가합니다.
메서드가 방지할 수 있는 오류를 나타내는 선택 해제된 예외(예: IllegalArgumentException
또는 IllegalStateException
)를 발생시키는 경우 예외가 발생하는 이유를 설명하는 예외를 문서화합니다. 발생한 예외는 발생 이유도 나타내야 합니다.
일부의 경우는 확인되지 않은 예외가 암시적으로 간주되며 문서화할 필요가 없습니다. 예를 들어 인수가 @IntDef
또는 API 계약을 메서드 서명에 삽입하는 유사한 주석과 일치하지 않는 NullPointerException
또는 IllegalArgumentException
가 있습니다.
/**
* ...
* @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.");
}
// ...
또는 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")
}
}
// ...
메서드가 예외를 발생시킬 수 있는 비동기 코드를 호출하는 경우 개발자가 이러한 예외를 발견하고 이에 응답하는 방법을 고려하세요. 일반적으로 예외를 콜백에 전달하고 이를 수신하는 메서드에서 발생한 예외를 문서화하는 작업이 포함됩니다. 비동기 예외는 주석 처리된 메서드에서 실제로 다시 발생하지 않는 한 @throws
로 문서화해서는 안 됩니다.
문서의 첫 문장을 마침표로 끝내기
Doclava 도구는 문서를 단순하게 파싱하여 마침표(.) 뒤에 공백이 나오면 개요 문서(첫 번째 문장으로, 수업 문서 상단의 빠른 설명에 사용됨)를 종료합니다. 이로 인해 다음과 같은 두 가지 문제가 발생합니다.
- 짧은 문서가 마침표로 끝나지 않고 해당 구성원이 도구에서 가져온 문서를 상속한 경우, 요약에서도 이러한 상속된 문서를 가져옵니다. 예를 들어
R.attr
문서의actionBarTabStyle
를 참고하세요. 여기에는 개요에 측정기준 설명이 추가되어 있습니다. - Doclava는 'g.' 뒤에 개요 문서를 종료하므로 첫 문장에서도 '예'를 사용하지 마세요. 예를 들어
View.java
의TEXT_ALIGNMENT_CENTER
를 참고하세요. Metalava는 마침표 뒤에 띄어쓰기 없는 공백을 삽입하여 이 오류를 자동으로 수정합니다. 하지만 처음부터 이러한 실수를 하지 마세요.
HTML로 렌더링할 문서 형식 지정
Javadoc은 HTML로 렌더링되므로 문서 형식을 적절하게 지정하세요.
줄바꿈에는 명시적인
<p>
태그를 사용해야 합니다. 닫는</p>
태그를 추가하지 마세요.ASCII를 사용하여 목록이나 표를 렌더링하지 마세요.
목록은 정렬되지 않은 목록의 경우
<ul>
, 정렬된 목록의 경우<ol>
를 사용해야 합니다. 각 항목은<li>
태그로 시작해야 하지만 닫는</li>
태그는 필요하지 않습니다. 마지막 항목 뒤에 닫는</ul>
또는</ol>
태그가 필요합니다.테이블은
<table>
, 행의 경우<tr>
, 헤더의 경우<th>
, 셀의 경우<td>
를 사용해야 합니다. 모든 표 태그에는 일치하는 닫는 태그가 필요합니다. 모든 태그에서class="deprecated"
를 사용하여 지원 중단을 나타낼 수 있습니다.인라인 코드 글꼴을 만들려면
{@code foo}
를 사용합니다.코드 블록을 만들려면
<pre>
를 사용합니다.<pre>
블록 내의 모든 텍스트는 브라우저에서 파싱되므로 괄호<>
를 사용해 주의하세요.<
및>
HTML 엔티티로 이스케이프 처리할 수 있습니다.또는 문제가 되는 섹션을
{@code foo}
로 래핑하는 경우 코드 스니펫에 원시 대괄호<>
를 남겨 둘 수 있습니다. 예를 들면 다음과 같습니다.<pre>{@code <manifest>}</pre>
API 참조 스타일 가이드 준수
클래스 요약, 메서드 설명, 매개변수 설명, 기타 항목의 스타일을 일관되게 유지하려면 Javadoc 도구의 문서 주석 작성 방법의 공식 Java 언어 가이드라인에 나온 권장사항을 따르세요.
Android 프레임워크별 규칙
이러한 규칙은 Android 프레임워크에 내장된 API 및 동작과 관련된 API, 패턴, 데이터 구조에 관한 규칙입니다 (예: Bundle
또는 Parcelable
).
인텐트 빌더는 create*Intent() 패턴을 사용해야 합니다.
인텐트의 크리에이터는 createFooIntent()
라는 메서드를 사용해야 합니다.
새 범용 데이터 구조를 만드는 대신 Bundle 사용
임의의 키-유형 값 매핑을 나타내는 새로운 범용 데이터 구조를 만들지 마세요. 대신 Bundle
를 사용하는 것이 좋습니다.
이는 일반적으로 플랫폼 외부 앱과 서비스 간의 통신 채널 역할을 하는 플랫폼 API를 작성할 때 발생합니다. 이 경우 플랫폼은 채널을 통해 전송된 데이터를 읽지 않으며 API 계약은 플랫폼 외부 (예: Jetpack 라이브러리)에서 부분적으로 정의될 수 있습니다.
플랫폼에서 데이터를 읽는 경우 Bundle
를 사용하지 말고 강타입 데이터 클래스를 사용하는 것이 좋습니다.
Parcelable 구현에 공개 CREATOR 필드가 있어야 함
Parcelable 확장은 원시 생성자가 아닌 CREATOR
를 통해 노출됩니다. 클래스가 Parcelable
를 구현하는 경우 CREATOR
필드도 공개 API여야 하며 Parcel
인수를 사용하는 클래스 생성자는 비공개여야 합니다.
UI 문자열에 CharSequence 사용
문자열이 사용자 인터페이스에 표시되면 CharSequence
를 사용하여 Spannable
인스턴스를 허용합니다.
사용자에게 표시되지 않는 키나 다른 라벨 또는 값인 경우 String
를 사용해도 됩니다.
enum 사용 피하기
IntDef
은(는) 모든 플랫폼 API에서 enum보다 사용해야 하며 번들 해제된 라이브러리 API에서는 적극적으로 고려해야 합니다. 새 값이 추가되지 않을 것이라고 확신하는 경우에만 enum을 사용하세요.
IntDef
의 이점:
- 시간 경과에 따라 값을 추가할 수 있습니다.
- 플랫폼에 추가된 enum 값으로 인해 더 이상 포괄적이지 않게 된 경우 Kotlin
when
문이 런타임에 실패할 수 있습니다.
- 플랫폼에 추가된 enum 값으로 인해 더 이상 포괄적이지 않게 된 경우 Kotlin
- 런타임에 사용되는 클래스나 객체가 없으며 프리미티브만 사용됩니다.
- R8 또는 최소화로 번들 해제된 라이브러리 API의 이 비용을 피할 수 있지만 이 최적화는 플랫폼 API 클래스에 영향을 줄 수 없습니다.
enum의 이점
- Java의 관용적 언어 기능인 Kotlin
- 포괄적 스위치,
when
문이 사용 설정됩니다.- 참고 - 값은 시간이 지남에 따라 변경되어서는 안 됩니다(이전 목록 참고).
- 명확한 범위와 검색 가능한 이름 지정
- 컴파일 시간 확인을 사용 설정합니다.
- 예를 들어 값을 반환하는 Kotlin의
when
문
- 예를 들어 값을 반환하는 Kotlin의
- 인터페이스를 구현하고, 정적 도우미를 보유하고, 구성원 또는 확장 메서드를 노출하고, 필드를 노출할 수 있는 작동하는 클래스입니다.
Android 패키지 계층 구조 따르기
android.*
패키지 계층 구조에는 암시적 순서가 있으며, 여기서 하위 수준 패키지는 상위 수준 패키지에 종속될 수 없습니다.
Google, 다른 회사, 해당 회사의 제품을 언급하지 않음
Android 플랫폼은 오픈소스 프로젝트이며 공급업체 중립성을 지향합니다. API는 일반 API여야 하며 필요한 권한이 있는 시스템 통합업체 또는 앱에서 동일하게 사용할 수 있어야 합니다.
Parcelable 구현은 최종이어야 함
플랫폼에서 정의한 Parcelable 클래스는 항상 framework.jar
에서 로드되므로 앱에서 Parcelable
구현을 재정의하려고 시도하는 것은 잘못된 동작입니다.
전송 앱이 Parcelable
를 확장하는 경우 수신 앱에는 압축을 풀 때 사용할 발신자의 맞춤 구현이 없습니다. 이전 버전과의 호환성에 관한 참고사항: 이전에 클래스가 final이 아니었지만 공개적으로 사용 가능한 생성자가 없는 경우에도 final
로 표시할 수 있습니다.
시스템 프로세스를 호출하는 메서드는 RemoteException을 RuntimeException으로 다시 발생시켜야 함
RemoteException
는 일반적으로 내부 AIDL에 의해 발생하며 시스템 프로세스가 종료되었거나 앱이 너무 많은 데이터를 전송하려고 시도하고 있음을 나타냅니다. 두 경우 모두 앱이 보안 또는 정책 결정을 유지하지 못하도록 하려면 공개 API를 RuntimeException
로 다시 발생시켜야 합니다.
Binder
호출의 다른 쪽이 시스템 프로세스라는 것을 알고 있다면 다음과 같은 템플릿 코드를 사용하는 것이 좋습니다.
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
API 변경에 관한 특정 예외 발생
공개 API 동작은 API 수준에 따라 변경될 수 있으며 앱 비정상 종료를 일으킬 수 있습니다 (예: 새 보안 정책 시행).
이전에 유효했던 요청에 대해 API가 발생시켜야 하는 경우 일반 예외 대신 새로운 특정 예외를 발생시킵니다. 예를 들어 SecurityException
대신 ExportedFlagRequired
를 사용합니다 (ExportedFlagRequired
는 SecurityException
를 확장할 수 있음).
이렇게 하면 앱 개발자와 도구가 API 동작 변경사항을 감지하는 데 도움이 됩니다.
클론 대신 복사 생성자 구현
Java clone()
메서드는 Object
클래스에서 제공하는 API 계약이 없고 clone()
를 사용하는 클래스를 확장하는 데 내재된 어려움이 있으므로 사용하지 않는 것이 좋습니다. 대신 동일한 유형의 객체를 사용하는 복사 생성자를 사용하세요.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
생성에 Builder를 사용하는 클래스는 사본을 수정할 수 있도록 Builder 사본 생성자를 추가하는 것이 좋습니다.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
FileDescriptor 대신 ParcelFileDescriptor 사용
java.io.FileDescriptor
객체의 소유권 정의가 잘못되어 종료 후 사용 버그가 발생할 수 있습니다. 대신 API는 ParcelFileDescriptor
인스턴스를 반환하거나 수락해야 합니다. 기존 코드는 필요한 경우 dup() 또는 getFileDescriptor()를 사용하여 PFD와 FD 간에 변환할 수 있습니다.
홀수 크기의 숫자 값 사용하지 않기
short
또는 byte
값을 직접 사용하지 마세요. 향후 API를 발전시킬 수 있는 방법이 제한되는 경우가 많기 때문입니다.
BitSet 사용 피하기
java.util.BitSet
는 구현에는 좋지만 공개 API에는 적합하지 않습니다. 변경 가능하며 빈번한 메서드 호출에 할당이 필요하며 각 비트가 나타내는 의미론적 의미를 제공하지 않습니다.
고성능 시나리오의 경우 @IntDef
와 함께 int
또는 long
를 사용하세요. 성능이 낮은 시나리오의 경우 Set<EnumType>
를 고려하세요. 원시 바이너리 데이터의 경우 byte[]
를 사용합니다.
android.net.Uri를 사용하는 것이 좋습니다.
android.net.Uri
는 Android API에서 URI에 선호되는 캡슐화입니다.
java.net.URI
는 URI 파싱에 지나치게 엄격하므로 사용하지 마세요. 또한 java.net.URL
는 등식 정의가 심각하게 손상되므로 사용하지 마세요.
@IntDef, @LongDef 또는 @StringDef로 표시된 주석 숨기기
@IntDef
, @LongDef
또는 @StringDef
로 표시된 주석은 API에 전달할 수 있는 유효한 상수 집합을 나타냅니다. 그러나 API 자체로 내보내면 컴파일러가 상수를 인라인 처리하고 (이제 더 이상 사용되지 않는) 값만 주석의 API 스텁 (플랫폼의 경우) 또는 JAR (라이브러리의 경우)에 남아 있게 됩니다.
따라서 이러한 주석의 사용은 플랫폼의 @hide
문서 주석 또는 라이브러리의 @RestrictTo.Scope.LIBRARY)
코드 주석으로 표시해야 합니다. API 스텁 또는 JAR에 표시되지 않도록 하려면 두 경우 모두 @Retention(RetentionPolicy.SOURCE)
로 표시해야 합니다.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
플랫폼 SDK 및 라이브러리 AAR을 빌드할 때 도구는 주석을 추출하여 컴파일된 소스와 별도로 번들로 묶습니다. Android 스튜디오는 이 번들 형식을 읽고 유형 정의가 적용되도록 합니다.
새 설정 제공업체 키를 추가하지 않음
Settings.Global
, Settings.System
또는 Settings.Secure
의 새 키를 노출하지 않습니다.
대신 관련 클래스(일반적으로 '관리자' 클래스)에 적절한 getter 및 setter Java API를 추가합니다. 필요에 따라 클라이언트에게 변경사항을 알리는 리스너 메커니즘 또는 브로드캐스트를 추가합니다.
SettingsProvider
설정에는 getter/setter에 비해 몇 가지 문제가 있습니다.
- 유형 안전성이 없습니다.
- 기본값을 제공하는 통합된 방법이 없습니다.
- 권한을 맞춤설정할 적절한 방법이 없습니다.
- 예를 들어 맞춤 권한으로 설정을 보호할 수는 없습니다.
- 맞춤 로직을 올바르게 추가할 방법이 없습니다.
- 예를 들어 설정 B의 값에 따라 설정 A의 값을 변경할 수 없습니다.
예: Settings.Secure.LOCATION_MODE
는 오랫동안 존재했지만 위치팀에서 적절한 Java API LocationManager.isLocationEnabled()
및 MODE_CHANGED_ACTION
브로드캐스트를 위해 지원 중단했습니다. 이에 따라 팀에 훨씬 더 많은 유연성이 생겼으며 API의 시맨틱이 훨씬 더 명확해졌습니다.
Activity 및 AsyncTask를 확장하지 않음
AsyncTask
는 구현 세부정보입니다. 대신 리스너 또는 androidx의 경우 ListenableFuture
API를 노출합니다.
Activity
서브클래스는 컴포지션할 수 없습니다. 기능의 활동을 확장하면 사용자가 동일한 작업을 해야 하는 다른 기능과 호환되지 않습니다. 대신 LifecycleObserver와 같은 도구를 사용하여 컴포지션을 사용하세요.
컨텍스트의 getUser() 사용
Context.getSystemService()
에서 반환되는 항목과 같이 Context
에 바인딩된 클래스는 특정 사용자를 타겟팅하는 멤버를 노출하는 대신 Context
에 바인딩된 사용자를 사용해야 합니다.
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);
}
}
예외: 메서드가 단일 사용자를 나타내지 않는 값(예: UserHandle.ALL
)을 허용하는 경우 사용자 인수를 허용할 수 있습니다.
일반 int 대신 UserHandle 사용
UserHandle
는 유형 안전성을 제공하고 사용자 ID를 uid와 혼동하지 않도록 하는 데 권장됩니다.
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
불가피한 경우 사용자 ID를 나타내는 int
에 @UserIdInt
주석을 추가해야 합니다.
Foobar getFoobarForUser(@UserIdInt int user);
브로드캐스트 인텐트에 리스너 또는 콜백 사용
브로드캐스트 인텐트는 매우 강력하지만 시스템 상태에 부정적인 영향을 미칠 수 있는 비상 동작을 일으켰으므로 새로운 브로드캐스트 인텐트는 신중하게 추가해야 합니다.
다음은 새로운 broadcast 인텐트를 도입하지 않는 것이 바람직한 이유에 관한 몇 가지 구체적인 문제입니다.
FLAG_RECEIVER_REGISTERED_ONLY
플래그 없이 브로드캐스트를 전송하면 아직 실행되지 않는 앱이 강제로 시작됩니다. 이는 의도된 결과일 수 있지만 수십 개의 앱이 일시에 실행되어 시스템 상태에 부정적인 영향을 미칠 수 있습니다. 다양한 전제 조건이 충족될 때 더 효과적으로 조정할 수 있도록JobScheduler
와 같은 대체 전략을 사용하는 것이 좋습니다.브로드캐스트를 전송할 때 앱에 전송되는 콘텐츠를 필터링하거나 조정할 수 있는 기능은 거의 없습니다. 따라서 향후 개인 정보 보호 문제에 대응하거나 수신 앱의 타겟 SDK를 기반으로 동작 변경사항을 도입하기가 어렵거나 불가능합니다.
브로드캐스트 큐는 공유 리소스이므로 과부하가 발생하여 이벤트가 제때 전송되지 않을 수 있습니다. 엔드 투 엔드 지연 시간이 10분 이상인 브로드캐스트 대기열이 여러 개 있는 것으로 확인되었습니다.
따라서 새로운 기능은 broadcast 인텐트 대신 리스너 또는 콜백 또는 JobScheduler
와 같은 기타 시설을 사용하는 것이 좋습니다.
브로드캐스트 인텐트가 여전히 이상적인 설계인 경우 고려해야 할 몇 가지 권장사항은 다음과 같습니다.
- 가능하면
Intent.FLAG_RECEIVER_REGISTERED_ONLY
를 사용하여 이미 실행 중인 앱으로 브로드캐스트를 제한합니다. 예를 들어ACTION_SCREEN_ON
은 이 디자인을 사용하여 앱이 깨어나지 않도록 합니다. - 가능하면
Intent.setPackage()
또는Intent.setComponent()
를 사용하여 관심 있는 특정 앱에서 브로드캐스트를 타겟팅하세요. 예를 들어ACTION_MEDIA_BUTTON
은 이 디자인을 사용하여 재생 컨트롤을 처리하는 현재 앱에 집중합니다. - 가능한 경우 악성 앱이 OS를 명의 도용하지 못하도록 브로드캐스트를
<protected-broadcast>
로 정의합니다.
시스템 바인딩 개발자 서비스의 인텐트
개발자가 확장하고 시스템에 바인딩할 서비스(예: NotificationListenerService
와 같은 추상 서비스)는 시스템의 Intent
작업에 응답할 수 있습니다. 이러한 서비스는 다음 기준을 충족해야 합니다.
- 서비스의 정규화된 클래스 이름이 포함된 클래스에
SERVICE_INTERFACE
문자열 상수를 정의합니다. 이 상수는@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
로 주석 처리되어야 합니다. - 개발자가 플랫폼에서 인텐트를 수신하려면
AndroidManifest.xml
에<intent-filter>
를 추가해야 한다고 클래스에 문서화합니다. - 악성 앱이 개발자 서비스에
Intent
를 전송하지 못하도록 시스템 수준 권한을 추가하는 것이 좋습니다.
Kotlin-Java 상호 운용성
가이드라인의 전체 목록은 공식 Android Kotlin-Java 상호 운용성 가이드를 참고하세요. 검색 가능성을 개선하기 위해 일부 가이드라인이 이 가이드에 복사되었습니다.
API 공개 상태
suspend fun
와 같은 일부 Kotlin API는 Java 개발자가 사용하도록 설계되지 않았습니다. 그러나 @JvmSynthetic
를 사용하여 언어별 공개 상태를 제어하려고 하면 디버거에서 API가 표시되는 방식에 부작용이 발생하여 디버깅이 더 어려워지므로 시도하지 마세요.
구체적인 안내는 Kotlin-Java 상호 운용성 가이드 또는 비동기 가이드를 참고하세요.
컴패니언 객체
Kotlin은 companion object
를 사용하여 정적 멤버를 노출합니다. 경우에 따라 이러한 메서드는 포함 클래스가 아닌 Companion
라는 내부 클래스에서 Java로 표시됩니다. Companion
클래스가 API 텍스트 파일에서 빈 클래스로 표시될 수 있습니다. 이는 정상적으로 작동하는 것입니다.
Java와의 호환성을 극대화하려면 동반 객체의 상수가 아닌 필드에 @JvmField
로, 공개 함수에 @JvmStatic
로 주석을 달아 포함 클래스에 직접 노출합니다.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Android 플랫폼 API의 진화
이 섹션에서는 기존 Android API에 적용할 수 있는 변경사항 유형과 기존 앱 및 코드베이스와의 호환성을 극대화하기 위해 이러한 변경사항을 구현하는 방법에 관한 정책을 설명합니다.
바이너리 변경사항
최종화된 공개 API 노출 영역에서 바이너리 브레이킹 체인지를 피하세요. 이러한 유형의 변경사항은 일반적으로 make update-api
를 실행할 때 오류를 발생시키지만 Metalava의 API 검사에서 포착하지 못하는 특이 사례가 있을 수 있습니다. 확실하지 않은 경우 Eclipse Foundation의 Java 기반 API 발전 가이드에서 Java에서 호환되는 API 변경 유형에 관한 자세한 설명을 참고하세요. 숨겨진 (예: 시스템) API의 바이너리 변경사항은 지원 중단/대체 주기를 따라야 합니다.
소스 변경사항
바이너리 브레이킹 변경사항이 아니더라도 소스 브레이킹 변경사항은 권장하지 않습니다. 바이너리 호환이지만 소스 변경을 중단하는 변경의 한 가지 예는 기존 클래스에 제네릭을 추가하는 것입니다. 이는 바이너리 호환이지만 상속 또는 모호한 참조로 인해 컴파일 오류가 발생할 수 있습니다.
소스 중단 변경사항은 make update-api
를 실행할 때 오류를 발생시키지 않으므로 기존 API 서명 변경사항의 영향을 신중하게 이해해야 합니다.
개발자 환경 또는 코드 정확성을 개선하기 위해 소스 중단 변경이 필요한 경우도 있습니다. 예를 들어 Java 소스에 null 허용 여부 주석을 추가하면 Kotlin 코드와의 상호 운용성이 개선되고 오류 발생 가능성이 줄어들지만, 소스 코드를 변경해야 하는 경우가 많습니다(심각한 변경이 필요한 경우도 있음).
비공개 API 변경사항
@TestApi
로 주석 처리된 API는 언제든지 변경할 수 있습니다.
@SystemApi
로 주석이 추가된 API는 3년 동안 보존해야 합니다. 다음 일정에 따라 시스템 API를 삭제하거나 리팩터링해야 합니다.
- API y - 추가됨
- API y+1 - 지원 중단
- 코드를
@Deprecated
로 표시합니다. - 대체 항목을 추가하고
@deprecated
문서 주석을 사용하여 지원 중단된 코드의 Javadoc에서 대체 항목으로 연결합니다. - 개발 주기 중에 내부 사용자에게 API가 지원 중단된다고 알리는 버그를 신고합니다. 이렇게 하면 대체 API가 적절한지 확인하는 데 도움이 됩니다.
- 코드를
- API y+2 - 소프트 삭제
- 코드를
@removed
로 표시합니다. - 원하는 경우 출시의 현재 SDK 수준을 타겟팅하는 앱에 대해 예외를 발생시키거나 무작위 작업을 실행합니다.
- 코드를
- API y+3 - 완전 삭제
- 소스 트리에서 코드를 완전히 삭제합니다.
디프리케이션
지원 중단은 API 변경으로 간주되며, 메이저 (예: 문자) 출시에서 발생할 수 있습니다. API를 지원 중단할 때 @Deprecated
소스 주석과 @deprecated
<summary>
문서 주석을 함께 사용합니다. 요약에는 마이그레이션 전략이 포함되어야 함니다. 이 전략은 대체 API로 연결하거나 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)
또한 XML에 정의되고 Java에 노출된 API(android.R
클래스에 노출된 속성 및 스타일 지정 가능 속성 포함)를 요약과 함께 지원 중단해야 합니다.
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
API를 지원 중단해야 하는 경우
지원 중단은 새 코드에서 API 채택을 억제하는 데 가장 유용합니다.
또한 API가 @removed
가 되기 전에 @deprecated
로 표시해야 하지만, 이로 인해 개발자가 이미 사용 중인 API를 마이그레이션할 동기가 생기지 않습니다.
API를 지원 중단하기 전에 개발자에게 미칠 영향을 고려하세요. API 지원 중단의 영향에는 다음이 포함됩니다.
javac
는 컴파일 중에 경고를 내보냅니다.- 지원 중단 경고는 전역적으로 억제하거나 기준으로 설정할 수 없으므로
-Werror
를 사용하는 개발자는 컴파일 SDK 버전을 업데이트하기 전에 지원 중단된 API의 모든 사용을 개별적으로 수정하거나 억제해야 합니다. - 지원 중단된 클래스 가져오기에 관한 지원 중단 경고는 억제할 수 없습니다. 따라서 개발자는 컴파일 SDK 버전을 업데이트하기 전에 지원 중단된 클래스의 모든 사용에 정규화된 클래스 이름을 인라인으로 추가해야 합니다.
- 지원 중단 경고는 전역적으로 억제하거나 기준으로 설정할 수 없으므로
d.android.com
에 관한 문서에 지원 중단 알림이 표시됩니다.- Android 스튜디오와 같은 IDE는 API 사용 사이트에 경고를 표시합니다.
- IDE에서 API의 순위를 낮추거나 자동 완성에서 API를 숨길 수 있습니다.
따라서 API를 지원 중단하면 코드 상태에 가장 관심이 많은 개발자 (-Werror
를 사용하는 개발자)가 새 SDK를 채택하지 않을 수 있습니다.
기존 코드의 경고에 관해 신경 쓰지 않는 개발자는 지원 중단을 완전히 무시할 수 있습니다.
대량의 지원 중단을 도입하는 SDK는 이러한 두 가지 사례를 모두 악화시킵니다.
따라서 다음과 같은 경우에만 API를 지원 중단하는 것이 좋습니다.
- 향후 출시에서는 이 API를
@remove
할 계획입니다. - API를 사용하면 호환성을 중단하지 않고는 수정할 수 없는 잘못되거나 정의되지 않은 동작이 발생합니다.
API를 지원 중단하고 새 API로 대체할 때는 이전 기기와 새 기기 모두를 간편하게 지원할 수 있도록 androidx.core
와 같은 Jetpack 라이브러리에 상응하는 호환성 API를 추가하는 것이 좋습니다.
현재 및 향후 버전에서 정상적으로 작동하는 API는 지원 중단하지 않는 것이 좋습니다.
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
API가 더 이상 문서화된 동작을 유지할 수 없는 경우에 지원 중단이 적절합니다.
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
지원 중단된 API 변경사항
지원 중단된 API의 동작은 유지해야 합니다. 즉, 테스트 구현은 동일하게 유지되어야 하며 API를 지원 중단한 후에도 테스트가 계속 통과해야 합니다. API에 테스트가 없는 경우 테스트를 추가해야 합니다.
향후 출시에서 지원 중단된 API 노출 영역을 확장하지 마세요. 기존의 지원 중단된 API에 린트 정확성 주석 (예: @Nullable
)을 추가할 수는 있지만 새 API를 추가해서는 안 됩니다.
새 API를 지원 중단된 것으로 추가하지 마세요. 사전 출시 주기 내에 API가 추가되었다가 이후 지원 중단된 경우 (따라서 처음에는 지원 중단된 것으로 공개 API 노출 영역에 진입함) API를 완성하기 전에 이를 삭제해야 합니다.
소프트 삭제
소프트 삭제는 소스 변경을 일으키는 변경사항이므로 API 협의회에서 명시적으로 승인하지 않는 한 공개 API에서는 이를 피해야 합니다.
시스템 API의 경우 조용히 삭제하기 전에 메인 출시 기간 동안 API를 지원 중단해야 합니다. API에 대한 모든 문서 참조를 삭제하고 API를 조용히 삭제할 때 @removed <summary>
문서 주석을 사용하세요. 요약에는 삭제 이유를 포함해야 하며 지원 중단에서 설명한 대로 이전 전략을 포함할 수 있습니다.
조용히 삭제된 API의 동작은 있는 그대로 유지할 수 있지만, 더 중요한 것은 API를 호출할 때 기존 호출자가 비정상 종료되지 않도록 유지해야 함니다. 경우에 따라 동작을 보존하는 것을 의미할 수도 있습니다.
테스트 적용 범위는 유지해야 하지만 동작 변경사항을 수용하기 위해 테스트 콘텐츠를 변경해야 할 수 있습니다. 테스트는 여전히 기존 호출자가 런타임에 비정상 종료되지 않는지 확인해야 합니다. 조용히 삭제된 API의 동작은 있는 그대로 유지할 수 있지만, 더 중요한 것은 API를 호출할 때 기존 호출자가 비정상 종료되지 않도록 이를 보존해야 함니다. 경우에 따라 동작을 보존하는 것을 의미할 수도 있습니다.
테스트 적용 범위를 유지해야 하지만 동작 변경사항을 수용하기 위해 테스트 콘텐츠를 변경해야 할 수 있습니다. 테스트는 여전히 기존 호출자가 런타임에 비정상 종료되지 않는지 확인해야 합니다.
기술적 수준에서는 @remove
Javadoc 주석을 사용하여 SDK 스텁 JAR 및 컴파일 시간 클래스패스에서 API를 삭제하지만 @hide
API와 마찬가지로 런타임 클래스패스에는 계속 존재합니다.
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
앱 개발자 관점에서 API는 더 이상 자동 완성에 표시되지 않으며 compileSdk
가 API가 삭제된 SDK와 같거나 이후 버전인 경우 API를 참조하는 소스 코드가 컴파일되지 않습니다. 그러나 이전 SDK를 대상으로 소스 코드가 계속 컴파일되고 API를 참조하는 바이너리가 계속 작동합니다.
특정 카테고리의 API는 소프트 삭제할 수 없습니다. 특정 카테고리의 API를 조용히 삭제해서는 안 됩니다.
추상 메서드
개발자가 확장할 수 있는 클래스의 추상 메서드를 소프트 삭제해서는 안 됩니다. 이렇게 하면 개발자가 모든 SDK 수준에서 클래스를 확장할 수 없습니다.
개발자가 클래스를 확장할 수 없었던 적이 있고 앞으로도 없을 드문 경우에도 추상 메서드를 소프트 삭제할 수 있습니다.
하드 삭제
하드 삭제는 바이너리 중단 변경사항이므로 공개 API에서는 발생해서는 안 됩니다.
권장되지 않는 주석
@Discouraged
주석은 대부분의 경우(95% 초과) API를 권장하지 않음을 나타냅니다. 권장되지 않는 API는 지원 중단을 방지하는 좁은 범위의 중요한 사용 사례가 있다는 점에서 지원 중단된 API와 다릅니다. API를 권장하지 않음으로 표시할 때는 설명과 대체 솔루션을 제공해야 합니다.
@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);
}
권장되지 않는 새 API를 추가해서는 안 됩니다.
기존 API 동작 변경사항
기존 API의 구현 동작을 변경해야 하는 경우도 있습니다. 예를 들어 Android 7.0에서는 개발자가 Binder
를 통해 전송하기에 너무 큰 이벤트를 게시하려고 할 때 이를 명확하게 전달하도록 DropBoxManager
를 개선했습니다.
그러나 기존 앱에 문제가 발생하지 않도록 하려면 이전 앱의 안전한 동작을 유지하는 것이 좋습니다. 이전에는 앱의 ApplicationInfo.targetSdkVersion
에 따라 이러한 동작 변경사항을 보호했지만 최근에는 앱 호환성 프레임워크를 사용하도록 이전했습니다. 다음은 이 새로운 프레임워크를 사용하여 동작 변경사항을 구현하는 방법의 예입니다.
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
}
}
}
이 앱 호환성 프레임워크 설계를 사용하면 개발자가 앱을 디버그하는 과정에서 수십 개의 동작 변경사항에 동시에 맞추기 위해 강제하는 대신 프리뷰 및 베타 출시 중에 특정 동작 변경사항을 일시적으로 사용 중지할 수 있습니다.
향후 호환성
향후 호환성은 시스템이 이후 버전용 입력을 허용하는 디자인 특성입니다. API 설계의 경우 개발자가 코드를 한 번 작성하고 한 번 테스트한 후 어디서나 문제 없이 실행되기를 기대하므로 초기 설계와 향후 변경사항에 특히 주의해야 합니다.
다음은 Android에서 가장 일반적인 전방 호환성 문제를 일으킵니다.
- 이전에 완전하다고 가정했던 집합 (예:
@IntDef
또는enum
)에 새 상수를 추가합니다 (예:switch
에 예외를 발생시키는default
가 있는 경우). - API 노출 영역에서 직접 캡처되지 않는 기능에 관한 지원을 추가합니다(예: 이전에는
<color>
리소스만 지원되었던 XML에서ColorStateList
유형 리소스 할당 지원). - 이전 버전에서 있었던
requireNotNull()
검사를 삭제하는 등 런타임 검사에 대한 제한을 완화합니다.
이 모든 경우에 개발자는 런타임에만 문제가 있음을 알게 됩니다. 더 나쁜 경우 현장의 이전 기기에서 비정상 종료 보고서가 발생하여 알게 될 수도 있습니다.
또한 이러한 경우는 모두 기술적으로 유효한 API 변경사항입니다. 바이너리 또는 소스 호환성을 손상시키지 않으며 API 린트는 이러한 문제를 포착하지 않습니다.
따라서 API 설계자는 기존 클래스를 수정할 때 각별히 주의해야 합니다. '이 변경사항으로 인해 최신 버전의 플랫폼에 만 작성되고 테스트된 코드가 이전 버전에서 실패하게 되나요?'라는 질문을 합니다.
XML 스키마
XML 스키마가 구성요소 간의 안정적인 인터페이스 역할을 하는 경우 이 스키마는 명시적으로 지정되어야 하며 다른 Android API와 마찬가지로 이전 버전과의 호환이 가능한 방식으로 진화해야 합니다. 예를 들어 XML 요소 및 속성의 구조는 다른 Android API 노출 영역에서 메서드와 변수가 유지되는 방식과 유사하게 유지되어야 합니다.
XML 지원 중단
XML 요소 또는 속성을 지원 중단하려면 xs:annotation
마커를 추가할 수 있지만 일반적인 @SystemApi
진화 수명 주기를 따라 기존 XML 파일을 계속 지원해야 합니다.
<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>
요소 유형은 보존되어야 함
스키마는 sequence
요소, choice
요소, all
요소를 complexType
요소의 하위 요소로 지원합니다. 그러나 이러한 하위 요소는 하위 요소의 수와 순서가 다르므로 기존 유형을 수정하면 호환되지 않는 변경사항이 됩니다.
기존 유형을 수정하려면 기존 유형을 지원 중단하고 이를 대체할 새 유형을 도입하는 것이 좋습니다.
<!-- 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>
메인라인별 패턴
메인라인은 전체 시스템 이미지를 업데이트하는 대신 Android OS의 하위 시스템 ('메인라인 모듈')을 개별적으로 업데이트할 수 있는 프로젝트입니다.
메인라인 모듈은 핵심 플랫폼에서 '번들 해제'되어야 합니다. 즉, 각 모듈과 나머지 항목 간의 모든 상호작용은 공식 (공개 또는 시스템) API를 사용하여 실행해야 합니다.
메인라인 모듈은 따라야 하는 특정 디자인 패턴이 있습니다. 이 섹션에서는 이를 설명합니다.
<Module>FrameworkInitializer 패턴
메인라인 모듈이 @SystemService
클래스 (예: JobScheduler
)를 노출해야 하는 경우 다음 패턴을 사용합니다.
모듈에서
<YourModule>FrameworkInitializer
클래스를 노출합니다. 이 클래스는$BOOTCLASSPATH
에 있어야 합니다. 예: StatsFrameworkInitializer@SystemApi(client = MODULE_LIBRARIES)
로 표시합니다.여기에
public static void registerServiceWrappers()
메서드를 추가합니다.Context
참조가 필요한 경우SystemServiceRegistry.registerContextAwareService()
를 사용하여 서비스 관리자 클래스를 등록합니다.Context
참조가 필요하지 않은 경우SystemServiceRegistry.registerStaticService()
를 사용하여 서비스 관리자 클래스를 등록합니다.SystemServiceRegistry
의 정적 이니셜라이저에서registerServiceWrappers()
메서드를 호출합니다.
<Module>ServiceManager 패턴
일반적으로 시스템 서비스 바인더 객체를 등록하거나 이에 대한 참조를 가져오려면 ServiceManager
를 사용해야 하지만 메인라인 모듈은 숨겨져 있으므로 이를 사용할 수 없습니다. 메인라인 모듈은 정적 플랫폼 또는 다른 모듈에서 노출하는 시스템 서비스 바인더 객체를 등록하거나 참조해서는 안 되므로 이 클래스는 숨겨져 있습니다.
메인라인 모듈은 대신 다음 패턴을 사용하여 모듈 내에서 구현된 바인더 서비스에 대한 참조를 등록하고 가져올 수 있습니다.
TelephonyServiceManager의 설계에 따라
<YourModule>ServiceManager
클래스를 만듭니다.클래스를
@SystemApi
로 노출합니다.$BOOTCLASSPATH
클래스 또는 시스템 서버 클래스에서만 액세스해야 하는 경우@SystemApi(client = MODULE_LIBRARIES)
를 사용하면 됩니다. 그 외의 경우에는@SystemApi(client = PRIVILEGED_APPS)
를 사용하면 됩니다.이 클래스는 다음으로 구성됩니다.
- 숨겨진 생성자이므로 정적 플랫폼 코드만 인스턴스화할 수 있습니다.
- 특정 이름의
ServiceRegisterer
인스턴스를 반환하는 공개 getter 메서드입니다. 바인더 객체가 하나인 경우 getter 메서드가 하나 필요합니다. 두 개가 있으면 두 개의 getter가 필요합니다. ActivityThread.initializeMainlineModules()
에서 이 클래스를 인스턴스화하고 모듈에서 노출하는 정적 메서드에 전달합니다. 일반적으로 이를 사용하는 정적@SystemApi(client = MODULE_LIBRARIES)
API를FrameworkInitializer
클래스에 추가합니다.
이 패턴은 다른 메인라인 모듈이 이러한 API에 액세스하는 것을 방지합니다. 다른 모듈은 get()
및 register()
API가 표시되더라도 <YourModule>ServiceManager
의 인스턴스를 가져올 방법이 없기 때문입니다.
전화 통신이 전화 통신 서비스 참조를 가져오는 방법은 다음과 같습니다. 코드 검색 링크
네이티브 코드에서 서비스 바인더 객체를 구현하는 경우 AServiceManager
네이티브 API를 사용합니다.
이러한 API는 ServiceManager
Java API에 해당하지만 네이티브 API는 메인라인 모듈에 직접 노출됩니다. 모듈에서 소유하지 않은 바인더 객체를 등록하거나 참조하는 데 이를 사용하지 마세요. 네이티브에서 바인더 객체를 노출하는 경우 <YourModule>ServiceManager.ServiceRegisterer
에는 register()
메서드가 필요하지 않습니다.
메인라인 모듈의 권한 정의
APK가 포함된 메인라인 모듈은 일반 APK와 동일한 방식으로 APK AndroidManifest.xml
에서 (맞춤) 권한을 정의할 수 있습니다.
정의된 권한이 모듈 내부에서만 사용되는 경우 권한 이름 앞에 APK 패키지 이름을 접두사로 지정해야 합니다. 예를 들면 다음과 같습니다.
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
정의된 권한을 업데이트 가능한 플랫폼 API의 일부로 다른 앱에 제공하려면 권한 이름 앞에 'android.permission'을 추가해야 합니다. (다른 모든 정적 플랫폼 권한과 마찬가지로) 모듈 패키지 이름을 사용하여 이름 충돌을 방지하면서 모듈의 플랫폼 API임을 알립니다. 예를 들면 다음과 같습니다.
<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" />
그러면 모듈은 이 권한 이름을 API 노출 영역에서 API 상수(예: HealthPermissions.READ_ACTIVE_CALORIES_BURNED
)로 노출할 수 있습니다.