I'm working on an app and I have a menu with a NavigationDrawer to navigate between fragments. In one of the fragments I make a call to the backend and then save the results in a list. When I navigate to another fragment and back, the results are gone, but I'd like to save the contents of the list temporarily. I wanted to use onSaveInstanceState(), but that method doesn't seem to get called ever. I also looked if the data is still in the fields when I return to the fragment, but that also wasn't the case. I think I'm doing something wrong with the FragmentManager, but I'm not sure about it.
This is the method used for the transactions for the Fragments:
private void openFragment(Class fragmentClass) {
Fragment fragment;
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
return;
}
contentFrame.removeAllViews();
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.contentFrame,fragment).commit();
}
I use a switch case to determine the Fragment's class and send that to this method.
I could probably figure out a hacky-snappy way to fix this, but I'd like to fix this without too much hacky-snappy code.
I hope someone has an idea on how to fix this. Thanks in advance.
EDIT:
Here is my fragment class:
public class LGSFragment extends Fragment {
#BindView(R.id.rvLGS)
RecyclerView rvLGS;
private List<LGS> lgsList;
private LGSAdapter adapter;
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
//I debugged here and all fields were null at this point
View view = inflater.inflate(R.layout.fragment_lgs,container,false);
ButterKnife.bind(this, view);
lgsList = new ArrayList<>();
LinearLayoutManager manager = new LinearLayoutManager(getContext());
rvLGS.setLayoutManager(manager);
adapter = new LGSAdapter(lgsList);
rvLGS.setAdapter(adapter);
getDatabaseLGSs();
return view;
}
/**
* Method to load in the LGSs from the database
*/
private void getDatabaseLGSs() {
String collection = getString(R.string.db_lgs);
FireStoreUtils.getAllDocumentsConverted(collection, LGS.class, new OperationCompletedListener() {
#Override
public void onOperationComplete(Result result, Object... data) {
if (result == Result.SUCCESS) {
lgsList.clear();
List<LGS> newLGSs = (List<LGS>) data[0];
List<String> ids = (List<String>) data[1];
int i = 0;
for (LGS lgs : newLGSs) {
lgs.setId(ids.get(i));
lgsList.add(lgs);
i++;
}
adapter.notifyDataSetChanged();
}
}
});
}
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
}
}
onSaveInstanceState is not called because there is no reason to, when you navigate between fragments, the older fragment doesn't get destroyed till the OS need the space they use (low Memory).
First of all create a back stack to keep fragments or just call addtoBackStack at the end of fragmentTransaction and then move the list initiation and data request to onCreate so it only called when the fragment is created:
lgsList = new ArrayList<>();
getDatabaseLGSs();
and after that every time you get back to fragment the view is recreated with available data.
Update:
Instead of keeping an reference on your own, you can add the fragment to the backstack and then retrieve it using corresponding tag. This let's fragmentManager manages the caching by itself. And the second time you access a fragment, it doesn't gets recreated:
#Override
public void onNavigationDrawerItemSelected(#NonNull MenuItem item) {
if (item.isChecked())
return;
item.setChecked(true);
setTitle(item.getTitle());
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
Fragment currentlyShown = fragmentManager.findFragmentByTag(currentlyShownTag);
Fragment dest;
switch (item.getItemId()){
case R.id.nav_lgs:
dest = fragmentManager.findFragmentByTag(LGSFragment.class.getName());
if (dest == null) {
Log.d("TRANSACTION", "instanciating new fragment");
dest = new LGSFragment();
currentlyShownTag = LGSFragment.class.getName();
transaction.add(R.id.contentFrame, dest, LGSFragment.class.getName());
}
break;
...
}
if(currentlyShown != null)
transaction.hide(currentlyShown);
transaction.show(dest);
transaction.commit();
drawerLayout.closeDrawers();
return true;
}
EDIT:
Although this solution works fine, this solution uses some bad practices, I recommend using the accepted solution instead.
I've solved the problem with the help of Keivan Esbati and denvercoder9 (Thanks for that!)
Since I only have 4 fragments I keep an instance of each of them in the MainActivity, I also have a variable to track the current Fragment. Everytime I open a fragment, I hide the current fragment using the FragmentManager and calling .hide() in the transaction. Then, if the Fragment is a new Fragment I call .add() in the transaction, otherwise I call .show in the transaction.
The code for the onNavigationItemSelected() method (which triggers when a user selects an item in the menu):
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
if (!item.isChecked()) {
item.setChecked(true);
setTitle(item.getTitle());
switch (item.getItemId()) {
case R.id.nav_lgs: {
if (lgsFragment == null) {
lgsFragment = new LGSFragment();
openFragment(lgsFragment, FragmentTag.LGS.toString());
} else {
openFragment(lgsFragment, "");
}
currentFragmentTag = FragmentTag.LGS;
break;
}
case R.id.nav_users: {
if (userFragment == null) {
userFragment = new UserFragment();
openFragment(userFragment, FragmentTag.USERS.toString());
} else {
openFragment(userFragment, "");
}
currentFragmentTag = FragmentTag.USERS;
break;
}
case R.id.nav_profile: {
if (profileFragment == null) {
profileFragment = new ProfileFragment();
openFragment(profileFragment, FragmentTag.PROFILE.toString());
} else {
openFragment(profileFragment, "");
}
currentFragmentTag = FragmentTag.PROFILE;
break;
}
case R.id.nav_my_lgs: {
if (myLGSFragment == null) {
myLGSFragment = new MyLGSFragment();
openFragment(myLGSFragment, FragmentTag.MY_LGS.toString());
} else {
openFragment(myLGSFragment, "");
}
currentFragmentTag = FragmentTag.MY_LGS;
break;
}
default: {
if (lgsFragment == null) {
lgsFragment = new LGSFragment();
openFragment(lgsFragment, FragmentTag.LGS.toString());
} else {
openFragment(lgsFragment, "");
}
currentFragmentTag = FragmentTag.LGS;
break;
}
}
}
drawerLayout.closeDrawers();
return true;
}
The openFragment() method used above:
private void openFragment(Fragment fragment, String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
if (currentFragmentTag != FragmentTag.NO_FRAGMENT) {
fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag(currentFragmentTag.toString())).commit();
}
if (!tag.equals("")) {
fragmentManager.beginTransaction().add(R.id.contentFrame,fragment,tag).commit();
} else {
fragmentManager.beginTransaction().show(fragment).commit();
}
}
Set up in onCreate():
currentFragmentTag = FragmentTag.NO_FRAGMENT;
if (lgsFragment == null) {
lgsFragment = new LGSFragment();
openFragment(lgsFragment, FragmentTag.LGS.toString());
} else {
openFragment(lgsFragment, "");
}
currentFragmentTag = FragmentTag.LGS;
Related
My question is quite similar to this one: fragment.getView() return null after backpressed
The problem is next: I have a special Fragment with two states: A and B. If Fragment is in the state B, Backpress should switch the state from B to A. (The difference between two states is in visibility of some elements on a layout) If Fragment is in state A, Backpress should close this Fragment. I overwrote the onBackPressed() method in my activity in next way:
#Override
public void onBackPressed() {
int count = getSupportFragmentManager().getBackStackEntryCount();
if (count > 1) {
if (isStateB) {
isStateB = false;
MessagesFragment messFragment = ((MessagesFragment) getSupportFragmentManager().findFragmentByTag("MessagesFragment"));
if (messFragment != null) {
Log.d(TAG, "Message Fragment Refresh");
messFragment.refreshFragWithoutMessChecked();
return;
} else {
Log.d(TAG, "Fail To Message Fragment Resfresh");
}
}
}
}
super.onBackPressed();
}
This code executes refreshFragWithoutMessChecked() but nothing happens. All elements from the layout which I am trying to close aren't null, but code doesn't affect them. Also I have a button in the MessageFragment which executes the similar method, and in case I press it, the code works well. In additional I found out that if I call getView() inside refreshFragWithoutMessChecked(). In case when it called from onBackPressed(), getView() returns NULL. In case when it called from onClick() it returns something isn't NULL.
So that is why I am asking, why Backpress makes my getView() returns NULL, and how can I solve my problem?
Fragment Code
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.frag_messages, container, false);
/*
Here is a big amount of elements initizalizations and onClick bindings like this:
check_message = view.findViewById(R.id.check_message);
check_message.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {MainActivity.isStateB = true;}});
*/
return view;
}
public void refreshFragWithoutMessChecked() {
MainActivity.isStateB = false;
MainActivity.posMessChecksEnabled.clear();
refreshMessageFragment();
Log.d(TAG,"Message Fragment Refreshed");
}
// Refresh Message Fragment
public void refreshMessageFragment() {
if(getView() != null){
Log.d(TAG,"A");
} else {
Log.d(TAG,"B");
}
// Hide Message Check Show
// THIS CODE DOESN'T WORK ON BACK PRESS BUT IT'S EXECUTED
if (MainActivity.isStateB == true) {
((MainActivity) getActivity()).getSupportActionBar().hide();
write_message_layout.setVisibility(View.GONE);
reply_forward_bottom.setVisibility(View.VISIBLE);
up_message_selected_panel.setVisibility(View.VISIBLE);
toolbar.setVisibility(View.GONE);
appbar_layout.setVisibility(View.GONE);
} else {
if (fromOpen.equalsIgnoreCase(MainActivity.MESSAGES_FILTER_CONTACTS) == false) {
write_message_layout.setVisibility(View.VISIBLE);
}
reply_forward_bottom.setVisibility(View.GONE);
up_message_selected_panel.setVisibility(View.GONE);
toolbar.setVisibility(View.VISIBLE);
appbar_layout.setVisibility(View.VISIBLE);
}
}
Can someone could tell me how to make this code more efficient and so that super.onBackpressed(); is only called once.
The problem is that the way I have it now is the only way that it works. The top part of the code is for the NavigationView. When the drawer is opened (if it is opened) it closes. And the second part is to change the icons in the BottomNavigationView to match the fragment that the user is on when they hit the Android back button. The problem with this way is that the images come back duplicated when I hit the Android back button.
I have tried various ways to have only one super.onBackPressed();, but as I have mentioned earlier, if I get rid of either one the page doesn't change.
Also tried changing the icons in the BottomNavigationView in onResume(); in the different fragments. But for some reason the app freezes then and closes.
How I have it now. Images come back duplicated
#Override
public void onBackPressed() {
if (mDrawer.isDrawerOpen(GravityCompat.START)) {
mDrawer.closeDrawer(GravityCompat.START);
}
super.onBackPressed();
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment instanceof HomeFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_home);
} else if (fragment instanceof AttendingEventFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_save);
} else if (fragment instanceof NotificationsFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_notifications);
} else if (fragment instanceof ProfileFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_profile);
}
super.onBackPressed();
}
Other ways I have tried to implement the code... Doesn't work...
#Override
public void onBackPressed() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (mDrawer.isDrawerOpen(GravityCompat.START)) {
mDrawer.closeDrawer(GravityCompat.START);
} else {
if (fragment instanceof HomeFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_home);
} else if (fragment instanceof AttendingEventFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_save);
} else if (fragment instanceof NotificationsFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_notifications);
} else if (fragment instanceof ProfileFragment) {
mBottomNavigationView.setSelectedItemId(R.id.nav_profile);
}
}
super.onBackPressed();
}
onResume() HomeFragment; - Did this for all fragments and app just freezes then closes...
#Override
public void onResume() {
super.onResume();
mBottomNavigationView = mActivity.findViewById(R.id.bottomNavigation);
mBottomNavigationView.setSelectedItemId(R.id.nav_home);
}
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
if (context instanceof Activity) {
mActivity = (Activity) context;
}
}
BottomNavigationView
private BottomNavigationView.OnNavigationItemSelectedListener navigationItemSelectedListener = menuItem -> {
switch (menuItem.getItemId()) {
case R.id.nav_home:
mSelectedFragment = new TabLayoutFragment();
break;
case R.id.nav_notifications:
seenNotification();
mSelectedFragment = new NotificationsFragment();
break;
case R.id.nav_profile:
SharedPreferences.Editor editor = getSharedPreferences("PREFS", MODE_PRIVATE).edit();
editor.putString("profileid", Objects.requireNonNull(FirebaseAuth.getInstance().getCurrentUser()).getUid());
editor.apply();
mSelectedFragment = new ProfileFragment();
break;
case R.id.nav_save:
mSelectedFragment = new AttendingEventFragment();
break;
}
if (mSelectedFragment != null) {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, mSelectedFragment, null).addToBackStack(null).commit();
}
return true;
};
I'm trying to replace fragments in ViewPager, but I'm facing a problem I've been unable to fix for several days. The relevant code and specific problem, as I understand it, are described below:
public class ViewPageAdapter extends FragmentStatePagerAdapter {
int mNumOfTabs;
FragmentManager mFragmentManager;
Fragment0 currentFragment0;
Fragment1 currentFragment1;
Fragment2 currentFragment2;
boolean getItemNeverCalled = true;
public ViewPageAdapter(FragmentManager fm, int numOfTabs){
super(fm);
mFragmentManager = fm;
this.mNumOfTabs = numOfTabs;
}
#Override
public Fragment getItem(int position){
switch (position){
case 0:
if(currentFragment0 == null){
Fragment0 tab0 = new Fragment0();
currentFragment0 = tab0;
return currentFragment0;
}
else {
mFragmentManager.beginTransaction().remove(currentFragment0).commit();
int value = selectedPlant.getMoistureFrag().getStat().getOptimalLevel();
currentFragment0 = Fragment0.newInstance(key0, value);
notifyDataSetChanged(); // calls getItem(0).
return currentFragment0;
}
case 1:
if(currentFragment1 == null){
LightFragment tab1 = new Fragment1();
currentFragment1 = tab1;
return currentFragment1;
}
else {
mFragmentManager.beginTransaction().remove(currentFragment1).commit();
int value = selectedPlant.getLightFrag().getStat().getOptimalLevel();
currentFragment1 = currentFragment1.newInstance(key1, value);
notifyDataSetChanged();
return currentFragment1;
}
case 2:
if(currentFragment2 == null){
Fragment2 tab2 = new Fragment2();
currentFragment2 = tab2;
return currentFragment2;
}
else {
mFragmentManager.beginTransaction().remove(currentFragment2).commit();
int value = selectedPlant.getTempFrag().getStat().getOptimalLevel();
currentFragment2 = Fragment2.newInstance(key2, value);
notifyDataSetChanged();
return currentFragment2;
}
default:
return null;
}
}
#Override
public int getCount(){
return mNumOfTabs;
}
#Override
public int getItemPosition(Object object){
return POSITION_NONE;
}
I've overriden the getItemPosition(Object object) method to always return POSITION_NONE, and called notifyDataSetChanged() when appropriate (I think). What ends up happening is that notifyDataSetChanged() calls getItem(0), which calls `notifyDataSethanged()... and so on. This causes a TransactionTooLargeException and crashes the app.
Just to give some background to the if/else statements in each case: the if is meant to load a blank Moisture/Light/etc Fragment onto the screen. This is intended to happen on start-up. The else statement is executed when a user presses on a item in the navigation drawer, which has some data. This data is then extracted and set as arguments for the fragments that are meant to replace the initial blank fragment.
I genuinely appreciate any help. This problem is driving me crazy.
Why in the world are you recreating fragments, when you can just update the old ones?
Also, when you are calling notifyDataSetChanged during getItem then you are forcing a new call to getItem which will force a new call...so you are actually creating a circular call!
Since you are always keeping the same fragment class in each position, and you are holding on to the fragment, then you should not replace fragment. Just change the fragment you are holding to show the new values. The code you are using is only needed if you want to change different fragment classes for position.
I have a main activity which creates fragments using the following code
private void launchFragment(int pos)
{
Fragment f = null;
String title = null;
if (pos == 1)
{
title = "Friends";
f = new FriendList();
}
else if (pos == 2)
{
title = "Notes";
f = new NoteList();
}
else if (pos == 3)
{
title = "Projects";
f = new ProjectList();
}
else if (pos == 5)
{
title = "About";
f = new AboutUs();
}
else if (pos == 6)
{
startActivity(new Intent(this, Login.class));
finish();
}
if (f != null)
{
while (getSupportFragmentManager().getBackStackEntryCount() > 0)
{
getSupportFragmentManager().popBackStackImmediate();
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_frame, f).addToBackStack(title)
.commit();
}
}
Here is the code of a fragment.
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.group_chat, null);
loadConversationList();
contactName = this.getArguments().getString("contactusername");
contactId = this.getArguments().getString("contactid");
ListView list = (ListView) v.findViewById(R.id.list);
adp = new ChatAdapter();
list.setAdapter(adp);
list.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
list.setStackFromBottom(true);
txt = (EditText) v.findViewById(R.id.txt);
txt.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
setTouchNClick(v.findViewById(R.id.btnCamera));
setTouchNClick(v.findViewById(R.id.btnSend));
return v;
}
I want to call a method in above fragment class. I was not able to do this as I have not given id of the fragment in the XML file. I am not loading the static fragment using XML. Therefore, I don't have Id.
I have already seen this and this questions on the StackOverFlow, but they are not solving my problem.
Kindly help if anyone knows how to tackle this scenario.
First of all make all your fragments implement an interface. This interface will return a String (for example) which will identify your fragment, and then cast your fragment to it after getting the fragment using findFragmentById() as follows:
Create your interface
public interface IFragmentName
{
public String getFragmentName();
}
Implement your interface (for example in NoteList)
public NoteList extends Fragment implements IFragmentName
{
//Do your stuff...
public String getFragmentName()
{
return "NoteList";
}
}
After this get your current fragment from your activity
IFragmentName myFragment = (IFragmentName) getSupportFragmentManager().findFragmentById(R.id.content_frame);
Finally check your getFragmentName() value and cast to the fragment you want:
if(myFragment.getFragmentName().equals("NoteList")
{
NoteList myNoteListFragment = (NoteList) myFragment;
myNoteListFragment.callMyMethod(); //here you call the method of your current Fragment.
}
I have coded these snippets without any IDE so maybe I have missed a semicolon or something like that :)
Hope it helps
I know I am too late for the party. But this will be useful for others.
If at all you are using ViewPager to render the fragment use this code in your parent Activity.
Check Solution here
So basically I have 2 Fragments - FragmentConverter and FragmentFavourites, and I have one MainActivity. I'm trying to pass 4 arrays from the first fragment to the second one using an Interface called Communicator. The specific snippets are show below:
public interface Communicator {
public void respond(String[] names, String[] codes, String[] symbols, int[] images);
}
This is a method inside FragmentFavourites:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// TODO Auto-generated method stub
String[] checkedNames = new String[counter];
String[] checkedCodes = new String[counter];
String[] checkedSymbols = new String[counter];
int[] checkedImages = new int[counter];
comm = (Communicator) getActivity();
int index = 0;
if (item.getItemId() == R.id.action_save){
for (int i=0;i<checked.size();i++){
if (checked.get(i) == true){
checkedNames[index] = names[i];
checkedCodes[index] = codes[i];
checkedSymbols[index] = symbols[i];
checkedImages[index] = images[i];
index++;
}
}
comm.respond(checkedNames, checkedCodes, checkedSymbols, checkedImages);
}
return super.onOptionsItemSelected(item);
}
This is the implemented interface method inside MainActivity:
#Override
public void respond(String[] names, String[] codes, String[] symbols,
int[] images) {
// TODO Auto-generated method stub
FragmentConverter frag = (FragmentConverter) fragmentPagerAdapter.getItem(1);
frag.changeData(names, codes, symbols, images);
}
And this is a method that collects the data in FragmentConverter:
public void changeData(String[] names, String[] codes, String[] symbols, int[] images){
this.names = names;
this.codes = codes;
this.symbols = symbols;
this.images = images;
Log.d("TEST", symbols.length + names.length + codes.length + images.length + "");
tvOneRate.setText(names[1]);
}
Now the problem is that whenever I try to change a ui component inside FragmentConverter, I get a NullPointerException, though the Log.d statement returns the correct results.
EDIT1: getItem() method of FragmentPagerAdapter:
#Override
public Fragment getItem(int i) {
// TODO Auto-generated method stub
Fragment frag = null;
if (i == 0){
frag = new FragmentFavourites();
}
if (i == 1){
frag = new FragmentConverter();
}
return frag;
}
EDITED:
When you call fragmentPagerAdapter.getItem(1) you are getting a new instance of the fragment so you are referring to a different object. this is why the view is null and you get the NullPointerException. If you need an adapter for only 2 fragments, you can try with something like that:
public class YourPagerAdapter extends android.support.v4.app.FragmentPagerAdapter {
private FragmentFavourites mFragFavourites;
private FragmentConverter mFragConverter;
public YourPagerAdapter() {
// ... your code above
this.mFragFavourites = new FragmentFavourites();
this.mFragConverter = new FragmentConverter();
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return mFragFavourites;
case 1:
return mFragConverter;
default:
return null;
}
}
}
As above carlo.marinangeli has suggested when you call fragmentPagerAdapter.getItem(1) you are getting a new instance of the fragment so you are referring to a different object
So to get same object from you adapter you need to store your object. you can try following method in your adapter -
public Fragment getFragmentAtPosition(int position) {
return registeredFragments.get(position);
}
where registeredFragments is -
private SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
and fill this sparseArray in getItem method like below -
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
fragment = FragmentPost.newInstance(position);
registeredFragments.put(position, fragment);
return fragment;
}
return null;
}
By using fragmentPagerAdapter.getItem(pos) method I was referring to a new object every time the respond() method was called. I fixed it by using findFragmentByTag() method instead:
#Override
public void respond(String[] names, String[] codes, String[] symbols,
int[] images) {
FragmentManager manager = getSupportFragmentManager();
FragmentConverter frag = (FragmentConverter) manager.findFragmentByTag("android:switcher:" + pager.getId() + ":" + 1);
frag.changeData(names, codes, symbols, images);
}
you can get that error because you are assuming that you have got the FragmentConverter and the views associated to it.
Without a logcat it becomes a little bit difficult to help you but basically what I would like to do in a situation like this is to pass everything through the activity without letting know the existence of the other fragment to the fragments.
F1 modifies a state object into the activity
F2 has to register as a
listener to the activity (be aware that the fragment can be attached
and detached in the view pager)
The Activity as soon it receives an updated, looks for all the registered listeners and if there is someone it delivers the updated