참고 자료
Android Developer 도큐먼트 - 프래그먼트 개요
Android Developer 도큐먼트 - 프래그먼트 만들기
Android Developer 도큐먼트 - 프래그먼트 관리자(FragmentManager)
choheeis.github.io - Fragment
Framgnet의 공식문서를 읽고 분석한 1편을 잠깐 복습해보면
프래그먼트를 호스트하는 Activity UI 레이아웃에 프래그먼트 컨테이너만 배치하는 방법으로 Activity를 호스팅할 경우, 컨테이너 위치에 프래그먼트를 추가/교체/삭제하는 작업을 프로그래밍적으로 할 수 있습니다.
이와 같은 작업을 프로그래밍적으로 하려면 FragmentManager
와 FragmentTransaction
이라는 걸 알고, 사용할 줄 알아야 합니다
FragmentManager
는 백스택을 관리하고 FragmentTransaction
을 생성합니다
FragmentTransaction
클래스는 Fragment를 추가/교체/삭제하는 작업을 제공합니다
FramgmentTransaction은 Android Jetpack Fragment 라이브러리에서 제공하는 클래스입니다
FramgmentTransaction 클래스는 Fragment를 추가/교체/삭제하는 작업을 제공합니다
이외에도 FramgmentTransaction은 FragmentManager가 수행할 단일 단위입니다. 따라서 하나의 FramgmentTransaction 단위 내에 FramgmentTransaction 클래스가 제공하는 프래그먼트 추가/삭제/교체 작업 등을 명시하면 됩니다
FramgmentTransaction 인스턴스는 FramgmentManager로부터 생성할 수 있습니다. FramgmentTransaction는 추상 클래스이며 FragmentManager클래스에 구현되어 있기 때문입니다
따라서 FragmentManager클래스에서 제공하는 beginTransaction함수 호출을 통해 FramgmentTransaction 인스턴스를 생성할 수 있습니다
// 프래그먼트 트랜잭션 인스턴스 생성 예시
val fragmentManager = supportFragmentManager
val fragmentTransaction = supportFragmentManager.beginTransaction()
FragmentManager 클래스가 제공하는 beginTransaction
함수 호출을 통해 FragmentTransaction 인스턴스를 생성해보았습니다
하지만 주의할 점은 반드시 FragmentTransaction의 호스트 Activity가 자신의 상태(resumed,stopped..등)를 저장하기 전에 생성되고 commit되어야 합니다
호스트 Activity는 사용자가 화면을 회전 시 Activity가 종료되고 재생성되는데 종료되기 직전에 수명주기 메소드 중 onSaveInstanceState() 메소드를 호출하여 자신의 상태를 저장하고 종료합니다. 종료된 Activity가 재생성될 때 저장된 상태를 불러와 이전 상태를 그대로 유지하도록 할 수 있습니다.
만약 호스트 Activity가 onSavaInstanceState() 메소드를 호출한 후 FragmentTransaction이 commit된다면 에러가 발생합니다
그 이유는 호스트 Activity의 상태가 저장될 때 자신에게 호스팅되어 있는 Fragment의 상태도 저장하게 되는데 이런 상태 저장 후 FragmentTransaction에 의해 프래그먼트 추가/교체/삭제 작업이 일어나면 Activity가 저장한 Fragment의 상태와 달라지기 때문입니다
FramgmentTransaction을 생성하고 프래그먼트 추가/교체/삭제 작업을 명시한 후에는 반드시 마지막에 commit을 해줘야 합니다
commit은 FramgmentTransaction 클래스에서 제공하는 commit 함수 호출을 통해 실행합니다
commit함수를 호출해야만 FramgmentManager가 해당 FramgmentTransaction 수행을 예약 합니다
commit함수는 비동기로 처리되는 함수이기 때문에 commit 함수 호출 시점에서 Transaction이 즉시 수행되는 것이 아니라 메인스레드에 예약됩니다
메인스레드가 예약된 Transaction을 수행할 준비가 되면 비로소 그때 Transaction이 수행되며 명시한 Fragment 작업들이 실행됩니다
FramgmentTransaction을 얻고 commit까지 하는 두 방법 중 아래 방법처럼 commit{} 블록을 사용하면 FramgmentTransaction 생성+커밋 작업을 한번에 호출할 수 있어 편리합니다
프래그먼트 트랜잭션 실행에 관한 코드를 작성하고 항상 마지막에 FragmentTransaction 클래스에서 제공하는 commit()을 호출해주어야 합니다. 커밋을 호출해줘야 프래그먼트 관리자가 해당 트랜재션을 메인 쓰레드에 예약할 수 있기 때문입니다
commit()은 비동기 처리 되는 함수입니다
즉 commit()함수를 호출하면 호출한 순간에 해단 트랜잭션이 즉시 수행되는 것이 아니라 메인쓰레드에 해당 트랜잭션이 예약됩니다
예약된 트랜잭션 수행이 가능한 시점이 되어야 비로소 수행됩니다=비동기 처리
하지만 만약 해단 트랜잭션이 비동기 처리되면 안되는 피치못할 상황이 있다면 메인 쓰레드에서 커밋한 순간 즉시 수행할 수 있는 commitNow()를 호출하면 됩니다
commitNow()를 호출하면 호출한 즉시 해당 프래그먼트 트랜잭션이 동기적으로 수행됩니다
그러나 commitNow() 함수를 호출하여 프래그먼트 트랜잭션을 커밋하면 addToBackStack() 함수가 예상한대로 동작하지 않을 수 있다는 점을 기억해야 합니다
상황을 가정하여, 백 스택에 추가하는 작업이 포함된 트랜잭션(트랜잭션1)에 관해 commit() 함수를 호출한 뒤, 이어서 백 스택에 추가하는 작업이 포함된 다른 트랜잭션(트랜잭션2)에 관해 commitNow() 함수를 호출했다고 가정해봅시다.
만약 commit() 함수를 호출한 시점에 바로 메인 쓰레드 준비가 완료되어 트랜잭션1이 즉시 실행된다면(트랜잭션2보다 먼저 실행) 백 스택에 저장되는 트랜잭션의 순서(bottom -> top 순서)는 트랜잭션1, 트랜잭션2 일 것입니다.
그러나 만약 commit() 함수를 호출한 시점에 메인 쓰레드 준비가 되지 않아 예약만 될 경우, 트랜잭션1보다 트랜잭션2가 먼저 실행될 것입니다. 그럼 백 스택에 저장되는 트랜잭션 순서(bottom -> top 순서)는 트랜잭션2, 트랜잭션 1일 것입니다.
결론적으로, addToBackStack() 함수를 포함하는 여러 트랜잭션이 존재하고 이를 commit() 과 commitNow() 함수를 혼용하여 커밋한다면 개발자가 의도한 백 스택 저장 순서를 100% 보장할 수 없습니다.
따라서 commitNow()를 호출하여 커밋하는 프래그먼트 트랜잭션 내부에 addToBackStack() 함수가 명시되어 있으면 IllegalStateException 에러가 발생합니다.
FramgmentTransaction에서 제공하는 프래그먼트를 조작하는 작업 중 프래그먼트를 추가 하는 작업이 있습니다
이 작업을 위해 FramgmentTransaction 클래스의 add()함수를 호출하면 됩니다.
add() 함수는 오버로딩되어 여러가지 모양으로 존재하기 때문에 해야할 작업에 적절한 모양의 add() 함수를 호출합니다
add() 함수 호출을 통해 수행되는 프래그먼트 추가 작업은 호스트 Activity의 수명 주기에 프래그먼트 수명 주기를 추가하는 것입니다
위 그림처럼 호스트 Activity의 수명 주기에 프래그먼트가 추가되면 위와 같은 순서로 Activity수명주기와 프래그먼트 수명 주기간의 관계가 형성됩니다
프래그먼트 수명주기에서 onAttach()때 Fragment가 자신의 호스트인 Activity에 붙고, onCreateView에서 프래그먼트 View가 inflate되며 Fragment Container 위치에 보이게 됩니다
FragmentTransaction 인스턴스를 생성하고 add()함수를 호출해 ExampleFragment를 추가하는 작업을 한 후 Activity와 Fragment의 수명주기를 로그에 찍어봤습니다
위 로그처럼 add()가 호출되어 호스트 Activity 수명주기에 ExampleFragment 수명주기가 추가된 것을 알 수 있습니다
빨간화면은 Activity이고 검은 화면이 Fragment인데 Fragment가 Activity의 FragmentContainer위에 잘 배치되었습니다
위처럼 같은 Fragment를 add()할지라도 각 Fragment는 다른 인스턴스로 생성되며 각자의 수명주기를 가지게 됩니다 따라서 Activity의 FragmentContiner위에 각 프래그먼트가 하나씩 (거의 동시에) 추가되고 마지막에 추가된 Fragment의 UI에 의해 먼저 추가된 Fragment는 사라져 보이지 않습니다
FragmentTransaction이 제공하는 프래그먼트 조작 작업 중 Fragment 제거 작업이 있습니다
FragmentTransaction의 remove() 함수를 호출하면 됩니다
remove함수를 호출하면 호스트 Activity에 생성되어 있는 프래그먼트 중 remove() 함수 인자로 전달한 프래그먼트를 제거합니다 즉 호스트 Activity에서 해당 프래그먼트를 떼어냅니다.
링크텍스트
호스트 Activity에 추가되어 있는 프래그먼트 중 원하는 프래그먼트를 TAG를 통해 찾기 위해 add()로 프래그먼트를 추가할때 인자에 TAG를 추가로 전달합니다. MainActivity에 버튼을 하나 만들고 fundFragmentByTag() 함수로 Example2Fragment의 참조를 가져와 remove()의 인자로 전달합니다
FragmentTransaction이 제공하는 프래그먼트 조작 작업 중 Fragment 교체 작업이 있습니다
FragmentTransaction의 replace() 함수를 호출하면 됩니다
replace()의 '교체'라는 어감때문에 replace()를 호출하면 호스트 Activity에 생성되어 있는 프래그먼트 중 어느 두개의 프래그먼트가 서로 교체될 것이라고 예상될 것입니다. 그러나 호스트 Activity에 생성되어 있는 프래그먼트 중 replace() 함수 인자로 지정된 프래그먼트를 제외한 나머지 모든 프래그먼트가 제거(remove)됩니다. 따라서 프래그먼트 컨테이너에 남아있는 프래그먼트 자체 UI레이아웃은 replace()함수의 인자로 지정된 프래그먼트 뿐이기 때문에 사용자의 눈에는 해당 프래그먼트만 보이게 됩니다.
위 코드처럼 서로 다른 프래그먼트 3개를 동일한 FragmentContainer에 추가합니다 버튼을 누르면 Example2Fragment가 replace되도록 했습니다() (ExampleFragment는 검은색, Example2Framgnet는 파란색, Example3Fragment는 노란색으로 구분함)
위 영상처럼 버튼을 누르면 파란색 화면인 Example2Fragment가 보이게 됩니다
replace()가 호출된 후 수명주기를 보면 교체대상인 Example2Fragment 를 제외한 나머지 두 프래그먼트가 차례로 Detach()됩니다 제거되는 순서는 Example3Fragment->Examplefragment 순서이므로 프래그먼트 컨테이너 가장 위쪽(스택형식으로 가장 마지막에 add한 프래그먼트)가 차례대로 제거됩니다
결과만 보면 replace()를 호출하는건 호스트 Activity에 생성되어 있는 모든 프래그먼트를 remove() 한 후 replace()함수의 인자로 전달한 프래그먼트를 add()하는 것과 같습니다
두개의 서로 다른 FragmentTransaction이 존재한다고 가정해봅시다. 하나의 Transaction내부에는 프래그먼트 A를 추가하는 작업이 명시되어 있꼬 다른 하나의 Transaction내부에는 프래그먼트 B로 교체하는 작업이 명시되어 있습니다
이 두 Transaction이 동시에 같이 실행된다면 프래그먼트 A는 생성되었다가 replace(Fragment B) 함수에 의해 제거될 것입니다. 즉 생성되자마자 제거되어 버립니다
그러나 프래그먼트 트랜잭션 내부에 FragmentTrasaction 클래스가 제공하는 setReorderingAllowed(true) 함수를 명시해주면 생성되자마자 제거되는 작업은 생략할 수 있습니다
프래그먼트 A를 추가하는 작업이 취소되어 실행되지 않고 오직 프래그먼트 B만 추가되는 작업이 실행됩니다. 결과적으로 프래그먼트 A를 추가하고 프래그먼트 B로 교체하는 것과 똑같은 결과이기 때문입니다. 즉 프래그먼트 A가 생성되자마자 제거되는 불필요한 작업은 아예 실행되지 않게 할 수 있습니다
기본적으로 setReorderingAllowed(true)는 false 상태입니다. 따라서 불필요한 작업을 취소하고 싶다면 setReorderingAllowed(true)를 트랜잭션 내부에 명시해줘야 합니다
버튼을 클릭하면 두개의 Transaction이 함께 실행되면서 Example2Fragment가 add되자마자 ExampleFragment로 replace됩니다 즉 Example2Fragment는 생성되자마자 제거되어버리는 불필요한 작업이 실행됩니다
이번엔 버튼을 클릭시 두 Transaction에 모두 setReorderingAllowed(true) 핳ㅁ수를 호출해봅시다(두 Transaction 모두에 호출해야합니다. 둘 중 하나만 호출하면 불필요한 작업은 취소되지 않습니다)
버튼을 클릭해서 replace()가 호출되어도 Example2Fragment가 생성되고 제거되어버리는 불필요한 작업이 실행되지 않습니다
안드로이드 플랫폼에서는 백스택이라는 자료구조를 통해 Activity의 히스토리를 저장합니다
앱이 실행되자마자 생성되는 Activity가 FirstActivity라고 했을 때 앱이 실행되면 가장 먼저 FirstActivity가 백스택에 저장됩니다 FirstActivity에서 어떤 버튼을 누르면 SecondActivity로 전환된다고 했을때(intent보낼때 FirstActivity를 finish시키지 않는다는 전제 하에) SecondActivity가 생성되고 백스택에 저장되게 됩니다
백스택에 저장된 Activity 중 가장 최상단 Activity가 현재 foreground에서 실행되고 있어 사용자 눈에 보이는 Activity입니다
백스택에 저장함으로써 사용자가 뒤로버튼을 눌렀을 떄 되돌리기 작업을 실행할 수 있습니다
만약 SecondActivity가 생성되어 사용자의 눈에 보이고 있는 상황에서 사용자가 뒤로 버튼을 눌렀다면 백스택에서 가장 최상단에 있는 SecondActivity가 POP되어 사라집니다 이로써 최상단을 차지하게된 FirstActivity가 다시 실행되고 사용자 눈에는 FirstActivity로 되돌려진 작업처럼 보입니다
프래그먼트 관리자는 Activity가 관리하는 이러한 백스택에 프래그먼트 트랜잭션을 기록합니다
프래그먼트 트랜잭션을 백스택에 기록하는 이유 역시, 사용자가 뒤로 버튼을 눌렀을 때 되돌리기 작업을 실행하기 위해서입니다
Activity는 백스택에 Activity가 쌓이는 작업이 자동으로 되지만 프래그먼트는 자동으로 안됩니다. 따라서 프래그먼트 트랜잭션 내부에 백스택에 기록하겠다는 함수를 호출해야만 해당 프래그먼트 트랜잭션이 기록됩니다
또한 백스택에 기록되는것은 프래그먼트가 아니라 프래그먼트 트랜잭션입니다
FragmentTransaction 클래스가 제공하는 popBackStack() 함수를 호출하면 백스택에 저장되어 있는 프래그먼트 트랜잭션 중 스택의 가장 최상단에 존재하는 트랜잭션이 pop되어 사라집니다(프래그먼트 관리자가 백스택을 관리하는 역할을 함)
사용자가 뒤로 버튼을 눌렀을때 내부적으로 popBackStack() 함수가 호출됩니다
위영상처럼 MainActivity에서 버튼을 눌러 SecondActivity를 생성하면 바로 ExampleFragment가 추가됩니다. 또 버튼을 누르면 Example2Fragment가 추가됩니다. 이 상태에서 뒤로가기 버튼을 누르면 다시 MainActivity로 돌아갑니다. Activity는 자동으로 백스택에 저장되기 떄문에 가장 최상단에 있던 SecondActivity는 pop되고 MainActivity가 최상단으로 오게됩니다. 이때 뒤로가기를 누르면 백스택엔 아무것도 없으니 앱이 종료됩니다
만약 위처럼 SecondActivity에서 실행되는 프래그먼트 트랜잭션에 addToBackStack() 함수를 호출해서 프래그먼트 트랜잭션을 백스택에 저장하면 어떻게 될까요?
백 스택에는 위와 같이 저장될 것입니다
영상에서처럼 첫번째 뒤로 버튼을 누르면 최상단 프랜잭션이 Example2Fragment를 추가하는 트랜잭션이 pop되어 최상단 트랜잭션은 ExampleFragment를 추가하는 트랜잭션이 차지하게 됩니다 백스택의 최상단 데이터가 foreground에서 실행되고 현재 사용자의 눈에 보이는 것이므로 ExampleFragment가 보입니다. 두번쨰 뒤로 버튼을 누르면 최상단 트랜잭션이 pop되어 SecondActivity가 최상단을 차지하게 됩니다. 따라서 사용자의 눈에는 SecondActivity가 보이게 됩니다. 네번째 뒤로 버튼을 누르면 최상단 MainActivity가 백스택에서 비워지고 앱이 종료됩니다
따라서 백스택에 프래그먼트 트랜잭션을 기록하는 이유는 뒤로가기 작업이 실행될 경우 되돌리기 작업을 구현하기 위함입니다
FragmentTransaction 클래스가 제공하는 함수 중 show(), hide()를 사용하면 프래그먼트 컨테이너에 이미 추가되어 있는 프래그먼트 자체 UI에 한하여 보이거나 안보이게 할 수 있습니다
이 두 함수는 프래그먼트 수명주기에 영향을 주지 않고 오직 프래그먼트 UI를 visibility 설정할 뿐입니다. 즉 show()/hide()를 호출하는건 add()/remove()를 호출하는 것처럼 프래그먼트가 새로 생성되고 제거되지 않는다는 말입니다.
단지 프래그먼트는 생성되어 있고, 프래그먼트 컨테이너에 올려져있는 자체 UI를 사용자 눈에서 보이거나 안보이게 하는 설정입니다
show()/hide()는 프래그먼트 컨테이너에 쌓인 자체 UI 순서에 영향을 주지 않고 자신이 쌓여있는 위치에서 사용자 눈에서 보이기/안보이기 설정만 바뀌는 겁니다
FragmentTransaction 클래스가 제공하는 함수 중 detach() 함수를 호출하면 해당 프래그먼트가 자신의 자체 UI로부터 떼지고(=inflate되어 있던 프래그먼트 클래스와 자체 UI가 deflate된다는 의미) 자체 UI 뷰 계층은 파괴(destroying)됩니다. 따라서 프래그먼트 컨테이너에 쌓여있던 해당 프래그먼트의 UI도 사라지게 됩니다. 프래그먼트(=프래그먼트 클래스)가 자체 UI로부터는 떼어졌지만 여전히 호스트 Activity에는 존재합니다.
FragmentTransaction 클래스가 제공하는 함수 중 attach() 함수를 호출하면 해당 프래그먼트가 deflate되어있던 자신의 자체 UI와 다시 inflate됩니다 UI 뷰 계층은 다시 재생성되어 프래그먼트 컨테이너에 다시 쌓여 사용자 눈에 보이게 됩니다(프래그먼트 컨테이너의 가장 위에 다시 쌓입니다)
detach는 해당 프래그먼트가 자신의 자체 UI로부터 떼지고(=inflate되어 있던 프래그먼트 클래스와 자체 UI가 deflate된다는 의미) 자체 UI 뷰 계층은 파괴되는 것입니다. 따라서 fragment객체는 메모리에 남아있고 onDestroyView까지만 호출되는 것을 확인할 수 있습니다.
fragment 인스턴스 자체가 메모리에서 제거되는 것과, UI 뷰 계층만 파괴되는 것은 메모리 사용량에 차이를 만들기 때문에 어떤 메서드를 사용해 fragment인스턴스를 관리할 지 결정하는건 매우 중요합니다!