Synchronize ListFragment and SupportMapFragment selection - android

I have 2 views of essentially the same data:
List of items in a android.support.v4.app.ListFragment
Markers on a map in a com.google.android.gms.maps.SupportMapFragment
Both of the above are using loader pattern to obtain the same data (extending LoaderCallbacks, querying ContentProvider, and so on)
Both are hosted within a single activity inside a ViewPager.
What will be the best strategy to synchronize currently selected list item / marker for both of these fragments? (Think of "My Places" edit UI, or "Directions" of the Google Maps with their left-hand pane and a map in the center).
Scenarios i'm thinking of so far:
Make every fragment manually notify it's counterpart about selection change via callback interface (this will probably involve underlying activity to coordinate inter-fragment communications, as it is suggested by Android docs).
Somehow make both fragments use the same Cursor, or even ListAdapter (whatever it means for a map, because now it's populated directly from the cursor).
(Something else?)
Maybe someone has already dealt with this exact case? (I'll sure find some solution, just wanted to avoid "reinventing the wheel". Sorry for a too conceptual question.)
EDIT (Solution)
I think Maciej has answered my exact question ("best strategy", and so on..), so the answers are both 1 and 2 ;-)
Going into more details, my implementation went like this:
At first I frightened by enormous overhead of dealing with publisher/subscriber pattern in Java (involving interfaces, finding proper places for callbacks, and what's not). Fortunately, Otto bus implementation caught my eyes, which made communication between fragments a trivial thing. Not only it is possible to notify all subscribers about selection change, but also the whole Loader Patter fit nicely:
Borrow BusProvider class from Otto's sample code.
Create few message contracts to carry notification data:
public class LocationSelectedEvent {
public long id;
}
public class LocationsLoadedEvent {
public Cursor cursor;
}
Annotate "receiver" methods in fragments with #Subscribe (example below is for loader case, for selection change it's no more complex):
#Subscribe
public void onLoadFinished(LocationsLoadedEvent event) {
final CursorAdapter a = (CursorAdapter) getListAdapter();
a.swapCursor(event.cursor);
}
Make fragments "listening" to notifications:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
BusProvider.getInstance().register(this);
}
Make fragments to stop listening when they're not "alive" (specially true for fragments API, learned it the hard way):
#Override
public void onDestroy() {
super.onDestroy();
BusProvider.getInstance().unregister(this);
}
Finally, trigger notifications where desired (example below deomnstrates how to notify from LocationList activity when cursor has been loaded):
#Override
public void onResume() {
if(null == getLoaderManager().getLoader(0)) {
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
#Override
public Loader<Cursor> onCreateLoader(int paramInt, Bundle paramBundle) {
return new CursorLoader(LocationsList.this, Locations.CONTENT_URI, null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> paramLoader, Cursor cursor) {
BusProvider.getInstance().post(new LocationsLoadedEvent(cursor));
}
#Override
public void onLoaderReset(Loader<Cursor> paramLoader) {
BusProvider.getInstance().post(new LocationsLoadedEvent(null));
}
});
}
super.onResume();
}
Bonus: notifications flow visualization

For click-coordination your point 1 is what I would do, using Activity and interfaces of course.
I just have hard time understanding why would you want to load the same data from ContentProvider twice. Why not load it once in a shared object? Some object inside Application, injected singleton or even another Fragment, which notifies Activity of data load complete and it pushes data to your two Fragments?

Related

Pass MatrixCursor of data from Activity to Fragment

The core of this question is how to send a MatrixCursor of data from an activity to a fragment.
I am doing my search functionality in my activity and am returning a fragment which contains a list that will be filled with data from the query response that is a Matrix Cursor.
Bundle and parcelable thus far are not working out for me. Any tips or guidance?
I see three potential options.
Try Gson. You may be able to convert the instance to a String to pass it and then reinstantiate it from the String data. However, this doesn't work for everything.
Create a new method in your Fragment. You're not meant to pass custom arguments in the constructor, but you could pass it later:
private MatrixCursor cursor;
public void setCursor(MatrixCursor cursor) {
this.cursor = cursor;
}
Since it's the same instance, changes made to the one in your Fragment will be reflected in your Activity. However, this will cause issues if you rotate your device or cause another configuration change. To fix that, add the following to your <activity> attribute in your Manifest:
android:configChanges="orientation|keyboardHidden"
Fragments retain a reference to their parent Activity. You could add helper methods to your Activity that essentially proxy the ones you need from your MatrixCursor instance:
public void addRow(Object[] columnValues) {
cursor.addrow(columnValues);
}
//etc
Then, in your Fragment, you can do:
((MyActivityClass) getActivity()).addRow(columnValues);
Option 3 would probably be the best option, since it doesn't rely on something that might not work or what's basically a hack.
Make a interface Searchable
public interface Searchable {
MatrixCursor getSearchResult()
}
make sure you implement this interface to your activity.
public MainActivity extends AppCompatActivity implements Searchable {
private MatrixCursor mSearchResultMatrixCursor;
...
#Override public MatrixCursor getSearchResult() {
return mSearchResultMatrixCursor;
}
}
In you Fragment's onCreate or wherever you want to use MatrixCursor,
you can call,
if(getActivity != null && getActivity instanceOf Searchable) {
MatrixCursor matrixCursor = ((Searchable)getActivity).getSearchResult()
}
This will persist as long as activity is not recreated.

Refreshing fragments in FragmentActivity after sync service runs

does anybody have any elegant solution for refreshing the Views in Fragments in a FragmentActivity's ViewPager after a sync Service from a SyncAdapter runs?
I've tried calling notifyDataSetChanged() and notifyDataSetInvalidated() on my adapter, as well as refreshDrawableState() on my views (GridViews), but to no avail. Perhaps I've been calling them from the wrong places -- I've tried doing it at setUserVisibleHint where isVisible=true, hoping to trigger it whenever the fragment comes into view, but it doesn't work.
I've also been using ASync calls to the SQLite database for my data needs, rather than a Content Provider, which I think would have made this a bit easier. I can think of a couple of ways to do it without a Content Provider, but neither are very nice.
Any ideas? I can provide code if wished. Thanks.
I'll assume that you're using an AsyncTask for loading the cursor just for the sake of the explanation, but it would work the same if you're using a Loader, an ThreadPool or whatever.
From the service, as soon as new data was changed I would send a LocalBroadcast. The activity might be there or not, so a broadcast is a good way to let it know there's new data. So from the service you would do:
// that's an example, let's say your SyncAdapter updated the album with this ID
// but you could create a simply "mybroadcast", up to you.
Intent i = new Intent("albumId_" + albumId);
LocalBroadcastManager.getInstance(this).sendBroadcast(i);
and then from the activity/fragment that have the Cursor, you'll be listening to this broadcast like this:
public void onResume(){
// the filter matches the broadcast
IntentFilter filter = new IntentFilter("albumId_" + albumId);
LocalBroadcastManager.getInstance(this).registerReceiver(myReceiver, filter);
}
public void onPause(){
LocalBroadcastManager.getInstance(this).unregisterReceiver(myReceiver);
}
// and of course you have to create a BroadcastReceiver
private BroadcastReceiver myReceiver = new BroadcastReceiver(){
#Override
public void onReceive(Context context, Intent intent){
// here you know that your data have changed, so it's time to reload it
reloadData = new ReloadData().execute(); // you should cancel this task onPause()
}
};
as I said, this next part varies depending on what threading method you're using to load the Cursor, for this example I'll show in a AsyncTask because it's very popular (but I really believe you and every developer in the world should use the Loaders pattern).
private class ReloadData extends AsyncTask<Void, Void, Cursor> {
protected Cursor doInBackground(Void... void) {
// here you query your data base and return the new cursor
... query ...
return cursor;
}
protected void onPostExecute(Cursor result) {
// you said you're using a subclass of CursorAdater
// so you have the method changeCursor, that changes the cursor and closes the old one
myAdapter.changeCursor(result);
}
}
The above approach I tested and used before and I know it works. There's a way of making it work with the flag FLAG_REGISTER_CONTENT_OBSERVER and override onContentChanged() to re-execute the query and swap the cursor, but I've never tested it. It will be something like that:
init your adapter with the constructor CursorAdapter(Context context, Cursor c, int flags) passing the flag FLAG_REGISTER_CONTENT_OBSERVER and override onContentChanged(). Inside onContentChanged you will execute the AsyncTask just like above. This way you don't have to use the LocalBroadcastManager as the database will alert. The reason that method is not my main answer, it's because I've never tested it.
Note that autoRequery have been deprecated and it's discouraged as it performs data loading in the UI thread.
edit:
I just noticed that the content observer is an API 11 thing. You have two options: 1 use the support library instead: https://developer.android.com/reference/android/support/v4/widget/CursorAdapter.html or the broadcast option.
Register a BroadcastReceiver in the fragments you have and in its onReceive call refresh - this method is supposed to update the UI depending what you have inside. For making your code easy to use, have a base Fragment class and do the registering/unregistering there along with an abstract refresh method that will be implemented by children fragments. Something like:
public abstract class BaseRefreshableFragment extends Fragment {
private BroadcastReceiver refreshReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if ("package_name.REFRESH_ACTION".equals(intent)) {
refresh();
}
}
};
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
IntentFilter filter = new IntentFilter("package_name.REFRESH_ACTION");
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(refreshReceiver, filter);
}
#Override
public void onDestroyView() {
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(refreshReceiver);
super.onDestroyView();
}
protected abstract void refresh();
}
In your Service, when your work is done, broadcast an intent with above action. So if there are fragments to show updated data, their receiver will be notified and that will call refresh ON EACH FRAGMENT. Something like from your service:
Intent intent = new Intent("package_name.REFRESH_ACTION");
LocalBroadcastManager.getInstance(MySyncService.this).sendBroadcast(intent);
The advantage is that you don't need to care when the fragment is shown or not since the receiver is there for the life-time of your fragment's view.
Recreate your adapter from scratch with the new data, and reassign it to the ListView (or whatever Views you have).

Global Loader (LoaderManager) for reuse in multiple Activities / Fragments

What I would like to achieve:
I have two different fragments. I would like them both to show the same data in two forms (in a list and on a map). I would like them to share one Loader (AsyncTaskLoader in particular). Everything works fine, but the Loader isn't re-used. Another one is created and the data is loaded twice.
What I do:
In the Fragments I use LoaderManager lm = getActivity().getSupportLoaderManager();
In both of them I implement LoaderCallbacks<ArrayList<Item>> and the required methods.
In both I use lm.initLoader(0, args, this);.
But when I output the lm.toString() it appears that these are two different Loaders. And the data is downloaded twice.
How to re-connect to the same Loader from a different Activity/Fragment than the one it was started in?
It should be possible since the context is attached to the Loader anyway on every onCreate(), e.g. on configuration change.
How to re-connect to the same Loader from a different Activity/Fragment than the one it was started in?
You should not reuse Loaders that are being managed by a LoaderManager instance across multiple Activitys and Fragments.
The LoaderManager will start/stop those Loaders with respect to the Activity/Fragment lifecycle, so there is no way of guaranteeing that those Loaders will exist once you are in another Activity.
From the documentation:
LoaderManager.LoaderCallbacks is a callback interface that lets a
client interact with the LoaderManager.
Loaders, in particular CursorLoader, are expected to retain their data
after being stopped. This allows applications to keep their data
across the activity or fragment's onStop() and onStart() methods, so
that when users return to an application, they don't have to wait for
the data to reload. You use the LoaderManager.LoaderCallbacks methods
when to know when to create a new loader, and to tell the application
when it is time to stop using a loader's data.
In other words, it is often the case that your Loaders will be specific to some Activity (or Fragment). When you have your Activity implement the LoaderManager.LoaderCallbacks interface, your Activity is given type LoaderManager.LoaderCallbacks. Each time you call initLoader(int ID, Bundle args, LoaderCallbacks<D> callback), the LoaderManager either creates or reuses a Loader that is specific to some instance of the LoaderManager.LoaderCallbacks interface (which in this case is an instance of your Activity). This essentially binds your Activity with a Loader, and its callback methods will be called as the loader state changes.
That being said, unless you can find a way to have your two separate Activitys share the same callback methods, I doubt there is a clean way to do this (i.e. having an Activity and a Fragment share the same callbacks sounds like it would be tricky, if not impossible). I wouldn't worry about it too much though. In all of the sample code I have ever seen, I've never seen two Activitys and/or Fragments share the same callback methods. Further, given that Activitys and Fragments are both supposed to be designed for reuse, sharing Loaders in this way just doesn't seem like something that would be encouraged.
Yes. It worked for me. I have 3 different Fragments in a Navigation Drawer where the same data is populated in different ListViews. (All Fragments are a part of the SAME Activity).
My AsyncTaskLoader:
public class MyTaskLoader extends AsyncTaskLoader<HashMap<String, Integer>> {
public MyTaskLoader(Context context) {
super(context);
}
#Override
public HashMap<String, Integer> loadInBackground() {
...
return hashMap;
}
...
}
Use the Same Loader Id in all Fragments.
Fragment1:
public class Fragment1 extends BaseFragment implements LoaderManager.LoaderCallbacks<HashMap<String, Integer>> {
#Override
public void onCreate(Bundle savedInstanceState) {
//initialize adapter
getActivity().getSupportLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<HashMap<String, Integer>> onCreateLoader(int arg0, Bundle arg1) {
// TODO Auto-generated method stub
return new MyTaskLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<HashMap<String, Integer>> arg0,
HashMap<String, Integer> data) {
// TODO Auto-generated method stub
listAdapter.setData(data.keySet());
}
#Override
public void onLoaderReset(Loader<HashMap<String, Integer>> arg0) {
// TODO Auto-generated method stub
listAdapter.setData(null);
}
}
Use the same Id for Fragment2:
public class Fragment2 extends BaseFragment implements LoaderManager.LoaderCallbacks<HashMap<String, Integer>> {
#Override
public void onCreate(Bundle savedInstanceState) {
//initialize adapter
getActivity().getSupportLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<HashMap<String, Integer>> onCreateLoader(int arg0, Bundle arg1) {
// TODO Auto-generated method stub
return new MyTaskLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<HashMap<String, Integer>> arg0,
HashMap<String, Integer> data) {
// TODO Auto-generated method stub
listAdapter.setData(data.keySet());
}
#Override
public void onLoaderReset(Loader<HashMap<String, Integer>> arg0) {
// TODO Auto-generated method stub
listAdapter.setData(null);
}
}
The adapter should be initialized before initializing the loader.
Works so far.
But, is this the right way? Is there a better method for using a common loader for multiple Fragments?
I'm not quite sure what you're trying to archieve after going through the discussions. But there is an application.registerActivityLifecycleCallbacks() method, which accept global activity lifecycle listeners (like onActivityCreated()).
I think you'll have the desired behavior if you use the same Loader ID across your different Fragments and Activities. Make sure the loader ID are unique to the data to load though. PhotosLoader and VideoLoader shouldn't have the same id for example.

LoaderCallbacks as a static inner class (to handle multiple Loaders with different data types returned)

I have an activity which uses two Loaders. Each of them returns different type of data. To get data from a single Loader one just implements LoaderCallbacks<D> into an Activity. I guess I could just implement LoaderCallbacks<Object> and check the type of the object and then decide which of the two LoaderCallbacks it is, but it seems like a hack to me (mostly because of the lack of type safety here).
So I thought about making the LoaderCallbacks object a static inner class, something like this:
private static class geocoderLoaderCallbacks implements LoaderCallbacks<List<Address>>{
#Override
public Loader<List<Address>> onCreateLoader(int arg0, Bundle arg1) {
GeocoderTask loader = new GeocoderTask(context, "");
return loader;
}
#Override
public void onLoadFinished(Loader<List<Address>> loader, List<Address> data) {
// TODO Auto-generated method stub
}
#Override
public void onLoaderReset(Loader<List<Address>> loader) {
// TODO Auto-generated method stub
}
}
And then using lm.initLoader(0, null, geocoderLoaderCallbacks).
Two question arise: is it ok to do, or should I rather stick to implementing LoaderCallbacks into Activity? And how do I safely pass the context into the onCreateLoader? Should I just make a constructor in geocoderLoaderCallbacks and pass the context there like this lm.initLoader(0, null, geocoderLoaderCallbacks(this))?
There is a similar question here LoaderManager with multiple loaders: how to get the right cursorloader but it doesn't explain how to manage two loaders with different data types.
It is always ok to move code away from a potentially giant class and it's much cleaner to do it with different classes then with one that can handle everything. You might even want to make them real external classes instead of inner classes if you feel that your Activity has too much code inside. LoaderCallbacks is an interface so you can and mostly should implement it in it's own class.
Passing the Context in the constructor is fine as long as you don't keep static or otherwise cached references to it.

Best practices to query SQLite database in ListFragment with CursorLoader?

I'm using Android Compatibility Library in my project. I've set up ListFragment as described in the DevGuide (http://developer.android.com/reference/android/app/Fragment.html), and using a simple CursorLoader Christian made be used without content provider (CursorLoader usage without ContentProvider).
The question is where, in my ListFragment / parent Activity, should I open database, return the Cursor, create Adapter and setListAdapter?
So in my app, I have TitlesFragment, DetailsFragment, FragmentLayoutActivity, DetailsLayoutActivity.
Is the best practice...
to open database in ListFragment's onActivityCreatedand close it in ListFragment's onDestroy like in code sample below
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Open database
playersDatabaseHelper = new PlayersDBAdapter(getActivity());
playersDatabaseHelper.open();
getLoaderManager().initLoader(0, null, this);
...
}
#Override
public void onDestroy() {
super.onDestroy();
if (playersDatabaseHelper != null) {
playersDatabaseHelper.close();
}
}
query database and return the cursor in onCreateLoader, and create the Adapter and setListAdapter in onLoadFinished like in code sample below
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new MyCursorLoader(getActivity()) {
#Override
public Cursor loadInBackground() {
playersCursor = playersDatabaseHelper.getAllPlayers();
return playersCursor;
}
};
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Create an empty adapter we will use to display the loaded data.
playersAdapter = new RowAdapter(getActivity(), playersCursor, R.layout.players_overview_row);
// Allocate the adapter to the List displayed within this fragment.
setListAdapter(playersAdapter);
playersAdapter.swapCursor(cursor);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
Am I on the right track or should I move some of those somewhere? Thanks for your time!
Sorry no experience in CursorLoader yet and Fragment, but I already experienced use of SQLiteOpenHelper in the context of concurrent access by different threads & activities.
I will assume that PlayersDBAdapter is internally using a SQLiteOpenHelper class. but it is not clear what your methods open() and close() are doing?
What I did:
define your SQLiteOpenHelper as an application wide singleton, not activity wide as you seem to do
instantiate SQLiteOpenHelper single instance in your Application onCreate
DON'T release SQLiteOpenHelper instance in any activity onDestroy, as when an activity stops, another one might still have to open the DB
I guess SQLiteOpenHelper instance should be cleared in application onTerminate (not sure as onTerminate is in practice almost never called)
I have DBAdapter object which gets a SQLiteDatabase reference with mySQLiteOpenHelper.getWritableDatabase()
these DBAdapter are typically allocated in activity onCreate and released in onDestroy
At least this works, no crashs in an application with several thousands users.
Suggestions to improve that are welcome :-)

Categories

Resources