You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android启动时LiveData与NavigationView关联的索引越界异常问题

问题

我开发了一款带有NavigationView的应用,其中包含一个使用Google一键登录流程的登录/注销菜单项:

<item
    android:id="@+id/NavigationLogInOut"
    android:icon="@drawable/material_login"
    android:orderInCategory="95"
    android:title="@string/button_log_in" />

我使用MutableLiveData控制该菜单项的启用/禁用状态,以便在登录过程中临时禁用它:

public final MutableLiveData<Boolean> logInOutEnabled = new MutableLiveData<>(false);

在Activity的OnCreate方法中添加了如下观察者:

logInOutEnabled.observe(
    this,
    (enabled) -> {
        application.logger.d(
            "Log In/Out Enabled: " + enabled
        );

        logInOutMenuItem.setEnabled(enabled);
    }
);

应用启动时,Logcat中显示调试信息:

08:34:29.350 13869-13869  D  Log In/Out Enabled: false

此时不会崩溃。

OnCreate方法最后会检查用户是否有缓存凭据,若有则更新LiveData:

if (!isLoggedIn()) {
    // start the login flow here
} else {
    activity.application.logInOutEnabled.postValue(true);
}

Logcat中会立即显示:

08:34:29.664 13869-13869  D  Log In/Out Enabled: true

但之后约50%的概率应用会崩溃,堆栈信息如下:

08:34:29.722 13869-13869  E  FATAL EXCEPTION: main
                             Process: com.company.game, PID: 13869
                             java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 6(offset:6).state:7 com.google.android.material.internal.NavigationMenuView{d4afb4b VFED.V... ......ID 0,0-640,1632 #7f0900d2
                             app:id/design_navigation_view}, adapter:com.google.android.material.internal.NavigationMenuPresenter$NavigationMenuAdapter@e3e4641, layout:androidx.recyclerview.widget.LinearLayoutManager@131e7e6, context:com.company.game.GameActivity@9fccb96
                                at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6183)
                                at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
                                at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
                                at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
                                at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
                                at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
                                at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
                                at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
                                at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
                                at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
                                at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at androidx.drawerlayout.widget.DrawerLayout.onLayout(DrawerLayout.java:1287)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1873)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
                                at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
                                at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
                                at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
                                at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
                                at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
                                at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
                                at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
                                at com.android.internal.policy.DecorView.onLayout(DecorView.java:799)
                                at android.view.View.layout(View.java:23693)
                                at android.view.ViewGroup.layout(ViewGroup.java:6413)
                                at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3999)
                                at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3372)
                                at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2328)
                                at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9087)
                                at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1231)
                                at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1239)
08:34:29.724 13869-13869  E     at android.view.Choreographer.doCallbacks(Choreographer.java:899)
                                at android.view.Choreographer.doFrame(Choreographer.java:832)
                                at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1214)
                                at android.os.Handler.handleCallback(Handler.java:942)
                                at android.os.Handler.dispatchMessage(Handler.java:99)
                                at android.os.Looper.loopOnce(Looper.java:201)
                                at android.os.Looper.loop(Looper.java:288)
                                at android.app.ActivityThread.main(ActivityThread.java:7872)
                                at java.lang.reflect.Method.invoke(Native Method)
                                at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
                                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

第一次更新菜单项启用状态成功,为何第二次更新会引发崩溃?注释观察者中的setEnabled()代码后,应用不会崩溃。我推测NavigationView使用RecyclerView展示菜单项,但两次setEnabled()之间未增删菜单项,且操作的是同一个MenuItem,无法理解为何会影响其在RecyclerView中的位置。

解决方案

问题根源

NavigationView内部的NavigationMenuAdapter在收到MenuItem状态变更(比如setEnabled)时,会触发整个RecyclerView的重排,但此时Activity的布局初始化可能还未完全完成——第二次调用setEnabled时,NavigationView的RecyclerView正处于布局计算的关键阶段,状态变更导致Adapter的数据与RecyclerView的布局状态不一致,从而抛出索引越界异常。

修复方案

  1. 延迟状态更新到布局完成后
    确保在Activity的布局完全初始化完成后,再更新菜单项的启用状态。可以通过View.post()来延迟执行:

    logInOutEnabled.observe(
        this,
        (enabled) -> {
            application.logger.d("Log In/Out Enabled: " + enabled);
            // 延迟到布局线程空闲时执行
            logInOutMenuItem.post(() -> logInOutMenuItem.setEnabled(enabled));
        }
    );
    
  2. 调整LiveData更新的时机
    将检查缓存凭据的逻辑移到onPostCreate()方法中,此时Activity的布局已经完成初始化:

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        if (isLoggedIn()) {
            activity.application.logInOutEnabled.postValue(true);
        } else {
            // start the login flow here
        }
    }
    
  3. 使用NavigationView的菜单更新API
    不要直接操作缓存的MenuItem实例,而是通过NavigationView.getMenu()重新获取菜单后修改状态,确保变更被正确通知到内部Adapter:

    logInOutEnabled.observe(
        this,
        (enabled) -> {
            application.logger.d("Log In/Out Enabled: " + enabled);
            navigationView.getMenu().findItem(R.id.NavigationLogInOut).setEnabled(enabled);
        }
    );
    

补充说明

这种偶发崩溃源于Android布局流程的异步特性:第一次setEnabled在布局初始化前执行,不会触发RecyclerView的重排;第二次在布局初始化过程中执行,导致Adapter与RecyclerView的状态不同步。通过延迟执行或调整执行时机,就能避免这种不一致问题。


内容的提问来源于stack exchange,提问作者Tim Gustafson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 05:34:55