Skip to content

Commit

Permalink
Merge pull request #304 from gerin98/allow-lazySetNavigator-to-remove…
Browse files Browse the repository at this point in the history
…-navigables

Support removing navigables from LazySetNavigator
  • Loading branch information
cmathew authored Oct 25, 2024
2 parents a1df2cb + 2123832 commit a103949
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.wealthfront.magellan.navigation

import android.content.Context
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import com.wealthfront.magellan.Direction
import com.wealthfront.magellan.ScreenContainer
Expand All @@ -22,6 +24,9 @@ public open class LazySetNavigator(
private val navigationPropagator: NavigationPropagator = NavigationPropagator
private var ongoingTransition: MagellanTransition? = null

@VisibleForTesting
internal var existingNavigables: MutableSet<NavigableCompat> = mutableSetOf()

@VisibleForTesting
internal var containerView: ScreenContainer? = null
private var currentNavigable: NavigableCompat? = null
Expand All @@ -36,9 +41,44 @@ public open class LazySetNavigator(
}

public fun addNavigable(navigable: NavigableCompat) {
existingNavigables.add(navigable)
lifecycleRegistry.attachToLifecycleWithMaxState(navigable, LifecycleLimit.CREATED)
}

@RequiresApi(Build.VERSION_CODES.N)
public fun removeNavigables(navigables: Set<NavigableCompat>) {
for (navigable in navigables) {
removeNavigable(navigable)
}
}

@RequiresApi(Build.VERSION_CODES.N)
public fun removeNavigable(navigable: NavigableCompat) {
existingNavigables.removeIf { it == navigable }
if (lifecycleRegistry.children.contains(navigable)) {
lifecycleRegistry.removeFromLifecycle(navigable)
}
}

public fun safeAddNavigable(navigable: NavigableCompat) {
if (!existingNavigables.contains(navigable)) {
addNavigable(navigable)
}
}

@RequiresApi(Build.VERSION_CODES.N)
public fun updateNavigables(navigables: Set<NavigableCompat>, handleCurrentTabRemoval: () -> Unit) {
val navigablesToRemove = existingNavigables subtract navigables
val navigablesToAdd = navigables subtract existingNavigables

if (navigablesToRemove.contains(currentNavigable)) {
handleCurrentTabRemoval()
}

removeNavigables(navigablesToRemove)
addNavigables(navigablesToAdd)
}

override fun onShow(context: Context) {
containerView = container()
currentNavigable?.let { currentNavigable ->
Expand All @@ -54,6 +94,7 @@ public open class LazySetNavigator(

override fun onDestroy(context: Context) {
lifecycleRegistry.children.forEach { lifecycleRegistry.removeFromLifecycle(it) }
existingNavigables.clear()
}

public fun replace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.wealthfront.magellan.lifecycle.LifecycleState
import com.wealthfront.magellan.lifecycle.transitionToState
import com.wealthfront.magellan.transitions.CrossfadeTransition
import io.mockk.MockKAnnotations.init
import io.mockk.clearAllMocks
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
Expand All @@ -28,6 +29,8 @@ class LazySetNavigatorTest {
private lateinit var navigator: LazySetNavigator
private lateinit var step1: DummyStep
private lateinit var step2: DummyStep
private lateinit var step3: DummyStep
private lateinit var step4: DummyStep
@MockK private lateinit var navigableListener: NavigationListener

@Before
Expand All @@ -38,13 +41,16 @@ class LazySetNavigatorTest {
navigator = LazySetNavigator { ScreenContainer(activityController.get()) }
step1 = DummyStep()
step2 = DummyStep()
step3 = DummyStep()
step4 = DummyStep()

NavigationPropagator.addNavigableListener(navigableListener)
}

@After
fun tearDown() {
NavigationPropagator.removeNavigableListener(navigableListener)
clearAllMocks()
}

private fun initMocks() {
Expand Down Expand Up @@ -100,6 +106,7 @@ class LazySetNavigatorTest {
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step2.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Shown::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2)

verify { navigableListener.beforeNavigation() }
verify { navigableListener.onNavigatedFrom(step1) }
Expand Down Expand Up @@ -136,5 +143,153 @@ class LazySetNavigatorTest {
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Shown::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2)
}

@Test
fun removeNavigables() {
navigator.addNavigables(setOf(step1, step2, step3, step4))
navigator.transitionToState(LifecycleState.Resumed(activityController.get()))

navigator.replace(step1, CrossfadeTransition())
step1.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2, step3, step4)

verify { navigableListener.beforeNavigation() }
verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) }
verify { navigableListener.onNavigatedTo(step1) }
verify { navigableListener.afterNavigation() }
clearMocks(navigableListener)
initMocks()

navigator.removeNavigables(setOf(step2, step4))

step1.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step3)

verify(exactly = 0) { navigableListener.onNavigatedTo(any()) }
}

@Test
fun attemptRemoveNavigables_afterOnDestroy() {
navigator.addNavigables(setOf(step1, step2, step3, step4))
navigator.transitionToState(LifecycleState.Resumed(activityController.get()))

navigator.replace(step1, CrossfadeTransition())
step1.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2, step3, step4)

verify { navigableListener.beforeNavigation() }
verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) }
verify { navigableListener.onNavigatedTo(step1) }
verify { navigableListener.afterNavigation() }
clearMocks(navigableListener)
initMocks()

navigator.transitionToState(LifecycleState.Destroyed)
navigator.removeNavigables(setOf(step2, step4))

assertThat(navigator.existingNavigables).isEmpty()
verify(exactly = 0) { navigableListener.onNavigatedTo(any()) }
}

@Test
fun updateMultipleNavigables() {
navigator.updateNavigables(setOf(step1, step2)) {}
navigator.transitionToState(LifecycleState.Resumed(activityController.get()))

navigator.replace(step1, CrossfadeTransition())
step1.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2)

verify { navigableListener.beforeNavigation() }
verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) }
verify { navigableListener.onNavigatedTo(step1) }
verify { navigableListener.afterNavigation() }
clearMocks(navigableListener)
initMocks()

navigator.updateNavigables(setOf(step1, step3, step4)) {
navigator.replace(step1, CrossfadeTransition())
}
navigator.replace(step3, CrossfadeTransition())

step3.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step3.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Shown::class.java)
assertThat(step3.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step3, step4)

verify { navigableListener.beforeNavigation() }
verify { navigableListener.onNavigatedFrom(step1) }
verify { navigableListener.onNavigatedTo(step3) }
verify { navigableListener.afterNavigation() }
}

@Test
fun updateMultipleNavigables_andRemoveCurrentNavigable() {
navigator.updateNavigables(setOf(step1, step2)) {}
navigator.transitionToState(LifecycleState.Resumed(activityController.get()))

navigator.replace(step2, CrossfadeTransition())
step2.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step2.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step2.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step2)

verify { navigableListener.beforeNavigation() }
verify(exactly = 0) { navigableListener.onNavigatedFrom(any()) }
verify { navigableListener.onNavigatedTo(step2) }
verify { navigableListener.afterNavigation() }
clearMocks(navigableListener)
initMocks()

navigator.updateNavigables(setOf(step1, step3, step4)) {
navigator.replace(step1, CrossfadeTransition())
}

step1.view!!.viewTreeObserver.dispatchOnPreDraw()
shadowOf(Looper.getMainLooper()).idle()
assertThat(navigator.containerView!!.childCount).isEqualTo(1)
assertThat(navigator.containerView!!.getChildAt(0)).isEqualTo(step1.view)
assertThat(step1.currentState).isInstanceOf(LifecycleState.Resumed::class.java)
assertThat(step3.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(step4.currentState).isInstanceOf(LifecycleState.Created::class.java)
assertThat(navigator.existingNavigables).containsExactly(step1, step3, step4)

verify { navigableListener.beforeNavigation() }
verify { navigableListener.onNavigatedFrom(step2) }
verify { navigableListener.onNavigatedTo(step1) }
verify { navigableListener.afterNavigation() }
}
}

0 comments on commit a103949

Please sign in to comment.