안드로이드 ButterKnife 설정과 @Bind 뷰 바인딩 정리

Butter Knife는 안드로이드 뷰와 뷰 이벤트를 애노테이션으로 연결해 주는 라이브러리입니다. 필드마다 findViewById를 부르고 캐스팅하던 코드를 애노테이션 한 줄로 대신할 수 있습니다. 이 글은 7.0.1 기준으로 정리했던 설정 메모에, 이후 8.0에서 바뀐 API와 현재 저장소에 붙어 있는 지원 중단 안내를 덧붙인 것입니다. 옛 프로젝트를 다시 열어 본 경우와 지금 새로 시작하는 경우를 나눠서 적었습니다.
findViewById를 걷어내는 애노테이션 바인딩
화면 하나를 만들면 XML에 선언한 뷰를 코드에서 다시 찾아와야 합니다. 텍스트뷰가 세 개면 조회가 세 줄, 캐스팅이 세 번 붙습니다. Butter Knife는 이 반복을 애노테이션으로 대신합니다. Butter Knife 공식 문서는 필드에 애노테이션과 뷰 ID를 붙여 두면 해당 뷰를 찾아서 자동으로 캐스팅한다고 설명합니다.
바인딩 대상은 뷰에서 끝나지 않습니다. 같은 문서의 기능 목록에는 문자열, 색상, 치수 같은 리소스를 필드에 넣는 @BindString, @BindColor, @BindDimen, 클릭이나 항목 선택 같은 이벤트를 메서드에 붙이는 @OnClick, @OnItemSelected, 그리고 여러 뷰를 리스트나 배열로 묶는 @BindViews가 함께 들어 있습니다. 뷰가 없을 수 있는 자리는 필드에 @Nullable, 메서드에 @Optional을 붙여 예외를 막습니다. 묶어 둔 뷰에 같은 동작을 한 번에 적용할 때는 ButterKnife.apply()를 씁니다.
그래서 이 라이브러리가 실제로 줄여 주는 코드는 대략 이런 것들입니다.
- 뷰 조회와 캐스팅 반복
- 익명 리스너 선언
- 리소스 조회 코드
화면 코드가 아니라 서버 응답을 다루는 쪽이라면 이야기가 달라집니다. 자바 객체와 JSON 사이 변환은 안드로이드 Gson 사용법: 자바 객체와 JSON 변환 정리에 따로 적어 두었습니다.
Gradle 설정과 7.0.1 시절의 의존성
원문 메모는 Gradle에 아래 한 줄을 넣는 것으로 시작합니다.
compile 'com.jakewharton:butterknife:7.0.1'
7.0.1은 2015년 6월 30일 릴리스입니다. 변경 이력을 보면 바로 앞 버전인 7.0.0에서 @InjectView와 @InjectViews가 @Bind로, ButterKnife.inject와 ButterKnife.reset이 각각 ButterKnife.bind와 ButterKnife.unbind로 이름이 바뀌었습니다. 7.0.1 자체는 @Nullable 배열 바인딩에서 뷰가 빠졌을 때 나던 ClassCastException을 고친 릴리스입니다. 아래 예제가 @Bind와 ButterKnife.unbind(this)를 쓰는 이유가 여기 있습니다. 7.x API를 그대로 따른 코드입니다.
의존성이 한 줄로 끝나는 것도 7.x의 모습입니다. 런타임과 컴파일러가 두 아티팩트로 나뉜 것은 8.0.0부터라고 변경 이력에 적혀 있습니다. 현재 저장소 README가 안내하는 설정은 두 줄입니다.
implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
README는 이 설정과 함께 sourceCompatibility와 targetCompatibility를 JavaVersion.VERSION_1_8로 맞추라고 적고 있습니다. 라이브러리 모듈에서 쓸 때는 Butter Knife Gradle 플러그인을 적용하고 애노테이션 안에서 R 대신 R2를 쓰라는 안내도 붙어 있습니다. 원문의 compile 구성 자리에 README는 implementation을 씁니다.
액티비티와 프래그먼트에 바인딩하기
원문에 남겨 둔 7.x 코드는 다음과 같습니다. 액티비티는 setContentView 다음에 ButterKnife.bind(this)를 부릅니다.
class ExampleActivity extends Activity {
@Bind(R.id.title) TextView title;
@Bind(R.id.subtitle) TextView subtitle;
@Bind(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
프래그먼트는 인플레이트한 뷰를 두 번째 인자로 넘기고, onDestroyView에서 해제합니다.
public class FancyFragment extends Fragment {
@Bind(R.id.button1) Button button1;
@Bind(R.id.button2) Button button2;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
ButterKnife.bind(this, view);
// TODO Use fields...
return view;
}
@Override public void onDestroyView() {
super.onDestroyView();
ButterKnife.unbind(this);
}
}
쓰고자 하는 뷰에 ID만 명시해 주면 바로 쓸 수 있습니다. 액티비티와 프래그먼트가 갈리는 지점은 뷰 계층을 어디서 가져오느냐입니다. ButterKnife 클래스 문서를 보면 bind는 Activity, View, Dialog 하나만 받는 형태와, 대상 객체와 함께 Activity, Dialog, View를 소스로 넘기는 형태로 나뉩니다. 프래그먼트는 자기 자신이 뷰가 아니므로 뒤쪽 형태를 씁니다.
@Bind에서 @BindView로, unbind에서 Unbinder로
위 코드를 8.0 이상에서 그대로 쓰면 컴파일되지 않습니다. 2016년 4월 25일 나온 8.0.0에서 @Bind가 @BindView와 @BindViews로 갈라졌기 때문입니다. 하나의 뷰는 @BindView, 여러 뷰는 @BindViews입니다. 해제 방식도 함께 바뀌어서, bind 호출이 Unbinder 인스턴스를 돌려주고 이 객체로 참조를 비웁니다. 변경 이력은 이것이 기존 unbind API를 대체하며 리스너를 정리할 수 있는 기능이 추가된 것이라고 적고 있습니다.
공식 문서에 실린 지금 형태의 프래그먼트 예제는 다음과 같습니다.
public class FancyFragment extends Fragment {
@BindView(R.id.button1) Button button1;
private Unbinder unbinder;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
unbinder = ButterKnife.bind(this, view);
return view;
}
@Override public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}
같은 8.0.0에서 배열과 TypedArray를 필드에 넣는 @BindArray, 리소스의 Bitmap을 넣는 @BindBitmap도 들어왔습니다. ProGuard 규칙이 라이브러리 안에 담겨 자동으로 포함되도록 바뀐 것도 이 버전입니다. 7.x 코드를 올릴 때 손봐야 할 곳은 애노테이션 이름, Unbinder 필드 추가, 의존성 두 줄 분리입니다.
지금 새로 만든다면 뷰 바인딩
현재 Butter Knife 저장소 첫머리에는 지원 중단 안내가 붙어 있습니다. 뷰 바인딩으로 옮기라는 문구와 함께, 기존 버전은 계속 동작하지만 AGP 연동과 관련된 치명적 버그 수정만 검토하며 기능 개발과 일반 버그 수정은 멈췄다고 적혀 있습니다. 유지 중인 프로젝트라면 당장 걷어낼 일은 아니지만, 새 모듈까지 이 라이브러리로 시작할 이유는 줄었습니다.
안드로이드 공식 문서의 뷰 바인딩 항목을 보면 모듈 단위로 기능을 켭니다.
android {
buildFeatures {
viewBinding true
}
}
켜 두면 레이아웃 XML마다 바인딩 클래스가 생성됩니다. 파일 이름을 파스칼 케이스로 바꾸고 Binding을 붙이는 규칙이라, result_profile.xml은 ResultProfileBinding이 됩니다. 이 클래스는 ID가 있는 뷰에 대한 참조와 루트 뷰를 돌려주는 getRoot()를 갖습니다. 액티비티에서는 inflate()로 인스턴스를 만들고 루트 뷰를 setContentView에 넘깁니다.
프래그먼트 쪽 주의사항은 Butter Knife 때와 성격이 같습니다. 프래그먼트가 자기 뷰보다 오래 살기 때문에 문서는 onDestroyView에서 참조를 비우라고 명시합니다.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
문서가 findViewById 대비 장점으로 드는 것은 null 안정성과 타입 안정성입니다. 존재하지 않는 ID나 어긋난 타입이 런타임 예외가 아니라 빌드 실패로 드러난다는 설명입니다.
지금 이 코드를 만났다면
새로 만드는 화면이라면 선택지는 하나입니다. 저장소가 지원 중단 상태이고 README도 뷰 바인딩으로 옮기라고 안내하니 buildFeatures { viewBinding true }부터 켜면 됩니다. 유지보수 중인 코드라면 기준은 버전입니다. @Bind와 unbind(this)가 보이면 7.x이고, 8.0.0으로 올리는 순간 @BindView 분리와 Unbinder 반환 때문에 그대로는 컴파일되지 않습니다.
버전을 특정해 올릴 계획이라면 변경 이력을 먼저 훑어 두는 편이 좋습니다.
build.gradle을 열어 butterknife 의존성의 버전 번호부터 확인해 보세요.
출처: Butter Knife 공식 문서, Butter Knife 저장소 README, Butter Knife 변경 이력, ButterKnife 클래스 문서, 안드로이드 뷰 바인딩 문서