I have a peculiar issue with Loaders. Currently I am unsure if this is a bug in my code or I misunderstand loaders.
The app
The issue arises with conversations (imagine something similar to Whatsapp).
The loaders I use are implemented based on the AsyncTaskLoader example. I am using the support library.
In OnCreate, I start a loader to retrieve cached messages.
When the CachedMessageLoader finishes, it starts a RefreshLoader to retrieve (online) the newest messages.
Each loader type as a distinct ID (say, offline:1 online:2)
This works very well, with the following exception.
Problem
When I open another fragment (and add the transaction to the backstack) and then use the Back-Key to go back to the conversationFragment, onLoadFinished is called again with both results from before.
This call happens before the fragment has had any chance to start a loader again...
This delivering of "old" results that I obtained before results in duplicated messages.
Question
Why are those results delivered again?
Do I use these loaders wrong?
Can I "invalidate" the results to ensure that I only get them delivered once or do I have to eliminate duplicates myself?
Stack trace of call
MyFragment.onLoadFinished(Loader, Result) line: 369
MyFragment.onLoadFinished(Loader, Object) line: 1
LoaderManagerImpl$LoaderInfo.callOnLoadFinished(Loader, Object) line: 427
LoaderManagerImpl$LoaderInfo.reportStart() line: 307
LoaderManagerImpl.doReportStart() line: 768
MyFragment(Fragment).performStart() line: 1511
FragmentManagerImpl.moveToState(Fragment, int, int, int, boolean) line: 957
FragmentManagerImpl.moveToState(int, int, int, boolean) line: 1104
BackStackRecord.popFromBackStack(boolean) line: 764
...
Update 1
The loaders mentioned here are initiated by the conversation fragment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
Bundle args = getArguments();
m_profileId = args.getString(ArgumentConstants.ARG_USERID);
m_adapter = new MessageAdapter(this);
if (savedInstanceState != null) {
restoreInstanceState(savedInstanceState);
}
if (m_adapter.isEmpty()) {
Bundle bundle = new Bundle();
bundle.putString(ArgumentConstants.ARG_USERID, m_profileId);
getLoaderManager().restartLoader(R.id.loader_message_initial, bundle, this);
} else {
// Omitted: Some arguments passed in Bundle
Bundle b = new Bundle().
getLoaderManager().restartLoader(R.id.loader_message_refresh, b, this);
}
}
#Override
public void onResume() {
super.onResume();
// Omitted: setting up UI state / initiating other loaders that work fine
}
#Override
public AbstractMessageLoader onCreateLoader(final int type, final Bundle bundle) {
final SherlockFragmentActivity context = getSherlockActivity();
context.setProgressBarIndeterminateVisibility(true);
switch (type) {
case R.id.loader_message_empty:
return new EmptyOnlineLoader(context, bundle);
case R.id.loader_message_initial:
return new InitialDBMessageLoader(context, bundle);
case R.id.loader_message_moreoldDB:
return new OlderMessageDBLoader(context, bundle);
case R.id.loader_message_moreoldOnline:
return new OlderMessageOnlineLoader(context, bundle);
case R.id.loader_message_send:
sendPreActions();
return new SendMessageLoader(context, bundle);
case R.id.loader_message_refresh:
return new RefreshMessageLoader(context, bundle);
default:
throw new UnsupportedOperationException("Unknown loader");
}
}
#Override
public void onLoadFinished(Loader<Holder<MessageResult>> loader, Holder<MessageResult> holder) {
if (getSherlockActivity() != null) {
getSherlockActivity().setProgressBarIndeterminateVisibility(false);
}
// Omitted: Error handling of result (can contain exception)
List<PrivateMessage> unreadMessages = res.getUnreadMessages();
switch (type) {
case R.id.loader_message_moreoldDB: {
// Omitted error handling (no data)
if (unreadMessages.isEmpty()) {
m_hasNoMoreCached = true;
// Launch an online loader
Bundle b = new Bundle();
// Arguments omitted
getLoaderManager().restartLoader(R.id.loader_message_moreoldOnline, b, ConversationFragment.this);
}
// Omitted: Inserting results into adapter
}
case R.id.loader_message_empty: { // Online load when nothing in DB
// Omitted: error/result handling handling
break;
}
case R.id.loader_message_initial: { // Latest from DB, when opening
// Omitted: Error/result handling
// If we found nothing, request online
if (unreadMessages.isEmpty()) {
Bundle b = new Bundle();
// Omitted: arguments
getLoaderManager().restartLoader(R.id.loader_message_empty, b, this);
} else {
// Just get new stuff
Bundle b = new Bundle();
// Omitted: Arguments
getLoaderManager().restartLoader(R.id.loader_message_refresh, b, this);
}
break;
}
// Omitted: Loaders that do not start other loaders, but only add returned data to the adapter
default:
throw new IllegalArgumentException("Unknown loader type " + type);
}
// Omitted: Refreshing UI elements
}
#Override
public void onLoaderReset(Loader<Holder<MessageResult>> arg0) { }
Update 2
My MainActivity (which ultimatively hosts all fragments) subclasses SherlockFragmentActivity and basically launches fragments like this:
Fragment f = new ConversationFragment(); // Setup omitted
f.setRetainInstance(false);
// Omitted: Code related to navigation drawer
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.fragment_container_frame, f).commit();
The conversation fragment starts the "display profile" fragment like this:
DisplayProfileFragment f = new DisplayProfileFragment();
// Arguments omitted
FragmentManager manager = getSherlockActivity().getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.fragment_container_frame, f).addToBackStack(null).commit();
There are other similar questions such as Android: LoaderCallbacks.OnLoadFinished called twice However the behavior of the loader manager hooks are what they are. You can either destroy the loader after getting the first set of results
public abstract void destroyLoader (int id)
or you can handle the onLoaderReset and tie your UI data more closely to the loader data
public abstract void onLoaderReset (Loader<D> loader)
Called when a previously created loader is being reset, and thus
making its data unavailable. The application should at this point
remove any references it has to the Loader's data.
Personally, I would use a ContentProvider and a CursorLoader for this (each row of data would need to have a unique _ID but for messages that should not be a problem).
use following this in onResume()
#Override
public void onResume() {
super.onResume();
getLoaderManager().initLoader(0, null, this);
}
i solved my problem from this
this is same type Q
Related
My MainActivity calls a fragment,
#Override
public void onNavigationDrawerItemSelected(int position) {
// update the main content by replacing fragments
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, PlaceholderFragment.newInstance(position + 1),
"placeHolderFragmentTag")
.commit();
}
placeholderFragment that fills a RecyclerView with an adapter,
public static PlaceholderFragment newInstance(int sectionNumber) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dbHelper = new DatabaseHelper(getActivity().getApplicationContext());
dbHelper.getWritableDatabase();
dbHelper.openDataBase();
adapterUpcomingGames = new AdapterUpcomingGames(retrieveGames(dbHelper.getUpcomingGames()));
}
adapterUpcomingGames. The adapter gets data from a database that the user has entered, being a list of upcoming sporting events.
#Override // still placeholderFragment
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext()));
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
switch(getArguments().getInt(ARG_SECTION_NUMBER))
{
case 1: // This will be upcoming games
mRecyclerView.setAdapter(adapterUpcomingGames);
break;
case 2: // This will be past games
AdapterPastGames adapterPastGames;
adapterPastGames = new AdapterPastGames(retrieveGames(dbHelper.getPastGames()));
mRecyclerView.setAdapter(adapterPastGames);
break;
default: // Default is, well, anything that will work.
adapterUpcomingGames1 = new AdapterUpcomingGames(retrieveGames(dbHelper.getGames()));
mRecyclerView.setAdapter(adapterUpcomingGames1);
}
newGame.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View arg0){
Intent createGame = new Intent(getActivity(),
ActivityCreateGame.class);
startActivity(createGame);
adapterUpcomingGames.notifyDataSetChanged();
}
});
setRetainInstance(true);
}
With this view, there is an button to add games i.e. modify the database. I am achieving this by calling a new activity, activityCreateGame, that hosts a fragment,
#Override // activityCreateGame
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_game);
fragmentCreateGame = new FragmentCreateGame();
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.attach(fragmentCreateGame)
.replace(R.id.newGameContainer, fragmentCreateGame)
.commit();
restoreActionBar();
}
fragmentCreateGame, that allows the user to enter in the desired information, and then append that to the database. The user saves this information using an option on the navbar within that activity.
#Override // activityCreateGame
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case android.R.id.home:
finish();
break;
case R.id.action_save:
// check if an error has occured
if (fragmentCreateGame.CreateNewGame())
{
Toast toast = Toast.makeText(this, R.string.toast_success_game_create,
Toast.LENGTH_SHORT);
toast.show();
finish();
}
else{
Toast toast = Toast.makeText(this, R.string.toast_failed_game_create,
Toast.LENGTH_SHORT);
toast.show();
}
break;
}
return super.onOptionsItemSelected(item);
}
That action finishes the activityCreateGame. Now, how do I go about notify the adapter, back in placeholderFragment that the database has been updated and thus update the RecyclerView to update itself?
From looking at your code, I can think of three solutions for you. In order of (my opinion) quality of the solution from least to best.
Move your fragment fragmentCreateGame to be hosted by you first activity
Start Activity For Result
Use Otto Event Bus. I would recommend otto. Especially if you may have multiple objects listening to an event.
First add Otto to your project. In your build.gradle, add this to your dependancies.
dependencies {
....
compile 'com.squareup:otto:1.3.6'
....
}
In your activity that needs notified, add this at the beginning of your onCreate:
Bus bus = new Bus();
bus.register(this);
Then, create an "event" class. It is just an object you pass to notify subscribers. You can add info to these too, to pass more info.
public class SomeCoolEvent{
private String yourWisdom;
public SomeCoolEvent(String yourWisdom){
this.yourWisdom = yourWisdom;
}
public String getWisdom(){
return yourWisdom;
}
}
Then, in your class that is doing something to notify other classes, do this:
Bus bus = new Bus();
bus.post(new SomeCoolEvent(yourWisdomStringFromThisClass));
Now, back to your first activity, add a method that is annotated with #Subscribe
#Subscribe
public void onAnyMethodNameYouWantCauseItDoesntMatter(SomeCoolEvent event){
//this will be called when ANY class calls bus.post(SomeCoolEvent)
String boomYouAreNotified = event.getWisdom();
}
On your #Subscribe method, few things to note.
* The method name does not matter, so give it something that makes sense to you. I like to prefix it with "on" because its like a callback.
* The method must be public and return void.
* You are not explicitly calling this methods, so some IDE's may appear that it is unused. Android studio will let you fix that with "ALT + ENTER" then select "_ignore warnings for methods annotated with Subscribe".
* You should call bus.unregister(this) on destroy.
* You can have multiple classes listening to one post.
Now things can get more complicated if you start running on different threads. If you do, there are solutions to this at the bottom of the Otto page I provided.
I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.
From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.
I am having hard time doing a task which is supposed to be easy. I am not sure whether it is because of the Android platform's bad design or if I am missing something. I simply would like to refresh a fragment's view on resume. Here are the details:
I have two activities, a SplashActivity which retrieves data from a server (using a AsyncTask) and passes the data to my MainActivity. In my MainActivity, I get this data and pass it to a fragment named SummaryFragment. I have a few fragments and a navigation drawer (in my MainActivity). The first visible fragment is the SummaryFragment which reads the data passed to it from the MainActivity and consequently draws a graph.
When the app starts, there might be no active Internet connection, in that case, in my summary fragment I ask the user to enable WiFi. What I want to do is to refresh this SummaryFragment's view after the user comes back to the app after enabling WiFi. What I do right now is that in the onResume() of my MainActivity, I check if the SummaryFragment in visible, and if so, I start the SplashActivity again (and close the current activity). SplashActivity must fetches the data (like it does when the app starts) and start the MainActivity (fed with the fetched data) which loads the summary fragment and shows the graph.
The problem is that it takes a considerably long time (30-40 seconds) after the app is resumed to go from the SplashActivity to the MainActivity and show the graph (meanwhile the users sees the splash screen), whereas when the app starts it takes 1-2 seconds to do so. Before using the current solution (redirecting user to the SplashActivity), in MainActivity.onResume() I tried using the same AsyncTask class that I am using in the SplashScreen to fetch the data and show the summary fragment afterwards, but the result is the same, there is a significant delay.
The following code is my MainActivity's onResume():
Fragment fragment = getVisibleFragment();
if (fragment instanceof SummaryFragment) {
Intent intentSplashActvity = new Intent(this, SplashActivity.class);
Log.d(TAG, "about to start the splash activity");
startActivity(intentSplashActvity);
// close current activity
finish();
super.onResume();
return;
}
super.onResume();
The SplashActivity:
public class SplashActivity extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash_screen);
new PrefetchData(this).execute();
}
}
The PrefetchData:
public class PrefetchData extends AsyncTask<Void, Void, Void> {
String serverResponseJson = null;
private String data1 = null,
data2 = null,
data3 = null;
private SplashActivity mSplashActivity = null;
private MainActivity mMainActivity;
public PrefetchData(Activity sourceActivity){
if (sourceActivity.getClass() == SplashActivity.class) {
mSplashActivity = (SplashActivity) sourceActivity;
}
}
#Override
protected Void doInBackground(Void... arg0) {
JsonParser jsonParser = new JsonParser();
try {
if (CaratApplication.isInternetAvailable()) {
serverResponseJson = jsonParser
.getJSONFromUrl("http://aURL");
}
} catch (Exception e) {
Log.d("SplashActivity", e.getStackTrace().toString());
}
if (serverResponseJson != null && serverResponseJson != "") {
try {
JSONArray jsonArray = new JSONObject(serverResponseJson).getJSONArray("arrayName");
// Using Java reflections to set fields by passing their name to a method
try {
setFieldsFromJson(jsonArray, 0, "data1");
setFieldsFromJson(jsonArray, 1, "data2");
setFieldsFromJson(jsonArray, 2, "data3");
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}
} catch (JSONException e) {
}
}
return null;
}
#Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (mSplashActivity != null) {
Intent intentMainActvity = new Intent(mSplashActivity, MainActivity.class);
if (gotDataSuccessfully()) {
intentMainActvity.putExtra("data1", data1);
intentMainActvity.putExtra("data2", data2);
intentMainActvity.putExtra("data3", data3);
} else {
intentMainActvity.putExtra("data1", Constants.DATA_NOT_AVAIABLE);
intentMainActvity.putExtra("data2", Constants.DATA_NOT_AVAIABLE);
intentMainActvity.putExtra("data3", Constants.DATA_NOT_AVAIABLE);
}
mSplashActivity.startActivity(intentMainActvity);
mSplashActivity.finish();
}
}
}
In MainActivity, upon selection of the "summary" entry in the navigation drawer, I initialize the SummaryFragment, and then replace it using a fragment transaction (replaceFragment(mSummaryFragment, mSummaryFragmentLabel)). Here is the method I use to initialize the summary fragment:
private void initSummaryFragment() {
if (mData1 == 0 && mData2 == 0 && mData3 == 0) {
Intent intent = getIntent();
String data1 = intent.getStringExtra("data1");
String data2 = intent.getStringExtra("data2");
String data3 = intent.getStringExtra("data3");
boolean isDataAvaiable = !data1.equals(Constants.DATA_NOT_AVAIABLE)
&& !data2.equals(Constants.DATA_NOT_AVAIABLE) && !data3.equals(Constants.DATA_NOT_AVAIABLE);
if (isDataAvaiable) {
mData1 = Integer.parseInt(data1);
mData2 = Integer.parseInt(data2);
mData3 = Integer.parseInt(data3);
}
}
mSummaryFragment = new SummaryFragment();
mArgs = new Bundle();
mArgs.putInt("data1", mData1);
mArgs.putInt("data2", mData2);
mArgs.putInt("data3", mData3);
mSummaryFragment.setArguments(mArgs);
mSummaryFragmentLabel = getString(R.string.summary);
}
The SummaryFragment can now retrieve the data it needs from the bundle passed to it.
The problem was with the (rather) undocumented behavior of Android's AsyncTask. An AsyncTask does not run in a separate thread, it runs in a thread that is shared with other AsyncTasks, so if you have other AsyncTasks (or even a plain thread), if they start running before your current AsyncTask, this AsyncTask waits for them to complete their action. I explicitly specified that I would like my AsyncTask to run in a separate thread, and this reduced the delay from about 20-25 seconds to 3-4 seconds for fetching a JSON object (I have other network access calls in progress in parallel). I run the following code as part of my preInitFragments() method in onCreate() method of my main activity.
// The following PrefetchData class is of type AsyncTask<void, void, void>. I moved this class from a stand-alone class to an inner class inside my main activity for easier refreshing of my fragment.
PrefetchData prefetchData = new PrefetchData();
// run this asyncTask in a new thread [from the thread pool] (run in parallel to other asyncTasks)
// (do not wait for them to finish, it takes a long time)
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB)
prefetchData.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
else
prefetchData.execute();
After this I call my method initSummaryFragment(). Please note that in my AsyncTask, in doInBackGround(), I set the two fields (mField1 and mField2) you see below (I used to pass them as a bundle to the summary screen, but that way would cause a problem when you were just detaching and attaching the fragment (to refresh its view) without passing a bundle to your fragment). In the onPostExecute() of my asyncTask, I invoke my method refreshSummaryFragment():
private boolean isStatsDataAvailable() {
return mWellbehaved != 0 && mHogs != 0 && mBugs != 0;
}
private void initSummaryFragment() {
if (! isStatsDataAvailable()) {
mSummaryFragment = new SummaryFragment();
}
mSummaryFragmentLabel = "Summary";
}
For refreshing my summary fragment, I simply detach, and attach my summary fragment, and then commit the pending fragment transaction:
public void refreshSummaryFragment() {
if (isStatsDataAvailable()) { // blank summary fragment already attached. detach and attach to refresh
FragmentManager manager = getSupportFragmentManager();
// init the fragment (for later use when user selects an item from the navigation drawer)
mSummaryFragment = getSupportFragmentManager().findFragmentByTag("Summary");
FragmentTransaction fragTransaction = manager.beginTransaction();
// refresh the summary fragment:
fragTransaction.detach(mSummaryFragment);
fragTransaction.attach(mSummaryFragment);
fragTransaction.commit();
} else {
Log.e(TAG, "refreshSummaryFragment(): stats data not avaiable!");
}
}
This might have already answered but I am still troubling with a function like this. Let's say I have activity A and activity B. B holds a viewpager with several fragments in it. I would like to call a function in the fragment held by activity B from activity A.
I used callbacks many times to communicate between activites and fragments but every single time it was only the fragment and its holder activity. I do not want to make a static method (the callback listener cannot be static anyway) so it causes a headache for me. The simple static solution to make a static method in the fragment and have it called from the other actually works very well, but I am not sure if it was a good idea as I need to change several things static.
So communicating between Activity B and its fragments is ok, but I cannot call this method in Activity A.
Activity B:
public class ActivityB extends FragmentActivity implements Fragment1.OnWhateverListener
{
...
#Override
public void onWhateverSelected(int position) {
//stuff, here I can call any function in Fragment 1
}
}
The following code snippet is a wrong solution (doesnt even work) but makes a better picture what I would like to do.
Activity A:
ActivityB ab = new ActivityB ();
ab.onWhateverSelected(number);
So how can I do this?
Thank you!
EDIT
Activity A: the method I call
Bundle args = new Bundle();
args.putString("ID", id); // the data to send
Intent frag_args = new Intent(Intent.ACTION_VIEW);
frag_args.setClass(this, MainActivity.class);
frag_args.putExtra("args", args);
startActivity(frag_args);
Activity B:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
...
processIntent(getIntent()); //last line of onCreate, always gets called here
}
#Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
processIntent(intent); // this never gets called here only in OnCreate
}
private void processIntent(Intent intent) {
Bundle args = intent.getBundleExtra("args");
if (args != null) { // check if ActivityB is started to pass data to fragments
String id = args.getString("ID");
Log.i("ID_FROM", "id: " + id); //works well
if (id != null) {
List<Fragment> fragments = new ArrayList<Fragment>();
fragments = getSupportFragmentManager().getFragments();
//NULLPOINTER for the following line
FragmentMainDiscover fr = (FragmentMainDiscover) fragments.get(0);
fr.RefreshHoverView(id);
}
}
}
You are right to stay away from statics. Way too risky, for visual objects that may or may not be on screen.
I would recommend going through activity B, since it is the parent of your target fragment. Create an Intent that starts activity B, and include an intent extra that tells activity B what it should do to the target fragment. Then activity B can make sure that the fragment is showing, and pass the information on to it.
One other idea to pass the info to the fragment is to use setArguments, rather than direct calls. This is a nice approach because Android will restore the arguments automatically if the activity and its fragments are removed from memory.
Does this make sense? Do you want the code?
EDIT
To use arguments, you still need to have activity A go through activity B. This is because activity A doesn't know if activity B, and all its fragments, is running unless it sends it an Intent. But you can include data targeted for the fragments, by putting them inside the intent. Like this:
public class ActivityA extends Activity {
public static final String KEY_FRAG = "frag"; // tells activity which fragment gets the args
public static final String KEY_ARGS = "args";
public static final String KEY_MY_PROPERTY = "myProperty";
public void foo() {
Bundle args = new Bundle();
args.putString(KEY_FRAG, "frag1Tag"); // which fragment gets the data
args.putCharSequence(KEY_MY_PROPERTY, "someValue"); // the data to send
// Send data via an Intent, to make sure ActivityB is running
Intent frag_args = new Intent(Intent.ACTION_VIEW);
frag_args.setClass(this, ActivityB.class);
frag_args.putExtra(KEY_ARGS, args);
startActivity(frag_args);
}
}
public class ActivityB extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//TODO configure activity including fragments
processIntent(getIntent()); // this call is in case ActivityB was not yet running
}
#Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
processIntent(intent); // this call is in case ActivityB was already running
}
private void processIntent(Intent intent) {
Bundle args = intent.getBundleExtra(ActivityA.KEY_ARGS);
if (args != null) { // check if ActivityB is started to pass data to fragments
String fragTag = args.getString(ActivityA.KEY_FRAG);
if (fragTag != null) {
Fragment frag = getSupportFragmentManager().findFragmentByTag(fragTag);
frag.setArguments(args);
//TODO either show the fragment, or call a method on it to let it know it has new arguments
}
}
}
}
public class Fragment1 extends Fragment {
public static final String TAG = "frag1Tag";
#Override
public void onResume() {
super.onResume();
Bundle args = getArguments();
String value = args.getString(ActivityA.KEY_MY_PROPERTY);
...
}
}
Is it always necessary to initLoader from onCreate in a Fragment? What if critical arguments for the loader are dependent on the results of another loader?
i.e. You have 2 loaders: LoaderA, and LoaderB. LoaderB needs the result from LoaderA to run. Both LoaderA and LoaderB are initialized in onCreate of a fragment, but LoaderB is given no arguments so that it intentionally fails.
Once LoaderA finishes, LoaderB is restarted with new arguments so that it can perform its desired request.
Loader initialization in fragment:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_A, new Bundle(), this);
getLoaderManager().initLoader(LOADER_B, null, mLoaderBCallback);
}
Call backs for LOADER_A in fragment:
#Override
public Loader<MyResultObject> onCreateLoader(int id, Bundle args) {
return new LoaderA(getActivity(), args);
}
#Override
public void onLoadFinished(Loader<MyResultObject> loader, final MyResultObject result) {
if (result != null) {
Bundle args = new Bundle();
args.putInt("id", result.getId());
getLoaderManager().restartLoader(LOADER_B, args, mLoaderBCallback);
}
}
Definition of mLoaderBCallback in fragment:
private LoaderBCallback mLoaderBCallback = new LoaderBCallback();
(The implementation of LoaderBCallback is not important, its just the standard LoaderCallbacks interface that creates an instance of LoaderB and handles when the loader is finished.)
LoaderB class (please excuse any potential compiler errors with this class definition, its just an example):
public class LoaderB<List<AnotherResultObject>> extends AsyncTaskLoader<List<AnotherResultObject>> {
private Bundle mArgs;
public LoaderB(Context context, Bundle args) {
super(context);
mArgs = args;
}
#Override
public List<AnotherResultObject> loadInBackground() {
if (mArgs == null) {
// bail out, no arguments.
return null;
}
// do network request with mArgs here
return MyStaticClass.performAwesomeNetworkRequest(mArgs);
}
// boiler plate AsyncTaskLoader stuff here
......
}
Is there a better way? Can we do without the initLoader for LoaderB?
Edit: I am under the impression that loaders ALWAYS have to be initialized in onCreate, so that they can handle configuration changes. This may be true ONLY for loaders in Activities . Do loaders created in Fragments get managed no matter where they are initialized?
You can init a loader anywhere in your code.
In your case you should replace your restartLoader in onLoadFinished with initLoader. Just remove the initLoader from your onActivityCreated for LOADER_B
Also, you should check the ID of the loader in onLoadFinished so you know which loader finished.
edit: you are using a separate listener for the LOADER_B callback so my ID checking point kinda gets defeated there.. but at any rate.. you can combine them into one if you want
#Override
public void onLoadFinished(Loader<MyResultObject> loader, final MyResultObject result) {
switch (loader.getId())
{
case LOADER_A:
if (result != null) {
Bundle args = new Bundle();
args.putInt("id", result.getId());
// i put "this" as the callback listener. you can use your custom one here if you want
getLoaderManager().initLoader(LOADER_B, args, this);
}
break;
case LOADER_B:
//do whatever
break;
}