From 278f98b37161bc78b71ed14f167b622debb410f4 Mon Sep 17 00:00:00 2001 From: thesurix Date: Thu, 12 May 2016 08:16:36 +0200 Subject: [PATCH] Realm RecyclerView adapter (#14) --- adapters/build.gradle | 1 + .../io/realm/RealmRecyclerAdapterTests.java | 225 +++++++++++++++ .../realm/adapter/RealmRecyclerAdapter.java | 55 ++++ .../realm/entity/UnsupportedCollection.java | 258 ++++++++++++++++++ .../io/realm/RealmBaseRecyclerAdapter.java | 162 +++++++++++ 5 files changed, 701 insertions(+) create mode 100644 adapters/src/androidTest/java/io/realm/RealmRecyclerAdapterTests.java create mode 100644 adapters/src/androidTest/java/io/realm/adapter/RealmRecyclerAdapter.java create mode 100644 adapters/src/androidTest/java/io/realm/entity/UnsupportedCollection.java create mode 100644 adapters/src/main/java/io/realm/RealmBaseRecyclerAdapter.java diff --git a/adapters/build.gradle b/adapters/build.gradle index 2e842a7..3dd313b 100644 --- a/adapters/build.gradle +++ b/adapters/build.gradle @@ -26,6 +26,7 @@ configurations { dependencies { compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:recyclerview-v7:23.3.0' androidTestCompile 'com.android.support.test:runner:0.4.1' androidTestCompile 'com.android.support.test:rules:0.4.1' diff --git a/adapters/src/androidTest/java/io/realm/RealmRecyclerAdapterTests.java b/adapters/src/androidTest/java/io/realm/RealmRecyclerAdapterTests.java new file mode 100644 index 0000000..26b39d4 --- /dev/null +++ b/adapters/src/androidTest/java/io/realm/RealmRecyclerAdapterTests.java @@ -0,0 +1,225 @@ +/* + * Copyright 2016 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; +import android.support.test.rule.UiThreadTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.widget.FrameLayout; + +import io.realm.adapter.RealmRecyclerAdapter; +import io.realm.entity.AllJavaTypes; +import io.realm.entity.UnsupportedCollection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +@RunWith(AndroidJUnit4.class) +public class RealmRecyclerAdapterTests { + + @Rule + public final UiThreadTestRule uiThreadTestRule = new UiThreadTestRule(); + + private Context context; + + private static final int TEST_DATA_SIZE = 47; + private static final boolean AUTOMATIC_UPDATE = true; + + private Realm realm; + + @Before + public void setUp() throws Exception { + context = InstrumentationRegistry.getInstrumentation().getContext(); + RealmConfiguration realmConfig = new RealmConfiguration.Builder(context).modules(new RealmTestModule()).build(); + Realm.deleteRealm(realmConfig); + realm = Realm.getInstance(realmConfig); + + realm.beginTransaction(); + for (int i = 0; i < TEST_DATA_SIZE; i++) { + AllJavaTypes allTypes = realm.createObject(AllJavaTypes.class, i); + allTypes.setFieldString("test data " + i); + } + realm.commitTransaction(); + } + + @After + public void tearDown() throws Exception { + realm.close(); + } + + @Test + public void constructor_testRecyclerAdapterParameterExceptions() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + try { + new RealmRecyclerAdapter(null, resultList, AUTOMATIC_UPDATE); + fail("Should throw exception if context is null"); + } catch (IllegalArgumentException ignore) { + } + } + + @Test + @UiThreadTest + public void clear() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + realm.beginTransaction(); + resultList.deleteAllFromRealm(); + realm.commitTransaction(); + + assertEquals(0, realmAdapter.getItemCount()); + assertEquals(0, resultList.size()); + } + + @Test + @UiThreadTest + public void updateData_realmResultInAdapter() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + resultList.sort(AllJavaTypes.FIELD_STRING); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, false); + assertEquals(resultList.first().getFieldString(), realmAdapter.getData().first().getFieldString()); + assertEquals(resultList.size(), realmAdapter.getData().size()); + + realm.beginTransaction(); + AllJavaTypes allTypes = realm.createObject(AllJavaTypes.class, TEST_DATA_SIZE); + allTypes.setFieldString("test data " + TEST_DATA_SIZE); + realm.commitTransaction(); + assertEquals(resultList.last().getFieldString(), realmAdapter.getData().last().getFieldString()); + assertEquals(resultList.size(), realmAdapter.getData().size()); + + RealmResults emptyResultList = realm.where(AllJavaTypes.class).equalTo(AllJavaTypes.FIELD_STRING, "Not there").findAll(); + realmAdapter.updateData(emptyResultList); + assertEquals(emptyResultList.size(), realmAdapter.getData().size()); + } + + @Test + @UiThreadTest + public void updateData_realmUnsupportedCollectionInAdapter() { + try { + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, null, AUTOMATIC_UPDATE); + realmAdapter.updateData(new UnsupportedCollection()); + fail("Should throw exception if there is unsupported collection"); + } catch (IllegalArgumentException ignore) { + } + } + + @Test + @UiThreadTest + public void getItemCount_emptyRealmResult() { + RealmResults resultList = realm.where(AllJavaTypes.class).equalTo(AllJavaTypes.FIELD_STRING, "Not there").findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + assertEquals(0, resultList.size()); + assertEquals(0, realmAdapter.getData().size()); + assertEquals(0, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void getItem_testGettingData() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + + assertEquals(resultList.first().getFieldString(), realmAdapter.getItem(0).getFieldString()); + assertEquals(resultList.size(), realmAdapter.getData().size()); + assertEquals(resultList.last().getFieldString(), realmAdapter.getData().last().getFieldString()); + } + + @Test + @UiThreadTest + public void getItem_testGettingNullData() { + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, null, AUTOMATIC_UPDATE); + assertNull(realmAdapter.getItem(0)); + } + + @Test + @UiThreadTest + public void getItemId_testGetItemId() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + for (int i = 0; i < resultList.size(); i++) { + assertEquals(i, realmAdapter.getItemId(i)); + } + } + + @Test + @UiThreadTest + public void getItemCount_testGetCount() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + assertEquals(TEST_DATA_SIZE, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void getItemCount_testNullResults() { + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, null, AUTOMATIC_UPDATE); + assertEquals(0, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void getItemCount_testNotValidResults() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + + realm.close(); + assertEquals(0, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void getItemCount_testNonNullToNullResults() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + realmAdapter.updateData(null); + + assertEquals(0, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void getItemCount_testNullToNonNullResults() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, null, AUTOMATIC_UPDATE); + assertEquals(0, realmAdapter.getItemCount()); + + realmAdapter.updateData(resultList); + assertEquals(TEST_DATA_SIZE, realmAdapter.getItemCount()); + } + + @Test + @UiThreadTest + public void viewHolderTestForSimpleView() { + RealmResults resultList = realm.where(AllJavaTypes.class).findAll(); + RealmRecyclerAdapter realmAdapter = new RealmRecyclerAdapter(context, resultList, AUTOMATIC_UPDATE); + + RealmRecyclerAdapter.ViewHolder holder = realmAdapter.onCreateViewHolder(new FrameLayout(context), 0); + assertNotNull(holder.textView); + + realmAdapter.onBindViewHolder(holder, 0); + assertEquals(resultList.first().getFieldString(), holder.textView.getText()); + } +} diff --git a/adapters/src/androidTest/java/io/realm/adapter/RealmRecyclerAdapter.java b/adapters/src/androidTest/java/io/realm/adapter/RealmRecyclerAdapter.java new file mode 100644 index 0000000..c376d4d --- /dev/null +++ b/adapters/src/androidTest/java/io/realm/adapter/RealmRecyclerAdapter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.adapter; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import io.realm.RealmBaseRecyclerAdapter; +import io.realm.RealmResults; +import io.realm.entity.AllJavaTypes; + +public class RealmRecyclerAdapter extends RealmBaseRecyclerAdapter { + + public static class ViewHolder extends RecyclerView.ViewHolder { + public TextView textView; + + public ViewHolder(final View itemView) { + super(itemView); + textView = (TextView) itemView.findViewById(android.R.id.text1); + } + } + + public RealmRecyclerAdapter(final Context context, final RealmResults realmResults, final boolean automaticUpdate) { + super(context, realmResults, automaticUpdate); + } + + @Override + public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { + View view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, final int position) { + AllJavaTypes item = getItem(position); + holder.textView.setText(item.getFieldString()); + } +} diff --git a/adapters/src/androidTest/java/io/realm/entity/UnsupportedCollection.java b/adapters/src/androidTest/java/io/realm/entity/UnsupportedCollection.java new file mode 100644 index 0000000..8a110aa --- /dev/null +++ b/adapters/src/androidTest/java/io/realm/entity/UnsupportedCollection.java @@ -0,0 +1,258 @@ +/* + * Copyright 2016 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.entity; + +import android.support.annotation.NonNull; + +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import io.realm.OrderedRealmCollection; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; +import io.realm.Sort; + +/** + * Class that implements {@link OrderedRealmCollection} interface and can be used in testing supported + * collections along with {@link RealmResults} and {@link io.realm.RealmList}. + */ +public class UnsupportedCollection implements OrderedRealmCollection { + + @Override + public E first() { + return null; + } + + @Override + public E last() { + return null; + } + + @Override + public RealmResults sort(final String fieldName) { + return null; + } + + @Override + public RealmResults sort(final String fieldName, final Sort sortOrder) { + return null; + } + + @Override + public RealmResults sort(final String fieldName1, final Sort sortOrder1, final String fieldName2, final Sort sortOrder2) { + return null; + } + + @Override + public RealmResults sort(final String[] fieldNames, final Sort[] sortOrders) { + return null; + } + + @Override + public void deleteFromRealm(final int location) { + + } + + @Override + public boolean deleteFirstFromRealm() { + return false; + } + + @Override + public boolean deleteLastFromRealm() { + return false; + } + + @Override + public void add(final int location, final E object) { + + } + + @Override + public boolean add(final E object) { + return false; + } + + @Override + public boolean addAll(final int location, final Collection collection) { + return false; + } + + @Override + public boolean addAll(final Collection collection) { + return false; + } + + @Override + public void clear() { + + } + + @Override + public boolean contains(final Object object) { + return false; + } + + @Override + public boolean containsAll(final Collection collection) { + return false; + } + + @Override + public E get(final int location) { + return null; + } + + @Override + public int indexOf(final Object object) { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @NonNull + @Override + public Iterator iterator() { + return null; + } + + @Override + public int lastIndexOf(final Object object) { + return 0; + } + + @Override + public ListIterator listIterator() { + return null; + } + + @NonNull + @Override + public ListIterator listIterator(final int location) { + return null; + } + + @Override + public E remove(final int location) { + return null; + } + + @Override + public boolean remove(final Object object) { + return false; + } + + @Override + public boolean removeAll(final Collection collection) { + return false; + } + + @Override + public boolean retainAll(final Collection collection) { + return false; + } + + @Override + public E set(final int location, final E object) { + return null; + } + + @Override + public int size() { + return 0; + } + + @NonNull + @Override + public List subList(final int start, final int end) { + return null; + } + + @NonNull + @Override + public Object[] toArray() { + return new Object[0]; + } + + @NonNull + @Override + public T[] toArray(final T[] array) { + return null; + } + + @Override + public RealmQuery where() { + return null; + } + + @Override + public Number min(final String fieldName) { + return null; + } + + @Override + public Number max(final String fieldName) { + return null; + } + + @Override + public Number sum(final String fieldName) { + return null; + } + + @Override + public double average(final String fieldName) { + return 0; + } + + @Override + public Date maxDate(final String fieldName) { + return null; + } + + @Override + public Date minDate(final String fieldName) { + return null; + } + + @Override + public boolean deleteAllFromRealm() { + return false; + } + + @Override + public boolean isLoaded() { + return false; + } + + @Override + public boolean load() { + return false; + } + + @Override + public boolean isValid() { + return false; + } +} diff --git a/adapters/src/main/java/io/realm/RealmBaseRecyclerAdapter.java b/adapters/src/main/java/io/realm/RealmBaseRecyclerAdapter.java new file mode 100644 index 0000000..4596e40 --- /dev/null +++ b/adapters/src/main/java/io/realm/RealmBaseRecyclerAdapter.java @@ -0,0 +1,162 @@ +/* + * Copyright 2016 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; + +/** + * The RealmBaseRecyclerAdapter class is an abstract utility class for binding RecyclerView UI elements to Realm data. + *

+ * This adapter will automatically handle any updates to its data and call notifyDataSetChanged() as appropriate. + * Currently there is no support for RecyclerView's data callback methods like notifyItemInserted(int), notifyItemRemoved(int), + * notifyItemChanged(int) etc. + * It means that, there is no possibility to use default data animations. + *

+ * The RealmAdapter will stop receiving updates if the Realm instance providing the {@link OrderedRealmCollection} is + * closed. + * + * @param type of {@link RealmObject} stored in the adapter. + * @param type of RecyclerView.ViewHolder used in the adapter. + */ +public abstract class RealmBaseRecyclerAdapter extends RecyclerView.Adapter { + + protected final LayoutInflater inflater; + protected final Context context; + private final boolean hasAutoUpdates; + private final RealmChangeListener listener; + private OrderedRealmCollection adapterData; + + public RealmBaseRecyclerAdapter(Context context, OrderedRealmCollection data, boolean autoUpdate) { + if (context == null) { + throw new IllegalArgumentException("Context can not be null"); + } + + this.context = context; + this.adapterData = data; + this.inflater = LayoutInflater.from(context); + this.hasAutoUpdates = autoUpdate; + this.listener = hasAutoUpdates ? new RealmChangeListener() { + @Override + public void onChange(BaseRealm results) { + notifyDataSetChanged(); + } + } : null; + } + + @Override + public void onAttachedToRecyclerView(final RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + if (hasAutoUpdates && isDataValid()) { + addListener(adapterData); + } + } + + @Override + public void onDetachedFromRecyclerView(final RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + if (hasAutoUpdates && isDataValid()) { + removeListener(adapterData); + } + } + + /** + * 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 notifyDataSetChanged() or {@link #updateData(OrderedRealmCollection)} has been called. + * + * @param index position of item in the adapter. + * @return current item ID. + */ + @Override + public long getItemId(final int index) { + return index; + } + + @Override + public int getItemCount() { + return isDataValid() ? adapterData.size() : 0; + } + + /** + * Returns the item associated with the specified position. + * Can return {@code null} if provided Realm instance by {@link OrderedRealmCollection} is closed. + * + * @param index index of the item. + * @return the item at the specified position, {@code null} if adapter data is not valid. + */ + public T getItem(int index) { + return isDataValid() ? adapterData.get(index) : null; + } + + /** + * Returns data associated with this adapter. + * + * @return adapter data. + */ + public OrderedRealmCollection getData() { + return adapterData; + } + + /** + * Updates the data 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 data the new {@link OrderedRealmCollection} to display. + */ + public void updateData(OrderedRealmCollection data) { + if (hasAutoUpdates) { + if (adapterData != null) { + removeListener(adapterData); + } + if (data != null) { + addListener(data); + } + } + + this.adapterData = data; + notifyDataSetChanged(); + } + + private void addListener(OrderedRealmCollection 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 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()); + } + } + + private boolean isDataValid() { + return adapterData != null && adapterData.isValid(); + } +}