본문 바로가기

Android

Fragment에 리스트 출력하기 Part.1

요구사항 다시 짚어보기

이번에 구현하려고 하는 기능은 Fragment에 RecyclerView를 사용해서 데이터를 리스트 형태로 출력하는 것이다.
이때 메뉴 선택에 따라 리스트에 담길 데이터의 내용이 달라질 수 있다.

Fragment + RecyclerView라는 구성의 XML 뷰라는 동일한 틀을 가지고 여러 개의 스크린을 보여줄 수 있어야 한다.
현재 1 Activity - Mutiple Fragments 구성을 사용하고 Navigation을 위해 Navigation Component를 사용해보려고 한다.

 

데이터의 종류는 Activity에 속해있는 Navigation Drawer의 메뉴 선택에 따라 결정된다.
즉 Drawer의 메뉴를 선택 시에 다른 내용의 리스트를 출력하는 것이 목표이다.

 

접근법 #1

TaskListFragment 클래스 틀을 가지고 있고, 메뉴를 선택시 new Fragment()를 사용하여 프래그먼트 인스턴스를 여러 개 만들면 위 기능을 구현할 수 있을거라고 생각했다.
Bundle 데이터를 프래그먼트를 생성할 때 첨부하여 전송하고, 생성된 프래그먼트는 첨부된 Bundle을 확인하여 알맞은 데이터를 가져와서 리스트로 출력할 수 있다.

큰 고민 없이 같은 xml화면을 가지지만 내용만 달라지는 여러 개의 화면을 각각 생성하면 된다고 생각하고 구현을 시작했다.

첫번째 문제는 Navigation Component의 사용이었다. 이번 프로젝트에서 네비게이션을 위해서 Jetpack의 Navigation Component를 사용하고자 하는데. Navigation에 사용되는 정보를 nav_graph.xml에 정의해둔 후 호출해서 사용하는 방식이다.

 

하나의 화면은 Destination이라고 부르고 Destination 간의 이동을 Action으로 정의하는 방식이다.
이때 TaskListFragment는 유일한 화면으로 홈스크린이 된다.

기본적으로 액션을 정의하려면 2개의 Destination이 필요해보이는데, 옵션중에 Self direction을 적용해보았다.

실행해보니 백스택에 있는 TaskListFragment가 불려와지고 새롭게 프래그먼트가 그려지지 않았다.
(백스택에 TaskListFragment 인스턴스가 1개 밖에 없다.)
우리가 원하던 결과가 아니다.

그렇다면 이전에 사용하던 방식인 FragmentTransaction 방식으로 프래그먼트 인스턴스를 생성해보자.
새로운 프래그먼트 인스턴스는 만들어졌지만 다른 문제가 발생했다. (백스택에 TaskListFragment 인스턴스가 2개 생성된다.)

 

기존 프래그먼트가 종료되지 않았기 때문에 옵저버 리소스가 회수되지 않은 상태이고, 같은 TaskListFragment에서 다시 옵저버를 등록하려고하면서 Exception이 발생했다.

 

접근법 #2

위의 시행착오를 겪고 처음 생각했던 접근법이 완전히 틀렸음을 깨달았다.
같은 화면 구성을 사용하는 프래그먼트를 공유하는 가장 좋은 방법은 프래그먼트 객체를 여러 개 만드는 것이 아니라.
기존에 생성된 프래그먼트를 재사용하고 내부 리스트의 내용만 바꾸는 것이다.

Fragment는 거대한 객체이므로 새롭게 생성하는데 많은 비용이 들 것이다. 그렇기 때문에 같은 프래그먼트를 여러 개 생성하지 않고 재사용할 수 있다면 많은 자원을 아낄 수 있을 것이다.

우리의 목적은 RecyclerView의 데이터 목록이 메뉴 선택에 따라 변경되는 것이다.

그러므로 프래그먼트는 새롭게 생성할 필요가 전혀 없으며, 이미 생성된 프래그먼트에 속한 RV의 내용만 업데이트 해주는 것이 훨씬 효율적인 방법이 될 것이다.

그렇다면 다음으로 생각해볼 것은 이미 만들어져 있는 TaskListFragment 객체에게 데이터 리스트를 바꾸라는 신호를 알려줄 수 있는 방법이다.

보통 Fragment는 Activity에서 초기화 되어 실행될 때 Bundle을 첨부해서 데이터를 전달한다.
하지만 우리는 이미 Activity - TaskListFragment 형태로 화면이 생성된 상태이고, 메뉴의 선택은 Activity에서 처리하므로 Activity에서 메뉴가 선택 되었을때 TaskListFragment에게 변화를 알려줄 수 있어야 한다.

혹은 Fragment가 ParentActivity에 접근해서 메뉴처리 Logic을 Fragment에 옮겨와야한다. 이 방식은 여러 타입의 Fragment가 생기게 되면 메뉴 처리를 각 Fragment에서 별도로 처리해야하므로 바람직하지 않다.

XML 정의에서 메뉴는 Activity에 속해 있으므로 Activity 내에서 처리하는 로직이 맞을 것 같다.

 

이미 생성된 Fragment에게 데이터를 전달할 방법을 찾아보자.

1.인터페이스를 이용한다.

public class TaskListFragment extends DaggerFragment implements PostDataToFragment {
    // ...
    @Override
    public void sendDataToFragment(int filter) {
        Timber.d("이것은 Activity에서 받은 Filter 값 : %d", filter);
    }
}

interface PostDataToFragment {
    void sendDataToFragment(int filter);
}

 

프래그먼트에서 데이터를 받을 때 사용할 수 있는 인터페이스 PostDataToFragment를 정의한다.

public class MainActivity extends AppCompatActivity {
    PostDataToFragment postDataToFragmentInterface;

    // Activity에서 이미 생성된 TaskListFragment 인스턴스를 얻을 방법 필요.
}

이제 TaskListFragment 인스턴스를 가져오면 된다.

 

 

검색결과 Navigation Component에서 인스턴스를 제공하기 위한 별도의 메소드는 없다.
기존 FragmentManager를 사용할 때는

getSupportFragmentManager().findFragmentId(R.id...);

를 사용해서 스택에 있는 프래그먼트 인스턴스를 얻을 수 있다.

 

구글에서 검색해본 결과 NavController를 사용해서 네비게이팅할 때 인스턴스를 얻는 레퍼런스는 따로 없는 것 같고,
여러 사람이 같은 질문을 올려 놓은 것을 확인했다.

결과적으로 여러가지 시도 끝에 인스턴스를 얻는데는 성공했지만, 코드가 매우 비효율적이고 더러워졌다.

 

우선 NavHostFragment를 얻는다. Navigation Component는 NavHostFragment 위에 nav_graph.xml에 정의된 Destination을 그리는 방식으로 동작한다.

 

// NavHostFragment 얻기
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);

이제 NavHostFragment에 현재 그려져 있는(백스택의 Top에 위치한) Fragment를 얻으려면

navHostFragment.getChildFragmentManager().getPrimaryNavigationFragmenmt();

를 사용해서 얻을 수 있다.

 

이런 계층 구조를 가지는 View를 구성하게 된다.

 

우리는 TaskListFragment 인스턴스임을 확인 후에 데이터를 전송해야하므로

Fragment topFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
if (TaskListFragment.class.equals(topFragment.getClass())) {
    Timber.d("This is TaskListFragment that currently showed in screen.");
    TaskListFragment taskListFrag = (TaskListFragment) topFragment;
    taskListFragment.sendDataToFragment(5);
}

이런 복잡한 형변환 과정을 거쳐야한다.

 

인터페이스를 사용하여 Activity에서 이미 생성된 Fragment에게 데이터를 전달하는 것을 확인했다.
하지만 NavHostFragment를 얻고, 그걸 통해서 다시 TopFragment를 얻고,
얻은 Fragment를 형변환해서 사용하는 이 방식은 사용이 권장되지 않을 것 같다.

다음버전의 Navigation Component에서 이런 기능을 추가해서 쉽게 인스턴스를 제공받을 수 있다면 좋을 것같다.

 

부록

위에서 인터페이스를 사용해서 Activity -- send --> Fragment 하는 과정을 구현해보았다.

그렇다면 반대로 Fragment -- send --> Activity 방향으로 데이터를 전송하는 인터페이스는 어떻게 구현하면 될까?

기존 Multi-Activity 구조에서는 startActivityForResult()를 이용해서, 액티비티가 종료될 때 결과 값을 리턴할 수 있는 구조를 활용해서 로직을 구성했다.

 

이번에는 Multi-Fragment를 사용하는 구조인데. Fragment에서 Activity에 결과값을 전달해주는 인터페이스를 구현해보면 좋을 것 같았다.

 

위 과정과 반대로 진행하면 된다.

// 이번엔 Activity에서 데이터를 받는 구조이므로 여기에 인터페이스를 정의한다.
publuic class MainActivity extends AppCompatActivity implements PostResult {
    @Override
    public void onPostResult(String result) {
        Timber.d("[ This is the result of PostResult Interface] : %s", result);
    }
}

interface PostResult {
    void onpostResult(String result);
}
// Fragment는 위에서 정의한 인터페이스를 사용해서 액티비티에 데이터를 전달한다.
class TaskListFragment extends Fragment {
    PostResult postResultInterface;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        postResultInterface = (PostResult) getActivity();
    }
}

 

 

생성된 TaskListFragment에서 Fragment가 종료되지 않고도 Activity로 여러 번 데이터를 보내는 것을 확인할 수 있다.

액티비티 위에 프래그먼트가 올려져 있는 형태이므로, 프래그먼트에서 액티비티의 인스턴스를 사용하는 것은 비교적 자유롭다.
(액티비티의 생명주기가 더 길기 때문에 Fragment에서 Activity의 Context를 사용해도 괜찮다. Fragment가 먼저 소멸되기 때문에)

Activity to Fragment보단 Fragment to Activity일 때 사용할 수 있을 것 같다.