어떤 식으로 뷰를 Composition할 수 있는지 샘플코드를 보여드릴게요.
먼저 레이아웃 파일입니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="?attr/actionBarSize">
<FrameLayout
android:id="@+id/fragmentFrame"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationFrame"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<FrameLayout
android:id="@+id/bottomNavigationFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
</LinearLayout>
bottmNavigationFrame은 원래 BottomNavigationView가 있던 자리인데,BottomNavigationView을 집어넣어서 사용하거라 FameLayout으로 대체했습니다. FrameLayout에 프레그먼트를 add하는 것과 유사합니다.
BottomNavigationView를 별도의 클래스로 이동시킬겁니다. 이걸 bottmNavigationFrame에 끼워 넣을 건데, Activity 와 BottomNavigationView 간의 커뮤니케이션은 Observer patter을 통해 할 겁니다.
BottomNavigationMenu용 레이아웃 파일입니다.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
</menu>
이제 핵심 클래스인 BottomNavigationViewMvc입니다. BottomNavigationViewMvc 클래스는 BottomNavigationView 의 wrapper입니다. 뷰계열의 클래스가 아닌 이유는 재사용성을 높이기 위해서입니다. 이렇게 하면 여러 액티비티들에서 재사용이 아주 쉬워집니다.
// 옵저버 패턴을 사용하기 위한 인터페이스입니다. 옵저버 클래스에서 Observable을 구현한 클래스에 등록을 해줍니다.
public interface Observable<LISTENER> {
void registerListener(LISTENER listener);
void unregisterListener(LISTENER listener);
}
// 옵저버 등록과 해제를 처리하는 기본클래스 입니다.
public abstract class BaseObservable<LISTENER> implements Observable<LISTENER> {
protected Set<LISTENER> listeners = new HashSet<>();
@Override
public void registerListener(LISTENER listener) {
listeners.add(listener);
}
@Override
public void unregisterListener(LISTENER listener) {
listeners.remove(listener);
}
}
public enum BottomMenuItem {
HOME,
DASHBOARD,
NOTIFICATION
}
// BottomNaviationView는 BaseObservable을 구현하여 Observable이 됩니다.
// 이 클래스를 사용하는 클래스는 옵저버가 되고 등록을 해주면 됩니다.
public class BottomNavigationViewMvc extends BaseObservable<BottomNavigationViewMvc.Listener> implements OnNavigationItemSelectedListener {
public interface Listener {
void onBottomMenuClicked(BottomMenuItem menuItem);
}
private LayoutBottomNavigationBinding binding;
public BottomNavigationViewMvc(LayoutInflater layoutInflater) {
inflateLayout();
initBottomNavigation(layoutInflater);
}
private void inflateLayout() {
binding = LayoutBottomNavigationBinding.inflate(layoutInflater, null, false);
}
private void initBottomNavigation(LayoutInflater layoutInflater)) {
binding.navView.setOnNavigationItemSelectedListener(this);
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
BottomMenuItem bottomMenuItem;
switch (item.getItemId()) {
case R.id.navigation_home:
bottomMenuItem = BottomMenuItem.HOME;
break;
case R.id.navigation_dashboard:
bottomMenuItem = BottomMenuItem.DASHBOARD;
break;
case R.id.navigation_notifications:
bottomMenuItem = BottomMenuItem.NOTIFICATION;
break;
default:
throw new RuntimeException("Unexpected bottom menu: " + item.getItemId());
}
notifyBottomMenuClicked(bottomMenuItem);
return true;
}
// 옵저버들에게 메뉴가 클릭되었음을 알려줍니다.
private void notifyBottomMenuClicked(BottomMenuItem bottomMenuItem) {
for (Listener listener : listeners) {
listener.onBottomMenuClicked(bottomMenuItem);
}
}
public View getRoot() {
return binding.getRoot();
}
}
액티비티에서는 BottomNavigationViewMvc의 rootView을 bottomNavigationFrame에 집어 넣어 줍니다.
// 옵저버로 등록할 수 있도록 BottomNavigationViewMvc.Listener를 구현해 줍니다.
public class MainActivity extends AppCompatActivity implements BottomNavigationViewMvc.Listener {
private ActivityMainBinding binding;
private BottomNavigationViewMvc bottomNavigationViewMvc;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setupBottomMenu();
setContentView(binding.getRoot());
}
private void setupBottomMenu() {
bottomNavigationViewMvc = new BottomNavigationViewMvc(getLayoutInflater());
// bottomNavigationFrame에 BottomNaviagtionView를 추가합니다.
binding.bottomNavigationFrame.addView(bottomNavigationViewMvc.getRoot());
}
// onStart와 onStop에서 옵저버로 등록을 하고 해제하는 부분은 Memory leak 을 방지하가 위해 아주 중요합니다.
@Override
protected void onStart() {
super.onStart();
bottomNavigationViewMvc.registerListener(this);
}
@Override
protected void onStop() {
super.onStop();
bottomNavigationViewMvc.unregisterListener(this);
}
@Override
public void onBottomMenuClicked(BottomMenuItem menuItem) {
switch (menuItem) {
case HOME:
System.out.println("HOME clicked");
break;
case DASHBOARD:
System.out.println("DASHBOARD clicked");
break;
case NOTIFICATION:
System.out.println("NOTIFICATION clicked");
break;
}
}
}
BottomNavigationViewMvc는 다른 액티비티나 프레그먼트 들에서도 옵저버를 등록/해제 함으로써 사용할 수 있습니다. 그리고 ViewBinding을 사용하는지 findViewById를 사용하는지는 BottoomNavigationViewMvc 안에 감춰져 있습니다. BottoomNavigationView 관련 수정사항이 생긴다면 BottomNavigationViewMvc 클래스만 고치면 되겠죠.