코틀린 fragment 종료 - koteullin fragment jonglyo

현재 보여지는 프래그먼트를 종료하고 이전 화면으로 돌아갈때 기본적으로 사용하는 코드이다.

requireActivity().supportFragmentManager.beginTransaction().remove(this).commit()
requireActivity().supportFragmentManager.popBackStack()

나는 특정 아이콘을 눌렀을 때가 아니라 메뉴 바의 뒤로가기를 눌렀을때 실행하고 싶다.

이 Fragment를 띄운 Activity에서 onBackpressed 를 호출하는 Interface를 만들어 연결하도록 한다.

Fragment 에서는 MainActivity에서 생성한 onBackPressedListener을 상속받도록 한다.

제가 처음 공부할때 구글링에 검색해 보면 블로그 대부분 그냥 finish() 만으로 종료시킵니다 그러나 finish() 같은 경우 스택에 이전 view들이 존재 할 경우 앱 종료가 아니라 그 이전 view 가 나오기 때문에 finishAffinity() 를 할 경우 스택에 다른 view가 있다 하더라도 어느 경우에나 앱이 종료할 수 있습니다 ! 

작업 하다가 완료 버튼을 누르고 현재 Fragment를 없애고 이전 Fragment로 돌아갈 때 사용한다.

FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction().remove(CalendarFragment.this).commit();
fragmentManager.popBackStack();

fragment 특성상 바로 메소드 호출이 불가하므로 getActivity()를 사용합니다.

remove() 메소드를 사용하여 현재 fragment를 제거하고

popstack으로 이전 스택을 가져오면 완료

하지만 보통 BottomNav에서는 LinearLayout 가로 정렬에서 Weight를 1:1로 적용한 것처럼 두 버튼의 너비가 합쳐서 가로축을 꽉 채웠으면 좋겠다는 생각을 합니다.

코틀린 fragment 종료 - koteullin fragment jonglyo

1에서 Horizontal Chain을 채운 채로, 버튼의 Width를 0dp로 조정하면 마치, LinearLayout에서 Weight를 주는 것과 비슷한 효과를 만들 수 있습니다.

 

위에는 Fragment를 담아야 함으로, FragmentContainerView를 만들어줍니다. 그리고 이번엔 버튼중 하나와 FragmentContainerView에 Vertical Chain을 걸어줍니다. 이때, FragmentContainerView에 height를 0으로 걸어주면, 두 버튼 영역을 제외하고 Fragment가 가득 찬 모습을 확인할 수 있습니다.

하나의 레이아웃을 하나의 Activity와 나머지 Fragment들이 전부 재사용할 거기 때문에, 상단에 Fragment 등을 구분하고 이를 표시해주기 위한 textView를 상단에 하나 만들어줍니다.

 

적절한 이름과 부족한 제약속성을 채워줍니다. 

그렇게 만들어진 xml 파일은 다음과 같습니다.

<!-- 파일명 layout.xml -->

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.rai.MainActivity">

    <TextView
        android:id="@+id/tv_fragment_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="ID"
        android:textSize="40sp"
        app:layout_constraintBottom_toTopOf="@+id/v_container"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/v_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/btn_a_fragment"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_fragment_id" />

    <Button
        android:id="@+id/btn_a_fragment"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btn_b_fragment"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/v_container" />

    <Button
        android:id="@+id/btn_b_fragment"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_a_fragment"
        app:layout_constraintTop_toTopOf="@+id/btn_a_fragment" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

다음은 Fragment 를 생성하겠습니다.

A, A1, A2, B, B1, B2 Fragment를 생성합니다.

onCreateView에서 layout을 inflate 하는 곳에 아까 만들었던 layout.xml을 inflate하도록 설정합니다.

같은 레이아웃을 쓰면 구분하기가 어렵기 때문에, 이를 구분하기 위하여 만든 TextView에 Class이름이 표기되도록 합시다.

onViewCreated안에 view.findViewById<TextView>(R.id.tv_fragment_id).text = this.javaClass.name을 넣어줍시다.

 

class FragmentA : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.layout, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.findViewById<TextView>(R.id.tv_fragment_id).text= this.javaClass.name
    }



}

 

반복작업이므로, Fragment A를 생성하고 복사해서 클래스 이름만 바꿔주겠습니다.

코틀린 fragment 종료 - koteullin fragment jonglyo

다음과 같이 A ~ B2까지 만드시면 됩니다.

그리고 다음으로 Fragment 바꾸는 함수와 BackStack단위로 종료하는 함수를 만들 건데, 함수를 만들어 모든 Fragment에 복사해서 넣으면 번거롭고 코드 중복이 발생하므로, 부모를 하나 만들어서 상속해주기로 합시다.

따라서 부모에 해당하는 Fragment를 만듭니다. 이름은 fragment를 담는 fragment이므로, ContainerFragment로 합시다.

Kotlin의 경우 기본적으로 class의 상속을 허용하지 않기 때문에, 상속이 가능하다는 의미에서 class 앞에 open을 붙여줍시다.

 

    open class ContainerFragment : Fragment() // 클래스 앞에 open을 붙여줍니다.
    
    // in ContainerFragment Class
    
    fun changeFragment(fragment: Fragment, id: FragmentContainerViewID, tag : String? = null) {
        val transaction = childFragmentManager.beginTransaction();
        transaction.replace(id, fragment, tag);
        transaction.addToBackStack(null);
        transaction.commit()
    }

fragment 관리는 각각 자식 fragment가 하기 때문에 만드려는 fragment를 자식에게서 넘겨받고

부모는 현재 FragmentContainerView를 모르므로 (현재 예제에서는 모든 fragment가 같은 레이아웃을 공유하고 있으나 이를 응용하는 과정에서 fragment마다 독립된 레이아웃을 가질 수 있으므로), 자식으로부터 view ID를 넘겨받습니다.

 

맨 처음에는 Activity가 Fragment를 관리하므로 Activity에도 추가합니다.

다만, Activity의 경우 ContainerView는 알고있으므로, 굳이 넘겨줄 필요 없을 것 같습니다.

또한 fragment는 하위 fragment를 childFragmentManager로 관리하지만,

Activity에서는 SupportFragment에서 관리하기 떄문에 이를 바꿔줍니다.

    // in MainActivity Class
    
    private fun changeFragment(fragment: Fragment, tag : String? = null) {
        val transaction = supportFragmentManager.beginTransaction();
        transaction.replace(R.id.v_container, fragment, tag);
        transaction.addToBackStack(null);
        transaction.commit()
    }

 

이제 changeFramgnet까지 끝났으므로, 추가하는 로직을 작성합시다.

 

// main Activity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.layout)

    findViewById<Button>(R.id.btn_a_fragment).setOnClickListener {
        changeFragment(FragmentA())
    }

    findViewById<Button>(R.id.btn_b_fragment).setOnClickListener {
        changeFragment(FragmentB())
    }
}

// 상속 Fragment에서 ContainerFragment()로 바꾸셔야 합니다.
// fragmentA
class FragmentA : ContainerFragment()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    view.findViewById<TextView>(R.id.tv_fragment_id).text= this.javaClass.name

    view.findViewById<Button>(R.id.btn_a_fragment).setOnClickListener {
        changeFragment(FragmentA1(), R.id.btn_a_fragment)
    }

    view.findViewById<Button>(R.id.btn_b_fragment).setOnClickListener {
        changeFragment(FragmentA2(), R.id.btn_a_fragment)
    }
}

// fragmentB
class FragmentB : ContainerFragment()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    view.findViewById<TextView>(R.id.tv_fragment_id).text= this.javaClass.name

    view.findViewById<Button>(R.id.btn_a_fragment).setOnClickListener {
        changeFragment(FragmentB1(), R.id.v_container)
    }

    view.findViewById<Button>(R.id.btn_b_fragment).setOnClickListener {
        changeFragment(FragmentB2(), R.id.v_container)
    }
}

실행해보세요.

왼쪽 버튼과 오른쪽 버튼을 누르면 Fragment가 쌓이는 것을 보실 수 있습니다.

 

 

 

Fragment A위에는 A1, A2가

Fragment B위에는 B1, B2가 쌓이는 것을 확인하실 수 있습니다.

 

Fragment B -> A-> A1 -> A2를 쌓은 후, Back버튼을 눌러보세요.

그러면 A2가 꺼지면서 A1이 나와야 하는데, B가 나타날 겁니다.

 

순차적으로 종료하기 위해서 저희는 backPressed 함수를 오버라이딩해서 처리할 겁니다.

 

Container Fragment로 와서 popFragment 함수를 작성합시다.

대략적으로 저희들이 원하는 바를 정리해봅시다.

popFragment에서는 현재 화면에 보이는 Fragment에 backStack이 있는 경우, 해당 Fragment를 POP합니다.

하지만, 없는 경우 아무것도 하지 않습니다.

pop을 했는지 안했는지 반환을 해야, pop을 하지 않은 경우는 상위 화면에서 pop을 할 수 있으므로, 해당 여부를 Boolean으로 반환해줍시다.

 

그리고 현재 보이는 Fragment를 가져오기 위해서는 현 화면의 fragmentContainerView의 ID를 가져와야 하는데, pop은 상위 fragment 혹은 Activity에서 호출되기 때문에 현재 화면의 fragmentContainerView의 ID를 가지고 있지 않습니다.