在底部导航视图上多次点击时应用程序崩溃



我的应用程序有一个托管 3 个片段的活动。这些片段可以通过点击底部导航视图来导航。它工作得很好,只有当我尝试多次点击底部导航视图时,它在运行时崩溃并出现以下错误:

java.lang.IllegalArgumentException: saveBackStack("48c3d9bf-beff-4ec0-8a1b-fb91b56a2765") must be self contained and not reference fragments from non-saved FragmentTransactions. Found reference to fragment SecondFragment{57f9be2} (dd3744e7-8aa3-4c45-b6bc-312a9d46afb4 id=0x7f0a00b0) in BackStackEntry{ba06b73 48c3d9bf-beff-4ec0-8a1b-fb91b56a2765} that were previously added to the FragmentManager through a separate FragmentTransaction.
at androidx.fragment.app.FragmentManager.saveBackStackState(FragmentManager.java:2052)
at androidx.fragment.app.FragmentManager$SaveBackStackState.generateOps(FragmentManager.java:3172)
at androidx.fragment.app.FragmentManager.generateOpsForPendingActions(FragmentManager.java:1953)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1643)
at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:480)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6819)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)

我已经检查了整个网站和其他几个网站以解决此问题,但没有找到。所以我希望有人能帮忙。

这是我当前活动的代码:

public class HomeActivity extends AppCompatActivity {
private DrawerLayout drawer;
// Last update time, click sound, search button, search panel.
TextView time_field;
MediaPlayer player;
ImageView Search;
EditText textfield;
// For scheduling background image change(using constraint layout, start counting from dubai, down to statue of liberty.
ConstraintLayout constraintLayout;
public static int count = 0;
int[] drawable = new int[]{R.drawable.dubai, R.drawable.central_bank_of_nigeria, R.drawable.eiffel_tower, R.drawable.hong_kong, R.drawable.statue_of_liberty};
Timer _t;
private WeatherDataViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
// use home activity layout.
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Allow activity to make use of the toolbar
drawer = findViewById(R.id.drawer_layout);
viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar
, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();
time_field = findViewById(R.id.textView9);
Search = findViewById(R.id.imageView4);
textfield = findViewById(R.id.textfield);
//  find the id's of specific variables.
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
// host 3 fragments along with bottom navigation.
final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
assert navHostFragment != null;
final NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNavigationView, navController);
// For scheduling background image change
constraintLayout = findViewById(R.id.layout);
constraintLayout.setBackgroundResource(R.drawable.dubai);
_t = new Timer();
_t.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// run on ui thread
runOnUiThread(() -> {
if (count < drawable.length) {
constraintLayout.setBackgroundResource(drawable[count]);
count = (count + 1) % drawable.length;
}
});
}
}, 5000, 5000);
Search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// make click sound when search button is clicked.
player = MediaPlayer.create(HomeActivity.this, R.raw.click);
player.start();
getWeatherData(textfield.getText().toString().trim());
// make use of some fragment's data
Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
if (currentFragment instanceof FirstFragment) {
FirstFragment firstFragment = (FirstFragment) currentFragment;
firstFragment.getWeatherData(textfield.getText().toString().trim());
} else if (currentFragment instanceof SecondFragment) {
SecondFragment secondFragment = (SecondFragment) currentFragment;
secondFragment.getWeatherData(textfield.getText().toString().trim());
} else if (currentFragment instanceof ThirdFragment) {
ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
thirdFragment.getWeatherData(textfield.getText().toString().trim());
}
}
private void getWeatherData(String name) {
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
try {
assert response.body() != null;
time_field.setVisibility(View.VISIBLE);
time_field.setText("First Updated:" + " " + response.body().getDt());
} catch (Exception e) {
time_field.setVisibility(View.GONE);
time_field.setText("First Updated: Unknown");
Log.e("TAG", "No City found");
Toast.makeText(HomeActivity.this, "No City found", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
});
}
}

编辑

第二个片段:

public class SecondFragment extends Fragment {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public SecondFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SecondFragment.
*/
// TODO: Rename and change types and number of parameters
public static SecondFragment newInstance(String param1, String param2) {
SecondFragment fragment = new SecondFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false);
}
public void getWeatherData(String trim) {
}
}

导航图:

<?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/my_nav"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.wiz.lightweatherforecast.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/secondFragment"
android:name="com.wiz.lightweatherforecast.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
<fragment
android:id="@+id/thirdFragment"
android:name="com.com.wiz.lightweatherforecast.ThirdFragment"
android:label="fragment_third"
tools:layout="@layout/fragment_third" />
</navigation>

我通过单击底部导航视图(用红色勾号表示)来浏览片段:https://i.stack.imgur.com/ScFW6.jpg 利用此依赖项; 实现"androidx.navigation:navigation-fragment:2.4.0-alpha01"

使用NavigationUI在setupWithNavController方法中设置第三个参数(saveState)false,这将修复此崩溃。

NavigationUI.setupWithNavController(bottomNavigationView, navController,false)

您可能已经猜到了,问题在于旧片段和新片段之间的竞争条件。所以这是你可以做的。

静态易失性 - 守门人。如果较旧的尚未完成,这将忽略较新的。

private static volatile boolean isClicking = false;
...
Search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v)
{
if(!isClicking) {
isClicking = true;
// make click sound when search button is clicked.
player = MediaPlayer.create(HomeActivity.this, R.raw.click);
player.start();
getWeatherData(textfield.getText().toString().trim());
// make use of some fragment's data
Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
if(currentFragment instanceof FirstFragment) {
FirstFragment firstFragment = (FirstFragment) currentFragment;
firstFragment.getWeatherData(textfield.getText().toString().trim());
} else if(currentFragment instanceof SecondFragment) {
SecondFragment secondFragment = (SecondFragment) currentFragment;
secondFragment.getWeatherData(textfield.getText().toString().trim());
} else if(currentFragment instanceof ThirdFragment) {
ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
thirdFragment.getWeatherData(textfield.getText().toString().trim());
}

isClicking = false;
}
}
});
...

静态同步- 创建一个帮助程序函数,用于调用onClick()中的所有内容。这将对任务进行排队。

...
Search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
syncOnClick();
}
});
...
//Outside of "onCreate()".
private static synchronized void syncOnClick()
{
// make click sound when search button is clicked.
player = MediaPlayer.create(HomeActivity.this, R.raw.click);
player.start();
getWeatherData(textfield.getText().toString().trim());
// make use of some fragment's data
Fragment currentFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
if(currentFragment instanceof FirstFragment) {
FirstFragment firstFragment = (FirstFragment) currentFragment;
firstFragment.getWeatherData(textfield.getText().toString().trim());
} else if(currentFragment instanceof SecondFragment) {
SecondFragment secondFragment = (SecondFragment) currentFragment;
secondFragment.getWeatherData(textfield.getText().toString().trim());
} else if(currentFragment instanceof ThirdFragment) {
ThirdFragment thirdFragment = (ThirdFragment) currentFragment;
thirdFragment.getWeatherData(textfield.getText().toString().trim());
}
}

更新

我认为问题是您正在将东西添加到已经存在的 BackStack 中。我仍然没有看到全貌,因为我不知道您的 NavController 如何执行 backStack 操作,但我认为我们可以在您的代码中添加以下内容来消除异常:

首先,您需要向bottomNavigationView添加一个侦听器,然后在单击新项目时弹出后退堆栈。如果您只是将以下代码添加到您的活动 onCreate() 方法中,它应该可以工作。

bottomNavigationView.setOnNavigationItemSelectedListener(new NavigationView
.OnNavigationItemSelectedListener() {
@Override public boolean onNavigationItemSelected(@NonNull MenuItem item) {

if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
}
}
}); 

诚然,这会弄乱您的后退导航,因为您告诉Android在单击新片段时忘记过去的片段。换句话说,按下后退按钮将显示您过去的活动,而不是过去的片段。但它应该允许您反复按任何导航项而不会引发异常。

还有一些方法可以在保留后退导航的同时做到这一点,但我认为最好先尝试一下,看看它是否能在继续之前解决问题。

旧答案

我写这个作为答案,因为它可能太长而无法发表评论。 这不是一个正确的答案,因为我需要为此查看更多您的代码:

例外情况是告诉您,您的片段后退堆栈存在问题(这基本上只是Android记住并存储您过去活动和片段状态的地方,以便您看到与之前看到的内容相同按下后退按钮)。我无法确定问题是什么,因为我没有看到您的片段类,但听起来在您的代码中可能存在某种循环引用或类似的 smtng。也许从您的片段中添加代码。在您的位置上,我会查看名为 SecondFragment 的片段,它是异常中的引用,特别是它的 saveInstanceState 方法。不确定以某种方式人为地使您的片段singleTasksingleInstance是否有帮助。我建议阅读BackStack.以下关于新 FragmentManager relese 的文档似乎触及了您的问题

java.lang.IllegalArgumentException: saveBackStack must be self contained and not reference fragments from non-saved FragmentTransactions. Found reference to fragment SecondFragment in BackStackEntry that were previously added to the FragmentManager through a separate FragmentTransaction.

试图理解:

  • 当您SecondFragment尝试从BottomNavigationView;它说这个特定的片段 (SecondFragment) 已经存在于带有旧/单独FragmentTransaction;因此,将其重新添加到后堆栈是错误的。>> 相反,它应该被重用。

    这意味着而不是做:fm.beginTransaction().hide(currentFragment).show(newFragment).commit()

    相反,它确实:fm.beginTransaction().add(R.id.frag_container, newFragment, tag).commit()

  • 当反复点击BottomNavView时,由于某种原因它 到达未保存的fragmentTransaction(未提交);和 在那之后,当您尝试导航到另一个片段时,它说 "你不应该从当前未提交的片段中引用一个新的片段 交易"。

现在尝试在项目(重新选择)时解决此问题,方法是在BottonNavView中通过设置OnItemReselectedListener来弹出后退堆栈:

BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
// host 3 fragments along with bottom navigation.
final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
assert navHostFragment != null;
final NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNavigationView, navController);
bottomNavigationView.setOnItemReselectedListener((BottomNavigationView.OnNavigationItemReselectedListener)
item -> navController.popBackStack(item.getItemId(), false)
);  

最新更新