Android Navigation 报错does not have a NavController set on xxxxx 解决方案

最近发现当把xml中的标签替换为,然后在Activity的onCreate方法获取NavController,就会发生does not have a NavController set on xxxxx 的错误

Android Navigation 报错does not have a NavController set on xxxxx 解决方案_第1张图片

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navController:NavController = findNavController(R.id.fragmentContainerView)
      
    }

但是当替换回标签后,就可以正常获取NavController对象,只不过会有一个黄色警告
Android Navigation 报错does not have a NavController set on xxxxx 解决方案_第2张图片
Android Navigation 报错does not have a NavController set on xxxxx 解决方案_第3张图片

接下来分析源码,看看究竟为什么使用会报错
回到MainActivity,跟进findNavController(R.id.fragmentContainerView)方法

fun Activity.findNavController(@IdRes viewId: Int): NavController =
        Navigation.findNavController(this, viewId)

一个kotlin的扩展方法,里面调用的是Navigation的findNavController方法,继续跟进

    @NonNull
    public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
    	//根据传入的viewId找到对应的View,所以view是不会为空的
        View view = ActivityCompat.requireViewById(activity, viewId);
        NavController navController = findViewNavController(view);
        if (navController == null) {
            throw new IllegalStateException("Activity " + activity
                    + " does not have a NavController set on " + viewId);
        }
        return navController;
    }

可以看到,错误是在这里抛出的,也就是当找不到NavController的时候,会抛出异常。进入findViewNavController(view)方法

@Nullable
    private static NavController findViewNavController(@NonNull View view) {
        while (view != null) {
            NavController controller = getViewNavController(view);
            if (controller != null) {
                return controller;
            }
            ViewParent parent = view.getParent();
            view = parent instanceof View ? (View) parent : null;
        }
        return null;
    }

这里是一个循环,就是当在当前view找不到的时候,会跑到父view去找。如果找到,直接返回NavController,没有找到,继续循环,直到循环到没有父view,才停止。
我们继续跟进getViewNavController(view);方法

@Nullable
    private static NavController getViewNavController(@NonNull View view) {
        Object tag = view.getTag(R.id.nav_controller_view_tag);
        NavController controller = null;
        if (tag instanceof WeakReference) {
            controller = ((WeakReference<NavController>) tag).get();
        } else if (tag instanceof NavController) {
            controller = (NavController) tag;
        }
        return controller;
    }

这里会通过view的getTag()方法来拿tag,强转NavController后返回。到这里可以知道,这个NavController对象是存放在view的tag里面的。
回到原来的问题,之所以会报错,是因为没有找到NavController,而我们传入的view是不可能为空的,那只有一种可能,就是NavController没有设置到view的tag里面,所以才导致找不到。

那么这个controller是什么时候设置到view的tag里面的呢?
查看Navigation类的源码,会发现下面这个方法

    public static void setViewNavController(@NonNull View view,
            @Nullable NavController controller) {
        view.setTag(R.id.nav_controller_view_tag, controller);
    }

可以猜测,应该是调用这个方法设置的
打个断点,查看一下
先注释掉MainActivity的代码,防止报错
Android Navigation 报错does not have a NavController set on xxxxx 解决方案_第4张图片

在这里插入图片描述
debug运行
Android Navigation 报错does not have a NavController set on xxxxx 解决方案_第5张图片
通过堆栈信息可以发现,是从Activity的onStart开始,最后调用到NavHostFragment的onViewCreated方法。
先来看NavHostFragment的onViewCreated方法

@Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        ...
        //在这里把mNavController设置到view的tag
        Navigation.setViewNavController(view, mNavController);
        ...
    }

而这个onViewCreated方法,是从Activity的onStart调下来的,现在,已经找到解决问题的答案了。
在onCreate的时候,还没有调用的这个方法,所以当我们获取NavController的时候,肯定是获取不到的,因为NavController是在onStart才开始设置,所以,我们只需要在onStart执行完后再获取就不会报错的。


    override fun onStart() {
        super.onStart()
        val navController:NavController = findNavController(R.id.fragmentContainerView)
    }

但我就是想在onCreate中获取,有没有什么办法呢?
办法还是有的。
查看NavHostFragment的onCreate方法

 	@CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();
		//在这里创建的NavHostController对象
        mNavController = new NavHostController(context);
      
      ...
      
    }

NavHostController是在这里创建的,那么我们只需要通过NavHostFragment来拿就可以啦,而NavHostFragment是一个Fragment,所以通过FragmentManager对象又可以拿到NavHostFragment。

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

        //1、先拿NavHostFragment
        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
        //2、再拿NavController
        val navController:NavController = navHostFragment.navController
    }

那如果我就想在onCreate中通过原有的findNavController()拿NavController可不可以呢?
答案是肯定的,我们回到之前的debug页面,可以看到是从onStart方法开始调,最后才设置了NavController,看一下onStart源码

AppCompatActivity的onStart

    @Override
    protected void onStart() {
        super.onStart();
        getDelegate().onStart();
    }

跟进super.onStart();

    @Override
    protected void onStart() {
        super.onStart();

        mStopped = false;

        if (!mCreated) {
            mCreated = true;
            //从这里开始
            mFragments.dispatchActivityCreated();
        }

   ...
    }

NavController 就是从这句mFragments.dispatchActivityCreated();一直往下,最后到NavHostFragment的onViewCreated设置的,所以我么只需要在onCreate中手动调一下这个dispatchActivityCreated();方法即可


    final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

mFragments这个成员变量声明在FragmentActivity中,而且权限是默认的,所以我们无法在MainActivity中直接调用。既然如此,只能用反射啦。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initNavController()
        val navController: NavController = findNavController(R.id.fragmentContainerView)

    }


    fun initNavController() {
        //获取mFragments成员变量
        val mFragmentsField = FragmentActivity::class.java.getDeclaredField("mFragments").apply {
            isAccessible = true
        }
        //获取mCreated成员变量
        val mCreatedField = FragmentActivity::class.java.getDeclaredField("mCreated").apply {
            isAccessible = true
        }
        //获取dispatchActivityCreated方法
        val dispatchActivityCreatedMethod =
            FragmentController::class.java.getDeclaredMethod("dispatchActivityCreated").apply {
                isAccessible = true
            }
        //调用dispatchActivityCreated方法
        dispatchActivityCreatedMethod.invoke(mFragmentsField.get(this))

        //别忘了把mCreated设置为true,防止dispatchActivityCreated在onStart中再次调用
        mCreatedField.set(this, true)
    }
}

最后,可以愉快的用原有的findNavController()啦。

你可能感兴趣的:(Android)