everybody! Today I was trying to solve the next problem: I've created room database for List of languages, prefill it with five ready object for different one's and then I was trying to transfer them into spinner adapter something like that:
Entity and DAO code for the Language object:
#Entity
public class Language {
#PrimaryKey
private long id;
#ColumnInfo(name = "language")
private String language;
public Language(String language) {
this.language = language;
}
public static Language[] populateData() {
return new Language[]{new Language("English"), new Language("French"), new Language(
"Spanish"), new Language("Russian"), new Language("Italian")};
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
#Dao
public interface LanguageDao {
#Query("SELECT * FROM language")
List<Language> getAll();
#Insert
void insertAll(Language... languages);
}
Further I created database object with Singleton in the AppDatabase class like that:
#Database(entities = {Language.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase INSTANCE;
public abstract LanguageDao languageDao();
public synchronized static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = buildDatabase(context);
}
return INSTANCE;
}
private static AppDatabase buildDatabase(final Context context) {
return Room.databaseBuilder(context, AppDatabase.class, "my-database")
.addCallback(new Callback() {
#Override
public void onCreate(#NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
Executors.newSingleThreadScheduledExecutor().execute(new Runnable() {
#Override
public void run() {
getInstance(context).languageDao()
.insertAll(Language.populateData());
}
});
}
})
.allowMainThreadQueries()
.build();
}
}
As you can see I've inserted prefill data of language objects into the instance of Database. I know that's allowMainThreadQueries() method is not recommended here (just use it to simplify current training).
Further, I've created the following method which returns spinner object and put it into activity code:
private Spinner createLanguageSpinner(){
Spinner spinner = findViewById(R.id.language_spinner);
List<Language> languages = AppDatabase.getInstance(this).languageDao().getAll();
List<String>languageStrings = new LinkedList<>();
for(int i = 0; i < languages.size(); i++){
languageStrings.add(languages.get(i).getLanguage());
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line,
languageStrings);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
return spinner;
}
My problem is here:
List<Language> languages = AppDatabase.getInstance(this).languageDao().getAll();
I can't fill current List with predefined objects, which is resulted into empty spinner without options to choose. Could you tell where i'm getting wrong? I also would like to hear opinions about how can I simplify the creating of adapter.
I'm a bit late to the party, but in case if someone has this question too.
The problem is that you're trying to insert new objects without the PrimaryKey id value. As the result you're getting an empty table in the db.
You either should to set id value manually, e.g.:
#Entity
public class Language {
...
public Language(long id, String language) {
this.id = id; // or create method to generate a unique id as a PrimaryKey value must be unique
this.language = language;
}
public static Language[] populateData() {
return new Language[]{
new Language(1, "English"),
new Language(2, "French"),
new Language(3, "Spanish"),
new Language(4, "Russian"),
new Language(5, "Italian")
};
}
}
Or use the autoGenerate property of the PrimaryKey to let SQLite generate the unique id:
#PrimaryKey(autoGenerate = true)
private long id;
See the reference for more information.
No other changes in your code are required.
For the second question:
I also would like to hear opinions about how can I simplify the
creating of adapter
You can create a custom adapter for your spinner and pass a List<Language> to it directly:
public class MyAdapter extends BaseAdapter implements SpinnerAdapter {
private LayoutInflater mInflater;
private List<Language> mItems;
public MyAdapter(Context context, List<Language> items) {
mInflater = LayoutInflater.from(context);
mItems = items;
}
#Override
public int getCount() {
return mItems.size();
}
#Override
public Object getItem(int position) {
return mItems.get(position);
}
#Override
public long getItemId(int position) {
return mItems.get(position).getId();
}
// This is for the default ("idle") state of the spinner.
// You can use a custom layout or use the default one.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.spinner_item, parent, false);
}
Language item = (Language) getItem(position);
TextView textView = view.findViewById(R.id.text);
textView.setText(item.getTitle());
return view;
}
// Drop down item view as stated in the method name.
#Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.spinner_dropdown_item, parent, false);
}
Language item = (Language) getItem(position);
TextView textView = view.findViewById(R.id.text);
textView.setText(item.getTitle());
return view;
}
}
In your Activity:
List<Language> languages = AppDatabase.getInstance(this).languageDao().getAll();
Spinner spinner = findViewById(R.id.spinner);
MyAdapter myAdapter = new MyAdapter(this, languages);
spinner.setAdapter(myAdapter);
See BaseAdapter and SpinnerAdapter reference.
Or you can use ArrayAdapter and simply override toString method of your object to determine what text will be displayed for the item in the list (reference):
#Entity
public class Language {
...
#NonNull
#Override
public String toString() {
// A value you want to be displayed in the spinner item.
return language;
}
}
and in your Activity:
List<Language> languages = AppDatabase.getInstance(this).languageDao().getAll();
Spinner spinner = findViewById(R.id.spinner);
// Pass your list as the third parameter. No need to convert it to List<String>
ArrayAdapter<Language> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, languages);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
I try the new DiffUtil to get the differences in a RecyclerView.Adapter. But the old cursor on a reload is closed before the diff can be calculated and I don't know why. This CursorCallback is the Callback base, this Adapter is my base and here is my activity code:
public class RecyclerActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{
private RecyclerView recyclerView;
private ItemAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setHasFixedSize( true );
recyclerView.setLayoutManager(new LinearLayoutManager(this) );
recyclerView.setAdapter(adapter = new ItemAdapter(this));
recyclerView.setItemAnimator(new ItemAnimator());
ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
long id = recyclerView.getAdapter().getItemId( viewHolder.getAdapterPosition() );
viewHolder.itemView.getContext().getContentResolver().delete(ContentUris.withAppendedId(CategoryContract.CONTENT_URI, id), null, null);
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
getSupportLoaderManager().initLoader(0, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, CategoryContract.CONTENT_URI, CategoryContract.PROJECTION, null, null, CategoryContract.COLUMN_ID + " DESC");
}
public void addItem( View button ) {
int count = recyclerView != null ? recyclerView.getChildCount() : 0;
ContentValues v = new ContentValues(1);
v.put(CategoryContract.COLUMN_NAME, "Foo Nr. " + count);
getContentResolver().insert(CategoryContract.CONTENT_URI, v);
}
private Task setter;
#Override
public void onLoadFinished( final Loader<Cursor> loader, final Cursor data) {
if( setter != null) {
setter.cancel(true);
}
setter = new Task( adapter );
AsyncTaskCompat.executeParallel(setter, adapter.getCursor(), data );
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.changeCursor(null);
}
public static class Task extends AsyncTask<Cursor, Void, Pair<Cursor, DiffUtil.DiffResult>> {
private final CursorRecyclerViewAdapter adapter;
Task(CursorRecyclerViewAdapter adapter) {
this.adapter = adapter;
}
#Override
protected Pair<Cursor, DiffUtil.DiffResult> doInBackground(Cursor... params) {
return Pair.create( params[1], DiffUtil.calculateDiff( new ItemCallback( params[0], params[1]) ) );
}
#Override
protected void onPostExecute(Pair<Cursor, DiffUtil.DiffResult> diffResult) {
if( isCancelled() )
return;
adapter.swapCursor(diffResult.first);
diffResult.second.dispatchUpdatesTo(adapter);
}
}
public static class ItemAdapter extends CursorRecyclerViewAdapter<ItemHolder>
{
ItemAdapter( Context context ) {
super(context, null);
}
#Override
public void onBindViewHolder(ItemHolder viewHolder, Cursor cursor) {
CategoryModel model = CategoryModel.FACTORY.createFromCursor( cursor );
viewHolder.textView.setText( model.getId() + " - " + model.getName() );
}
#Override
public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ItemHolder(LayoutInflater.from( parent.getContext() ).inflate( R.layout.item, parent, false ));
}
}
public static class ItemHolder extends RecyclerView.ViewHolder {
TextView textView;
ItemHolder(View itemView) {
super(itemView);
textView = ( TextView ) itemView.findViewById(R.id.textView);
}
}
public static class ItemCallback extends CursorCallback<Cursor> {
public ItemCallback(Cursor newCursor, Cursor oldCursor) {
super(newCursor, oldCursor);
}
#Override
public boolean areRowContentsTheSame(Cursor oldCursor, Cursor newCursor) {
CategoryModel oldCategory = CategoryModel.FACTORY.createFromCursor(oldCursor);
CategoryModel newCategory = CategoryModel.FACTORY.createFromCursor(newCursor);
return oldCategory.getName().equals( newCategory.getName() );
}
#Override
public boolean areCursorRowsTheSame(Cursor oldCursor, Cursor newCursor) {
return oldCursor.getLong(0) == newCursor.getLong(0);
}
}
}
Any help is welcome. Maybe the old cursor is closed when a new cursor with same query is returned. The cursor is open at the moment I call getCursor() in onLoadFinished() but closed inside CursorCallback on first usage.
You have encountered an expected behaviour of CursorLoader — they close the old Cursor after another one arrives, whether you are currently using it or not.
The event sequence in your case is like this:
You get the (still open) Cursor and start a diff computation in some background thread X of AsyncTask thread pool
Something somewhere calls ContentResolver.notifyChanged
The CursorLoader is loading a new Cursor in another background thread Y. That new Cursor is posted to onLoadFinished and, probably, already swapped into the list adapter.
CursorLoader closes old Cursor.
Your background thread X does not know about point 2,3 and keeps using old Cursor until it finds out that it is closed by CursorLoader. An Exception is thrown.
In order to keep using the Cursor from background thread, you will have to manually manage your Cursor (without aid of CursorLoader): close it yourself if configuration change or onDestroy happen.
Alternatively, just intercept the exception and treat it as a sign that your background diff computation is being cancelled (it will shortly be performed for another Cursor anyway).
I want simple example for MVP structure in android to refresh recyclerview item not the whole list of recyclerview.
It will refresh only items in recyclerview of android.
This is a problem I've thought about quite a lot. There are two possible ways of doing it:
Pass the new List of data to the Adapter and it does the work of working out what's changed and updating the correct items.
Keep a record of the current items in your model then when the new list is calculated send ListChangeItems to the Adapter.
I'll outline both in more detail below. In both cases you need to calculate the differences between what is currently showing and the new data. I have a helper class ListDiffHelper<T> which does this comparison:
public class ListDiffHelper<T> {
private List<T> oldList;
private List<T> newList;
private List<Integer> inserted = new ArrayList<>();
private List<Integer> removed = new ArrayList<>();
public List<Integer> getInserted() {return inserted;}
public List<Integer> getRemoved() {return removed;}
public ListDiffHelper(List<T> oldList, List<T> newList) {
this.oldList = oldList;
this.newList = newList;
checkForNull();
findInserted();
findRemoved();
}
private void checkForNull() {
if (oldList == null) oldList = Collections.emptyList();
if (newList == null) newList = Collections.emptyList();
}
private void findInserted() {
Set<T> newSet = new HashSet<>(newList);
newSet.removeAll(new HashSet<>(oldList));
for (T item : newSet) {
inserted.add(newList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return lhs - rhs;
}
});
}
private void findRemoved() {
Set<T> oldSet = new HashSet<>(oldList);
oldSet.removeAll(new HashSet<>(newList));
for (T item : oldSet) {
removed.add(oldList.indexOf(item));
}
Collections.sort(inserted, new Comparator<Integer>() {
#Override
public int compare(Integer lhs, Integer rhs) {
return rhs - lhs;
}
});
}
}
For this to work properly you need to ensure that the equals() method of the Data class compares things in a suitable way.
Adapter Lead
In this case your Presenter calls getData() on the model (or subscribes to it if you're using Rx) and receives List<Data>. It then passes this List to the view through a setData(data) method which in turn give the list to the Adapter. The method in the Adapter would look something like:
private void setData(List<Data> data) {
if (this.data == null || this.data.isEmpty() || data.isEmpty()) {
this.data = data;
adapter.notifyDataSetChanged();
return;
}
ListDiffHelper<Data> diff = new ListDiffHelper<>(this.data, data);
this.data = data;
for (Integer index : diff.getRemoved()) {
notifyItemRemoved(index);
}
for (Integer index : diff.getInserted()) {
notifyItemInserted(index);
}
}
It is important to remove items first before adding new ones otherwise the order will not be maintained correctly.
Model Lead
The alternative approach is to keep the Adapter much dumber and do the calculation of what has changed in your model layer. You then need a wrapper class to send the individual changes to your View/Adapter. Something like:
public class ListChangeItem {
private static final int INSERTED = 0;
private static final int REMOVED = 1;
private int type;
private int position;
private Data data;
public ListChangeItem(int type, int position, Data data) {
this.type = type;
this.position = position;
this.data = data;
}
public int getType() {return type;}
public int getPosition() {return position;}
public Data getData() {return data;}
}
You would then pass a List of these to your Adapter via the view interface. Again it would be important to have the removals actioned before the inserts to ensure the data is in the correct order.
Do you guys have any best practices regarding using realm with a recyclerview ?
I know it's generic question but I found nothing on it on the internet. For example I run into a lot of troubles trying to implement a simple color change on a row . For example consider this typical usage:
public class User extends RealmObject {
#PrimaryKey
String name;
boolean isSelected;
...
constructor, getter and setters
}
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<User> users;
public UserAdapter(RealmResults<User> users) {
this.users = users;
}
...
public void markAsSelected(int position){
// get the old selected user and deselect it
notifyItemChanged(? how do i get the position given my User has no index ?);
// mark as selected the new user at position
}
I ran into a lot of issues since I couldn't find anything on the internet. I know this is because I don't know how to properly use realm. But finding the right way is a struggle in itself . I read all their documentation but to no avail.
EDIT : Since I was asked to --> Instead of saying "I have a bunch of issues with [that]", describe your issue(s) and we'll try to provide insights and answers to your incomprehensions.
So my problem is simple :
I have a RealmUser :
public class RealmUser extends RealmObject {
#PrimaryKey
private String key;
private String name;
private boolean isSelected;
private boolean editMode;
private RealmList<RealmItemList> lists;
public RealmUser() {}
public RealmUser(String name, RealmList<RealmItemList> lists, boolean isSelected , boolean editMode) {
this.key = UUID.randomUUID().toString();
this.name = name;
this.isSelected = isSelected;
this.editMode = editMode;
if (lists ==null){
this.lists = new RealmList<RealmItemList>();
}else{
this.lists = lists;
}
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
public boolean isEditMode() {
return editMode;
}
public void setEditMode(boolean editMode) {
this.editMode = editMode;
}
public RealmList<RealmItemList> getLists() {
return lists;
}
public void setLists(RealmList<RealmItemList> lists) {
this.lists = lists;
}
}
Which I put in a RealmResults array using :
RealmResults users = realm.where(RealmUser.class).findAll();
I pass my user array to my custom user adapter :
public class UserAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private RealmResults<RealmUser> users;
public UserAdapter(RealmResults<RealmUser> users) {
this.users = users;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if(viewType == 1){
View v = inflater.inflate(R.layout.detail_user, parent, false);
return new UserHolder(v);
}else if(viewType == 2){
View v = inflater.inflate(R.layout.edit_user, parent, false);
return new editUserHolder(v);
}else {
return null;
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
RealmUser user = users.get(position);
String userName = user.getName();
boolean isSelected = user.isSelected();
if (holder instanceof UserHolder ){
UserHolder uHolder = (UserHolder) holder;
uHolder.userText.setText(userName);
if (isSelected){
uHolder.userContainer.setBackgroundColor(Color.parseColor("#607D8B"));
}
}else if(holder instanceof editUserHolder){
editUserHolder eUserHolder = (editUserHolder) holder;
eUserHolder.userEditContainer.setBackgroundColor(Color.parseColor("#eeeeee"));
}
}
#Override
public int getItemViewType(int position) {
RealmUser user = users.get(position);
if (user.isEditMode()){
return 2;
}else {
return 1;
}
}
#Override
public int getItemCount() {
return users.size();
}
public void markAsSelected(int position, DrawerLayout mDrawerLayout , Toolbar toolbar, Realm realm){
// Here is my problem : How do I get the already selected user asuming there is one in my db and notify the UI that I changed that item.
}
That has a custom click Listener : that gets recyclerview item that was clicked using :
public class UserClickListener implements RecyclerView.OnItemTouchListener{
public static interface OnItemClickListener{
public void onItemClick(View v, int position);
}
private OnItemClickListener mListener;
private GestureDetector mGestureDetector;
public UserClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
{
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null)
{
mListener.onItemClick(childView, recyclerView.getChildPosition(childView));
return true;
}
return false;
}
});
}
#Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if(childView != null && mListener != null && mGestureDetector.onTouchEvent(e))
{
mListener.onItemClick(childView, view.getChildPosition(childView));
}
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
}
Which I add to my recyclerView with addOnItemTouchListener :
mListRecycler.addOnItemTouchListener(new UserClickListener(getActivity(), mListRecycler, new UserClickListener.OnItemClickListener(){
#Override
public void onItemClick(View view, int position)
{
UserAdapter myadapter = (UserAdapter) mListRecycler.getAdapter();
myadapter.markAsSelected(position, mDrawerLayout , mToolbar, realm);
}
}));
ANSWER FOR 0.89.0 AND ABOVE
For the latest versions, you should use RealmRecyclerViewAdapter in the realm-android-adapters repository.
Versions:
Use 1.5.0 up to 2.X
Use 2.1.1 up to 4.X
Use 3.0.0 above 5.X
OLD ANSWER FOR OLD VERSIONS:
I made this RealmRecyclerViewAdapter based on the implementation of RealmBaseAdapter.
This is for v0.89.0 AND ABOVE
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected OrderedRealmCollection<T> adapterData;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, OrderedRealmCollection<T> data) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.adapterData = data;
this.inflater = LayoutInflater.from(context);
this.listener = new RealmChangeListener<RealmResults<T>>() {
#Override
public void onChange(RealmResults<T> results) {
notifyDataSetChanged();
}
};
if (data != null) {
addListener(data);
}
}
private void addListener(OrderedRealmCollection<T> data) {
if (data instanceof RealmResults) {
RealmResults realmResults = (RealmResults) data;
realmResults.addChangeListener(listener);
} else if (data instanceof RealmList) {
RealmList realmList = (RealmList) data;
realmList.realm.handlerController.addChangeListenerAsWeakReference(listener);
} else {
throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
}
}
private void removeListener(OrderedRealmCollection<T> data) {
if (data instanceof RealmResults) {
RealmResults realmResults = (RealmResults) data;
realmResults.removeChangeListener(listener);
} else if (data instanceof RealmList) {
RealmList realmList = (RealmList) data;
realmList.realm.handlerController.removeWeakChangeListener(listener);
} else {
throw new IllegalArgumentException("RealmCollection not supported: " + data.getClass());
}
}
/**
* Returns how many items are in the data set.
*
* #return the number of items.
*/
#Override
public int getItemCount() {
if (adapterData == null) {
return 0;
}
return adapterData.size();
}
/**
* Get the data item associated with the specified position in the data set.
*
* #param position Position of the item whose data we want within the adapter's
* data set.
* #return The data at the specified position.
*/
public T getItem(int position) {
if (adapterData == null) {
return null;
}
return adapterData.get(position);
}
/**
* Get the row id associated with the specified position in the list. Note that item IDs are not stable so you
* cannot rely on the item ID being the same after {#link #notifyDataSetChanged()} or
* {#link #updateData(OrderedRealmCollection)} has been called.
*
* #param position The position of the item within the adapter's data set whose row id we want.
* #return The id of the item at the specified position.
*/
#Override
public long getItemId(int position) {
// TODO: find better solution once we have unique IDs
return position;
}
/**
* Updates the data associated with the Adapter.
*
* Note that RealmResults and RealmLists are "live" views, so they will automatically be updated to reflect the
* latest changes. This will also trigger {#code notifyDataSetChanged()} to be called on the adapter.
*
* This method is therefore only useful if you want to display data based on a new query without replacing the
* adapter.
*
* #param data the new {#link OrderedRealmCollection} to display.
*/
public void updateData(OrderedRealmCollection<T> data) {
if (listener != null) {
if (adapterData != null) {
removeListener(adapterData);
}
if (data != null) {
addListener(data);
}
}
this.adapterData = data;
notifyDataSetChanged();
}
}
This is for v0.84.0 AND ABOVE, BUT OLDER THAN v0.89.0 (updated for v0.87.5):
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected RealmResults<T> realmResults;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.realmResults = realmResults;
this.inflater = LayoutInflater.from(context);
this.listener = (!automaticUpdate) ? null : new RealmChangeListener() {
#Override
public void onChange() {
notifyDataSetChanged();
}
};
if (listener != null && realmResults != null) {
realmResults.realm.handlerController.addChangeListenerAsWeakReference(listener);
}
}
/**
* Returns how many items are in the data set.
*
* #return count of items.
*/
#Override
public int getItemCount() {
if (realmResults == null) {
return 0;
}
return realmResults.size();
}
/**
* Returns the item associated with the specified position.
*
* #param i index of item whose data we want.
* #return the item at the specified position.
*/
public T getItem(int i) {
if (realmResults == null) {
return null;
}
return realmResults.get(i);
}
/**
* Returns the current ID for an item. Note that item IDs are not stable so you cannot rely on the item ID being the
* same after {#link #notifyDataSetChanged()} or {#link #updateRealmResults(RealmResults)} has been called.
*
* #param i index of item in the adapter.
* #return current item ID.
*/
#Override
public long getItemId(int i) {
// TODO: find better solution once we have unique IDs
return i;
}
/**
* Updates the RealmResults associated to the Adapter. Useful when the query has been changed.
* If the query does not change you might consider using the automaticUpdate feature.
*
* #param queryResults the new RealmResults coming from the new query.
*/
public void updateRealmResults(RealmResults<T> queryResults) {
if (listener != null) {
// Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm
if (this.realmResults != null) {
this.realmResults.realm.removeChangeListener(listener);
}
if (queryResults != null) {
queryResults.realm.addChangeListener(listener);
}
}
this.realmResults = queryResults;
notifyDataSetChanged();
}
public void addChangeListenerAsWeakReference(RealmChangeListener realmChangeListener) {
if(realmResults != null) {
realmResults.realm.handlerController.addChangeListenerAsWeakReference(realmChangeListener);
}
}
}
This is for OLDER THAN 0.84.0:
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> { //put this in `io.realm`
protected LayoutInflater inflater;
protected RealmResults<T> realmResults;
protected Context context;
private final RealmChangeListener listener;
public RealmRecyclerViewAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) {
if(context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
this.context = context;
this.realmResults = realmResults;
this.inflater = LayoutInflater.from(context);
this.listener = (!automaticUpdate) ? null : new RealmChangeListener() {
#Override
public void onChange() {
notifyDataSetChanged();
}
};
if(listener != null && realmResults != null) {
realmResults.getRealm()
.addChangeListener(listener);
}
}
#Override
public long getItemId(int i) {
// TODO: find better solution once we have unique IDs
return i;
}
public T getItem(int i) {
if(realmResults == null) {
return null;
}
return realmResults.get(i);
}
public void updateRealmResults(RealmResults<T> queryResults) {
if(listener != null) {
// Making sure that Adapter is refreshed correctly if new RealmResults come from another Realm
if(this.realmResults != null) {
realmResults.getRealm().removeChangeListener(listener);
}
if(queryResults != null) {
queryResults.getRealm().addChangeListener(listener);
}
}
this.realmResults = queryResults;
notifyDataSetChanged();
}
#Override
public int getItemCount() {
if(realmResults == null) {
return 0;
}
return realmResults.size();
}
}
Some of the answers above include reflection, not to mention that a sectioned RecyclerView would cause complications. They also do not support adding and removing items. Here is my version of the RecyclerView Adapter that works with Realm, supports a sectioned RecyclerView, also adds and removes items at arbitrary positions if need be
Here is our AbstractRealmAdapter that takes care of all the low level stuff, displaying headers, footers, items, loading data inside RealmResults, managing item types
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmResults;
public abstract class AbstractRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
public static final int HEADER_COUNT = 1;
public static final int FOOTER_COUNT = 1;
//Our data source
protected RealmResults<T> mResults;
public AbstractRealmAdapter(Realm realm) {
//load data from subclasses
mResults = loadData(realm);
notifyDataSetChanged();
}
public int getHeaderCount() {
return hasHeader() ? HEADER_COUNT : 0;
}
public int getFooterCount() {
return hasFooter() ? FOOTER_COUNT : 0;
}
public boolean isHeader(int position) {
if (hasHeader()) {
return position < HEADER_COUNT;
} else {
return false;
}
}
public boolean isFooter(int position) {
if (hasFooter()) {
return position >= getCount() + getHeaderCount();
} else {
return false;
}
}
#Override
public long getItemId(int i) {
return i;
}
#Override
public final int getItemViewType(int position) {
if (isHeader(position)) {
return ItemType.HEADER.ordinal();
} else if (isFooter(position)) {
return ItemType.FOOTER.ordinal();
} else {
return ItemType.ITEM.ordinal();
}
}
/**
* #param position the position within our adapter inclusive of headers,items and footers
* #return an item only if it is not a header or a footer, otherwise returns null
*/
public T getItem(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
return mResults.get(position - getHeaderCount());
}
return null;
}
#Override
public final int getItemCount() {
return getHeaderCount() + getCount() + getFooterCount();
}
public final int getCount() {
return mResults.size();
}
public abstract boolean hasHeader();
public abstract boolean hasFooter();
public void setData(RealmResults<T> results) {
mResults = results;
notifyDataSetChanged();
}
protected abstract RealmResults<T> loadData(Realm realm);
public enum ItemType {
HEADER, ITEM, FOOTER;
}
}
To add items by some method or remove items by swipe to delete, we have an extension in the form of AbstractMutableRealmAdapter that looks as shown below
import android.support.v7.widget.RecyclerView;
import io.realm.Realm;
import io.realm.RealmObject;
public abstract class AbstractMutableRealmAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends AbstractRealmAdapter<T, VH> implements OnSwipeListener {
private Realm realm;
public AbstractMutableRealmAdapter(Realm realm) {
//call the superclass constructor to load data from subclasses into realmresults
super(realm);
this.realm = realm;
}
public void add(T item, boolean update) {
realm.beginTransaction();
T phraseToWrite = (update == true) ? realm.copyToRealmOrUpdate(item) : realm.copyToRealm(item);
realm.commitTransaction();
notifyItemRangeChanged(0, mResults.size());
}
#Override
public final void onSwipe(int position) {
if (!isHeader(position) && !isFooter(position) && !mResults.isEmpty()) {
int itemPosition = position - getHeaderCount();
realm.beginTransaction();
T item = mResults.get(itemPosition);
item.removeFromRealm();
realm.commitTransaction();
notifyItemRemoved(position);
}
}
}
Notice the use of the interface OnSwipeListener which looks like this
public interface OnSwipeListener {
/**
* #param position the position of the item that was swiped within the RecyclerView
*/
void onSwipe(int position);
}
This SwipeListener is used to perform a Swipe to delete inside our TouchHelperCallback which in turn is used to delete the objects from Realm directly and looks as follows
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public class TouchHelperCallback extends ItemTouchHelper.Callback {
private final OnSwipeListener mSwipeListener;
public TouchHelperCallback(OnSwipeListener adapter) {
mSwipeListener = adapter;
}
/**
* #return false if you dont want to enable drag else return true
*/
#Override
public boolean isLongPressDragEnabled() {
return false;
}
/**
* #return true of you want to enable swipe in your RecyclerView else return false
*/
#Override
public boolean isItemViewSwipeEnabled() {
return true;
}
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//We want to let the person swipe to the right on devices that run LTR and let the person swipe from right to left on devices that run RTL
int swipeFlags = ItemTouchHelper.END;
return makeMovementFlags(0, swipeFlags);
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mSwipeListener.onSwipe(viewHolder.getAdapterPosition());
}
}
The full implementation demo is available here for review https://github.com/slidenerd/SpamWordList/tree/spamphraser_with_realmresults_base Feel free to suggest any improvements
I replaced the notifyXXX methods with notifyDataSetChanged, RealmResults objects are live objects which means they automatically change when the data is updated, I tried calling notifyXXX methods and they caused an RecyclerView inconsistency exception, I am well aware of the fact that notifyDataSetChanged() would mess with animations, will keep you guys updated on a solution that overcomes the inconsistency error and at the same time provides a good adapter experience
Now that with Realm 0.88.2 we can make a RecyclerView adapter that updates the RecyclerView with more precision than using notifyDataSetChanged() every time. This can be accomplished by using the new ability create custom methods.
Overriding the equals method, in the realm object that will be used with the recycler adapter, is all that will be needed. (You don't actually need to override equals... but you may find that realm objects do not equal each other when you expect them to. This will lead to unnecessary recyclerview updates after running diff)
Then add Google's java-diff-utils to your gradle dependencies
compile 'com.googlecode.java-diff-utils:diffutils:1.3.0'
Using this RealmRecyclerViewAdapter implementation a copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update the RecyclerView as appropriate
public abstract class RealmRecyclerViewAdapter<T extends RealmObject, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
protected RealmResults<T> realmResults;
protected List<T> lastCopyOfRealmResults;
int maxDepth = 0;
private RealmChangeListener realmResultsListener;
Realm realm;
public RealmRecyclerViewAdapter(RealmResults<T> realmResults, boolean automaticUpdate) {
this(realmResults, automaticUpdate, 0);
}
/**
*
* #param realmResults
* #param automaticUpdate
* #param maxDepth limit of the deep copy when copying realmResults. All references after this depth will be {#code null}. Starting depth is {#code 0}.
* A copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update
* the RecyclerView as appropriate
*/
public RealmRecyclerViewAdapter(RealmResults<T> realmResults, boolean automaticUpdate, int maxDepth) {
this.realmResultsListener = (!automaticUpdate) ? null : getRealmResultsChangeListener();
if (realmResultsListener != null && realmResults != null) {
realmResults.addChangeListener(realmResultsListener);
}
this.realmResults = realmResults;
realm = Realm.getDefaultInstance();
this.maxDepth = maxDepth;
lastCopyOfRealmResults = realm.copyFromRealm(realmResults, this.maxDepth);
}
#Override
public int getItemCount() {
return realmResults != null ? realmResults.size() : 0;
}
/**
* Make sure this is called before a view is destroyed to avoid memory leaks do to the listeners.
* Do this by calling setAdapter(null) on your RecyclerView
* #param recyclerView
*/
#Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
if (realmResultsListener != null) {
if (realmResults != null) {
realmResults.removeChangeListener(realmResultsListener);
}
}
realm.close();
}
/**
* Update the RealmResults associated with the Adapter. Useful when the query has been changed.
* If the query does not change you might consider using the automaticUpdate feature.
*
* #param queryResults the new RealmResults coming from the new query.
* #param maxDepth limit of the deep copy when copying realmResults. All references after this depth will be {#code null}. Starting depth is {#code 0}.
* A copy of realmResults is made at start, and on every change to compare against future changes. Detected changes are used to update
* the RecyclerView as appropriate
*/
public void updateRealmResults(RealmResults<T> queryResults, int maxDepth) {
if (realmResultsListener != null) {
if (realmResults != null) {
realmResults.removeChangeListener(realmResultsListener);
}
}
realmResults = queryResults;
if (realmResults != null && realmResultsListener !=null) {
realmResults.addChangeListener(realmResultsListener);
}
this.maxDepth = maxDepth;
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,this.maxDepth);
notifyDataSetChanged();
}
public T getItem(int position) {
return realmResults.get(position);
}
public int getRealmResultsSize(){
return realmResults.size();
}
private RealmChangeListener getRealmResultsChangeListener() {
return new RealmChangeListener<RealmResults<T>>() {
#Override
public void onChange(RealmResults<T> element) {
if (lastCopyOfRealmResults != null && !lastCopyOfRealmResults.isEmpty()) {
if (realmResults.isEmpty()) {
// If the list is now empty, just notify the recyclerView of the change.
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
notifyDataSetChanged();
return;
}
Patch patch = DiffUtils.diff(lastCopyOfRealmResults, realmResults);
List<Delta> deltas = patch.getDeltas();
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
if (!deltas.isEmpty()) {
List<Delta> deleteDeltas = new ArrayList<>();
List<Delta> insertDeltas = new ArrayList<>();
for (final Delta delta : deltas) {
switch (delta.getType()){
case DELETE:
deleteDeltas.add(delta);
break;
case INSERT:
insertDeltas.add(delta);
break;
case CHANGE:
notifyItemRangeChanged(
delta.getRevised().getPosition(),
delta.getRevised().size());
break;
}
}
for (final Delta delta : deleteDeltas) {
notifyItemRangeRemoved(
delta.getOriginal().getPosition(),
delta.getOriginal().size());
}
//item's should be removed before insertions are performed
for (final Delta delta : insertDeltas) {
notifyItemRangeInserted(
delta.getRevised().getPosition(),
delta.getRevised().size());
}
}
} else {
notifyDataSetChanged();
lastCopyOfRealmResults = realm.copyFromRealm(realmResults,maxDepth);
}
}
};
}
}
Your post does not even contain a real question.
Have you checked out this post: http://gradlewhy.ghost.io/realm-results-with-recyclerview/ ?
Not sure why you wouldn't just use an ArrayList in your adapter and add all elements from the RealmResult to that list though. Could anyone explain why the solution in the blog post would be better?
Implementing the Realm Add-on from Thorben Primke is a very convenient method
for handling Recycler View applications with Realm databases. His github has good examples of the ways that it can be implemented.
I'll include mine here so you have an example. First modify your project build gradle for jitpack.io:
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
Then your module gradle to point to the library: (note , check for latest version)
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.github.thorbenprimke:realm-recyclerview:0.9.20'
Create the xml layout for a recycler view using the RealmRecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<co.moonmonkeylabs.realmrecyclerview.RealmRecyclerView
android:id="#+id/realm_recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rrvIsRefreshable="true"
app:rrvEmptyLayoutId="#layout/empty_view"
app:rrvLayoutType="LinearLayout"
app:rrvSwipeToDelete="true"
/>
</RelativeLayout>
Now in your RealmRecycler Fragment obtain a Realm query result of RealmObjects, inflate and define a Primke RealmAdapter:
Log.i(TAG, " Obtain Filtered List");
final RealmResults <Session> realmResults = queryD.findAllSorted(
"sessionId", Sort.DESCENDING);
Log.i(TAG, " Inflate realm List");
View view = inflater.inflate(R.layout.realm_card_recycler2, null);
Log.i(TAG, " Define and configure SessionRealmAdapter");
SessionRealmAdapter sessionRealmAdapter =
new SessionRealmAdapter(getActivity(), realmResults, true, true);`enter code here`
RealmRecyclerView realmRecyclerView =
(RealmRecyclerView) view.findViewById(R.id.realm_recycle_view);
realmRecyclerView.setAdapter(sessionRealmAdapter);
Finally configure the Realm Adapter for whatever you want for actions. I've got a couple for clicks and turned on the swipe to delete for deleting realm records.
public class SessionRealmAdapter
extends RealmBasedRecyclerViewAdapter<Session, SessionRealmAdapter.ViewHolder> {
public class ViewHolder extends RealmViewHolder {
public TextView sessionTextView;
public ViewHolder(FrameLayout container) {
super(container);
this.sessionTextView = (TextView) container.findViewById(R.id.session_text_view);
}
}
public SessionRealmAdapter(
Context context,
RealmResults<Session> realmResults,
boolean automaticUpdate,
boolean animateResults) {
super(context, realmResults, automaticUpdate, animateResults);
}
#Override
public ViewHolder onCreateRealmViewHolder(ViewGroup viewGroup, int viewType) {
View v = inflater.inflate(R.layout.session_simple_view, viewGroup, false);
return new ViewHolder((FrameLayout) v);
}
#Override
public void onBindRealmViewHolder(ViewHolder viewHolder, int position) {
final Session singleSession = realmResults.get(position);
viewHolder.sessionTextView.setText(singleSession.gettMethod());
viewHolder.sessionTextView.setOnClickListener(
new View.OnClickListener(){
#Override
public void onClick(View v){
selectSession(singleSession);
showMessage(" Selected "+singleSession.gettMethod());
}
}
);
viewHolder.sessionTextView.setOnLongClickListener(
new View.OnLongClickListener(){
#Override
public boolean onLongClick(View v){
showInformationDialog(singleSession);
showMessage("Long click selected for "
+singleSession.getSessionTitle());
return true;
}
}
);
}
}
I have the database already finished and I would like to add a few rows that are present on the first time opening the app. The main issue, not knowing where in the application to implement this. For example, when the user opens the app for the first time, there is an example item. The item can be deleted. After the row is deleted it will never show up again. I am using Androrm (object relational mapper) androrm home page. My main question: How do I add a single row to the database (where & how) before release. Within the onCreate, will add a row each time, the class is opened.
Took out most code to make it simple.
Implementation
public class LogFirst extends Model {
protected CharField db_oneName;
public LogFirst() {
super(true);
db_oneName = new CharField(80);
}
public void setDB_oneName(String name1) {
db_oneName.set(name1);
}
public String getDB_oneName() {
return db_oneName.get();
}
#Override
public String toString() {
return db_oneName.get();
}
public static List<LogFirst> all() {
return LogFirst.objects().all().toList();
}
public static QuerySet<LogFirst> objects() {
return LogFirst.objects(context(), LogFirst.class);
}
public boolean save() {
Format formatter = new SimpleDateFormat("ddmmhhss");
String id = formatter.format(Calendar.getInstance().getTime()) + "";
return this.save(context(), Integer.valueOf(id));
}
public boolean delete() {
return this.delete(context());
}
private static Context context() {
return ExtendsActivity.context();
}
}
Saving
LogFirst lf = new LogFirst();
lf.setDB_oneName(name.getText().toString());
lf.save();
Adapter
public class LogFirstAdapter extends ArrayAdapter<LogFirst> {
Context mContext;
List<LogFirst> mLogs;
public LogFirstAdapter(Context context, int textViewResourceId, List<LogFirst> logs) {
super(context, textViewResourceId);
mContext = context;
mLogs = logs;
}
public void setLogs(List<LogFirst> logs) {
mLogs = logs;
}
public List<LogFirst> getLogs() {
return mLogs;
}
public void add(LogFirst log) {
mLogs.add(log);
}
public void remove(LogFirst log) {
mLogs.remove(log);
}
public int getCount() {
return mLogs.size();
}
public LogFirst getItem(int position) {
return mLogs.get(position);
}
public View getView(int position, View convertView, ViewGroup parent) {
LogFirstRow view = (LogFirstRow) convertView;
if (view == null) {
view = new LogFirstRow(mContext);
}
LogFirst log = getItem(position);
view.setLog(log);
return view;
}
}
Ideally you'd do something like that in a migration. I'm not familiar with Androrm but it looks like they have some support for migration: http://www.androrm.com/documentation/models/migrations/
Try putting in the code to create the new records in the overriden migrate function. The docs say migrations will be kept track of but I'm not sure how it will play out so test to see what happens.