Skip to content
This repository was archived by the owner on Mar 1, 2023. It is now read-only.

Commit d38c138

Browse files
gardellMagnus Ernstsson
authored andcommitted
RecyclerView stable ids for rvadapter/rvdatabinding repository presenters. (#109)
Modified the testapp to use stable ids. Notes are sorted on their content text instead of ID. Whenever the content is changed causing a reorder, notice the animations caused by the RecyclerView ItemAnimator.
1 parent 2b241e5 commit d38c138

File tree

9 files changed

+157
-20
lines changed

9 files changed

+157
-20
lines changed

extensions/rvadapter/src/main/java/com/google/android/agera/rvadapter/RepositoryPresenterCompiler.java

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import com.google.android.agera.Function;
2323
import com.google.android.agera.Result;
2424
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPLayout;
25-
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPViewBinderCompile;
25+
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPViewBinderStableIdCompile;
2626

2727
import android.support.annotation.LayoutRes;
2828
import android.support.annotation.NonNull;
@@ -32,29 +32,31 @@
3232
import java.util.List;
3333

3434
@SuppressWarnings({"unchecked, rawtypes"})
35-
final class RepositoryPresenterCompiler implements RPLayout, RPViewBinderCompile {
35+
final class RepositoryPresenterCompiler implements RPLayout, RPViewBinderStableIdCompile {
3636
@NonNull
3737
private static final NullBinder NULL_BINDER = new NullBinder();
3838
private Function<Object, Integer> layoutForItem;
3939
@NonNull
4040
private Binder binder = NULL_BINDER;
41+
@NonNull
42+
private Function<Object, Long> stableIdForItem = staticFunction(RecyclerView.NO_ID);
4143

4244
@NonNull
4345
@Override
4446
public RepositoryPresenter<List> forList() {
45-
return new ListBasicRepositoryPresenter(layoutForItem, binder);
47+
return new ListBasicRepositoryPresenter(layoutForItem, binder, stableIdForItem);
4648
}
4749

4850
@NonNull
4951
@Override
5052
public RepositoryPresenter<Result> forResult() {
51-
return new SingleResultRepositoryPresenter(layoutForItem, binder);
53+
return new SingleResultRepositoryPresenter(layoutForItem, binder, stableIdForItem);
5254
}
5355

5456
@NonNull
5557
@Override
5658
public RepositoryPresenter<Result<List>> forResultList() {
57-
return new ListResultRepositoryPresenter(layoutForItem, binder);
59+
return new ListResultRepositoryPresenter(layoutForItem, binder, stableIdForItem);
5860
}
5961

6062
@NonNull
@@ -78,6 +80,13 @@ public Object bindWith(@NonNull final Binder binder) {
7880
return this;
7981
}
8082

83+
@NonNull
84+
@Override
85+
public Object stableIdForItem(@NonNull final Function stableIdForItem) {
86+
this.stableIdForItem = stableIdForItem;
87+
return this;
88+
}
89+
8190
private static final class NullBinder implements Binder {
8291
@Override
8392
public void bind(@NonNull Object o, @NonNull Object o2) {}
@@ -89,11 +98,15 @@ private abstract static class BasicRepositoryPresenter<TVal, T>
8998
private final Function<Object, Integer> layoutId;
9099
@NonNull
91100
private final Binder<TVal, View> binder;
101+
@NonNull
102+
private final Function<TVal, Long> stableIdForItem;
92103

93104
BasicRepositoryPresenter(@NonNull final Function<Object, Integer> layoutId,
94-
@NonNull final Binder<TVal, View> binder) {
105+
@NonNull final Binder<TVal, View> binder,
106+
@NonNull final Function<TVal, Long> stableIdForItem) {
95107
this.layoutId = checkNotNull(layoutId);
96108
this.binder = checkNotNull(binder);
109+
this.stableIdForItem = stableIdForItem;
97110
}
98111

99112
@NonNull
@@ -104,6 +117,11 @@ public final int getLayoutResId(@NonNull final T data, final int index) {
104117
return layoutId.apply(getValue(data, index));
105118
}
106119

120+
@Override
121+
public long getItemId(@NonNull T data, int index) {
122+
return stableIdForItem.apply(getValue(data, index));
123+
}
124+
107125
@SuppressWarnings("unchecked")
108126
@Override
109127
public void bind(@NonNull final T data, final int index,
@@ -116,8 +134,8 @@ private static final class ListBasicRepositoryPresenter<T>
116134
extends BasicRepositoryPresenter<T, List<T>> {
117135

118136
public ListBasicRepositoryPresenter(@NonNull final Function<Object, Integer> layoutId,
119-
@NonNull final Binder<T, View> binder) {
120-
super(layoutId, binder);
137+
@NonNull final Binder<T, View> binder, Function<T, Long> stableIdForItem) {
138+
super(layoutId, binder, stableIdForItem);
121139
}
122140

123141
@Override
@@ -136,8 +154,8 @@ private abstract static class ResultRepositoryPresenter<TVal, T>
136154
extends BasicRepositoryPresenter<TVal, Result<T>> {
137155

138156
ResultRepositoryPresenter(@NonNull final Function<Object, Integer> layoutId,
139-
@NonNull final Binder<TVal, View> binder) {
140-
super(layoutId, binder);
157+
@NonNull final Binder<TVal, View> binder, Function<TVal, Long> stableIdForItem) {
158+
super(layoutId, binder, stableIdForItem);
141159
}
142160

143161
@NonNull
@@ -160,8 +178,8 @@ public final int getItemCount(@NonNull final Result<T> data) {
160178
private static final class SingleResultRepositoryPresenter<T>
161179
extends ResultRepositoryPresenter<T, T> {
162180
public SingleResultRepositoryPresenter(@NonNull final Function<Object, Integer> layoutId,
163-
@NonNull final Binder<T, View> binder) {
164-
super(layoutId, binder);
181+
@NonNull final Binder<T, View> binder, Function stableIdForItem) {
182+
super(layoutId, binder, stableIdForItem);
165183
}
166184

167185
@Override
@@ -180,8 +198,8 @@ private static final class ListResultRepositoryPresenter<T>
180198
extends ResultRepositoryPresenter<T, List<T>> {
181199

182200
public ListResultRepositoryPresenter(@NonNull final Function<Object, Integer> layoutId,
183-
@NonNull final Binder<T, View> binder) {
184-
super(layoutId, binder);
201+
@NonNull final Binder<T, View> binder, Function stableIdForItem) {
202+
super(layoutId, binder, stableIdForItem);
185203
}
186204

187205
@Override

extensions/rvadapter/src/main/java/com/google/android/agera/rvadapter/RepositoryPresenterCompilerStates.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ interface RPViewBinder<TVal, TRet> {
6868
TRet bindWith(@NonNull Binder<TVal, View> viewBinder);
6969
}
7070

71+
/**
72+
* Compiler state to specify how to generate stable IDs when
73+
* {@link android.support.v7.widget.RecyclerView.Adapter#setHasStableIds(boolean)} is true.
74+
*/
75+
interface RPStableId<TVal, TRet> {
76+
77+
/**
78+
* Specifies a {@link Function} providing a stable id for the given item.
79+
* Called only if stable IDs are enabled with {@link RepositoryAdapter#setHasStableIds
80+
* RepositoryAdapter.setHasStableIds(true)}, and therefore this method is optional with a
81+
* default implementation of returning {@link RecyclerView#NO_ID}. If stable IDs are enabled,
82+
* the returned ID and the layout returned by {@link RPLayout#layoutForItem(Function)} or
83+
* {@link RPLayout#layout(int)} for the given item should together uniquely identify this item
84+
* in the whole {@link RecyclerView} throughout all changes.
85+
*/
86+
@NonNull
87+
TRet stableIdForItem(@NonNull Function<TVal, Long> stableIdForItem);
88+
}
89+
7190
/**
7291
* Compiler state to create the @{link RepositoryPresenter}.
7392
*/
@@ -99,6 +118,11 @@ interface RPCompile<TVal> {
99118
/**
100119
* Compiler state allowing to specify view binder, view recycler or compile.
101120
*/
102-
interface RPViewBinderCompile<TVal>
103-
extends RPViewBinder<TVal, RPCompile<TVal>>, RPCompile<TVal> {}
121+
interface RPViewBinderCompile<TVal> extends RPViewBinder<TVal, RPCompile<TVal>>,
122+
RPCompile<TVal> {}
123+
/**
124+
* Compiler state allowing to specify view binder, view recycler, stable id function or compile.
125+
*/
126+
interface RPViewBinderStableIdCompile<TVal> extends RPViewBinderCompile<TVal>,
127+
RPStableId<TVal, RPViewBinderCompile<TVal>> {}
104128
}

extensions/rvadapter/src/main/java/com/google/android/agera/rvadapter/RepositoryPresenters.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import com.google.android.agera.Repository;
1919
import com.google.android.agera.Result;
2020
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPLayout;
21-
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPViewBinderCompile;
21+
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPViewBinderStableIdCompile;
2222

2323
import android.support.annotation.NonNull;
2424
import android.support.annotation.Nullable;
@@ -37,7 +37,7 @@ public final class RepositoryPresenters {
3737
*/
3838
@SuppressWarnings({"unchecked", "UnusedParameters"})
3939
@NonNull
40-
public static <T> RPLayout<T, RPViewBinderCompile<T>> repositoryPresenterOf(
40+
public static <T> RPLayout<T, RPViewBinderStableIdCompile<T>> repositoryPresenterOf(
4141
@Nullable final Class<T> type) {
4242
return new RepositoryPresenterCompiler();
4343
}

extensions/rvadapter/src/test/java/com/google/android/agera/rvadapter/RepositoryPresentersTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import com.google.android.agera.Binder;
1515
import com.google.android.agera.Function;
16+
import com.google.android.agera.Functions;
1617
import com.google.android.agera.Result;
1718

1819
import android.support.v7.widget.RecyclerView;
@@ -39,6 +40,7 @@ public class RepositoryPresentersTest {
3940
private static final Result<List<String>> LIST_FAILURE = Result.<List<String>>failure();
4041
private static final int LAYOUT_ID = 0;
4142
private static final int DYNAMIC_LAYOUT_ID = 1;
43+
private static final long STABLE_ID = 2;
4244
@Mock
4345
private Binder<String, View> binder;
4446
@Mock
@@ -162,4 +164,45 @@ public void shouldGenerateLayoutForItemOfRepositoryPresenterOfResultList() {
162164
public void shouldHavePrivateConstructor() {
163165
assertThat(RepositoryPresenters.class, hasPrivateConstructor());
164166
}
167+
168+
@Test
169+
public void shouldReturnStableIdForRepositoryPresenterOfResult() {
170+
final RepositoryPresenter<Result<String>> resultRepositoryPresenter =
171+
repositoryPresenterOf(String.class)
172+
.layout(LAYOUT_ID)
173+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
174+
.forResult();
175+
assertThat(resultRepositoryPresenter.getItemId(STRING_RESULT, 0), is(STABLE_ID));
176+
}
177+
178+
@Test
179+
public void shouldReturnStableIdForRepositoryPresenterOfResultList() {
180+
final RepositoryPresenter<Result<List<String>>> resultListRepositoryPresenter =
181+
repositoryPresenterOf(String.class)
182+
.layout(LAYOUT_ID)
183+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
184+
.forResultList();
185+
assertThat(resultListRepositoryPresenter.getItemId(STRING_LIST_RESULT, 1), is(STABLE_ID));
186+
}
187+
188+
@Test
189+
public void shouldReturnStableIdForRepositoryPresenterOfList() {
190+
final RepositoryPresenter<List<String>> listRepositoryPresenter =
191+
repositoryPresenterOf(String.class)
192+
.layout(LAYOUT_ID)
193+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
194+
.forList();
195+
assertThat(listRepositoryPresenter.getItemId(STRING_LIST, 1), is(STABLE_ID));
196+
}
197+
198+
@Test
199+
public void shouldReturnStableIdForRepositoryPresenterOfListWithBinder() {
200+
final RepositoryPresenter<List<String>> resultRepositoryPresenter =
201+
repositoryPresenterOf(String.class)
202+
.layout(LAYOUT_ID)
203+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
204+
.bindWith(binder)
205+
.forList();
206+
assertThat(resultRepositoryPresenter.getItemId(STRING_LIST, 1), is(STABLE_ID));
207+
}
165208
}

extensions/rvdatabinding/src/main/java/com/google/android/agera/rvdatabinding/DataBindingRepositoryPresenterCompiler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import android.support.annotation.NonNull;
3535
import android.support.annotation.VisibleForTesting;
3636
import android.support.v4.util.Pair;
37+
import android.support.v7.widget.RecyclerView;
3738
import android.view.View;
3839

3940
import java.util.ArrayList;
@@ -46,6 +47,8 @@ final class DataBindingRepositoryPresenterCompiler
4647
private final List<Pair<Integer, Object>> handlers;
4748
private Function<Object, Integer> layoutFactory;
4849
private Function itemId;
50+
@NonNull
51+
private Function<Object, Long> stableIdForItem = staticFunction(RecyclerView.NO_ID);
4952

5053
DataBindingRepositoryPresenterCompiler() {
5154
this.handlers = new ArrayList<>();
@@ -77,6 +80,7 @@ public Object itemIdForItem(@NonNull final Function itemIdForItem) {
7780
public RepositoryPresenter<List<Object>> forList() {
7881
return repositoryPresenterOf(null)
7982
.layoutForItem(layoutFactory)
83+
.stableIdForItem(stableIdForItem)
8084
.bindWith(new ViewBinder(itemId, new ArrayList<>(handlers)))
8185
.forList();
8286
}
@@ -86,6 +90,7 @@ public RepositoryPresenter<List<Object>> forList() {
8690
public RepositoryPresenter<Result<Object>> forResult() {
8791
return repositoryPresenterOf(Object.class)
8892
.layoutForItem(layoutFactory)
93+
.stableIdForItem(stableIdForItem)
8994
.bindWith(new ViewBinder(itemId, new ArrayList<>(handlers)))
9095
.forResult();
9196
}
@@ -95,6 +100,7 @@ public RepositoryPresenter<Result<Object>> forResult() {
95100
public RepositoryPresenter<Result<List<Object>>> forResultList() {
96101
return repositoryPresenterOf(null)
97102
.layoutForItem(layoutFactory)
103+
.stableIdForItem(stableIdForItem)
98104
.bindWith(new ViewBinder(itemId, new ArrayList<>(handlers)))
99105
.forResultList();
100106
}
@@ -113,6 +119,13 @@ public Object layoutForItem(@NonNull Function layoutForItem) {
113119
return this;
114120
}
115121

122+
@NonNull
123+
@Override
124+
public Object stableIdForItem(@NonNull final Function stableIdForItem) {
125+
this.stableIdForItem = stableIdForItem;
126+
return this;
127+
}
128+
116129
private static final class ViewBinder implements Binder<Object, View> {
117130
private final Function<Object, Integer> itemId;
118131
@NonNull

extensions/rvdatabinding/src/main/java/com/google/android/agera/rvdatabinding/DataBindingRepositoryPresenterCompilerStates.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.android.agera.Function;
1919
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPCompile;
20+
import com.google.android.agera.rvadapter.RepositoryPresenterCompilerStates.RPStableId;
2021

2122
import android.support.annotation.LayoutRes;
2223
import android.support.annotation.NonNull;
@@ -36,5 +37,6 @@ interface DBRPHandlerBinding<TRet> {
3637
}
3738

3839
interface DBRPHandlerBindingCompile<TVal>
39-
extends RPCompile<TVal>, DBRPHandlerBinding<DBRPHandlerBindingCompile<TVal>> {}
40+
extends RPCompile<TVal>, DBRPHandlerBinding<DBRPHandlerBindingCompile<TVal>>,
41+
RPStableId<TVal, DBRPHandlerBindingCompile<TVal>> {}
4042
}

extensions/rvdatabinding/src/test/java/com/google/android/agera/rvdatabinding/DataBindingRepositoryPresentersTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import com.google.android.agera.Binder;
1515
import com.google.android.agera.Function;
16+
import com.google.android.agera.Functions;
1617
import com.google.android.agera.Result;
1718
import com.google.android.agera.rvadapter.RepositoryPresenter;
1819

@@ -46,6 +47,7 @@ public class DataBindingRepositoryPresentersTest {
4647
private static final int DYNAMIC_ITEM_ID = 4;
4748
private static final int HANDLER_ID = 5;
4849
private static final int SECOND_HANDLER_ID = 6;
50+
private static final long STABLE_ID = 2;
4951
@Mock
5052
private Binder<String, View> binder;
5153
@Mock
@@ -188,6 +190,39 @@ public void shouldGenerateItemIdForItemOfRepositoryPresenterOfResultList() {
188190
resultListRepositoryPresenter.bind(STRING_LIST_RESULT, 1, viewHolder);
189191
}
190192

193+
@Test
194+
public void shouldReturnStableIdForRepositoryPresenterOfResult() {
195+
final RepositoryPresenter<Result<String>> resultRepositoryPresenter =
196+
dataBindingRepositoryPresenterOf(String.class)
197+
.layout(LAYOUT_ID)
198+
.itemId(ITEM_ID)
199+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
200+
.forResult();
201+
assertThat(resultRepositoryPresenter.getItemId(STRING_RESULT, 0), is(STABLE_ID));
202+
}
203+
204+
@Test
205+
public void shouldReturnStableIdForRepositoryPresenterOfResultList() {
206+
final RepositoryPresenter<Result<List<String>>> resultListRepositoryPresenter =
207+
dataBindingRepositoryPresenterOf(String.class)
208+
.layout(LAYOUT_ID)
209+
.itemId(ITEM_ID)
210+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
211+
.forResultList();
212+
assertThat(resultListRepositoryPresenter.getItemId(STRING_LIST_RESULT, 0), is(STABLE_ID));
213+
}
214+
215+
@Test
216+
public void shouldReturnStableIdForRepositoryPresenterOfList() {
217+
final RepositoryPresenter<List<String>> listRepositoryPresenter =
218+
dataBindingRepositoryPresenterOf(String.class)
219+
.layout(LAYOUT_ID)
220+
.itemId(ITEM_ID)
221+
.stableIdForItem(Functions.<String, Long>staticFunction(STABLE_ID))
222+
.forList();
223+
assertThat(listRepositoryPresenter.getItemId(STRING_LIST, 0), is(STABLE_ID));
224+
}
225+
191226
@Test
192227
public void shouldHavePrivateConstructor() {
193228
assertThat(DataBindingRepositoryPresenters.class, hasPrivateConstructor());

testapp/src/main/java/com/google/android/agera/testapp/NotesActivity.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ protected void onCreate(final Bundle savedInstanceState) {
9090
.add(notesStore.getNotesRepository(), dataBindingRepositoryPresenterOf(Note.class)
9191
.layout(R.layout.text_layout)
9292
.itemId(com.google.android.agera.testapp.BR.note)
93+
.stableIdForItem(input -> (long) input.getId())
9394
.handler(com.google.android.agera.testapp.BR.click,
9495
(Receiver<Note>) note -> {
9596
final EditText editText = new EditText(this);
@@ -106,6 +107,7 @@ protected void onCreate(final Bundle savedInstanceState) {
106107
(Predicate<Note>) notesStore::deleteNote)
107108
.forList())
108109
.whileStarted(this);
110+
adapter.setHasStableIds(true);
109111

110112
// Setup the recycler view using the repository adapter
111113
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.result);

0 commit comments

Comments
 (0)