[안드로이드] Fragment와 FragmentManager

hee09·2022년 1월 1일
4
post-thumbnail

Fragment

프롤로그

요즘 많은 앱들을 보면 하나의 액티비티에 여러 Fragment를 배치하여 구성하고 있습니다.
이러한 Fragment를 알아보도록 하겠습니다.


Fragment의 특징

  • 프래그먼트는 앱 UI의 재사용이 가능한 부분을 나타냅니다.

  • 프래그먼트는 액티비티와 마찬가지로 자체적으로 레이아웃을 정의 및 관리하고 자체 생명주기를 가지며(액티비티와 비슷합니다) 자체적으로 이벤트를 처리할 수 있습니다.

  • 프래그먼트는 단독으로 존재할 수 없으며 액티비티 또는 다른 프래그먼트에 의해 호스팅 되어야 합니다.

  • 프래그먼트의 뷰 계층은 host(액티비티나 다른 프래그먼트)의 view 계층의 일부가 되거나 이 계층에 연결이 됩니다.

  • 프래그먼트는 Android Jetpack 라이브러리인 Navigation, BottomNavigationView, ViewPager2와 같이 자주 사용됩니다.


모듈성

프래그먼트는 UI를 개별 부분으로 나눌 수 있도록 하여 액티비티의 UI에 모듈성과 재사용성을 도입합니다. 따라서 프래그먼트와 액티비티는 다음과 같은 차이가 있습니다.

  • 액티비티 : 앱의 사용자 인터페이스와 관련된 전역적인(전체적인) 요소들을 배치하는 곳

  • 프래그먼트 : 단일 화면 또는 화면 일부분으로 사용자 인터페이스를 정의하기에 적합한 곳


위의 그림은 화면에 크기에 따라 같은 화면을 보여주는 두 가지의 예시입니다. 전체적으로 보자면 두 화면 모두 액티비티안에 파란색 부분인 Navigation을 배치하고 menu의 선택에 따라서 액티비티의 연두색으로 표시된 부분인 프래그먼트가 변경되는 예시를 보여줍니다. 위의 그림에서는 액티비티가 프래그먼트를 호스팅하고 있습니다.


프래그먼트의 장점

  • 프래그먼트로 UI를 나누는 것은 runtime 때 액티비티의 UI 모습을 사용자와 상호작용 하며 변경할 수 있습니다.

    • 위의 그림을 예로 들어, 앱이 실행되는 도중 Navigation의 메뉴 중 하나를 선택하면 그 메뉴와 관련된 화면이 나오는데 이를 프래그먼트를 사용하여 구현한다는 것입니다.
  • 액티비티의 생명주기가 STARTED 이거나 그 이상인 동안에, 프래그먼트는 add되고, replace되고, remove 될 수 있습니다.

  • 이러한 프래그먼트의 변화의 기록(add, replace, remove)을 프래그먼트 매니저에 의해 관리되는 백스택에 저장됩니다.


Fragment 주의 사항

  • 같은 액티비티 안에서나, 다양한 액티비티들이나, 심지어 다른 프래그먼트의 자식으로 같은 프래그먼트 객체를 여러번 사용할 수(호스팅 될 수) 있습니다.

  • 위의 사항으로 인해서 프래그먼트는 자체 UI를 관리하는 로직만 구현해야 하고 다른 액티비티나 다른 프래그먼트를 직접 조작하는 로직을 포함하면 안됩니다.(모듈성, 재사용성을 해침)

  • 프래그먼트를 다른 액티비티나 프래그먼트에 의존하면 안됩니다.


Fragment 생성

프래그먼트는 호스팅된 액티비티가 running하는 동안에 추가되거나 삭제될 수 있습니다.

이는 위에서 설명했고 이제 액티비티안에서 프래그먼트를 만드는 방법을 알아보겠습니다.

종속성 설정

프래그먼트는 AndroidX Fragment Library 의존성이 필요합니다.

우선 project 수준의 build.gradle 파일 안에 아래와 같이 Google Maven repositiory를 추가합니다.

프로젝트를 만들면 기본으로 들어가 있습니다.

buildscript {
    ...

    repositories {
        google()
        ...
    }
}

allprojects {
    repositories {
        google()
        ...
    }
}

만약 AndroidKTX 중 하나인 FragmentKTX를 사용한다면 build.gradle 파일에 아래와 같이 의존성을 설정합니다.

dependencies {
    def fragment_version = "1.3.6"

    // Kotlin
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

Fragment 클래스 생성하기

프래그먼트 클래스를 생성하기 위해서는 AndroidX Fragment 클래스를 상속 받고, Fragment의 메서드들을 오버라이드하여 앱의 로직을 구현합니다. 다른 로직을 생략하고 레이아웃만 초기화하여 사용하는 Fragment를 선언하기 위해서는 아래의 두 개의 생성자 중 하나를 선택해 만들면 됩니다.

공식홈페이지에 나와있는 Fragment의 생성자는 위와 같습니다.

  • 생성자에 아무것도 넣지 않는 Fragment는 Fragment의 생명 주기 중 onCreateView() 메서드 안에서 LayoutInfalter 클래스를 이용해 레이아웃 파일을 초기화해야 합니다.

  • 생성자로 Layout 파일을 넣어주면 onCreateView() 메서드 안에서 레이아웃을 초기화 할 필요없이 스스로 레이아웃을 초기화합니다.

class ExampleFragment: Fragment(R.layout.fragment_example) {


}

또는

class ExampleFragment: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_example, container, false)

        return view
    }
}

액티비티에 프래그먼트 추가하기

프래그먼트가 액티비티의 layout의 부분으로 포함되려면 해당 액티비티는 AndroidX의 FragmentActivity를 상속받고 있어야 합니다.

  • FragmentActivity : 프래그먼트를 사용하기를 원하는 액티비티를 위한 기초 클래스입니다.

하지만 액티비티를 선언하면 기본적으로 상속받는 AppCompatActivity는 FragmentActivity를 상속받고 있기에 AppCompatActivity를 상속받는다면 따로 추가 할 필요가 없습니다. 위의 사진은 안드로이드 공식 홈페이지에 AppCompatActivity 클래스에 대한 설명인데 FragmentActivity를 상속받고 있습니다.


프래그먼트를 액티비티 layout 파일의 계층 구조로 정의하는 방법은 아래의 두 가지 방법이 있습니다.

  • 액티비티의 레이아웃 파일 안에서 프래그먼트를 직접 정의하는 방법

  • 액티비티의 레이아웃 파일 안에서 프래그먼트의 container(레이아웃 그룹과 같은 것)를 정의하고 액티비티 코드 안에서 프래그먼트를 add, replace, remove 하는 방법


방법은 다르지만 두 방법 모두 액티비티의 레이아웃 계층구조 안에서 프래그먼트를 배치할 위치를 정의하는 FragmentContainerView를 추가해야 합니다. 안드로이드 공식 사이트에서 이전에 사용하던 FrameLayout말고 FragmentContainerView의 사용을 강력하게 권장하고 있습니다.

  • FragmentContainerView는 프래그먼트를 위해 특별히 설계된 레이아웃 입니다. FrameLayout을 확장하여(상속받아) 프래그먼트 transaction을 안정적으로 처리할 수 있고 프래그먼트의 동작을 조정할 수 있는 추가 기능도 있습니다.

XML에 프래그먼트를 직접 추가하기

우선 액티비티의 레이아웃 파일 안에 FragmentContainerView 요소를 정의합니다.

XML

<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.ExampleFragment" />

android:name(class 속성도 가능) 속성으로 인스턴스화 하려는 프래그먼트 클래스의 이름을 넣습니다. 이렇게 되면 액티비티의 레이아웃이 초기화될 때, 이 프래그먼트는 인스턴스화되고 이 프래그먼트가 onInflate()를 호출합니다.


프래그먼트를 코드를 통해 추가하기

코드를 통해 프래그먼트를 액티비티 레이아웃에 추가하기 위해서는 FragmentContainerView를 프래그먼트의 컨테이너와 같은 역할을 하도록 선언해야 합니다. 이 방법은 사용자의 이벤트(BottomNavigation의 메뉴를 선택하는 예시)에 따라서 동적으로 프래그먼트를 add하고 remove하고 replace하기 위해 사용합니다. 이와 같은 방법이 많이 사용됩니다.

XML
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

직접 android:name 속성을 이용하여 프래그먼트를 인스턴스화 하지 않습니다. 대신 FragmentTransacion을 이용해 프래그먼트를 인스턴스화하고 액티비티의 레이아웃에 프래그먼트 객체를 추가합니다.


액티비티가 실행되는 동안, 프래그먼트를 add하고 remove하고 replace 할 수 있습니다. 이를 위해서 FragmentActivity에서(AppcompatActivity가 상속받고 있음) 프래그먼트를 추가/삭제/교체하는 역할인 FragmentTransaction을 만드는데 사용하는 FragmentManager의 인스턴스를 얻어야 합니다.

액티비티 코드
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if(savedInstanceState == null) {
            // FragmentManager를 통해서 FragmentTransaction 획득하기
            val fragmentTransaction: FragmentTransaction =
                supportFragmentManager.beginTransaction()
            // add를 통해 container에 Fragment 추가
            fragmentTransaction.add(R.id.fragment_container, FirstFragment())
            fragmentTransaction.setReorderingAllowed(true)
            // commit을 통해 transaction 등록
            fragmentTransaction.commit()


            // FragmentKTX의 기능을 사용하여 위의 코드를 깔끔하게 변경
            // commit 함수 내부에 FragmentTransaction을 수신객체로 받는
            // 함수 타입이 있어서 아래와 같이 작성 가능
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add(R.id.fragment_container, FirstFragment())
            }
        }
    }
}

위의 코드는 savedInstanceState가 널일 경우에만 fragment transaction을 생성합니다. 이것은 액티비티가 처음 만들어질 때, 프래그먼트가 한 번만 추가되는 것을 보장합니다. 화면의 회전과 같은 구성요소(환경)의 변화가 있더라도 savedInstanceState는 널이 아니기에 프래그먼트는 또 추가되지 않습니다.

위의 코드에서는 두 가지 스타일로 Fragment Transaction을 실행했습니다. 첫 번째 코드는 FragmentManager의 beginTransaction() 메소드를 통해 FragmentTransaction 객체를 획득한 후 add와 같은 메소드를 통해 프래그먼트를 추가/삭제/변경하고 마지막에 commit()을 호출하여 Transaction을 등록하는 코드입니다. 그 아래 코드는 FragmentKTX를 사용하여 FragmentManager의 commit 메소드를 호출하는데 매개변수에 FragmentTransaction이 수신 객체인 함수 타입이 선언되어 있습니다. 따라서 commit 메소드의 안에서 Fragment Transaction의 메소드들을 호출하여 프래그먼트를 추가/삭제/변경하고 자동으로 commit을 해주기에 코드가 깔끔해집니다.


위에서 언급하였지만 코드에서 FragmentManager 클래스를 인스턴스화 하는 이유는 프래그먼트 추가/교체/삭제 작업을 제공하는 FragmentTransaction 클래스를 사용하기 위함입니다.

FragmentTransaction은 추상 클래스이고 FragmentManager를 통해서 객체를 획득합니다. 위의 코드에서는 beginTransaction()을 통해 FragmentTransaction을 획득하고 있습니다.

FragmentManager도 FragmentTransaction과 같이 추상 클래스입니다. 다만 FragmentManager는 위에서 언급한 FragmentActivity를 통해 획득할 수 있습니다. FragmentActivity의 getSupportFragmentManager() 메소드(코틀린은 supportFragmentManager 프로퍼티 사용)를 사용해서 FragmentManager를 획득하는 구조입니다.

  • 참조 : 언제나 setReorderingAllowed(true)를 FragmentTransaction이 작동하는 동안 사용하라고 공식 홈페이지에 나와있습니다. 이유는 이 메소드는 transaction과 관련된 프래그먼트의 상태 변경을 최적화하여 애니메이션과 전환이 올바르게 작동하도록 합니다.

Fragment Manager

FragmentManager는 프래그먼트를 add, remove, replace와 같은 작업을 백 스택에 추가하는 작업을 담당하는 클래스입니다.

Jetpack Navigation 라이브러리를 사용하면 FragmentManager와 직접적으로 상호작용 할 필요가 없습니다. 다만 Navigation 은 FragmentManager를 내부적으로 사용하여 구현되어 있습니다. 그렇기에 FragmentManager를 자세히 알아보겠습니다.


액티비티나 프래그먼트에서 FragmentManager로 접근

  • 액티비티에서 접근

    • FragmentActivity에서 FragmentManager에 접근하기 위해서 getSupportFragmenManager() 함수를 사용합니다.(코틀린은 supportFragmentManager 프로퍼티 이용)

  • 프래그먼트에서 접근

    • 프래그먼트도 액티비티와 마찬가지로 하나 또는 여러 개의 자식 프래그먼트들을 가질 수 있습니다.

    • 관리하는(호스팅하는) 프래그먼트에서 자식(호스팅되는) 프래그먼트를 관리해주는 FragmentManager를 얻으려면 getChildFragmentManager() 함수를 사용합니다(코틀린은 childFragmentManager 프로퍼티)

    • 자식(호스팅되는) 프래그먼트에서 관리하는(호스팅하는) 프래그먼트의 FragmentManager를 얻으려면 getParentFragmentManager() 함수를 사용합니다(코틀린은 parentFragmentManager 프로퍼티)


Fragment - Fragment, Fragment - Activity 관계 예시

  • 첫 번째 그림은 하나의 Host Activity(초록색)안에 하나의 Host Fragment(하늘색)을 넣고, 그 Host Fragment(하늘색)안에 Child Fragment(하얀색)를 두개 넣은 예시입니다.

  • 두 번째 그림은 하나의 Host Activity(초록색)안에 하나의 Host Fragment(하늘색)을 넣고, 그 Host Fragment(하늘색)안에 Child Fragment(하얀색)을 하나 배치하고 Swipe로 연결한 예시입니다.

이러한 화면속에서 프래그먼트를 관리하기 위해서 사용되는 FragmentManager의 관계가 중요한데, 예시의 Host Activity, Host Fragment, Child Fragment의 Fragment Manager 관계는 아래와 같습니다.

  • 각 host에는 프래그먼트를 관리하는 고유한 Fragment Manager가 존재하고, 자식 프래그먼트(호스팅되는)에서는 host의 Fragment Manager를 접근할 수 있습니다.

Fragment Manager 역할

  • Back Stack 관리

    • FragmentManager는 프래그먼트의 백스택을 관리합니다. 실행도중에 유저의 상호작용에 의해 프래그먼트를 추가/삭제/교환하는 작업(FragmentTransaction)을 백스택에 추가합니다.

    • FragmentTransaction의 addToBackStack() 메소드를 통해서 백스택에 추가되고 디바이스의 뒤로가기 버튼이나 Fragment.popBackstack()을 통해 백스택에서 나옵니다(pop). 이와 관련된 자세한 내용은 FragmentTransaction에서 확인하겠습니다.

  • FragmentTransaction 생성 및 실행

    • 이미 위의 예시코드에서 확인하였지만 FragmentManager는 FragmentTransaction을 생성합니다. FragmentTransaction은 프래그먼트를 추가/삭제/변경하는 역할을 수행하고 코드는 아래와 같이 사용합니다.
// FragmentKTX 이용
// commit 메소드안의 메소드들을 실행 후 자동으로 commit
supportFragmentManager.commit {
   // 컨테이너안을 ExampleFragment로 변경
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   // 백스택에 추가
   addToBackStack("name") // name can be null
}
  • 프래그먼트 찾기

    • FragmentManager의 findFragmentById()를 사용하여 컨테이너에 있는 현재 프래그먼트의 참조를 얻을 수 있습니다.

    • 프래그먼트에 태그를 할당하고 findFragmentByTag()를 사용하여 프래그먼트에 대한 참조를 얻을 수 있습니다.


findFragmentById()를 사용하여 현재 프래그먼트 참조 얻기

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

findFragmentByTag()를 사용하여 프래그먼트 참조 얻기

supportFragmentManager.commit {
   // 프래그먼트에 Tag 등록
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag")
             as ExampleFragment

참조안드로이드 developer - 프레그먼트
안드로이드 프래그먼트의 모든 것

틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글