I need to make a ListAdapter that presents data from multiple ContentProviders. The ContentProviders themselves represent one table each from relational database.
I want to use the CursorLoader system to retrieve aggregate data into ListView. Is this possible to do with 1 loader or do I need to use multiple loaders? I'd prefer to use one.
I'm not sure how I can have 2 ContentProviders interact with each other beyond doing the join manually in code which doesn't seem like a great option either.
You will have to write a Custom Loader class. For example:
public class FooLoader extends AsyncTaskLoader {
Context context;
public FooLoader(Context context) {
super(context);
this.context = context;
}
#Override
public Cursor loadInBackground() {
Log.d(TAG, "loadInBackground");
YourDatabase dbHelper = new YourDataBase(context);
SQLiteDatabase db= dbHelper.getReadableDatabase();
/*** create a custom cursor whether it is join of multiple tables or complex query**/
Cursor cursor = db.query(<TableName>, null,null, null, null, null, null, null);
return cursor;
}
}
In the calling activity or fragments onCreate() method, you would need to call the custom loader class:
public class MyFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate():" + mContent);
Loader loader = getLoaderManager().initLoader(0, null, this);
loader.forceLoad();
}
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Log.d(TAG, "onCreateLoader()") ;
return new FooLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
Log.d(TAG, "onLoadFinished");
}
#Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
}
}
You might want to take a look at CursorJoiner.
I'm new to ContentLoaders myself, but I haven't yet seen a way that you could use one ContentLoader to handle multiple ContentProviders.
Are the tables you're querying in separate databases? It isn't clear from your question. If the tables are all in the same database, one alternative might be to instead use one ContentProvider for the separate tables. The data can be joined and returned to one cursor, which means you could use one CursorLoader. The SQLiteQueryBuilder.setTables() method has some information on this:
http://developer.android.com/reference/android/database/sqlite/SQLiteQueryBuilder.html#setTables%28java.lang.String%29
and you can see it in action here:
http://code.google.com/p/openintents/source/browse/trunk/shoppinglist/ShoppingList/src/org/openintents/shopping/provider/ShoppingProvider.java
this might also be helpful:
https://stackoverflow.com/a/3196484/399105
Related
I'm developing an application which relies on database (local) - and this database updates frequently.
What I'm trying to achieve is:
Suppose the data is shown in a listview. I want the listview to update the dataset as soon as any change in the database happens (or a specific table to be precise).
So far I've thought of these options:
SQLiteOpenHelper class: whenever an update/insert is done it'll notify the activity to update listview via BroadcastReceiver.
ContentProvider with CursorLoader (haven't used it before so a little skeptical)
Something else? Please suggest.
Which is the best way to achieve consistent and immediate updates without blocking the UI (performance)?
As suggested by #Karakuri created a custom CursorLoader by extending CursorLoader class without ContentProvider.
Here's the solution:
CustomCursorLoader.class
public class CustomCursorLoader extends CursorLoader {
private final ForceLoadContentObserver forceLoadContentObserver = new ForceLoadContentObserver();
public CustomCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
super(context, uri, projection, selection, selectionArgs, sortOrder);
}
#Override
public Cursor loadInBackground() {
Cursor cursor = /* get cursor from DBHandler class */;
cursor.setNotificationUri(getContext().getContentResolver(), CONTENT_URI);
if (cursor != null) {
cursor.getCount();
cursor.registerContentObserver(forceLoadContentObserver);
}
return cursor;
}
}
Every time you make a change to DB, do:
getContentResolver().notifyChange(CONTENT_URI, null);
In Activity class:
implement interface LoaderManager.LoaderCallbacks<Cursor>
initiate loader getLoaderManager().initLoader(0, null, this);
and override these methods:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CustomCursorLoader(this, CONTENT_URI, null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
customCursorLoaderAdapter.swapCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
customCursorLoaderAdapter.swapCursor(null);
}
extend CursorAdapter class to create listview adapter and you're done.
If performance is crucial for your app you should take a look on Realm for Android database. It provides better efficiency than SQLite and you can use RealmChangeListener to listen for changes in the database.
Actually I know how to send extra values from activity to fragment, but I need to send contacts cursor to fragment and after that I am passing that cursor to cursor adapter and showing data in listview.
There are a number of ways to accomplish what you are trying to do. Only some of them require you to possess the Cursor prior to creating the Fragment.
You could create a method that creates your fragment and pass in the cursor then:
public class YourFragment extends Fragment {
private Cursor mCursor;
public static YourFragment createYourFragmentWithCursor( Cursor cursor ) {
YourFragment fragment = new YourFragment();
fragment.setCursor( cursor );
return fragment;
}
#Override
public void onViewCreated (View view, Bundle savedInstanceState) {
ListView listView = (ListView) findViewById( R.id.yourListView );
listView.setAdapter( new CursorAdapter( getActivity(), getCursor() );
}
protected Cursor getCursor() {
return mCursor;
}
private Cursor setCursor( Cursor cursor ) {
mCursor = cursor;
}
}
You could pass the cursor from the activity to a fragment it controls would be to have the activity implement an interface containing a method that returns the cursor. Then, in your fragment you can get a reference to that interface.
For example:
public interface CursorProvidingInterface {
Cursor getCursor();
}
public class YourActivity extends Activity implements CursorProvidingInterface {
...
#Override
public Cursor getCursor() {
Cursor cursor = whateverYouDoToAcquireYourCursor();
return cursor;
}
...
}
public class YourFragment extends Fragment {
private CursorProvidingInterface cursorProvidingInterface;
#Override
public void onAttach( Activity activity ) {
try {
cursorProvidingInterface = (CursorProvidingInterface) activity;
}
catch (ClassCastException e) {
throw new RuntimeException( activity.getClass().getName() + " must implement " + CursorProvidingInterface.class.getName() );
}
}
#Override
public void onViewCreated (View view, Bundle savedInstanceState) {
ListView listView = (ListView) findViewById( R.id.yourListView );
listView.setAdapter( new CursorAdapter( getActivity(), cursorProvidingInterface.getCursor() );
}
}
Depending on your situation the above strategies may not be the best to choose.
Assuming your cursor comes from a database I would suggest that you simply acquire the cursor from the database when your fragment is created. You could pass in any query parameters you might need as arguments to the fragment if necessary.
In this case, I prefer to use some form of dependency injection such as RoboGuice and create a #Singleton class that will handle your database transactions that you can then #Inject and then call upon when needed.
You might consider implementing ContentProvider and using it to get the cursor you need. This could be especially helpful if the data you are showing from your Cursor is likely to change frequently.
These last two cases are the more robust ways to go about passing data around in a Cursor and I suggest you familiarize yourself with them. A search for tutorials on these methods will yield much better examples than I will provide here.
use constructor to take the values to the fragment. then assign the values from constructor to instance variable. then you can use cursor inside onCreateView() method to initialize it with fragment starting.
Updated:
create setter method for set the Cursor in Fragment, so add following method
public static setMyCursor(Cursor pCursor){
this.cursor=pCursor;
}
use constructor as public
public MyFragment(){
}
set the cursor using setMyCursor() method
I continue to struggle with getting a query to work with a CursorLoader in a ListFragment. I suspect part of my problem is that I'm unsure about certain details. I have an xml file, myfragment.xml, which defines the two fragments in my app. The first fragment, my ListFragment, is identified by:
android:id="#+id/frag_mylist"
When I call SimpleCursorAdapter in my ListFragment class, I believe I should do this:
String[] dataColumns = { "fieldname", "_id" };
int[] viewIDs = { R.id.frag_mylist };
mAdapter = new SimpleCursorAdapter(getActivity(), R.layout.myfragment, null, dataColumns, viewIDs, 0);
setListAdapter(mAdapter);
getLoaderManager().initLoader(0, info, (LoaderCallbacks<Cursor>) this);
where info is a Bundle that I've passed from a previous activity. Is that right? Also, I've seen some examples with 0 as the last parameter for SimpleCursorAdapter, others with CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER. What's the difference? Finally, this page may indicate that I have to retrieve a LoaderManager in my code like so:
private LoaderManager mLoaderManager;
public void onCreate(savedInstanceState) {
super.onCreate(savedInstanceState);
mLoaderManager = this.getSupportLoaderManager();
}
but this is the only place I've seen this. Is this necessary? I'm hoping that getting answers to these questions will help me dig down to why my query is returning no results. I'm fairly confident that my database is being created and populated at this point. Thanks much!
As requested below, here are the three methods of my LoaderManager.LoaderCallbacks interface:
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String selection = "level='" + args.getString("Level") + "'";
return (Loader<Cursor>) new CursorLoader(getActivity(), MY_URI,
PROJECTION, selection, null, null);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
switch (loader.getId()) {
case LOADER_ID:
mAdapter.swapCursor((android.database.Cursor) cursor);
break;
}
}
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
Let me add that I've verified through the debugger that args.GetString("Level") in the onCreateLoader method is "Beginning", which is what it should be.
Add this line within your onLoadFinished after you swap the cursor
mAdapter.notifyDataSetChanged()
I am using a very simple CursorLoader with an ImageDownloader. The ImageDownloader is running and everything but the CursorLoader finishes, and THEN the ImageDownloader begins its job of downloading the images, but the GridView is not updating with the downloaded images..
Within my ListFragment I have the following onActivityCreated method:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.d(TAG, "onActivityCreated");
super.onActivityCreated(savedInstanceState);
cursorAdapter = new OURSVPImageAdapter(getActivity(), null);
// set the adapter on the gridview
mGridView.setAdapter(cursorAdapter);
// load the data
getActivity().getSupportLoaderManager().initLoader(2, null, this);
}
My CursorLoader is as follows:
public static final class PhotoCursorLoader extends SimpleCursorLoader {
Context mContext;
public PhotoCursorLoader(Context context) {
super(context);
Log.d("PhotoCursorLoader", "Constructor");
mContext = context;
}
#Override
public Cursor loadInBackground() {
Log.d("PhotoCursorLoader", "loadInBackground");
PhotosDataSource datasource = new PhotosDataSource(mContext);
# STEP 1
return datasource.getAllPhotos(((EventActivity) mContext).getEventId());
}
}
The line labeled # STEP 1 that retrieves all of the photos is just a method that retrieves a Cursor, as seen here:
public Cursor getAllPhotos(long event_id) {
Log.d(TAG, "getAllPhotos");
Cursor mCursor = getWritableDatabase().query(true, TABLE_NAME, COLUMNS_PHOTOS, DatabaseConstants.KEY_EVENT_ID + "=" + event_id,
null, null, null, null, null);
return mCursor;
}
So, this being the CursorLoader for the ListFragment, it assumes its complete when that Cursor is returned, which is correct. From my understanding the setAdapter() method is what would actually trigger the getView method.
The Trouble I'm having is that Everything seems to be running fine, my log output is outputting the urls for the images correctly, breakpoints all showing legit data, the issue though, is that my grid view never gets updated with the images that the ImageDownloader retrieves.
EDIT
This is the SimpleCursorLoader I'm using: https://gist.github.com/1217628
Once again, the problem is most likely that you are managing your Fragment's loaders from the parent Activity. The Activity and Fragment lifecycles do not sync with each other (at least not in a predictable manner), so your attempts at debugging the issue aren't actually getting you anywhere. The easiest way to guarantee predictable behavior is to have each separate component make use of its own LoaderManager (as opposed to having your Fragments access getActivity().getSupportLoaderManager()).
As with another of my questions.. the refresh shouldn't be forced, and when using a provider you can have the provider notify the contentresovler of updates. I have a method that inserts new photos, and that method uses the data provider. The data provider needed to
getContext().getContentResolver().notifyChange(uri, null);
to actually update the Cursor.
I have a simple contentProvider, a layout with a ListView and a button for adding Items in content Provider and a CursorLoader. The android.content.Loader, D reference states that
The Loader will monitor for changes to the data, and report them to
you through new calls here. You should not monitor the data yourself.
For example, if the data is a Cursor and you place it in a
CursorAdapter, use the CursorAdapter(android.content.Context,
android.database.Cursor, int) constructor without passing in either
FLAG_AUTO_REQUERY or FLAG_REGISTER_CONTENT_OBSERVER (that is, use 0
for the flags argument). This prevents the CursorAdapter from doing
its own observing of the Cursor, which is not needed since when a
change happens you will get a new Cursor throw another call here.
But the Log.info line in the onLoadFinished method was not executed and listView didn't refreshed. Here is my (simple) code:
public class HomeActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>{
static final String TAG = "HomeActivity";
SimpleCursorAdapter adapter;
ListView listAnnunciVicini;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home);
listAnnunciVicini = (ListView) findViewById(R.id.lista_annunci_vicini);
adapter = new SimpleCursorAdapter(this, R.layout.list_item, null,
new String[] {
ContentDescriptor.Annunci.Cols.ID,
ContentDescriptor.Annunci.Cols.TITOLO,
ContentDescriptor.Annunci.Cols.DESCRIZIONE
}, new int[] {
R.id.list_annunci_item_id_annuncio,
R.id.list_annunci_item_titolo_annuncio,
R.id.list_annunci_item_descrizione_annuncio
}, 0);
listAnnunciVicini.setAdapter(adapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getSupportLoaderManager().initLoader(0, null, this).forceLoad();
}
public void addRandomItem(View sender) {
ContentValues dataToAdd = new ContentValues();
dataToAdd.put(ContentDescriptor.Annunci.Cols.TITOLO, "Titolo");
dataToAdd.put(ContentDescriptor.Annunci.Cols.DESCRIZIONE, "Lorem ipsum dolor sit amet.");
this.getContentResolver().insert(ContentDescriptor.Annunci.CONTENT_URI, dataToAdd);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// creating a Cursor for the data being displayed.
String[] proiezione = new String[] {ContentDescriptor.Annunci.Cols.ID, ContentDescriptor.Annunci.Cols.TITOLO, ContentDescriptor.Annunci.Cols.DESCRIZIONE };
CursorLoader cl = new CursorLoader(this, ContentDescriptor.Annunci.CONTENT_URI, proiezione, null, null, null);
return cl;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
adapter.swapCursor(data);
Log.i(TAG, "I dati sono stati ricaricati");
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
adapter.swapCursor(null);
}
}
Any suggestion?
AFAIK, you need to implement notification yourself in ContentProvider. For this, add something like getContext().getContentResolver().notifyChange(uri, null); to insert,update and delete method of ContentProvider and invoke.setNotificationUri(getContext().getContentResolver(), uri) on your Cursor in queryas described in official documentation. Here you can find the whole picture.
Note: You should not close the cursor (cursor.close()) in order to get
notifications about the changes.