I have an object Foo which can be configured using a fragment (FooFragment). The Foo class contains a static reference to FooFragment and the method public Fragment getConfigurationFragment(); this method assigns the current object to the FooFragment and returns it.
public class Foo{
private static FooFragment fooFragment = new FooFragment();
public Fragment getConfigurationFragment(){
fooFragment.setObject(this);
return fooFragment;
}
//various getters and setters
}
FooFragment is roughly as follows:
public class FooFragment extends Fragment{
private Foo f;
private EditText field1, field2, etc;
public void setObject(Foo f){
this.f = f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//inflate view
setupFieldListeners();
//return view object
}
#Override
private void onStart(){
super.onStart();
setupFields();
}
private void setupFields(){
field1.setText(f.getField1());
field2.setText(f.getField2());
//etc
}
private void setupListeners(){
field1.addTextChangedListener(new TextWatcher(){
#Override
public void afterTextChanged(Editable e){
f.setField1(e.getText().toString());
});
//Other empty necessary methods
field2.addTextChangedListener(new TextWatcher(){
//...
});
}
}
When I use the Fragment for the first time on object foo1, everything works great. The proper information is displayed and everything works.
When I use the Fragment for the second time on another object foo2, opening the Fragment causes all of the properties from foo1 (field1, field2, etc) to be written to the foo2. I believe that this is because when the Fragment is added again, the restoreViewState() method runs, which changes the values of the EditText fields to the values for foo1, causing the afterTextChanged() method to fire, and writing the values of foo1 into foo2.
I have tried the following to fix the problem:
-Creating a new FooFragment object whenever getConfigurationFragment() is called. This works, but I believe it's not optimal, as I understand that it's good to avoid needlessly creating objects on a mobile platform.
-Overriding onSaveInstanceState() in the Fragment and sending a null Bundle. This doesn't work as it doesn't look to be called when the Fragment is closed. It doesn't work.
-Placing the setupFields() call in OnStart(), OnResume(), OnCreateView(). None of these work as when the restoreViewState() runs, it clobbers foo2 object.
-Setting the Bundle to null in onCreate() and onCreateView(). Doesn't work.
What can I do to get the foo2 information to load into the reused Fragment?
I do not have the best understanding of the FragmentTransaction mechanism. Is it possible to somehow tell the FragmentManager to restore the View state?
Is the idea of reusing a configuration Fragment as I'm trying to do fundamentally flawed and to be avoided? If so why?
Is there some other magic that will cause this to work?
Thanks in advance for your time.
I had a similar struggle with fragment view state restoration. One way to prevent it was to clear package-private mSavedViewState field before restoreViewState() is called:
package androidx.fragment.app;
import android.os.Bundle;
import android.view.View;
public class StatelessFragment extends Fragment {
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mSavedViewState = null;
}
}
Related
I have an application that shows News articles in a listview inside a fragment.
When the fragment is first created, I start a thread that will fetch the list of articles (Stories) through an API call
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mContext = getActivity();
new GetStoriesThread(mContext,this).start();
Both the Fragment and the Thread implement the same Interface for passing the data from the thread to the Fragment
public interface GetStoriesThreadInterface {
public void onGetStoriesThreadResult(final ArrayList<Story> result);
}
After the Thread is done processing, it will call the interface method and pass the data back to the calling Fragment.
The problem
Now when I get the result in the fragment, through this code:
#Override
public void onGetStoriesThreadResult(final ArrayList<Story> result)
{
if(result!=null)
{
mStoriesList.clear(); //mStoriesList is the list that i supply to the adapter of the ListView
mStoriesList.addAll(result);
adapter.notifyDataSetChanged(); //Exception here
}
}
I get the following exception:
04-28 18:03:58.432: E/ViewRootImpl(21513): com.says.news.Stories : Only the original thread that created a view hierarchy can touch its views.
I know that using getActivity().runOnUiThread(new Runnable... solves the issue, but I dont understand why. And sometimes getActivity() returns null and that`s a whole different issue.
Thanks in advance !!
Are you calling onGetStoriesThreadResult() from within the working thread? You shouldn't. Consider using AsyncTask<> instead of bare Thread , override the onPostExecute() method and call your events from there.
I am writing an Android app (4.4) that uses Fragments. Each Fragment is in it's own .java file (and its own class), and each one has it's own .XML (layout) file. In the main FragmentActivity, my "getItem" routine reads the "position" argument, and creates instances of these classes as needed.
When the app starts, when Fragment 0 (zero) starts up, it runs some code in the "onCreateView." Based on what happens in that code, I need to change the UI of the Fragment 1 (buttons appear & disappear based on that logic).
However, the code RUNS with no errors, but the UI changes do not take effect. I'm thinking that perhaps I need to run my "startup" code somewhere else with a wider scope. I could be wrong.
Can anyone suggest a way for me to be able to control the UI of various layouts at startup?
Thanks!
If you can post some of your code, would be easier.
anyway if I got your problem, you need to change the UI of the fragment 1 from the fragment 0.
What you need is what is explained in the document Communicating with Other Fragments
you should do something like:
public class MyActivity extends FragmentActivity implements MyInterface{
#Override
public void changeUI(String sometext) {
Fragment1 fragment1 = (Fragment1) getFragmentManager().findFragmentByTag("tagCommittedFragment1");
fragment1.applyChange(sometext);
}
}
public class Fragment0 extends Fragment{
MyInterface mMyInterface;
public interface MyInterface {
public void changeUI(String sometext);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mMyInterface = (MyInterface) activity;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mMyInterface.changeUI("newtext");
}
}
public class Fragment1 extends Fragment{
public void applyChange(String sometext){
// do your work
}
}
You must make an interface to communicate between Fragments which would be implemented by your MainActivity:
public interface Communicator {
public void respond(String data);
}
Now you need to use this Interface to send data from FragmentA:
Communicator comm = getAcitivity(); //your activity must implement this interface
comm.respond(data);
As your MainActivity implements the above interface, it will also implement the respond() method which can be used to pass data to FragmentB:
public void respond(String data){
FragmentManager manager = getSupportFragmentManager();
FragmentB fragB = manager.findFragmentById(R.id.fragment_b);
fragB.changeData(data);
}
Now all you need to do is collect this data and make changes in FragmentB using a changeData() method:
public void changeData(String data){
textView.setText(data);
}
NOTE: As FragmentB has no use of the Interface, it should not be visible to it, therefore you can also create the interface inside FragmentA instead.
Hope you're fine & Congrats for your Work !
Scenario :
I have 3 fragments files, Pagegauchefragment, PageMilieufragment & PageDroiteFragment managed by 2 files FragmentsSliderActivity and MyPagerAdapter.
The first one and the second one are datas fields with EditText, filled by the user. Last one is a ListView where the datas user files are saved. From the listview (last fragment) the user clicks the file with 3 possibilities : 1. Import the file / 2. delete the file / 3. Cancel (I use the action bar process to save the datas). Now the issue : I can't pass my datas to refresh (with setText) to the other fragments.
Log : Tag : IIputConnectionWrapper / Text : getSelectedText on inactive InputConnection, setComposingText on inactive InputConnection.
View W =inflater.inflate(R.layout.page_droite_layout,container, false);
this.mListView = (ListView) W.findViewById(R.id.ListView01);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this.getActivity(), android.R.layout.simple_list_item_1, tabRevues);
this.mListView.setAdapter(adapter);
this.mListView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
View WW =inflater.inflate(R.layout.page_gauche_layout,container, false);
file_name = (EditText) WW.findViewById(R.id.label_LiquidBase);
//int item = position;
item_name = ((TextView)view).getText().toString();
AlertDialog.Builder ad = new AlertDialog.Builder(getActivity());
ad.setTitle("Selection Recettes : " + item_name);
ad.setMessage("Que souhaitez-vous faire ?");
ad.setPositiveButton("Importer",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
try {
Here I pass a text to setText (for example) :
file_name.setText("blahblah");
I think I'm in a Void method but I can't find out how to pass the variables... ANY help whatsoever will be greatly appreciated!
Christopher
Passing data directly between Fragments is never recommended. Instead, pass your data from Fragment X up to your FragmentsSliderActivity which will pass this data on to your Fragment Y. You do that by way of an interface defined in your fragment class and instantiate a callback that is defined in onAttach().
More information on how to do this here
Communication With other Fragments
Quick example, consider Fragment A and Fragment B. Fragment A is a list fragment and whenever an item is selected it will change what is displayed in Fragment B. Simple enough, right?
At first, define Fragment A as such.
public class FragmentA extends ListFragment{
//onCreateView blah blah blah
}
And here's Fragment B
public class FragmentB extends Fragment{
//onCreateView blah blah blah
}
And here's my FragmentActivity that will govern them both
public class MainActivity extends FragmentActivity{
//onCreate
//set up your fragments
}
Presumably you have something like this already, now here's how you would change FragmentA(the list fragment that we need to get some data from).
public class FragmentA extends ListFragment implements onListItemSelectedListener, onItemClickListener{
OnListItemSelectedListener mListener;
//onCreateView blah blah blah
// Container Activity must implement this interface
public interface OnListItemSelectedListener {
public void onListItemSelected(int position);
}
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mListener = (OnListItemSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnListItemSelectedListener");
}
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
//Here's where you would get the position of the item selected in the list, and pass that position(and whatever other data you need to) up to the activity
//by way of the interface you defined in onAttach
mListener.onListItemSelected(position);
}
The most important consideration here is that your parent Activity implements this interface, or else you will get an exception. If implemented successfully, everytime an item in your list fragment is selected, your Activity will be notified of it's position. Obviously you could alter your interface with any number or type of parameters, in this example we're just passing in our integer position. Hope this clarifies a bit man, good luck.
This is really weird all other listeners work like onClick etc.. but this listener doesnt seem to be working, heres my code:
public class HeloActivity extends Activity implements OnGenericMotionListener{
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
View root = findViewById(R.id.root );
root.setOnGenericMotionListener(this);
}
#Override
public boolean onGenericMotion(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.d( "special",v.toString() );
return false;
}
}
why is this not working?
This is an old question, but I'm currently looking at the same thing so I'll add this for future reference.
It might be because Activity implements an onGenerericMotionEvent method which receives motion events, but you're also implementing the method from View - onGenericMotion in the same class.
The Android docs say to implement one or the other.
I've not tried how you're doing it, but I know it works by using a different class that implements OnGenericMotionListener and using setOnGenericMotionListener on the root view.
You do realize that you both implements the listener and then attach it to the view? At the very least it is redundant....
Thank you.
I have 1 activity and 2 fragments. Both fragments use a custom AsyncTaskLoader to get some data from a webservice and as i'm using a Loader it should keep the data across activity and fragment re-creations. Both fragments override the onActivityCreated method and calls getLoaderManager().initLoader(0, null, this) which either creates a new or reuses an existing loader.
When the activity is first created, it adds Fragment #1 in a FrameLayout by default, loads the data, internally calls LoaderCallbacks.onLoadFinished() method and displays the result. I have a button which replaces Fragment #1 with Fragment #2 on click and Fragment #1 is pushed to the fragment-backstack. When the user hits the BACK key, it switches back to Fragment #1.
onActivityCreated gets called again on Fragment #1 and then obviously calls iniLoader() again. This time the data already exists in the loader and i expect it to automatically call the LoaderCallbacks.onLoadFinished method again, because it already has data available, as described here: http://goo.gl/EGFJk
Ensures a loader is initialized and active. If the loader doesn't already exist, one is created and (if the activity/fragment is currently started) starts the loader. Otherwise the last created loader is re-used.
In either case, the given callback is associated with the loader, and will be called as the loader state changes. If at the point of call the caller is in its started state, and the requested loader already exists and has generated its data, then callback onLoadFinished(Loader, D) will be called immediately (inside of this function), so you must be prepared for this to happen.
But the method is never called even if the loader exists and has generated data ready to deliver.
Edit #1
The problem from a users perspective:
User starts activity and sees fragment1 with some data
User clicks something which changes the first fragment to another, with different
data
User hits the BACK key
User is now looking at fragment1 again, but there's no data. (which means i need to get it from the webservice again - and i'd like to avoid that if possible)
Here is my activity:
public class FragmentTestsActivity extends Activity implements OnClickListener {
private Button btn1;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btn1 = (Button) findViewById(R.id.btn1);
btn1.setOnClickListener(this);
Fragment newFragment = new Fragment1();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.fragmentContainer, newFragment).commit();
}
#Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.btn1) {
showNewFragment();
}
}
public void showNewFragment() {
// Instantiate a new fragment.
Fragment2 newFragment = new Fragment2();
// Add the fragment to the activity, pushing this transaction
// on to the back stack.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.fragmentContainer, newFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.addToBackStack(null);
ft.commit();
}
}
My Fragment #1:
public class Fragment1 extends Fragment implements LoaderCallbacks<String> {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
LoaderManager.enableDebugLogging(true);
getLoaderManager().initLoader(0, null, this);
}
private static class TestLoader extends AsyncTaskLoader<String> {
String result;
public TestLoader(Context context) {
super(context);
}
#Override
public String loadInBackground() {
// Some long-running call to a webservice - replaced with a simple string to test with
return "FirstTimeData";
}
#Override
public void deliverResult(String data) {
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
if (takeContentChanged() || result == null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
cancelLoad();
}
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new TestLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<String> loader, String result) {
Log.d("Fragment1", "onLoadFinished: " + result);
}
#Override
public void onLoaderReset(Loader<String> loader) {
// TODO Auto-generated method stub
}
}
Anyone know a solution to this or what i'm doing wrong here? Any help is greatly appreciated.
The correct answer, for me at least, was to move the entire Loader initialisation from onViewCreated or onActivityCreated to onStart.
After that it works fine!
From my point of view the onLoadFinished will only be called the first time cause the load has already finished, it finishes just once for both fragments. What you could do is to store the result in a property in the activity and check for it in the second fragment creation.
Update: After further investigation I found my original answer to be wrong. restartLoader will also destroy the loader if it already exists.
Nevertheless I solved my own problem. I create a standard CursorLoader in onCreateLoader and swap the cursor of my CursorAdapter in onLoadFinished. I needed to call initLoader after initializing the CursorAdapter. Now the data is still there when the fragment is returned from the backstack.
Original answer: I found two possible ways to solve this issue.
Apparently when initLoader(int id, Bundle args, LoaderCallbacks<D> callback) is called for the second time in onActivityCreated(Bundle savedInstanceState) after the Fragment is returned from the backstack, the method onLoadFinished(Loader<String> loader, String result) is never called (as you described).
However, a call to restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) after or instead of initLoader will finally cause onLoadFinished to be called. To improve performance, I use a boolean argument to determine whether the Fragment is a new instance or not. Then I call restartLoader only if the Fragment is returned from the backstack.
As far as I can tell, old data persists in the loader and is not reloaded, but I'm not sure. The second possibility (especially when not using the backstack but instead creating a new Fragment instance in a transaction) is to call destroyLoader(int id) before the Fragment goes away (e.g in onPause).
I already had that issue, I can't really explain why that bug but I know that line :
getLoaderManager().initLoader(0, null, this);
don't work in my code, So you can changed it for that :
LoaderManager lm = getLoaderManager();
lm.initLoader(LOADER_ID, null, this);
The code start the methods :
onCreateLoader
you can try...