Stop Fragment Refresh in Bottom Nav Using Navhost

Stop fragment refresh in bottom nav using navhost

Try this:

public class MainActivity extends AppCompatActivity {

final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();

}

private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {

@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return true;

case R.id.navigation_dashboard:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
return true;

case R.id.navigation_notifications:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
return true;
}
return false;
}
};

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();

if (id == R.id.action_settings) {
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
}

return super.onOptionsItemSelected(item);
}

}

Or You can follow Google's recommended solution: Google Link

Prevent fragment refreshing with bottom navbar

The problem is that you are creating a new instance every time. You can cache the instance like:

    private Fragment mHomeFragment = new HomeFragment();
private Fragment mDashboardFragment = new DashboardFragment();
private Fragment mNotificationsFragment = new NotificationsFragment();

@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
Fragment fragment = null;

switch (item.getItemId()) {
case R.id.navigation_home:
fragment = mHomeFragment;
break;

case R.id.navigation_dashboard:
fragment = mDashboardFragment;
break;

case R.id.navigation_notifications:
fragment = mNotificationsFragment;
break;

}

return loadFragment(fragment);
}

How to keep a specific screen when using Navigatioin Component?

There are two ways to fix this issue either use version_navigation 2.4.0-alpha01 which is the easiest way or use NavigationExtensions
to use NavigationExtensions you have to add this to your project NavigationExtensions and then in the navigation menu create separate navigation files for each tab.
In the main activity, layout replace fragment with FragmentContainerView and remove NavHostFragment.

In the Mainactivty

class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener {
private val viewModel by viewModels<MainViewModel>()
private var currentNavController: LiveData<NavController>? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
setSupportActionBar(toolbar)

/*
appBarConfiguration = AppBarConfiguration(
// navController.graph,
setOf(
R.id.navigate_home, R.id.navigate_collection, R.id.navigate_profile
),
drawerLayout
)

setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
bottomNavView.setupWithNavController(navController)

// make sure appbar/toolbar is not hidden upon fragment switch
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}
*/
// Add your tab fragments

val navGraphIds = listOf(R.navigation.home, R.navigation.albumlist, R.navigation.test)
val controller = bottomNavView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
// Whenever the selected controller changes, setup the action bar.
controller.observe(this, Observer { navController ->

setupActionBarWithNavController(navController)
// optional NavigationView for Drawer implementation
// navView.setupWithNavController(navController)

addOnDestinationChangedListener(navController)
})
currentNavController = controller
}

private fun addOnDestinationChangedListener(navController: NavController) {
// ensure only one listener is active
navController.removeOnDestinationChangedListener(this)
navController.addOnDestinationChangedListener(this)
}

override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}}

Stop refreshing the bottom navigation bar fragments

Ok. I found out that this behavior was because I'm always setting the fragment without checking the current selected fragment. When I added a check it works correctly.

 private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (item.itemId == lastSelectedItemId) { // added this
return@OnNavigationItemSelectedListener true
}
when (item.itemId) {
R.id.navigation_home -> {

val fragment = TestFragment()
supportFragmentManager.beginTransaction()
.replace(frame.id, fragment)
.commit()
lastSelectedItemId = item.itemId
return@OnNavigationItemSelectedListener true
}

Is there a way to keep fragment alive when using BottomNavigationView with new NavController?

Try this.

Navigator

Create custom navigator.

@Navigator.Name("custom_fragment")  // Use as custom tag at navigation.xml
class CustomNavigator(
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?) {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()

val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}

var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = destination.createFragment(args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}

transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()

dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
}
}

NavHostFragment

Create custom NavHostFragment.

class CustomNavHostFragment: NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
}
}

navigation.xml

Use custom tag instead of fragment tag.

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation"
app:startDestination="@id/navigation_first">

<custom_fragment
android:id="@+id/navigation_first"
android:name="com.example.sample.FirstFragment"
android:label="FirstFragment" />
<custom_fragment
android:id="@+id/navigation_second"
android:name="com.example.sample.SecondFragment"
android:label="SecondFragment" />
</navigation>

activity layout

Use CustomNavHostFragment instead of NavHostFragment.

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment
android:id="@+id/nav_host_fragment"
android:name="com.example.sample.CustomNavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/navigation" />

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

</androidx.constraintlayout.widget.ConstraintLayout>

Update

I created sample project. link

I don't create custom NavHostFragment. I use navController.navigatorProvider += navigator.

Disable Android Fragment reloading with BottomBar and fragment container

Alright i seemed to figure out a work around for the time being. It keeps the fragment state after switching tabs so I'm satisfied.

In the base activity class that hosts the fragment container i have the following

public class BaseActivity extends AppCompatActivity
{
AFragment AFragment = new AFragment();
BFragment BFragment = new BFragment();

Fragment currentFragment;

Boolean aIsActive = false;
Boolean bIsActive = false;

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base);

BottomBar bottomBar = BottomBar.attach(this, savedInstanceState);

bottomBar.setItems(
new BottomBarTab(null,"A"),
new BottomBarTab(null,"B")
);

bottomBar.setDefaultTabPosition(0);

bottomBar.setOnTabClickListener(new OnTabClickListener()
{
@Override
public void onTabSelected(int position)
{
if (position == 0)
{
if(!aIsActive)
{
getSupportFragmentManager().beginTransaction().add(R.id.fragmentContainer,AFragment).commit();
aIsActive = true;
}
else
{
getSupportFragmentManager().beginTransaction().hide(currentFragment).show(AFragment).commit();
}

currentFragment = AFragment;
}
else if(position == 1)
{
if(!bIsActive)
{
getSupportFragmentManager().beginTransaction().add(R.id.fragmentContainer,BFragment).commit();
bIsActive = true;
}
else
{
getSupportFragmentManager().beginTransaction().hide(currentFragment).show(BFragment).commit();
}

currentFragment = BFragment;
}
}

@Override
public void onTabReSelected(int position) {

}
});

}

}

And loe and behold it works as expected without refreshing the fragments :)
any suggestions or feedback please let me know and feel free to comment.

Data resets when bottom navigation views are clicked

Your problem is that your FirstFragment is not saving its state properly. As per the Saving state with fragments guide:

To ensure the user's state is saved, the Android framework automatically saves and restores the fragments and the back stack. Therefore, you need to ensure that any data in your fragment is saved and restored as well.

But you aren't saving the last name that you pass to getWeatherData, nor are you saving your Example object that you get from your API call in order to repopulate your views when they are recreated.

So need to actually use the APIs described in that guide to save your state. Namely, you should file the Guide to app architecture, which explains how you can separate your from your data loading by using ViewModels to store data across configuration changes (like rotating your device) and LiveData to automatically populate your UI whenever your data is loaded.

The first thing we want to do is move the data loading to a ViewModel. This object survives configuration changes which means any data stored in this class is automatically saved when you rotate your device. This is how we can save your Example class and avoid calling the server over and over.

By using the APIs in the Saved State module for ViewModel (specifically, the SavedStateHandle class), any data you save in there will survive your process being killed and later recreated (say, if your device is low on memory, etc.). This is how we can save the last name so that we will automatically requery for your data.

Here, our ViewModel handles all of the loading from the server and uses a LiveData to let our UI automatically update as the data is loaded.

public class WeatherDataViewModel extends ViewModel {
// This will save the city name
private SavedStateHandle state;

// This is where we'll store our result from the server
private MutableLiveData<Example> mutableWeatherData = new MutableLiveData<>();

public WeatherDataViewModel(SavedStateHandle savedStateHandle) {
state = savedStateHandle;
String savedCityName = state.get("name");
if (savedCityName != null) {
// We already had a previously saved name, so we'll
// start loading right away
loadData();
}
}

// This is what our Fragment will use to get the latest weather data
public LiveData<Example> getWeatherDataLiveData() {
return mutableWeatherData;
}

// When you get a new city name, we'll save that in our
// state, then load the new data from the server
public void setCityName(String name) {
state.set("name", name);
loadData();
}

private void loadData() {
// Get the last name that was set
String name = state.get("name");

// Now kick off a load from the server
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) {
// Save the response we've gotten
// This will automatically update our UI
mutableWeatherData.setValue(response.body());
}

@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
}

Now you can rewrite your FirstFragment to use the WeatherDataViewModel as the source of truth for your UI:

public class FirstFragment extends Fragment {

private WeatherDataViewModel viewModel;

public FirstFragment() {
// Required empty public constructor
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
final TextView current_temp = rootView.findViewById(R.id.textView10);
final TextView current_output = rootView.findViewById(R.id.textView11);
final TextView rise_time = rootView.findViewById(R.id.textView25);
final TextView set_time = rootView.findViewById(R.id.textView26);
final TextView temp_out = rootView.findViewById(R.id.textView28);
final TextView Press_out = rootView.findViewById(R.id.textView29);
final TextView Humid_out = rootView.findViewById(R.id.textView30);
final TextView Ws_out = rootView.findViewById(R.id.textView33);
final TextView Visi_out = rootView.findViewById(R.id.textView34);
final TextView Cloud_out = rootView.findViewById(R.id.textView35);

// Get our ViewModel instance
viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);

// And whenever the data changes, refresh the UI
viewModel.getWeatherDataLiveData().observe(getViewLifecycleOwner(), data -> {
if (data != null) {
current_temp.setVisibility(View.VISIBLE);
current_temp.setText(data.getMain().getTemp() + " ℃");
current_output.setVisibility(View.VISIBLE);
current_output.setText(data.getWeather().get(0).getDescription());
rise_time.setVisibility(View.VISIBLE);
rise_time.setText(data.getSys().getSunrise() + " ");
set_time.setVisibility(View.VISIBLE);
set_time.setText(data.getSys().getSunset() + " ");
temp_out.setVisibility(View.VISIBLE);
temp_out.setText(data.getMain().getTemp() + " ℃");
Press_out.setVisibility(View.VISIBLE);
Press_out.setText(data.getMain().getPressure() + " hpa");
Humid_out.setVisibility(View.VISIBLE);
Humid_out.setText(data.getMain().getHumidity() + " %");
Ws_out.setVisibility(View.VISIBLE);
Ws_out.setText(data.getWind().getSpeed() + " Km/h");
Visi_out.setVisibility(View.VISIBLE);
Visi_out.setText(data.getVisibility() + " m");
Cloud_out.setVisibility(View.VISIBLE);
Cloud_out.setText(data.getClouds().getAll() + " %");
} else {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
rise_time.setVisibility(View.GONE);
set_time.setVisibility(View.GONE);
temp_out.setVisibility(View.GONE);
Press_out.setVisibility(View.GONE);
Humid_out.setVisibility(View.GONE);
Ws_out.setVisibility(View.GONE);
Visi_out.setVisibility(View.GONE);
Cloud_out.setVisibility(View.GONE);
Toast.makeText(requireActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
});

return rootView;
}

public void getWeatherData(String name) {
// The ViewModel controls loading the data, so we just
// tell it what the new name is - this kicks off loading
// the data, which will automatically call through to
// our observe() call when the data load completes
viewModel.setCityName(name);
}
}

With these changes, you'll find that your Fragment now correctly handles:

  • Being put on the Fragment back stack
  • Configuration changes (i.e., rotating your device)
  • Process death and recreation (i.e., testing with 'Don't keep activities' on)

You'll note how we use new ViewModelProvider(this).get(WeatherDataViewModel.class) - that creates a WeatherDataViewModel that is tied to this - your Fragment itself. This is best if the data your ViewModel loads is only used in that one Fragment.

If you also wanted to use this same data in your Activity, your activity could use new ViewModelProvider(this).get(WeatherDataViewModel.class) to create a WeatherDataViewModel that is scoped to the entire Activity. Any Fragment could then use new ViewModelProvider(requireActivity()).get(WeatherDataViewModel.class) to get that Activity owned ViewModel. This would potentially mean that you wouldn't need a getWeatherData() method on your Fragment at all - instead, your Activity would directly call viewModel.setCityName(name) itself and all Fragments would just instantly update (as they read from the same ViewModel).

How to Stop Fragment from Reloading When Changing Tabs?

Here's an article which describes your case perfectly and in detail.

Basically, it creates a fragment for each tab in memory, and saves them as a local variable in the activity:

final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;

You add all 3 fragments to the manager, but hide 2 of them, so only 1 will be visible:

fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();

You implement the OnNavigationItemSelectedListener of the BottomNavigationView, check which item was pressed, and then show that fragment while hiding the previous:

case R.id.navigation_dashboard:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;

Android navigation component: how save fragment state

As per the open issue, Navigation does not directly support multiple back stacks - i.e., saving the state of stack B when you go back to B from A or C since Fragments do not support multiple back stacks.

As per this comment:

The NavigationAdvancedSample is now available at https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample

This sample uses multiple NavHostFragments, one for each bottom navigation tab, to work around the current limitations of the Fragment API in supporting multiple back stacks.

We'll be proceeding with the Fragment API to support multiple back stacks and the Navigation API to plug into it once created, which will remove the need for anything like the NavigationExtensions.kt file. We'll continue to use this issue to track that work.

Therefore you can use the NavigationAdvancedSample approach in your app right now and star the issue so that you get updates for when the underlying issue is resolved and direct support is added to Navigation.

BottomNavigationView doubleclick

If you only want to prevent fragments from recreating when client press already active menu item this is the only code you need:

bottomNavigatinView.setOnNavigationItemReselectedListener { 
// Empty Block -> Do not write any code here
}



Related Topics



Leave a reply



Submit