[Android] Fragment와 FragmentManager
UI 유연성의 필요
UI 유연성이라하면 사용자나 장치가 요구하는 것에 따라 런타임 시에 액티비티의 뷰를 구성하거나 변경할 수 있는 능력이다. 그런데 액티비티는 이런 유연성을 제공하도록 설계되지 않았다. 액티비티의 뷰들은 런타임 시에 변경되며, 이 뷰들을 제어하는 코드는 액티비티 내부에 있어야 한다. 따라서 액티비티는 사용하는 특정 화면과 강하게 결합되어 있다.
프래그먼트 개요
하나 이상의 프래그먼트(fragment) 로 앱의 UI를 관리하면 유연성이 좋아진다. 프래그먼트는 액티비티의 작업 수행을 대행할 수 있는 컨트롤러 객체다. 여기서 작업이란 UI 관리를 말하며, UI는 화면 전체 또는 일부분이 될 수 있다.
UI를 관리하는 프래그먼트를 UI 프래그먼트라 한다. UI 프래그먼트는 레이아웃 파일로부터 인플레이트 inflate되는 자신의 뷰를 하나 갖는다. 프래그먼트 뷰는 사용자가 보면서 상호 작용하기를 원하는 UI 요소들을 포함한다.
액티비티의 뷰는 자신의 UI를 갖는 대신 프래그먼트를 넣을 컨테이너를 가지며, 이 컨테이너에는 인플레이트된 프래그먼트의 뷰가 추가된다. 이 장에서는 액티비티가 하나의 프래그먼트만 포함하지만, 액티비티는 여러 개의 다른 프래그먼트 뷰를 수용하는 다수의 컨테이너를 가질 수 있다.
UI 프래그먼트를 사용하면 앱의 UI를 조립 가능한 요소로 분리할 수 있어서 유용하며, 탭 인터페이스를 비롯한 여러 가지를 쉽게 만들 수 있다.
새로운 안드로이드 Jetpack API 중에서도 내비게이션 컨트롤러와 같이 프래그먼트를 잘 활용하는 API가 있다. 따라서 프래그먼트를 사용하면 Jetpack API를 같이 사용할 때도 유용하다.
프래그먼트를 이용한 앱 개발 시작
예시로 사용되는 앱의 화면은 CrimeFragment라는 UI 프래그먼트가 관리하며, CrimeFragment의 인스턴스는 MainActivity라는 액티비티가 호스팅한다.
액티비티는 자신의 뷰 계층 구조에 프래그먼트와 그 뷰를 포함하는 곳을 제공하는데, 이것을 호스팅이라고 생각하면 된다. 프래그먼트는 화면에 보이는 뷰를 자체적으로 가질 수 없으며, 액티비티의 뷰 계층에 추가될 때만 화면에 자신의 뷰가 보인다.
CrimeFragment를 호스팅하는 MainActivity
액티비티로만 이루어진 앱에서 액티비티들이 했던, UI를 생성하고 관리하며 모델 객체들과 상호 작용하는 일을 CrimeFragment가 한다는 것을 아래의 다이어그램에서 알 수 있다.
프래그먼트를 사용하는 앱의 객체 다이어그램
FragmentManager에 UI 프래그먼트 추가하기
Fragment 클래스가 허니콤 honeycomb 버전에 추가되면서 FragmentManager를 호출하는 코드를 포함하도록 Activity 클래스가 변경되었다. FragmentManager는 프래그먼트 리스트와 프래그먼트 트랜잭션의 백 스택 back stack을 처리한다. FragmentManager는 프래그먼트의 뷰를 액티비티의 뷰 계층에 추가하고 프래그먼트의 생명주기를 주도하는 책임을 갖는다.
프래그먼트 트랜잭션
FragmentManager에 프래그먼트를 관리하도록 넘겨주는 코드를 MainActivity.kt에 추가한다.
CrimeFragment 추가하기 코드 1 (MainActivity.kt)
1 | class MainActivity : AppCompatActivity() { |
액티비티에 프래그먼트를 추가하기 위해 액티비티의 FragmentManager를 호출했다. 이때 Jetpack 라이브러리와 AppCompatActivity 클래스를 사용하고 있으므로 supportFragmentMananger 속성을 사용해서 액티비티의 프래그먼트 매니저를 참조할 수 있다.
supportFragmentMananger의 이름 앞 'support’는 v4 지원 라이브러리로부터 유래된 것이다. 그러나 지금은 v4 지원 라이브러리가 Jetpack 내부에 androidx 라이브러리로 포함되었다.
프래그먼트 트랜잭션 fragment transaction 을 생성하고 커밋
1 | supportFragmentManager |
프래그먼트 트랜잭션은 프래그먼트 리스트에 프래그먼트를 추가 add, 삭제 remove, 첨부 attach, 분리 detach, 변경 replace하는데 사용된다. 프래그먼트 트랜잭션을 사용하면 여러 개의 오퍼레이션 (트랜잭션으로 실행되는 각 함수 코드)을 묶어서 수행할 수 있다. 예를 들어, 다수의 프래그먼트를 동시에 서로 다른 컨테이너에 추가하는 경우다. 프래그먼트로 런타임 시에 화면을 구성 또는 변경하는 방법의 핵심이 바로 프래그먼트 트랜잭션이다.
FragmentManager는 프래그먼트 트랜잭션의 백 스택을 유지 관리한다. 따라서 프래그먼트 트랜잭션이 다수의 오퍼레이션을 포함한다면 해당 트랜잭션이 백 스택에서 제거될 때 이 오퍼레이션들이 역으로 실행된다. 그러므로 다수의 프래그먼트 오퍼레이션들을 하나의 트랜잭션으로 묶으면 UI 상태를 더욱 잘 제어할 수 있다.
FragmentManager.beginTransaction
함수는 FragmentTranscation의 인스턴스를 생성해 반환한다. FragmentTransaction 클래스는 플루언트 인터페이스 fluent interface를 사용한다. (플루언트 인터페이스는 코드를 이해하기 쉽게 해주는 객체지향 기법이며, 일반적으로 함수의 연쇄 호출 형태로 구현된다.) 즉, FragmentTransaction을 구성하는 함수들이 Unit 대신 FragmentTransaction 객체를 반환하기 때문에 이 함수들을 연쇄 호출할 수 있다 (코틀린의 Unit은 하나의 인스턴스만 생성되는 싱글톤 객체이며 자바의 void와 같이 함수의 반환 값이 없음을 나타내는 데 사용된다). 따라서 위의 코드는 '새로운 프래그먼트 트랜잭션 인스턴스를 생성하고 이 인스턴스에 add()
오퍼레이션을 포함시킨 후 커밋해라’라는 의미다.
add(...)
함수는 컨테이너 뷰 ID와 새로 생성된 CrimeFragment 인스턴스를 매개변수로 갖는다. 여기서 컨테이너 뷰 ID는 activity_main.xml에 정의했던 FrameLayout의 리소스 ID다.
컨테이너 뷰 ID는 다음 두 가지 목적으로 사용된다.
- 액티비티 뷰의 어느 위치에 프래그먼트 뷰가 나타나야 하는지를 FragmentManager에 알려준다.
- FragmentManager의 리스트에서 프래그먼트를 고유하게 식별하는 데 사용된다.
FragmentManager로부터 CrimeFragment를 가져오려면 다음의 코드 첫째 줄처럼 컨테이너 뷰 ID로 요청한다.
1 | val currentFragment = |
FragmentManager가 FrameLayout의 리소스 ID를 사용해서 CrimeFragment를 식별한다는 것이 이상하게 보일지 모른다. 그러나 컨테이너 뷰의 리소스 ID로 UI 프래그먼트를 식별하는 것이 FragmentManager가 작동하는 방법이다. 만일 하나의 액티비티에 여러 개의 프래그먼트를 추가한다면, 각 프래그먼트에 대해 별도의 리소스 ID를 갖는 컨테이너 뷰를 생성하기 때문이다.
이제 코드 1 ↩이 어떻게 작동하는지 자세히 살펴보자.
우선 R.id.fragment_container의 컨테이너 뷰 ID와 연관된 프래그먼트를 FragmentManager에 요청한다. 만일 이 프래그먼트가 리스트에 이미 있다면, FragmentManager가 그것을 반환한다.
그런데 요청한 프래그먼트가 어째서 이미 프래그먼트 리스트에 있는 것일까? 여러 이유로 액티비티가 소멸되었다가 다시 생성될 때를 대비해서 리스트에 보존하기 대문이다. 즉, 장치가 회전되거나 안드로이드 운영체제의 메모리 회수로 MainActivity가 소멸되었다가 다시 생성되면 MainActivity.onCreate(Bundle?)
이 다시 호출된다. 따라서 액티비티가 소멸될 때는 이 액티비티의 FragmentManager 인스턴스가 해당 액티비티의 프래그먼트 리스트를 보존한다. 그리고 해당 액티비티가 다시 생성되면 새로운 FragmentManager 인스턴스가 그 리스트를 가져와서 리스트에 있는 프래그먼트를 다시 생성해 이전 상태로 복원한다.
이와는 달리 지정된 컨테이너 뷰 ID의 프래그먼트가 리스트에 없다면, fragment 변수는 null이 된다. 이때는 새로운 CrimeFragment와 새로운 프래그먼트 트랜잭션(프래그먼트를 리스트에 추가하는)을 생성한다.
이렇게 MainActivity가 CrimeFragment를 호스팅하게 되었다.
FragmentManager와 프래그먼트 생명주기
프래그먼트 생명주기는 액티비티 생명주기와 유사하다. 즉 중단 (stopped) 상태, 일시 중지 (paused) 상태, 실행 재개 (resumed) 상태를 가지며, 상태가 전환될 대 필요한 일을 처리하기 위해 오버라이드할 수 있는 함수들도 갖는다. 이 함수들은 액티비티 생명주기 함수들과 대응된다.
프래그먼트 생명주기 다이어그램
액티비티와 프래그먼트의 생명주기 함수가 대응된다는 점이 중요하다. 프래그먼트는 액티비티를 대신해 작동하므로 프래그먼트의 상태는 액티비티의 상태를 반영해야한다. 따라서 프래그먼트는 액티비티의 작업을 처리하기 위해 액티비티와 일치하는 생명주기 함수가 필요하다.
프래그먼트 생명주기와 액티비티 생명주기가 다른 점은 프래그먼트 생명주기 함수는 안드로이드 운영체제가 아닌 호스팅 액티비티의 FragmentManager가 호출한다는 점이다. 프래그먼트는 액티비티가 내부적으로 처리해서 안드로이드 운영체제는 액티비티가 사용하는 프래그먼트에 관해서는 아무것도 모른다.
onAttach(Context?)
, onCreate(Bundle?)
, onCreateView(...)
, onViewCreated(...)
함수들은 프래그먼트를 FragmentManager애 추가할 때 호출된다.
onActivityCreated(Bundle?)
함수는 호스팅 액티비티의 onCreate(Bundle?)
함수가 실행된 후 호출된다. 앱에서는 MainActivity.onCreate(Bundle?)
에서 CrimeFragment를 추가하는데, onActivityCreated(Bundle?)
함수는 프래그먼트가 추가된 후에 호출된다.
액티비티가 이미 실행 중일 때 프래그먼트를 추가하면 어떻게 될까? 이때 FragmentManager는 해당 프래그먼트가 호스팅 액티비티의 상태를 따라잡는데 필요한 프래그먼트 생명주기 함수를 몇 개이든 차례대로 즉시 호출한다. 예를 들어, 이미 실행 중인 액티비티에 프래그먼트가 추가되면 이 프래그먼트는 onAttach(Context?)
, onCreate(Bundle?)
, onActivityCreated(Bundle?)
, onStart()
, onResume()
의 순서로 이 함수들의 호출을 연속해서 받게 된다.
일단 프래그먼트의 상태가 액티비티의 상태를 따라잡으면 이후부터는 호스팅 액티비티의 FragmentManager가 액티비티 상태와 동조된 프래그먼트 상태를 유지한다. 즉, 안드로이드 운영체제로부터 액티비티 생명주기 함수들이 호출되면 이것과 부합되는 프래그먼트 생명주기 함수들을 호출해준다.
프래그먼트를 사용하는 애플리케이션 아키텍쳐
프래그먼트는 주요 컴포넌트를 재사용하게끔 캡슐화한다. 여기서 주요 컴포넌트는 앱의 전체 화면에 나타난다. 만일 한번에 너무 많은 프래그먼트를 화면에 넣는다면, 프래그먼트 트랜잭션 때문에 코드가 지저분하게 된다. 따라서 작은 컴퍼넌트들을 재사용할 때는 프래그먼트 대신 커스텀 뷰 (View의 서브 클래스 또는 View의 서브 클래스의 서브 클래스)로 추출하는 것이 좋은 방법이다.
일반적으로 한 화면에는 최대 두 개 또는 세 개 정도의 프래그먼트를 사용하는 것이 좋다.