안드로이드에서 코드를 작성할 때, 코드의 무결성에 도움을 주는 Annotation이 있습니다.
Annotation을 적용하면 Annotation의 타입에 따라서 어떤 것은 에러를, 어떤 것은 경고를 발생시킵니다. 개발자가 코드를 사용하는데 실수를 줄이기 위해 사용한다고 생각하면 될 것 같습니다.
Annotation은 변수, 파라미터, 리턴 값과 같은 곳에 사용할 수 있고, null pointer exception과 resource type 충돌과 같은 문제를 찾는데 도움을 줍니다. 아래에서 여러 종류의 annotation을 확인해보겠습니다.
안드로이드의 경우 resource의 아이디는 int로 정의되어 있습니다. 따라서 매개변수의 인자나 반환값으로 넘기는 경우 int로 받기에 일반적으로 사용하는 int형 변수와 혼동해 실수를 유발할 수도 있습니다. 이러한 에제로는 Color, Layout, String에서 사용하는 id들이 될 수 있습니다. 이때 Annotation을 활용한다면 정해진 Annotaion Resource Type에 맞지 않는 Resource Type이 들어올 경우 경고를 통해 알려주기에 개발자가 실수를 줄일 수 있습니다.
메소드에 해당 어노테이션이 붙은 경우 해당 스레드에서 호출될 수 있음을 나타냅니다. 만약, annotation이 클래스에 붙는다면 해당 클래스의 모든 메소드들은 해당 스레드에서만 호출할 수 있습니다.
@MainThread 와 @UiThread 어노테이션은 서로 교환이 가능해서 @MainThread 어노테이션이 붙은 메소드에서 @UiThread 메소드를 호출할 수 있고 그 반대의 경우도 마찬가지입니다. 다만 여러 스레드에 대한 다중 뷰를 가진 앱에서는 @UiThread 와 @MainThread 가 다를 수 있습니다. 따라서 @UiThread는 View 계층과 연결된 메소드에 달고, @MainThread는 앱의 수명주기와 관련된 메소드에 달아야 합니다.
해당 annotation은 매개변수로 전달되는 값을 검증해주는 역할을 수행합니다. 종류는 다음과 같고 @IntRange 와 @FloatRange 같은 경우는 사용자가 범위를 잘못 알 수 있는 파라미터에 적용할 때 유용합니다.
// alpha integer 타입 변수 0 ~ 255로 제한
fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) {
...
}
// alpha float 타입 변수 0.0 ~ 1.0
fun setAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float) {
...
}
fun showList(@Size(max=2) nameList: List<String>) {
nameList.forEach { println("name : $it") }
}
// value -> 정확한 크기 지정
// min -> 최소 크기
// max -> 최대 크기
// multiple -> 해당 인자의 배수 크기
anyof
속성을 사용하여 permission 리스트 중 하나만 만족하도록 검사할 수 있고, allof
속성을 사용하여 permission 리스트 모두를 만족하도록 검사할 수도 있습니다.// 단일 권한이 필요한 경우
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
fun setWallpaper(bitmap: Bitmap) {
}
// 하나 이상의 권한이 필요한 경우
@RequiresPermission(anyOf = [
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION
])
fun copyImageFile(dest: String, source: String) {
}
// 모든 권한이 필요한 경우
@RequiresPermission(allOf = [
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION
])
fun copyImageFile(dest: String, source: String) {
}
// Content Provider에 대해 읽기 및 쓰기가 별도의 권한이 필요한 경우
// Read나 Write를 사용해서 나눌 수 있습니다.
@RequiresPermission.Read(RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(RequiresPermission(WRITE_HISTORY_BOOKMARKS))
val BOOKMARKS_URI = Uri.parse("content://browser/bookmarks")
@Keep : 빌드 시 코드가 최적화 될 때 어노테이션이 연결된 요소를 제거하지 않아야 함을 나타냅니다.
@RequiresApi ; 해당 어노테이션은 @TargetApi 와 유사하게 지정된 API 레벨 이상에서만 호출되어야 함을 의미하지만 해당 메소드에 대한 호출자의 요구사항임을 더욱 명확하게 나타냅니다.
@VisibleForTesting : 선택적으로 가시성을 지정할 수 있습니다. 이 어노테이션이 달린다면 해당 메소드 혹은 필드의 가시성이 테스트를 위해 완화된 것임을 명시적으로 나타냅니다.
@ColorInt : AARRGGBB
와 같이 패킹된 color를 나타냅니다.
@Dimension : 변수나 필드 또는 메소드의 리턴 값이 dimension임을 나타냅니다. 인수로는 DP, PX, SP를 지정할 수 있습니다.
@Px : 변수나 필드 또는 메소드의 리턴 값이 pixel dimension임을 나타냅니다.
@GuardedBy : 어노테이션이 달린 메소드 또는 필드에 사용하려면 lock을 보유해야만 접근할 수 있음을 나타냅니다.
@ContentView : 생성자에 연결할 수 있는 어노테이션으로 LayoutRes
를 생성자의 인자로 받으면 setContentView를 대신할 수 있습니다.
class MainFragment: Fragment {
fun MainFragment() {
// This constructor is annotated with @ContentView
super(R.layout.main)
}
}
// Fragment 내부
@ContentView
public Fragment(@LayoutRes int contentLayoutId) {
this();
mContentLayoutId = contentLayoutId;
}
// 어노테이션 인자로 들어갈 수 있는 속성은 아래와 같습니다.
// api : 최소 API 레벨
// codename : String -> 최소 API의 코드명("R")
// lambda : Int -> 최소 API 레벨을 만족할 경우 실행될 람다의 파라미터 번호
// parameter : int -> 최소 API 레벨이 파라미터에 지정되며 첫 번째 파라미터 번호는 0 부터 시작
// 최소 O 버전 이상인지 검사
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
public static boolean isAtLeastO() {
return Build.VERSION.SDK_INT >= 26;
}
// 첫 번째 인자로 필요로 하는 API 레벨이 전달되며,
// SDK_INT가 그 이상으로 높으면 두 번째 매개변수의 함수(람다)가 실행된다
@ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
inline fun fromApi(value: Int, action: () -> Unit) {
if (Build.VERSION.SDK_INT >= value) {
action()
}
}
// 코틀린 프로퍼티에 사용
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.GINGERBREAD)
val isGingerbread: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD
참조