I have recently started using fragments have created a demo app which looks like this:
Clicking on each button switches between fragment 1, Fragment 2, and Fragment 3.
What I am trying to accomplish is to only have 1 instance of each Fragment and reuse that. (Please note that all the fragments are created and added dynamically). Currently I am doing this by creating a HashMap of fragment and placing each instance and grabbing it from there.
So my questions are:
Is there a better way of doing this:
By using FragmentManager's putFragment(...) method? putFragment (Bundle bundle, String key, Fragment fragment) I can't figure out how to use it in my case. If anyone can give me an example of how to use this method.
Is it expensive to hold onto a reference of each fragment in my activity? Does this keep all the fragments alive? I am using soft reference to tackle this but I am not sure if this is the proper way of doing this. Please point me towards any alternative way of doing this or let me know if this is best way to accomplish this.
Thanks in advance.
Here is my code:
UPDATE:
I am trying to reuse fragments from the back stack, trying to only add them if it does not exists in the back stack. The code below gives me the Illegal state exception after I navigate away from fragment one -> come back to it -> then try to press back button:
10-28 13:21:40.255: ERROR/MessageQueue-JNI(3548): java.lang.IllegalStateException: Fragment already added: FragmentOne{423db570 #0 id=0x7f050006 fragOne}
public class MainActivity extends Activity implements View.OnClickListener {
private Button btnOne;
private Button btnTwo;
private Button btnThree;
/* Called when the activity is first created.
*/
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initialize();
if(findViewById(R.id.fl) != null){
if(savedInstanceState != null)
return;
}
FragmentManager.enableDebugLogging(true);
updateView("fragOne");
}
private void initialize(){
btnOne = (Button) findViewById(R.id.button1);
btnTwo = (Button) findViewById(R.id.button2);
btnThree = (Button) findViewById(R.id.button3);
btnOne.setOnClickListener(this);
btnTwo.setOnClickListener(this);
btnThree.setOnClickListener(this);
fragHolder = new HashMap<String, SoftReference<Fragment>>();
}
private void updateView(String tag){
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment frag = getFragmentManager().findFragmentByTag(tag);
boolean addToStack = true;
if(frag == null){
if(tag.equals("fragOne"))
frag = new FragmentOne();
else if(tag.equals("fragTwo"))
frag = new FragmentTwo();
else if(tag.equals("fragThree"))
frag = new FragmentThree();
}else{
//Don't add to back stack
addToStack = false;
}
ft.replace(R.id.fl, frag, tag);
if(addToStack)
ft.addToBackStack(null);
ft.commit();
}
#Override
public void onClick(View v) {
switch(v.getId()){
case R.id.button1:
updateView("fragOne");
break;
case R.id.button2:
updateView("fragTwo");
break;
case R.id.button3:
updateView("fragThree");
break;
}
}
}
To demonstrate a FragmentTransaction, the following sample might be helpful to you.
First, you want to do all your initialization stuff in the onCreate() of your activity, which you have right, but we'll make a few changes.
public class MainActivity extends Activity implements View.OnClickListener {
private Button btnOne;
private Button btnTwo;
private Button btnThree;
/* Called when the activity is first created.*/
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initialize();
if(findViewById(R.id.fl) != null)
{
if(savedInstanceState != null)
{
FragmentTransaction trans = getFragmentManager().beginTransaction();
//This is where we add our first fragment
trans.add(R.id.fl, new FragmentOne());
trans.commit();
}
}
}
private void initialize()
{
btnOne = (Button) findViewById(R.id.button1);
btnTwo = (Button) findViewById(R.id.button2);
btnThree = (Button) findViewById(R.id.button3);
btnOne.setOnClickListener(this);
btnTwo.setOnClickListener(this);
btnThree.setOnClickListener(this);
}
public void onClick(View view)
{
//Here is where we'll actually transfer the fragments
FragmentTransaction trans = getFragmentManager().beginTransaction();
switch(v.getId()){
case R.id.button1:
trans.replace(R.id.fl, new FragmentOne());
trans.addToBackStack(null);
trans.commit();
break;
case R.id.button2:
trans.replace(R.id.fl, new FragmentTwo());
trans.addToBackStack(null);
trans.commit();
break;
case R.id.button3:
trans.replace(R.id.fl, new FragmentThree());
trans.addToBackStack(null);
trans.commit();
break;
}
}
This will allow you to easily transition from one Fragment to the next.
The FragmentManager does it's own memory management. It will kill/recreate or keep in memory your instances according to its logic. You can ensure your fragment's state is save using onSaveInstanceState()
Or you can for force the system to keep your instance alive using setRetainInstance(true) on your Fragment.
This is how you create a transaction.
FragmentTransaction fragmentTransaction = context.getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.layout, new MyFragment(), f.getClass().getName());
fragmentTransaction.commit();
Related
I have been struggling to implement a super simple app layout, where my MainActivity opens Fragment#1 in its onCreate method, then the Fragment#1 opens Fragment#2 when an item is clicked.
As of right now, when I open Fragment#1 from my MainActivity, I add Fragment#1 to the BackStack. After opening Fragment#2, when I hit the back button the first click does nothing, then the second click sends me all the way back to my login page, skipping past Fragment #1 and MainActivity.
How can I make it so when I hit the back button on Fragment#2, it opens Fragment#1 back up?
(MainActivity opens Fragment#1)
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
SearchListFragment fragment = new SearchListFragment();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.main_fragment_container, fragment);
transaction.addToBackStack(TAG);
transaction.commit();
}
}
(Fragment#1 opens Fragment#2)
public class SearchListFragment extends Fragment {
public void viewResults(SearchModel search) {
Bundle args = new Bundle();
args.putString("ID", search.getId());
ResultsFragment fragment = new ResultsFragment();
fragment.setArguments(args);
FragmentTransaction transaction = getParentFragmentManager().beginTransaction();
transaction.replace(R.id.main_fragment_container, fragment);
transaction.commit();
}
}
EDIT
I should have mentioned that I have tried to handle the back press event myself. I tried adding this to my MainActivity but it did not change the behavior:
#Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
this.finish();
} else {
getSupportFragmentManager().popBackStack();
}
}
onCreate with
Kotlin
val backpress = requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, true) {
// Handle the back button event
}
Java
OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
#Override
public void handleOnBackPressed() {
// Handle the back button event
}
});
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
You can provide your back operations in the relevant sections.
getSupportFragmentManager().beginTransaction().replace(R.id.frag_frame, fragment).addToBackStack("text").commit();
I am trying to call a function from my mainActivity to change a TextView in one of my fragments. I have read a couple posts on the best way of doing it but for some reason, none of them seem to work for me. I know the function is working because once a press the button the toast comes up but for some reason the text won't change. I was wondering what the issue could be, or if I am just missing an additional step.
here is the method being called in my mainActivity
public class MainActivity extends AppCompatActivity implements Tab1Fragment.OnCalcClickListener{
private static final String TAG = "MainActivity";
private SectionsPageAdapter mSectionsPageAdpater;
private ViewPager mViewPager;
Tab1Fragment tab1Fragment = new Tab1Fragment();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: Starting");
//initializing FragmentManager so the fragments can communicate
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.container, tab1Fragment);
fragmentTransaction.commit();
//declare sections page adapter
mSectionsPageAdpater = new SectionsPageAdapter(getSupportFragmentManager());
//Set up the view pager with the sections adapter
mViewPager = (ViewPager) findViewById(R.id.container);
setUpViewPager(mViewPager);
//create a tab layout object and set it's id to tabs (mainActivity.xml)
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
//
}
// create a sections page view adapter
private void setUpViewPager(ViewPager viewPager){
SectionsPageAdapter adapter = new SectionsPageAdapter(getSupportFragmentManager());
adapter.addFragment(new Tab1Fragment(), "Day");
adapter.addFragment(new Tab2Fragment(), "Info");
adapter.addFragment(new Tab3Fragment(), "Week");
viewPager.setAdapter(adapter);
}
//initialise calculator object
Calculator mainCalculator = new Calculator();
#Override
public void calculateClick(int to_calculate) {
switch (to_calculate){
case 1:
Toast.makeText(getBaseContext(),"working", Toast.LENGTH_SHORT).show();
mainCalculator.freqDay = mainCalculator.freqDay + 1;
mainCalculator.freqWeek = mainCalculator.freqWeek + 1;
mainCalculator.getTotalDay();
tab1Fragment.updateInfo();
break;
}
}
Here is the code for my fragment
public class Tab1Fragment extends Fragment implements
View.OnClickListener{
private static final String TAG = "Tab1Fragment";
//Establishing the buttons & Methods
Button btn1;
Button btn2;
TextView dayView;
OnCalcClickListener onCalcClickListener;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.tab1_fragment, container, false);
//Connecting the buttons to the xml
btn1 = (Button) view.findViewById(R.id.btn_1);
btn1.setOnClickListener(this);
btn2 = (Button) view.findViewById(R.id.btn_2);
btn2.setOnClickListener(this);
dayView = (TextView) view.findViewById(R.id.total_Sales_Day);
return view;
}
//Onclick listener for buttons
public void setOnClickListener(View.OnClickListener listener) {
btn1.setOnClickListener(listener);
btn2.setOnClickListener(listener);
}
//method that will bring data back from activity and set the text
public void updateInfo(){
Toast.makeText(getContext(),"65", Toast.LENGTH_SHORT).show();
dayView.setText("test");
}
To make calls to methods from your fragment in the activity class you need something like this:
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener{
...
public void onArticleSelected(int position) {
// The user selected the headline of an article from the HeadlinesFragment
// Do something here to display that article
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);
if (articleFrag != null) {
// If article frag is available, we're in two-pane layout...
// Call a method in the ArticleFragment to update its content
articleFrag.updateArticleView(position);
} else {
// Otherwise, we're in the one-pane layout and must swap frags...
// Create fragment and give it an argument for the selected article
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack so the user can navigate back
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
}
}
Here the activity is calling the updateArticleView method from the fragment, instead of your updateInfo method, but you will get the idea.
Also pay attention to the one-pane scenario, where you need to swap the content and push the arguments using a Bundle object.
See Deliver a Message to a Fragment for more details.
When you add the fragment set a tag to it like this:
MyFragment frag = new MyFragment();
frag.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, frag, "TAG").commit();
While updating the textview get the instance of fragment using findFragmentByTag()
MyFragment fragment = (MyFragment) getSupportFragmentManager().findFragmentByTag("TAG");
fragment.updateInfo();
So i've encountered a small problem today. I was making a bottom navigation view in my app, and after clicking buttons, it replaces the fragment on the screen (and it works perfectly!).
But just after launching the app, and without clicking any button, there is no fragment on the screen.
I've realized that the fragments are shown only after clicking a button, and I'd like to have a default fragment (kalkulatorFragment).
I've been trying my best to somehow set it up, but no success...
public class Main extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
}
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
kalkulatorFragment kalkulator_fragment = new kalkulatorFragment();
wzoryFragment wzory_fragment = new wzoryFragment();
definicjeFragment definicje_fragment = new definicjeFragment();
switch (item.getItemId()) {
case R.id.kalkulator:
ft.replace(android.R.id.content, kalkulator_fragment);
ft.commit();
return true;
case R.id.wzory:
ft.replace(android.R.id.content, wzory_fragment);
ft.commit();
return true;
case R.id.definicje:
ft.replace(android.R.id.content, definicje_fragment);
ft.commit();
return true;
}
return false;
}
Ok i just figured it out.
I moved the ft.replace to the onCreate() method, so the kalkulatorFragment is going to be shown just after creating an Activity.
public class Main extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
kalkulatorFragment kalkulator_fragment = new kalkulatorFragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(android.R.id.content, kalkulator_fragment);
ft.commit();
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
}
You need to use this code OUTSIDE of OnCreate Method:
navigation.setSelectedItemId(R.id.IdOFYourItemFromBottomNavigationMenuItems);
I don't know why, but it wont work inside OnCreate method. You can declare and initialize it inside OnCreate method, just can't set the default item in there.
In my case I am using it inside OnCreateOptionsMenu.
In my app, am using a navigation drawer (in Fragment A) to navigate to fragments:
public void displayView(int viewId){
Fragment fragment = null;
String title = getString(R.string.app_name);
switch (viewId) {
case R.id.nav_menu:
fragment = new MenuFragment();
title = getString(R.string.menu_title);
viewIsAtHome = false;
break;
case R.id.nav_reservation:
fragment = new ReservationFragment();
title = getString(R.string.reservation_title);
viewIsAtHome = false;
break;
...
if (fragment != null) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.commit();
}
// set the toolbar title
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(title);
}
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
}
}
displayView() is called in onNavigationItemSelected:
#Override
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
displayView(item.getItemId());
return true;
}
Now, in ReservationFragment I am displaying a list of reservations and a FloatingActionButton to start Activity B where the user can add a reservation if there are no reservations. When the user is done adding a reservation, I want to display it in the Reservation fragment. This requires me to "go back" to the Fragment.How do I accomplish this since Activity B knows nothing about Activity A?
What I've tried:
I tried creating a method in Activity A like this:
public void navigateToFragment(int viewId) {
displayView(R.id.nav_reservation);
}
and then called this method from Activity B:
saveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
new MainActivity().navigateToFragment(R.id.nav_reservation);
//MainActivity is Activity A
}
});
the app crashes due to a nullPointerException in displayView() from the line:
String title = getString(R.string.app_name);
This isn't surprising since am creating a new MainActivity object that knows nothing about the previous state of the Activity, right?
This question mirrors my problem but is based on Settings so I can't really use the answer in my case.
How do I accomplish this task?
There are three quick methods that come to my mind. First you can start activityB with startActivityForResult and handle the result in activityA after user does what he wants in activityB. Second you can set activityA as singleTop and before finishing activityB you can startActivityA with clearTop an intent flag called clear_top(https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP).
Last but not the least, you can connect two activity by binding service in both activities and communicate via that service that you bound.
In my MainActivity I call a Fragment and set the boolean mFragment to true when its active. Now I set some Text in the TextViews declared in the Fragment and it works fine. But when a Button declared in the Fragment calls a Method in the MainActivity suddently mFragment is false and I can't use getText from the TextViews because they are null. I have no idea why this is so.
Main Activity
public class MainActivity extends AppCompatActivity implements SensorEventListener {
MainFragment mainFragment;
HistoryActivity historyFragment;
boolean mFragment = false;
//....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainFragment = new MainFragment();
historyFragment = new HistoryActivity();
if(!mFragment) {
getMainFragment();
}
//...SenserManager and so on
#Override
public void onSensorChanged(SensorEvent event){
if (mFragment) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
DecimalFormat df = new DecimalFormat("0.000");
//This works constantly:
mainFragment.textViewX.setText("X = " + df.format(x));
mainFragment.textViewY.setText("Y = " + df.format(y));
mainFragment.textViewZ.setText("Z = " + df.format(z));
}
}
public void writeEntry(){ //called in Fragment
if(mFragment) {
//This doesn't work (isn't even called because mFragment is false)
String x = (String) mainFragment.textViewX.getText();
String y = (String) mainFragment.textViewY.getText();
String z = (String) mainFragment.textViewZ.getText();
}
}
public void getMainFragment() {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, mainFragment);
transaction.addToBackStack(null);
transaction.commit();
mFragment = true;
}
Fragment
public class MainFragment extends Fragment {
TextView textViewX;
TextView textViewY;
TextView textViewZ;
Button button;
MainActivity mainActivity = new MainActivity();
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
textViewX = (TextView) view.findViewById(R.id.textViewX);
textViewY = (TextView) view.findViewById(R.id.textViewY);
textViewZ = (TextView) view.findViewById(R.id.textViewZ);
button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mainActivity.writeEntry();
}});
return view;
}
}
Use callback interface to interact from fragment back to activity. Please refer. It's a very recommended method. http://developer.android.com/training/basics/fragments/communicating.html#Implement
Your architecture has many flaws. You create a fragment instance and are then calling methods on it. You can't do this in android for components such as activities, fragments, services etc (You can only do this for Java classes). You cannot simply create an object of fragment like this. All Fragments, Activities and Services in Android must go through their respective lifecycles so that they have a valid context attached to them.
Also the fragment activity interaction should be done via interfaces. Not by simply calling methods on their objects which must not be created in the first place. You might get it working but ultimately it will keep causing you problems. I recently answered two questions this and this which had the same problem. Seems like lot of people have this doubt.
Your communication and a few point is wrong.
With below usage probably you wanted to access current MainAcitivity's method or fields. But this isn't the current instance of MainAcitivity. You just have instantiated a new one (also without lifecycle of activity)
MainActivity mainActivity = new MainActivity();
So this why the mFragment boolen variable is false and also other items are null, like textviews.
You could refer below links
http://developer.android.com/intl/es/training/basics/fragments/communicating.html
http://www.truiton.com/2015/12/android-activity-fragment-communication/