Skip to content

Commit c7b937c

Browse files
committed
2.6.5: reverse-iterate completion listeners to fix #263
1 parent 8d37157 commit c7b937c

5 files changed

Lines changed: 87 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
# Change log
22

3+
-Simple Stack 2.6.5 (2022-11-11)
4+
--------------------------------
5+
6+
- FIX: `Backstack.CompletionListener` added to `Backstack` that unregistered themselves during dispatching notifications
7+
would cause either a ConcurrentModificationException or invalid results, this is now fixed and no longer the case (
8+
#263, thanks @angusholder)
9+
10+
- MINOR CHANGE: When `Backstack.CompletionListener`'s are being notified, the state changer is temporarily removed (
11+
similarly to dispatching `ScopedServices.Activated` events), so that navigation actions invoked on `Backstack` are
12+
deferred until all `Backstack.CompletionListener`s are notified.
13+
314
-Simple Stack 2.6.4 (2022-04-21)
415
--------------------------------
516

6-
- FIX: Attempt at fixing a crash related to `LinkedHashMap.retainAll()` specifically on Android 6 and Android 6.1 devices (#256).
17+
- FIX: Attempt at fixing a crash related to `LinkedHashMap.retainAll()` specifically on Android 6 and Android 6.1
18+
devices (#256).
719

820
- 2.6.3 had an issue with `maven-publish` and transitive dependencies were missing, and is therefore skipped.
921

10-
1122
-Simple Stack 2.6.2 (2021-06-07)
1223
--------------------------------
1324

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ and then, add the dependency to your module's `build.gradle.kts` (or `build.grad
6969

7070
``` kotlin
7171
// build.gradle.kts
72-
implementation("com.github.Zhuinden:simple-stack:2.6.4")
72+
implementation("com.github.Zhuinden:simple-stack:2.6.5")
7373

7474
implementation("com.github.Zhuinden.simple-stack-extensions:core-ktx:2.2.4")
7575
implementation("com.github.Zhuinden.simple-stack-extensions:fragments:2.2.4")
@@ -83,7 +83,7 @@ or
8383

8484
``` groovy
8585
// build.gradle
86-
implementation 'com.github.Zhuinden:simple-stack:2.6.4'
86+
implementation 'com.github.Zhuinden:simple-stack:2.6.5'
8787
8888
implementation 'com.github.Zhuinden.simple-stack-extensions:core-ktx:2.2.4'
8989
implementation 'com.github.Zhuinden.simple-stack-extensions:fragments:2.2.4'

simple-stack/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ afterEvaluate {
7474
register("mavenJava", MavenPublication::class) {
7575
groupId = "com.github.Zhuinden"
7676
artifactId = "simple-stack"
77-
version = "2.6.4"
77+
version = "2.6.5"
7878

7979
from(components["release"])
8080
artifact(sourcesJar.get())

simple-stack/src/main/java/com/zhuinden/simplestack/NavigationCore.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,17 @@ public void removeCompletionListeners() {
705705
}
706706

707707
private void notifyCompletionListeners(StateChange stateChange) {
708-
for(Backstack.CompletionListener completionListener : completionListeners) {
708+
final StateChanger currentStateChanger = stateChanger;
709+
if(currentStateChanger != null) {
710+
stateChanger = null;
711+
}
712+
for(int i = completionListeners.size() - 1; i >= 0; i--) {
713+
Backstack.CompletionListener completionListener = completionListeners.get(i);
709714
completionListener.stateChangeCompleted(stateChange);
710715
}
716+
if(stateChanger == null && currentStateChanger != null) {
717+
this.stateChanger = currentStateChanger; // do not use `setStateChanger(REATTACH)` here, it would try to start state changes twice in succession
718+
}
711719
}
712720

713721
// force execute

simple-stack/src/test/java/com/zhuinden/simplestack/BackstackCoreTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.ArrayList;
2727
import java.util.List;
2828
import java.util.concurrent.CountDownLatch;
29+
import java.util.concurrent.atomic.AtomicInteger;
2930
import java.util.concurrent.atomic.AtomicReference;
3031

3132
import javax.annotation.Nonnull;
@@ -273,6 +274,67 @@ public void handleStateChange(@Nonnull StateChange _stateChange, @Nonnull Callba
273274
Mockito.verify(completionListener, Mockito.only()).stateChangeCompleted(stateChange);
274275
}
275276

277+
@Test
278+
public void completionListenerCanRemoveItself() {
279+
TestKey initial = new TestKey("hello");
280+
final TestKey second = new TestKey("world");
281+
final Backstack backstack = new Backstack();
282+
backstack.setup(History.of(initial));
283+
284+
final AtomicInteger listener1Called = new AtomicInteger(0);
285+
final AtomicInteger listener2Called = new AtomicInteger(0);
286+
final AtomicInteger listener3Called = new AtomicInteger(0);
287+
288+
Backstack.CompletionListener completionListener1 = new Backstack.CompletionListener() {
289+
@Override
290+
public void stateChangeCompleted(@Nonnull StateChange stateChange) {
291+
listener1Called.addAndGet(1);
292+
}
293+
};
294+
Backstack.CompletionListener completionListener2 = new Backstack.CompletionListener() {
295+
@Override
296+
public void stateChangeCompleted(@Nonnull StateChange stateChange) {
297+
listener2Called.addAndGet(1);
298+
backstack.removeCompletionListener(this);
299+
}
300+
};
301+
Backstack.CompletionListener completionListener3 = new Backstack.CompletionListener() {
302+
@Override
303+
public void stateChangeCompleted(@Nonnull StateChange stateChange) {
304+
listener3Called.addAndGet(1);
305+
}
306+
};
307+
StateChanger stateChanger = new StateChanger() {
308+
@Override
309+
public void handleStateChange(@Nonnull StateChange _stateChange, @Nonnull Callback completionCallback) {
310+
stateChange = _stateChange;
311+
callback = completionCallback;
312+
}
313+
};
314+
backstack.addCompletionListener(completionListener1);
315+
backstack.addCompletionListener(completionListener2);
316+
backstack.addCompletionListener(completionListener2); // intentional duplicate line to reproduce #263
317+
backstack.addCompletionListener(completionListener3);
318+
backstack.setStateChanger(stateChanger);
319+
320+
callback.stateChangeComplete();
321+
322+
assertThat(backstack.isStateChangePending()).isFalse();
323+
324+
assertThat(listener1Called.get()).isEqualTo(1);
325+
assertThat(listener2Called.get()).isEqualTo(2);
326+
assertThat(listener3Called.get()).isEqualTo(1);
327+
328+
backstack.goTo(second);
329+
330+
callback.stateChangeComplete();
331+
332+
assertThat(listener1Called.get()).isEqualTo(2);
333+
assertThat(listener2Called.get()).isEqualTo(2);
334+
assertThat(listener3Called.get()).isEqualTo(2);
335+
}
336+
337+
276338
@Test
277339
public void removedCompletionListenerShouldNotBeCalled() {
278340
TestKey initial = new TestKey("hello");

0 commit comments

Comments
 (0)