안드로이드

[android/안드로이드] jetPack Navigation Fragment 재생성 이슈 해결

최효식 2022. 4. 26. 18:43

해당 글의 Navigation 버전은 2.3.5 까지만 적용이 되는걸 확인했습니다.

 

 

이전 글에서 jetPack Navigation 을 이용해서 BottomNavigation을 연동해봤습니다.

 

하지만 여기서 한가지 의문점을 발견했습니다.

 

Fragment에서 스크롤을 한다던가 체크박스에 체크를 한다던가 했을 때 다른 Fragment로 이동했다가 돌아오면 기존 사용중이던 Fragment가 아닌 새로운 Fragment를 재생성하고 있었습니다.

 

이런 이슈를 해결하기 위해서 FragmentNavigator 클래스를 커스텀으로 만들어서 해결하는 레퍼런스를 찾았고 시도했습니다.

커스텀으로 만든 클래스에서 tag를 지정하여 fragment를 최초로 생성시에는 FragmentManager에 add를 시키고

지정한 tag로 fragment가 있을시에는 가져와서 show만 시키도록 navigate() 함수를 오버라이딩 했습니다.

 

1. 커스텀 FragmentNavigator 클래스 코드

 

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.FragmentNavigator

@Navigator.Name("keep_state_fragment")
class KeepStateNavigator(
    private val context: Context,
    private val manager: FragmentManager,
    private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {

    override fun navigate(
        destination: Destination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ): NavDestination? {
        // destination tag
        val tag = destination.id.toString()

        val transaction = manager.beginTransaction()

        var initialNavigate = false
        val currentFragment = manager.primaryNavigationFragment

        // primaryNavigationFragment가 존재하면 기존 primaryFragment hide 처리 (재생성 방지)
        if (currentFragment != null) {
            transaction.hide(currentFragment)
        } else {
            initialNavigate = true
        }

        var fragment = manager.findFragmentByTag(tag)
        // 최초로 생성되는 fragment
        if (fragment == null) {
            // add로 fragment 최초 생성 (add)
            val className = destination.className
            fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
            transaction.add(containerId, fragment, tag)
        } else {
            // 이미 생성되어 있던 fragment라면 show
            transaction.show(fragment)
        }

        // destination fragment를 primary로 설정
        transaction.setPrimaryNavigationFragment(fragment)

        // transaction 관련 fragment 상태 변경 최적화
        transaction.setReorderingAllowed(true)
        transaction.commitNow()

        return if (initialNavigate) {
            destination
        } else {
            null
        }
    }
}

  

2. navigation 디렉토리의 bottom_nav.xml의 fragment 태그 수정

 

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/bottom_nav"
    app:startDestination="@id/homeFragment">

    <keep_state_fragment
        android:id="@+id/homeFragment"
        android:name="kr.co.jetpacknavi.HomeFragment"
        android:label="HomeFragment"
        tools:layout="@layout/fragment_home"
        />
    <keep_state_fragment
        android:id="@+id/myPageFragment"
        android:name="kr.co.jetpacknavi.MyPageFragment"
        android:label="MyPageFragment"
        tools:layout="@layout/fragment_myapge"
        />
    <keep_state_fragment
        android:id="@+id/memoFragment"
        android:name="kr.co.jetpacknavi.MemoFragment"
        android:label="MemoFragment"
        tools:layout="@layout/fragment_memo"
        />
    <keep_state_fragment
        android:id="@+id/contentFragment"
        android:name="kr.co.jetpacknavi.ContentFragment"
        android:label="ContentFragment"
        tools:layout="@layout/fragment_content"
        />
</navigation>

위의 1번에서 어노테이션으로 정의한 @Navigator.Name("keep_state_fragment") 이름으로 fragment태그 이름을 변경해줍니다.​

 

3. MainActivity에서 커스텀 Navigator 지정

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.navigation.NavHost
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import kr.co.jetpacknavi.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

   private var _binding : ActivityMainBinding ?= null

   private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_container) as NavHostFragment
        val navController = navHostFragment.findNavController()

        /** KeepStateNavigator navController에 추가 */
        val navigator = KeepStateNavigator(this , navHostFragment.childFragmentManager, R.id.nav_host_fragment_container)
        navController.navigatorProvider.addNavigator(navigator)
        navController.setGraph(R.navigation.bottom_nav)

        binding.bottomNavi.setupWithNavController(navController = navController)

    }
}

 

4. MainActivity에서 setGraph로 navGraph를 지정했으니 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=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/bottomNavi"
        />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavi"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/bottom_nav_menu"/>


</androidx.constraintlayout.widget.ConstraintLayout>

  

 

다시 앱을 재생하시면 BottomNavigation의 해당 fragment들을 Singleton처럼 사용하실 수 있습니다.

 

 

참고 자료 : https://hungseong.tistory.com/56