개요
이 글에서는 Fragmentation 라이브러리(단일 Activity + 다중 Fragment 구조)와 BRVAH(BaseRecyclerViewAdapterHelper)를 함께 사용할 때 발생하는 NullPointerException 문제를 다룹니다. 특히 메뉴 클릭 시 Fragment를 전환하는 과정에서 getTopFragment()가 null을 반환하는 문제에 초점을 맞춥니다.
문제 상황
DrawerLayout 내부의 RecyclerView 메뉴 항목을 클릭하면 해당 Fragment로 전환해야 합니다. 메뉴 Adapter에서 콜백 인터페이스를 정의하고 MainActivity에서 이를 구현했습니다. 하지만 getTopFragment()가 항상 null을 반환하는 문제가 발생합니다.
메인 레이아웃 (activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/menu_layout_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@drawable/menu_bg">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
</android.support.v4.widget.DrawerLayout>
문제 원인 분석
코드 흐름을 살펴보면:
- 사용자가 RecyclerView의 메뉴 항목을 클릭합니다.
- Adapter의
OnClickListener가 콜백을 호출합니다. - MainActivity의
OntopicClickListener구현 내에서 DrawerLayout을 닫고postDelayed로 약 300ms 후startFragment()를 호출합니다. startFragment()메서드에서getTopFragment()를 사용하려 시도하지만 null을 반환합니다.
핵심 문제는 Fragment 전환 작업이 UI 이벤트 루프와 타이밍 문제로 인해 올바른 Fragment 스택을 참조하지 못하는 데 있습니다. postDelayed는 지연 실행을 보장하지만, 그 시점에 Fragment 트랜잭션이 이미 완료되었거나, getTopFragment()가 예상과 다른 상태를 반환할 수 있습니다.
해결 방법
다음과 같이 코드를 리팩토링하여 문제를 해결할 수 있습니다.
1. Adapter 인터페이스 수정
public class ExpandableItemAdapter extends BaseMultiItemQuickAdapter<MultiItemEntity, BaseViewHolder> {
private OnMenuClickListener menuClickListener;
public interface OnMenuClickListener {
void onMenuClicked(String documentName);
}
public void setOnMenuClickListener(OnMenuClickListener listener) {
this.menuClickListener = listener;
}
@Override
protected void convert(BaseViewHolder holder, MultiItemEntity item) {
// ... (기존 로직)
holder.itemView.setOnClickListener(v -> {
int pos = holder.getAdapterPosition();
Level0Item levelItem = (Level0Item) item;
String name = levelItem.title;
if (name.contains(".doc") && menuClickListener != null) {
menuClickListener.onMenuClicked(name);
}
// 확장/축소 로직
});
}
}
2. MainActivity 리팩토링
public class MainActivity extends SupportActivity {
private DrawerLayout drawerLayout;
private RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 초기 Fragment 설정
if (findFragment(HomeFragment.class) == null) {
loadRootFragment(R.id.fl_container, HomeFragment.newInstance());
}
initViews();
}
private void initViews() {
drawerLayout = findViewById(R.id.drawer_layout);
recyclerView = findViewById(R.id.rv);
// Adapter 설정
List<MultiItemEntity> dataList = generateMenuData();
ExpandableItemAdapter adapter = new ExpandableItemAdapter(dataList);
adapter.setOnMenuClickListener(this::handleMenuClick);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
adapter.expandAll();
// Drawer 토글 설정
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawerLayout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
toggle.syncState();
}
private void handleMenuClick(String documentName) {
// Drawer 닫기
drawerLayout.closeDrawer(GravityCompat.START);
// 직접 Fragment 전환 (지연 제거)
navigateToFragment(documentName);
}
private void navigateToFragment(String tableName) {
ISupportFragment currentFragment = getTopVisibleFragment();
if (currentFragment == null) {
// 안전 장치: 현재 Fragment가 없으면 HomeFragment로 대체
loadRootFragment(R.id.fl_container, HomeFragment.newInstance());
currentFragment = getTopVisibleFragment();
}
switch (tableName) {
case "避雷器实验报告.doc":
startFragmentWithBundle(HomeFragment.class, currentFragment);
break;
case "电缆头施工记录.doc":
handleCableDocumentClick(currentFragment);
break;
}
}
private void startFragmentWithBundle(Class<? extends SupportFragment> fragmentClass,
ISupportFragment currentFragment) {
SupportFragment targetFragment = findFragment(fragmentClass);
if (targetFragment == null) {
targetFragment = (SupportFragment) Fragment.instantiate(this, fragmentClass.getName());
}
Bundle bundle = new Bundle();
bundle.putString("from", "From:" + currentFragment.getClass().getSimpleName());
targetFragment.putNewBundle(bundle);
start(targetFragment, SupportFragment.SINGLETASK);
}
private void handleCableDocumentClick(ISupportFragment currentFragment) {
DiscoverFragment existingFragment = findFragment(DiscoverFragment.class);
if (existingFragment != null) {
Bundle bundle = new Bundle();
bundle.putString("from", "From:" + currentFragment.getClass().getSimpleName());
existingFragment.putNewBundle(bundle);
start(existingFragment, SupportFragment.SINGLETASK);
} else {
// HomeFragment로 pop 후 새 Fragment 시작
popTo(HomeFragment.class, false, () -> {
DiscoverFragment newFragment = DiscoverFragment.newInstance();
Bundle bundle = new Bundle();
bundle.putString("from", "From:" + currentFragment.getClass().getSimpleName());
newFragment.putNewBundle(bundle);
start(newFragment, SupportFragment.SINGLETASK);
});
}
}
private ISupportFragment getTopVisibleFragment() {
// getTopFragment() 대신 getActiveFragment() 사용
return getSupportFragmentManager().findFragmentById(R.id.fl_container);
}
}
변경 사항 설명
- 타이밍 문제 제거:
postDelayed를 제거하고 직접 Fragment 전환 로직을 호출합니다. - Fragment 참조 개선:
getTopFragment()대신getSupportFragmentManager().findFragmentById()를 사용하여 현재 보이는 Fragment를 안정적으로 가져옵니다. - 코드 구조화: 메뉴 클릭 처리와 Fragment 전환 로직을 별도 메서드로 분리하여 가독성을 높였습니다.
- 널 체크 강화: Fragment가 null일 경우 기본 Fragment(HomeFragment)로 대체하는 안전 장치를 추가했습니다.
추가 권장 사항
- Fragment 라이프사이클 이해: Fragment 전환 시
onSaveInstanceState와 복원 로직을 적절히 구현하세요. - BRVAH 최신 버전 사용: BaseRecyclerViewAdapterHelper 최신 버전은 내부적으로 더 안정적인 콜백 처리를 지원합니다.
- 디버깅 로그:
getTopVisibleFragment()호출 시점의 Fragment 스택 상태를 로그로 확인하여 문제를 추적하세요.