I have a custom image gallery app that stores the image data in a database. The gallery is a GridView backed by CursorAdapter.
When first requesting an image decode ContentResolver.update updates the height, width, and orientation in a background thread. Since many images are visible at a time the database updates are happening rapidly as a user scrolls which causes the backing CursorLoader to refresh the GridView rapidly which manifests itself as a rapid flicker.
Is it not possible to use CursorLoader and CursorAdapter if the underlying source can rapidly change, or am I missing something here?
When there's a change to monitored data (selection, projection) within a database the LoaderManager simply supplies a new cursor. As such, as far as the CursorAdapter is concerned it will have an entirely new datasource (swapCursor) on every update to the database:
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor)
{
mGalleryAdapter.swapCursor(cursor);
}
Therefore the ideal CursorAdapter attached to a dynamic LoaderManager will need to micro-manage view updates to ensure that it does not bother updating views that are exactly the same. To do so (since my views are fairly complicated) I created a helper object to handle translations and comparisons between cursor and views (removed most views for the sake of brevity).
public static class GalleryItem
{
private String name;
private int rotation;
public static GalleryItem fromCursor(Context c, Cursor cursor)
{
GalleryItem item = new GalleryItem();
item.rotation = ImageUtils.getRotation(cursor.getInt(Meta.ORIENTATION_COLUMN));
item.name = cursor.getString(Meta.NAME_COLUMN);
return item;
}
public static GalleryItem fromViewHolder(ViewHolder vh)
{
GalleryItem item = new GalleryItem();
item.rotation = (int)vh.mImageView.getRotation();
item.name = (String) vh.mFileName.getText();
return item;
}
#Override
public boolean equals(Object o)
{
GalleryItem compare = (GalleryItem) o;
boolean sameRotation = rotation == compare.rotation;
boolean sameName = name == null ? compare.name == null : name.equals(compare.name);
return sameName && sameRotation;
}
}
Then use this to check if you need to update anything in the view:
#Override
public void onBindViewHolder(ViewHolder vh, final int position, Cursor cursor)
{
GalleryItem galleryItem = GalleryItem.fromCursor(mContext, cursor);
GalleryItem former = GalleryItem.fromViewHolder(vh);
// If nothing has changed avoid refreshing.
// The reason for this is that loaderManagers replace cursors meaning every change
// will refresh the entire data source causing flickering
if (former.equals(galleryItem))
return;
...
So long story short, with a dynamic LoaderManager you'll have to implement a way to skip recreating completely unchanged views.
Related
There seems to be various ways to implement RecyclerView lists, some more logical than others. Going beyond a simple list to one where the data changes increases the complexity. Additional complexity comes from implementing the ability to view the details of the list items.
Although I have had some success at implementing lists in this manner, I feel that what I've come-up with is not efficient and not what was intended when the framework was designed. Looking at the various methods I've used, I keep saying "this can't be the way they want me to do it".
The basic application I wish to examine is one that displays records from a SQLite database in a scrollable list, let's a user select items from the list to see details, and lets a user long-click to toggle an attribute of an item. And of course, the display should remain consistent through the various views, scrolling, redisplays, etc.
This image shows a basic use-case that does not have any underlying data changes. The words in blue are areas where implementation details are needed. In this case, "click" would require getting the model and the position into the detail activity, perhaps with intent.putExtra().
The above functionality is fairly straight-forward when compared to having to manage changes to the data. Below we have a scenario where we remain in the same activity, but the user takes action to update the data using a long click:
Where is the best place for the listener or observer? What objects need attention? Certainly the view needs to be updated (how)? How will the update to the database be managed? How can we make sure that the view will be redrawn properly?
Below we have two actions. The long-click is similar to the long-click, above, but when in the detail activity, are we operating with a serialized copy of the model list? If so, how do those changes get back to the 'real' model and the database?
Who is listening, what parameters are delivered, and how are those parameters used to keep the data synchronized? Where is the most logical and maintainable place to put the listener code, the code that keeps the database and views all in order?
What was the logical approach, intended by the designers of the framework, for handing this seemingly straight-forward functionality? Should there be some single instance overlord function in the application class? I haven't seen examples with that kind of thing, but might be an option.
I'd be surprised if this turned-out to be 'easy', but it has just got to be easier than the convoluted mess that I've been working through.
One of the easiest ways to manage your database from multiple locations in an app is to use a ContentProvider and designate content:// URI's for each table in your database.
To maintain the "master" list view:
Suppose you have a database table called "animals" and the URI to access the table via the ContentProvider is content://myPackage/animals. For your RecyclerView activity, in onCreate you would start a CursorLoader on the content://myPackage/animals URI.
Assuming you design your ContentProvider correctly (ie. insert, delete and update calls end with ContentProvider.notifyChange()), your loader will automatically query and requery the database any time the table changes. From the loader's onLoadFinished() callback, you take the cursor it returns and update your recycler view adapter with it. Although a somewhat simplified explanation, this is pretty much the essentials to keep the master list updated even as changes to the database are made in other parts of the app.
To handle clicks/long clicks in the list:
There is not necessarily a "best way" to do this, but in the adapter's onCreateViewHolder() method I usually take the base view for an item and set the ViewHolder containing the view as its on click listener. This is because when the item is clicked, the ViewHolder knows important information about where it is within the list (using getAdapterPosition() or getItemId()).
If for example the user long clicked an item with an ID of 3, I would update the database using ContentResolver.update() and a URI of content://mypackage/animals/3. After the update was successful, the master list would then automatically requery the database and refresh the list with the new state for the item with ID #3.
Here is one way to implement this set of functionality. All of the functionality described in the original question is implemented and is available here: Sample Application on GitHub. Also, if you have suggestions for improvement, please advise, either here or on GitHub.
Generally, there is a model held in the list activity, and that model is passed as an extra into the slide activity. Changes to the slide activity model are easily reflected there and in the database, and a bit of effort was required to allow the changes in the slide activity to appear in the list activity when the user pressed the back button.
The main activity onCreate creates an SQLite database adapter and puts some data in there. It creates an ArrayList<Model> and uses that to build the RecyclerView.Adapter:
public class RecyclerSqlListActivity extends AppCompatActivity {
....
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_list);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView1);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
try {
repository = new DbSqlliteAdapter(this);
repository.open();
//repository.deleteAll();
//repository.insertSome();
modelList = repository.loadModelFromDatabase();// <<< This is select * from table into the modelList
Log.v(TAG, "The modelList has " + modelList.size() + " entries.");
recyclerViewAdapter = new MyRecyclerViewAdapter(this, modelList);
recyclerView.setAdapter(recyclerViewAdapter);
recyclerView.hasFixedSize();
} catch (Exception e) {
Log.v(TAG, "Exception in onCreate " + e.getMessage());
}
}
...
}
Here's the layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</LinearLayout>
The RecyclerView.Adapter is where the listeners are. I listen for a click and a long click. The click starts the slide activity and the long click changes the database so that the icon shows or not. When the user takes action that changes data, I need to make sure the model, and database get updated and I need to notify the view that something has changed. Note that this implementation just manages things that have actually changed, as opposed to refreshing everything.
class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView iconImageView;
TextView nameTextView;
TextView secondLineTextView;
TextView dbItemTextView;
TextView hiddenTextView;
TextView descriptionTextView;
ViewHolder(View itemView) {
super(itemView);
iconImageView = (ImageView) itemView.findViewById(R.id.bIcon);
nameTextView = (TextView) itemView.findViewById(R.id.bName);
secondLineTextView = (TextView) itemView.findViewById(R.id.bSecondLine);
dbItemTextView = (TextView) itemView.findViewById(R.id.bDbItem);
hiddenTextView = (TextView) itemView.findViewById(R.id.bHidden);
descriptionTextView = (TextView) itemView.findViewById(R.id.bDescription);
}
}
private List<Model> mModelList;
private Context mContext;
MyRecyclerViewAdapter(Context context, List<Model> modelList) {
mContext = context;
mModelList = new ArrayList<>(modelList);
}
#Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View modelView = inflater.inflate(R.layout.list_item, parent, false);
return new ViewHolder(modelView);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
final Model model = mModelList.get(position);
viewHolder.nameTextView.setText(model.getName());
viewHolder.secondLineTextView.setText(model.getSecond_line());
viewHolder.dbItemTextView.setText(model.getId() + "");
viewHolder.hiddenTextView.setText(model.getHidden());
viewHolder.descriptionTextView.setText(model.getDescription());
if ("F".equals(model.getHidden())) {
viewHolder.secondLineTextView.setVisibility(View.VISIBLE);
viewHolder.iconImageView.setVisibility(View.INVISIBLE);
} else {
viewHolder.secondLineTextView.setVisibility(View.INVISIBLE);
viewHolder.iconImageView.setVisibility(View.VISIBLE);
}
// DEFINE ACTIVITY THAT HAPPENS WHEN ITEM IS CLICKED
viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.v(TAG, "setOnClickListener fired with view " + view); // view is RelativeLayout from list_item.xml
int position = mModelList.indexOf(model);
Log.v(TAG, "position was : " + position);
Intent intent = new Intent(mContext, DetailSlideActivity.class);
intent.putExtra(DetailSlideActivity.EXTRA_LIST_MODEL, (Serializable)mModelList);
intent.putExtra(DetailSlideActivity.EXTRA_POSITION, position);
((Activity)mContext).startActivityForResult(intent, RecyclerSqlListActivity.DETAIL_REQUEST);
}
});
// If the item is long-clicked, we want to change the icon in the model and in the database
viewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
Log.v(TAG, "setOnLongClickListener fired with view " + view); // view is RelativeLayout from list_item.xml
Log.v(TAG, "setOnLongClickListener getTag method gave us position: " + view.getTag());
int position = mModelList.indexOf(model);
Log.v(TAG, "position was : " + position);
String hidden = model.getHidden();
Log.v(TAG, "hidden string was : " + hidden);
if ("F".equals(hidden)) {
model.setHidden("T");
DbSqlliteAdapter.update(model);
view.findViewById(R.id.bIcon).setVisibility(View.INVISIBLE);
} else {
model.setHidden("F");
view.findViewById(R.id.bIcon).setVisibility(View.VISIBLE);
}
Log.v(TAG, "updating the database");
DbSqlliteAdapter.update(model);
Log.v(TAG, "notifyItemChanged being called");
notifyItemChanged(position);
boolean longClickConsumed = true; // no more will happen :)
return longClickConsumed;
}
});
}
Rather than put all of the code here, I'll just put a subset, and more details can be found by going to the github link, above. But one last comment on how the app was designed to get the list of items that were altered during the slide activity. Each time a change happened in the slide activity, the position was saved at the same time the database was updated. Then, when the slide activity ended, onActivityResult() pulled all of those positions and updated the model that is held in the original activity.
public class RecyclerSqlListActivity extends AppCompatActivity {
....
#Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if(requestCode == RecyclerSqlListActivity.DETAIL_REQUEST){
Log.v(TAG, "onActivityResult fired <<<<<<<<<< resultCode:" + resultCode);
//String[] changedItems = intent.getStringArrayExtra(RecyclerSqlListActivity.DETAIL_RESULTS); // We could return things we learned, such as which items were altered. Or we could just update everything
//modelList = repository.loadModelFromDatabase();// <<< This is select * from table into the modelList
Integer[] changedPositions = DbSqlliteAdapter.getChangedPositions();
for (Integer changedPosition : changedPositions) {
Model aModel = modelList.get(changedPosition);
DbSqlliteAdapter.loadModel(aModel);
recyclerViewAdapter.notifyItemChanged(changedPosition);
}
}
}
....
}
Here are a few of the classes that interact with the database. I decided against ContentProvider because, according to the documentation, that's for sharing data between applications, not within one application.
class DbSqlliteAdapter {
....
static Model loadModel(Model model) {
Model dbModel = DbSqlliteAdapter.getById(model.getId() + "");
Log.v(TAG, "looked up " + model.getId() + " and it found " + dbModel.toString());
model.setId(dbModel.getId());
model.setName(dbModel.getName());
model.setSecond_line(dbModel.getSecond_line());
model.setDescription(dbModel.getDescription());
model.setHidden(dbModel.getHidden());
return model;
}
private static class DatabaseHelper extends SQLiteOpenHelper {....}
....
static void update(Model model) {
ContentValues values = fillModelValues(model);
Log.v(TAG, "Working on model that looks like this: " + model);
Log.v(TAG, "Updating record " + values.get(Model.ID) + " in the database.");
mDb.update(SQLITE_TABLE, values, Model.ID + "=?", new String[] {"" + values.get(Model.ID)});
Model resultInDb = getById("" + values.get(Model.ID));
Log.v(TAG, "after update, resultInDb: " + resultInDb);
}
static void update(Model model, int position) {
positionSet.add(position);
update(model);
}
static Integer[] getChangedPositions() {
Integer[] positions = positionSet.toArray(new Integer[0]);
positionSet.clear();
return positions;
}
....
}
I am using custom built content provider to pull data from my database. There can be many records(like 10k) so instead of implementing some mechanism of lazy loading to a list, I took a chance and created content-provider as it has internally a mechanism for "lazy loading". All works well.
I need to add a field (column) which should not go in database, but it is a selector, a check-box. Say this is a list of some users loading from DB, and I want to create something close to WhatsApp "add contacts to broadcast" (see image)
The code for content provider is nothing special:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
................
Cursor cursor = customContactsDataBase.getContacts(id, projection, selection, selectionArgs, sortOrder);
return cursor;
}
And getContacts is a simple SQLiteQueryBuilder returning a cursor.
So, my question is, should I inject this new check-box column,
if yes, then where to keep the values (in a separate "matching by id"
list or a map and dynamically fill the values)
Or should I use some other mechanism ?
I've done something similar to this, a shopping list with favourites where you check the favourites to add them to your list.
I considered it a multi select context action bar (CAB), just forced permanently into the context action bar state. So I applied the same method I use for CABs.
In my adapter I add an array to hold checked items
private SparseBooleanArray checkedItems = new SparseBooleanArray();
I have public method in the adapter to toggle a check, get a list of all checked items and a few support methods as below.
public void toggleChecked(int pos) {
Log.d(TAG, "Position " + pos);
if (checkedItems.get(pos, false)) {
checkedItems.delete(pos);
} else {
checkedItems.put(pos, true);
}
notifyItemChanged(pos);
}
public boolean isChecked(int pos) {
return checkedItems.get(pos, false);
}
public void clearChecked() {
checkedItems.clear();
notifyDataSetChanged();
}
public int getCheckedItemCount() {
return checkedItems.size();
}
public List<Integer> getCheckedItems() {
List<Integer> items = new ArrayList<>(checkedItems.size());
for (int i = 0; i < checkedItems.size(); i++) {
items.add(checkedItems.keyAt(i));
}
return items;
}
You then just need to wire up your viewholder check event to go and update the adapter (I bubble the check event right up to the activity level then back into the adapter as I found it the easiest method with other validation required but that specific to my implementation).
I have a SortedList being displayed in a RecyclerView by my RecyclerView.Adapter.
I use 2 custom Comparator instances from withing the SortedListAdapterCallback.compare() method to either sort A-Z or Z-A.
static class A2Z implements Comparator<Item> {
#Override
public int compare(Item t0, Item t1) {
return t0.mText.compareTo(t1.mText);
}
}
static class Z2A extends A2Z {
#Override
public int compare(Item t0, Item t1) {
return -1 * super.compare(t0, t1);
}
}
Item simply contains a single String mText;
I use my comparators in the SortedListAdapterCallback.compare() method:
private Comparator<Item> a2z = new A2Z();
private Comparator<Item> z2a = new Z2A();
private Comparator<Item> comparator = z2a;
#Override
public int compare(Item t0, Item t1) {
return comparator.compare(t0, t1);
}
I change the comparators on a button press. The list on screen does not update.
After logging values in the various methods, I can tell that the list itself is not updating. Notifying the adapter of changes simply redraws the old list, without resorting it.
So how do I force the underlying SortedList to resort all the items?
Perhaps it is best to just create a new Adapter each time, as in this question:
RecyclerView change data set
SortedList does not have functionality to resort itself - each instance only has a single sort order.
Went with creating a new adapter for each resort, as per Yigit's answer to the above referenced question:
If you have stable ids in your adapter, you can get pretty good
results (animations) if you create a new array containing the filtered
items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders.
(vs in setAdapter, it has to recycle all views and re-create because
it does not know that the new adapter has the same ViewHolder set with
the old adapter).
Use a switch statement inside the compare method with a local control flag (an enum is a good idea).
After changing the switch flag, call sortedList.replaceAll.
#Override
public int compare(PmpRole pmpRoleA, PmpRole pmpRoleB) {
switch (mSorter){
case IDX:
return pmpRoleA.getIdx().compareTo(pmpRoleB.getIdx());
case TITLE:
return pmpRoleA.getTitleIdx().compareTo(pmpRoleB.getTitleIdx());
case ID_IDX:
return pmpRoleA.getIdIdx().compareTo(pmpRoleB.getIdIdx());
}
return -1;
}
public void setSorter(Sorter sorter){
mSorter = sorter;
mPmpRoleSortedList.replaceAll(mPmpRoles);
}
Maintains animation functionality etc.
I'm developing an android app which shows phone contact as ListView(used Cursoradapter).Now I Want to add checkbox to the listView ,My problem is How to Insert checkbox data into database, based on if it is checked or not?
In my database class, I have a function which use to add names and numbers to my database,
createntry(String number,String name) // in my database class
Should I invoke this function in my CursorAdapter class ?
Recently, I found out that I should use getView function,but unfortunately I have no idea about getView, My question are
1-I should use this function in My CursorAdapter or else?
2- how to implement this function?
My CursorAdapterClass
public class ContactCursorAdapterCT extends CursorAdapter {
public ContactCursorAdapterCT(Context context, Cursor c) {
super(context, c);
}
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
TextView name = (TextView)view.findViewById(R.id.contactlistTV1);
name.setText(cursor.getString
(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)));
TextView phone = (TextView)view.findViewById(R.id.contactlistTV2);
phone.setText(cursor.getString
(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.lvct, parent, false);
bindView(v, context, cursor);
return v;
}
public View getView(final int pos, View inView, ViewGroup parent) { //getView
}
My activity class
public class Contacts extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.contacts);
Cursor cursor = getContentResolver().query
(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null,null, null);
startManagingCursor(cursor);
ContactCursorAdapterCT adapter= new ContactCursorAdapterCT
(Contacts.this, cursor);
ListView contactLV = (ListView) findViewById(R.id.listviewblcontactsDB);
contactLV.setAdapter(adapter);
My database Class
public long creatEntry(String inputnumber , String name) { // for add data
// TODO Auto-generated method stub
ContentValues cv= new ContentValues();
cv.put(KEY_NUMBER, inputnumber);
cv.put(N_NAME, name);
Log.v(inputnumber, "adding to Database");
return ourdatabase.insert(DATABASE_TABLE, null, cv);
}
Firstly, no you don’t need getView. bindView in conjunction with newView is completely sufficient as a replacement for it, some would probably say even better. Moreover, you don’t need to call bindview in new view. here’s a restructuring of what should be there.
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = inflater.inflate(R.layout.lvct, parent, false);
return v;
}
This is totally your call but no I don’t think that you should your createEntry method into the adapter, at least in its methods. The thing is that methods in the adapter are called for each row of your listview, so you might have a lot happening redundantly and plus I personally find it wasteful to be making insertions into a database into in increments. rather I think you have no choice but to do it all at once, because what if someone unselects a checkbox? you delete your entry? not only is it wasteful but it would be too cumbersome to keep track of the cursor positions or _id, you’d need to re-query every-time that something had been added. What you should do is maintain a list of what needs to be added to the database and bulk insert it when it’s done.
First you need to make an object holding the data that you want inserted. An object is the cleanest way cause you need to hold multiple pieces of information. Here it is very simple, you insert values you want into the constructor and then retrieve with the getter methods.
public class ContactObject {
private String name;
private String phone;
public ContactObject(String name, String phone) {
super();
this.name = name;
this.phone = phone;
}
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
}
Now you need an object to hold these objects when they’re checked. weird I know, but it much more convenient if they’re identified and can be iterated over and in general are batched together for referencing. I think this sort of task calls for the HashMap. Make it in the constructor.
contactMap = new HashMap<Integer, ContactObject>(c.getCount());
Now it’s time to boogie. make methods for the checkbox to add and remove stuff from your HashMap.
cbInsert.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (cbInsert.isChecked()) {
contactMap.put(cursor.getPosition(), new ContactObject(str_name, str_phone));
} else {
contactMap.remove(cursor.getPosition());
}
}
});
We’ve used the cursor position like an id for our objects within the HashMap. And when our checkbox is unchecked and we want to remove the object that was put in, we can just refer that that identifier that we used. Someone more prudent might want to check if something is there at the position before removing, that’s at your discretion. Now we’re almost done. How do we convert our HashMap to entries in the database? You have to access a database object, loop through and then get at your object one by one. The question is now is where. You could do it right in the adapter, but I usually do something like this in my activity because in my cases I usually have a database already made for the activity for other tasks and I don’t like to make more objects than I’m pushed to. So, what we can do is finish up with a getter method in our adapter for our HashMap:
public HashMap<Integer, ContactObject> getContactMap() {
return contactMap;
}
Now I’d imagine that you’d do something like this when your app is leaving so here goes.
#Override
protected void onDestroy() {
super.onDestroy();
HashMap<Integer, ContactObject> contactMap = adapter.getContactMap();
DatabaseHelper db = new DatabaseHelper(this);
// iterate through hashmap
for (Map.Entry<Integer, ContactObject> entry : contactMap.entrySet()) {
Integer key = entry.getKey();
ContactObject value = entry.getValue();
db.creatEntry(key, value.getPhone(), value.getName());
}
db.close();
}
Now things look a little weird, what happened what did i do with your entry method?
public long creatEntry(Integer id, String inputnumber, String name) { // for add data
long lng;
String strId = id.toString();
String[] selectionArgs = {strId};
Cursor cursor = ourdatabase.query(DATABASE_TABLE, null, "other_id = ?", selectionArgs, null, null, null);
if (cursor.moveToFirst()) {
// it exists, i'd assume that you might not want anything else done here
lng = -1;
} else {
// it doesn't exist
ContentValues cv= new ContentValues();
cv.put(KEY_NUMBER, inputnumber);
cv.put(N_NAME, name);
cv.put(KEY_OTHERID, strId);
Log.v(inputnumber, "adding to Database");
lng = ourdatabase.insert(DATABASE_TABLE, null, cv);
}
// cursor.close();
return lng;
}
As you can i see I modified it so that it takes in the id as well. I thought that you an issue that you'd run into would be having repeats in your database. I thought you could manage it by having another field for an id that you can modify.This id refers to the id passed in from the HashMap. I figured that every time that you make an insertion you first check if that previous id is there, then decide what you want to do. This is not a perfect solution but i just wanted to alert you that that issue is possible and give a possible hint as to manage it. In general the insert method should be fine if you only want to insert a couple rows but if you have a lot of stuff to insert, you might wanna look into bulk transactions for performance.
One more thing, checkboxes in your listview cannot be expected to have their states persist as you might normally expect it. You must explicitly dictate what state the checkbox has at each position. I made it correspond with if your HashMap has something filled with its corresponding key. Here's the full adapter method in hopes that it's made clearer:
public class ContactCursorAdapterCT extends CursorAdapter {
private LayoutInflater inflater;
private HashMap<Integer, ContactObject> contactMap;
public ContactCursorAdapterCT(Context context, Cursor c) {
super(context, c);
inflater = LayoutInflater.from(context);
contactMap = new HashMap<Integer, ContactObject>(c.getCount());
// i used c.getCount() as a capacity limit for this.
// if you opened made this multiple times, it might get over inflated and
// slow things down.
}
#Override
public void bindView(View view, Context context, final Cursor cursor) {
TextView name = (TextView)view.findViewById(R.id.contactlistTV1);
TextView phone = (TextView)view.findViewById(R.id.contactlistTV2);
final CheckBox cbInsert = (CheckBox) view.findViewById(R.id.contactlistCB1);
String str_name = cursor.getString
(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String str_phone = cursor.getString
(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
name.setText(str_name);
phone.setText(str_phone);
boolean isFilled = contactMap.containsKey(cursor.getPosition());
cbInsert.setChecked(isFilled);
// insert, remove objects to hashmap
cbInsert.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (cbInsert.isChecked()) {
contactMap.put(cursor.getPosition(), new ContactObject(str_name, str_phone));
} else {
contactMap.remove(cursor.getPosition());
}
}
});
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = inflater.inflate(R.layout.lvct, parent, false);
return v;
}
public HashMap<Integer, ContactObject> getContactMap() {
return contactMap;
}
}
It would be a good idea to add a flag to your records which will indicate that they are checked or not. Meaning an entry will have a name, a number and a flag (checked/unchecked).
In your Adapter itself, you can check the value of this flag an act accordingly.
(A related answer was regarding checkboxes in ListView is given here)
I have a working implementation of a ContentProvider loading data via CursorLoader into a listview (with custom CursorAdapter). It's a list of events. Every item has a title, place, etc. but also a set of offers which should be displayed in a LinearLayout inside every list row.
The problem is that a Cursor row can only contain flat data, not a set of other items.
My only idea is to make a joined query on database like this:
SELECT * FROM events, offers WHERE events.id=offers.event_id;
But then I'll have as much rows as there are offers (and the list should display events, so it's not good) and the list would be overpopulated. Maybe there is a possibility to tell CursorAdapter to only populate list rows with unique events.id but somehow retrieve the offers data as well?
The best solution would be to put a Cursor or custom Object containing offers inside the events Cursor. But afaik it's not possible.
I was facing the same problem. In fact, I think a lot of people are.
The whole mechanism of URI - to Relational DB through contentprovider, and everything that was built around it (like the various change listeners, file and stream handling) - this is all very impressive and useful, but for very simple data models.
Once your application needs a more elaborate data model, like - a hierarchy of tables, object relational semantics - this model breaks.
I've found a bunch of ORM tools for Android, but they seem too 'bleeding edge' to me (plus, for the life of me, I couldn't figure out if they have data change notification support).
ORM is very common today, I really hope the Android folks agree and add ORM capabilities to the platform.
This is what I ended up doing:
A cursor of cursors, with a a leading index cursor that helps choose the correct internal curosr.
It's kind of a temp solution, I just needed to move on with my code and get back to this later. Hope this helps.
Of course if you use a listview, you probably need to also create a custom adapter to inflate the correct views, and do the binding.
public class MultiCursor implements Cursor {
private final String TAG = this.getClass().getSimpleName();
ArrayList<Cursor> m_cursors = new ArrayList<Cursor>();
Map<Long, CursorRowPair> m_idToCursorRow = Collections.synchronizedMap(new HashMap<Long, CursorRowPair>());
Set<Long> m_idSet = new HashSet<Long>();
Cursor m_idCursor;
/**
* #precondition: _id column must exist on every type of cursor, and has to have index of 0 (be the first)
* #param idCursor
*/
public MultiCursor(Cursor idCursor) {
m_idCursor = idCursor;// this cursor binds the order (1,2,3) to ids
// go over all the ids in id cursor and add to m_idSet
initIdSet();
// m_cursors.add(idCursor);
// m_position = -1;
}
private void initIdSet() {
m_idSet.clear();
long id;
m_idCursor.moveToPosition(-1);
while (m_idCursor.moveToNext()) {
id = m_idCursor.getLong(m_idCursor.getColumnIndex(ContentDescriptor.ShowViewItem.Cols.ID));
m_idSet.add(id);
}
m_idCursor.moveToFirst();
}
public void addCursor(Cursor cursor) {
// when something changes in the child cursor, notify parent on change, to notify subscribers
// cursor.registerContentObserver(new SelfContentObserver(this)); // calls my onchange, which calls the ui
m_cursors.add(cursor);
updateIdToCursorMap(cursor);
}
private class CursorRowPair {
public final Cursor cursor;
public final int row;
public CursorRowPair(Cursor cursor, int row) {
this.cursor = cursor;
this.row = row;
}
}
private void updateIdToCursorMap(Cursor cursor) {
// get object_type
// for each row in cursor, take id, row number
// add id, <cursor,rowNum> to map
long id;
int row = 0;
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
id = cursor.getLong(cursor.getColumnIndex(ContentDescriptor.ShowViewItem.Cols.ID));
if (m_idSet.contains(id)) m_idToCursorRow.put(id, new CursorRowPair(cursor, row));
row++;
}
cursor.moveToFirst();
}
private Cursor getInternalCursor() {
if (getPosition() < 0 || getCount()==0) return m_idCursor; // todo throw a proper exception
// get the id of the current row
long id = m_idCursor.getLong(m_idCursor.getColumnIndex(ContentDescriptor.BaseCols.ID));
CursorRowPair cursorRowPair = m_idToCursorRow.get(id);
if (null == cursorRowPair) return null;
Cursor cursor = cursorRowPair.cursor;
int row = cursorRowPair.row;
cursor.moveToPosition(row);
return cursor;
}
// //////////////////////////////////////////////
#Override
public void close() {
Log.d(TAG, "close");
for (Cursor cursor : m_cursors) {
cursor.close();
}
m_idCursor.close();
}
#Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
Log.d(TAG, "copyStringToBuffer");
getInternalCursor().copyStringToBuffer(columnIndex, buffer);
}
etc etc etc.
In you're adapter query the offers cursor for all records and make it a class variable. Then in your getView use the event id to iterate through the offer cursor and add the necessary textviews to your row layout when it find an appropriate match. It's not elegant, but it should work.
Unfortunately one CursorLoader can only load one Cursor. So the solution was to write a custom AsyncTaskLoader which returned two Cursors.