ViewStub이란?
ViewStub은 사이즈가 없는 보이지 않는 뷰로 런타임에서 늦은 전개(lazy-inflate)를 원할 때 사용할 수 있다. ViewStub을 보이게 만들거나 inflate() 메서드를 호출하면 레이아웃이 전개되면서 ViewStub을 대체하기 때문에 ViewStub은 사라진다. 전개된 뷰는 ViewStub의 부모 뷰에 추가 된다. 레이아웃에서 ViewStub을 사용하는 예제를 확인하자.
<ViewStub android:id="@+id/stub" android:inflatedId="@+id/subTree" android:layout="@layout/mySubTree" android:layout_width="120dp" android:layout_height="40dp" />
findViewById() 호출을 통해 ViewStub에 접근할 수 있다.
ViewStub viewStub = findViewById(R.id.stub);
생성되는 바인딩 클래스에서 ViewStub은 ViewStubProxy로 표현되며, ViewStub에 대해 접근할 수 있게 해준다.
ActivityMainBinding binding = ... ViewStubProxy viewStubProxy = binding.stub; ViewStub viewStub = viewStubProxy.getViewStub();
ViewStub에 지정된 레이아웃을 전개 시키기 위해서 서는 setVisibility() 또는 inflate()를 호출 할 수 있다.
ViewStub viewStub = ... viewStub.inflate(); //또는 viewStub.setVisibility(View.VISIBLE);
ViewStub은 복잡하게 구성된 레이아웃을 빠르게 전개시켜야하는 상황에서, 레이아웃의 전개 시기를 선택적으로 늦출 수 있다. 예를 들어 리스트 형태의 UI를 구성하고 하나의 뷰홀더가 전개 되는데 상당한 비용이 발생한다고 가정하자. 이 때 사용자가 빠르게 화면을 스크롤 할 경우 프레임 드랍이 발생 할 수 있다. 이럴 때 선택적으로 불필요한 레이아웃의 전개를 제어하고 전개 시기를 늦춤으로써 성능을 개선시킬 수 있다.
ViewStub과 바인딩 어댑터
만약 ViewStub에 지정된 레이아웃이 전개된 상태(inflated)라면 app:user=”@{user}”와 같은 바인딩 표현식을 사용할 수 있다.
ViewStub과 바인딩 표현식을 사용하는 경우 ViewStub에 반드시 아이디를 선언해야한다.
<?xml version="1.0" encoding="utf-8"?> <!--activity_user.xml--> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.charlezz.jetpacklibrarysample.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- id반드시 있어야함--> <ViewStub android:id="@+id/user_view_stub" android:layout_width="match_parent" android:layout_height="match_parent" android:layout="@layout/view_user" app:user="@{user}" /> </LinearLayout> </layout>
<?xml version="1.0" encoding="utf-8"?> <!--view_user.xml--> <layout> <data> <variable name="user" type="com.charlezz.jetpacklibrarysample.User" /> </data> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!--user 데이터와 뷰를 바인딩 --> ... </LinearLayout> </layout>
생성되는 ActivityUserBindingImpl 내부 코드를 살펴보면, 전개되지 않은 ViewStub이라면 바인딩 표현식이 적용되지 않도록 막아두었다. 아마 전개 되지 않은 바인딩 인스턴스에 접근하려하면 NPE가 발생할 수 있기 때문에 사전에 막은 듯 하다.
@Override protected void executeBindings() { ... if (this.userViewStub.isInflated()) this.userViewStub.getBinding().setVariable(BR.user, user); ... }
전개되지 않은 ViewStub에는 바인딩 표현식이 적용되지 않는 다는것을 알았으니, ViewStub을 전개하는 작업을 우선적으로 해보자. inflate()를 호출해도 되지만 android:visibility 속성을 이용한 바인딩 표현식으로 전개를 할 수 있다. 내부적으로는 View.setVisibility(int)가 호출 될것이다.
<?xml version="1.0" encoding="utf-8"?> <!--activity_user.xml--> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="android.view.View"/> <variable name="user" type="com.charlezz.jetpacklibrarysample.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- id반드시 있어야함--> <ViewStub android:id="@+id/user_view_stub" android:layout_width="match_parent" android:layout_height="match_parent" android:layout="@layout/view_user" android:visibility="@{user==null ? View.GONE : View.VISIBLE}" app:user="@{user}" /> </LinearLayout> </layout>
레이아웃내에서 View를 참조하고
android:visibility=”@{user==null ? View.GONE : View.VISIBLE}” 바인딩 표현식으로 User 데이터가 null이 아니면 전개를 한 뒤 바인딩을 하도록 코드를 고쳤다. 다시한번 ActivityUserBindingImpl코드를 살펴보자.
@Override protected void executeBindings() { ... if (!this.userViewStub.isInflated()) this.userViewStub.getViewStub() .setVisibility(userJavaLangObjectNullViewGONEViewVISIBLE); if (this.userViewStub.isInflated()) this.userViewStub.getBinding().setVariable(BR.user, user); ... }
앞의 코드를 살펴보면 ViewStub으로부터 레이아웃 전개를 먼저하고 user 데이터를 바인딩 하는 것을 확인 할 수 있다.
하지만 여기서 문제가 하나 있다.
레이아웃을 전개하는 setVisibility()은 ViewStub이 전개되지 않았을때만 호출되는 것이다. 그렇기 때문에 레이아웃을 동적으로 숨기고 싶을때는 바인딩 표현식을 사용할 수 없게 된다.
이를 해결하는 방법으로 사용자 정의 바인딩이 있다.
다음과 같이 User 데이터를 바인딩 시키기 위한 바인딩 어댑터를 선언할 수 있다. User 데이터가 있다면 ViewStub의 레이아웃을 전개시키고 데이터를 바인딩한다. User 데이터가 없다면 필요에 따라 이미 전개된 레이아웃을 감출 수도 있다.
public class UserBindingAdapter { @BindingAdapter(value = {"user", ""}, requireAll = false) public static void setUser(View view, User user, Void nothing){ //nothing to do } public static void setUser(ViewStubProxy proxy, User user, Void nothing){ if(proxy.getViewStub()!=null){ if(user !=null){ //User 정보가 있다면 레이아웃을 전개하고 데이터를 뷰에 바인딩 한다. proxy.getViewStub().setVisibility(View.VISIBLE); proxy.getBinding().setVariable(BR.user, user); proxy.getBinding().executePendingBindings(); }else{ // 레이아웃이 전개된적 있지만 User 정보가 없을때 레이아웃을 감춘다. proxy.getBinding().setVariable(BR.user, null); proxy.getBinding().getRoot().setVisibility(View.GONE); } } } }
바인딩 어댑터의 정의가 끝났다면 레이아웃에서는 user 속성을 이용한 바인딩 표현식을 사용할 수 있게 된다.
<?xml version="1.0" encoding="utf-8"?> <!--activity_user.xml--> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="android.view.View"/> <variable name="user" type="com.charlezz.jetpacklibrarysample.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- id반드시 있어야함--> <ViewStub android:id="@+id/user_view_stub" android:layout_width="match_parent" android:layout_height="match_parent" android:layout="@layout/view_user" app:user="@{user}" /> </LinearLayout> </layout>
ViewStub과 사용자 정의 바인딩 사용시 주의해야할 점이 있다.
레이아웃에 선언된 ViewStub은 ViewStubProxy로 표현된다. 그렇기 때문에 @BindingAdapter 메서드를 만들 때 첫번째 매개변수로 View를 지정하더라도 바인딩 클래스에서는 ViewStubProxy를 호출하기 때문에 이를 위한 정적 메서드를 하나 더 만들어야 한다. @BindingAdapter 메서드의 첫번째 매개변수는 반드시 View여야 하는 제약조건으로 인해 ViewStubProxy를 첫번째 매개변수로 하는 @BindingAdapter 메서드를 정의할 수는 없다.
BindingAdapter의 속성이 1개일 때(user)는 처음 다루었던 예제와 마찬가지로, 레이아웃이 전개되었을 때만 바인딩 수행하는 코드가 바인딩클래스에 생성된다. 그러므로 반드시 2개이상의 매개변수를 가져야 하기 때문에 더미 변수를 하나 더 추가하고, requireAll 속성을 false로 지정 한다.
@BindingAdapter 메서드를 올바르게 정의했다면, ActivityUserBindingImpl 클래스의 코드는 다음과 같이 생성된다.
@Override protected void executeBindings() { ... com.charlezz.jetpacklibrarysample.UserBindingAdapter .setUser(this.userViewStub, user, (java.lang.Void)null); ... }
TL;DR
ViewStub을 사용하면 불필요한 레이아웃의 전개를 막을 수 있기 때문에, 복잡한 레이아웃을 포함하는 RecyclerView 등에서 필수적으로 사용된다.
ViewStub과 사용자 정의 바인딩을 사용하기 위해서는 공식문서에서는 언급이 없는 몇가지 제약조건이 있으며, 그에 기반한 코드를 살펴보면 매우 Tricky하다. 복잡한 레이아웃을 구성하는 사람들에게 작은 도움이 되었으면 좋겠다.
5개의 댓글
Jeremih · 2022년 3월 23일 7:14 오후
안녕하세요
아래에서부터 위로 4번째에 있는 코드에
setVisibility안에 값에 오타가 있는거같아요.
View.VISIBLE인가 같습니당.
Charlezz · 2022년 3월 24일 12:26 오전
예제 코드에 포함된 int 자료형인 userJavaLangObjectNullViewGONEViewVISIBLE은
데이터 바인딩 프로세서에 의해 생성된 코드입니다.
Jeremih · 2022년 3월 24일 8:17 오전
BindingImpl의코드였군여
죄송합니다🙇♂️
sslee · 2022년 4월 15일 3:38 오후
ViewStubProxy 에 대한 바인딩 어뎁터를 정의할 때 인자가 하나면 ViewStub 이 inflate 되고나서만 실행되도록 impl 이 생성되는데 void 인자를 하나 더 생성하면 상관 없이 실행 되도록 inflate 되는 트릭인건가요? 왜 이렇게 되는 것인지 궁금하네요
Charlezz · 2022년 4월 16일 2:55 오전
타입(Void)은 상관없습니다.
다만 데이터바인딩의 애노테이션 프로세서가 @BindingAdapter의 정보를 토대로 setUser(ViewStubProxy, …) 코드를 자동으로 생성합니다.
이때 ViewStubProxy를 제외한 나머지 매개변수를 최소 2개이상 필요로 하기에 거기에 맞춰 만든 것입니다.
이 부분은 복잡한 내용이기도 하고, Compose 등장이후로 ViewStub 사용빈도나 중요성이 낮아졌기 때문에 Compose를 사용하신다면 그냥 넘어가셔도 될 것 같습니다!