본문 바로가기

android/Jetpack

Jetpack Navigation NavHostFragment

Jetpack Navigation 을 사용하여 Fragment 간에 전환하면 NavController 가 적절한 Navigator(예: FragmentNavigator) 를 선택하여 직접 FragmentManager 를 사용하여 FragmentTransaction 을 생성하고 각 Destination(NavBackStackEntry) 을 BackStack 에 push,pop 을 하기 때문에 개발자 입장에서는 굉장히 편리하게 화면 전환을 구현할 수 있습니다.

 

Chapter 01  Navigation Graph 의 각 Destination 을 호스팅하는 NavHostFragment

일반적으로 Jetpack Navigation 을 통해 Single Activity 에 여러 Fragment 를 사용할 때는 activity layout xml 에 FragmentContainerView 를 선언합니다. 이때 name 속성을 통해 FragmentContainerView 가 NavHostFragment 를 호스팅하도록 설정합니다. 이 NavHostFragment 는 하나의 FragmentContainerView 를 가지는 Fragment 이며, NavHostFragment 가 소유한 FragmentContainerView 에 Navigation Graph 의 각 Destination 을 호스팅하며 앱이 동작하게 됩니다. NavHostFramgent 의 각 Lifecycle 메서드에서 어떤 작업을 하는 지 하나씩 확인해보겠습니다.

 

첫번째 onAttach 에서는 parentFragmentManager(FragmentActivity 의 FragmentManager) 에 setPrimaryNavigationFragment 메서드를 호출하여 NavHostFragment 가 Back press 를 intercept 할 수 있도록 설정합니다. graphId 와 defaultNavHost 는 onInflate 에서 상위 Context 를 통해 속성을 파싱하여 설정합니다.

public open class NavHostFragment : Fragment(), NavHost {
	//...
    
	private var graphId = 0
	private var defaultNavHost = false
    
    @CallSuper
    public override fun onAttach(context: Context) {
        super.onAttach(context)
		//defaultNavHost 속성을 true 로 설정한 경우 onInflate 메서드에서 defaultNavHost flag 를 true 로 설정합니다.
        if (defaultNavHost) {
			//상위 FragmentManager 즉, Activity 의 FragmentManager 에 현재 Fragment 를 Primary Navigation Fragment 로 설정하여 BackPress 를 인터셉트할 수 있도록 합니다.
			parentFragmentManager.beginTransaction()
                .setPrimaryNavigationFragment(this)
                .commit()
        }
    }
    
    //...
}

 

 

두번째 onCreate 에서는 NavHostFragment 의 NavController 인스턴스를 생성하고 Back press 시 호출될 Callback 을 등록하며 NavController 가 Navigation 에 사용할 Navigation Graph 를 생성하는 등의 작업을 합니다.

	@CallSuper
    public override fun onCreate(savedInstanceState: Bundle?) {
		//NavHostFragment 내부에서 소유할 NavController 인스턴스를 생성합니다.
        var context = requireContext()
        navHostController = NavHostController(context)
        navHostController!!.setLifecycleOwner(this)
		
		//Back Press 했을 경우 호출될 콜백을 등록합니다.(콜백은 NavController 의 popBackStack 메서드를 호출합니다.)
        while (context is ContextWrapper) {
            if (context is OnBackPressedDispatcherOwner) {
                navHostController!!.setOnBackPressedDispatcher(
                    (context as OnBackPressedDispatcherOwner).onBackPressedDispatcher
                )
                // Otherwise, caller must register a dispatcher on the controller explicitly
                // by overriding onCreateNavHostController()
                break
            }
            context = context.baseContext
        }
        // Set the default state - this will be updated whenever
        // onPrimaryNavigationFragmentChanged() is called
        navHostController!!.enableOnBackPressed(
            isPrimaryBeforeOnCreate != null && isPrimaryBeforeOnCreate as Boolean
        )
        isPrimaryBeforeOnCreate = null
		
        navHostController!!.setViewModelStore(viewModelStore)
        onCreateNavHostController(navHostController!!)
        var navState: Bundle? = null
        if (savedInstanceState != null) {
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE)
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                defaultNavHost = true
                parentFragmentManager.beginTransaction()
                    .setPrimaryNavigationFragment(this)
                    .commit()
            }
            graphId = savedInstanceState.getInt(KEY_GRAPH_ID)
        }
        if (navState != null) {
            // Navigation controller state overrides arguments
            navHostController!!.restoreState(navState)
        }
		
		//onInflate 에서 설정한 Navigation Graph 의 id 를 NavController 가 참조할 Navigation Graph 로 설정합니다. 
        if (graphId != 0) {
            // Set from onInflate()
            navHostController!!.setGraph(graphId)
        } else {
            // See if it was set by NavHostFragment.create()
            val args = arguments
            val graphId = args?.getInt(KEY_GRAPH_ID) ?: 0
            val startDestinationArgs = args?.getBundle(KEY_START_DESTINATION_ARGS)
            if (graphId != 0) {
                navHostController!!.setGraph(graphId, startDestinationArgs)
            }
        }

        // We purposefully run this last as this will trigger the onCreate() of
        // child fragments, which may be relying on having the NavController already
        // created and having its state restored by that point.
        super.onCreate(savedInstanceState)
    }

 

onCreateView 에서는 FragmentContainerView 인스턴스를 하나 생성하고 id 를 설정한 후 반환합니다.

FragmentContainerView 의 id 는 Resource 에 미리 지정된 R.id.nav_host_fragment_container 를 참조합니다.

public override fun onCreateView(
	inflater: LayoutInflater,
	container: ViewGroup?,
	savedInstanceState: Bundle?
): View? {
	//NavHostFragment 는 FragmentContainerView 하나만 가진 Fragment 입니다. 
	val containerView = FragmentContainerView(inflater.context)
	containerView.id = containerId
	return containerView
}

 

onViewCreated 에서는 onCreateView 에서 반환한 FragmentContainerView 에 tag 를 설정합니다. key는 R.id.nav_controller_view_tag 이며 value 는 onCreate 에서 생성한 NavController 입니다.

public override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
		//onCreateView 에서 반환한 View 가 ViewGroup 이 아닌 경우 IllegalStateException 예외를 발생시킵니다.
        check(view is ViewGroup) { "created host view $view is not a ViewGroup" }
		//onCreateView 에서 반환한 View 인 FragmentContainerView 의 tag 에 R.id.nav_controller_view_tag ID 로 NavController 를 설정합니다. 
        Navigation.setViewNavController(view, navHostController)
        if (view.getParent() != null) {
            viewParent = view.getParent() as View
            if (viewParent!!.id == id) {
                Navigation.setViewNavController(viewParent!!, navHostController)
            }
        }
    }