Skip to content

Commit

Permalink
Merge pull request #53 from Trendyol/feature/fragment-stack-index-get…
Browse files Browse the repository at this point in the history
…ter-with-tag

Adds `getFragmentIndexInStackBySameType` function to `Navigator`
  • Loading branch information
bilgehankalkan authored Feb 16, 2024
2 parents dd46a06 + 289a75e commit 22f05b2
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 54 deletions.
3 changes: 2 additions & 1 deletion medusalib/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'

android {
namespace 'com.trendyol.medusalib'
Expand Down Expand Up @@ -29,7 +30,7 @@ android {

ext {
PUBLISH_GROUP_ID = 'com.trendyol'
PUBLISH_VERSION = '0.10.4'
PUBLISH_VERSION = '0.11.0'
PUBLISH_ARTIFACT_ID = 'medusa'
PUBLISH_DESCRIPTION = "Android Fragment Stack Controller"
PUBLISH_URL = "https://github.com/Trendyol/medusa"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.trendyol.medusalib.navigator.controller.FragmentManagerController
import com.trendyol.medusalib.navigator.data.FragmentData
import com.trendyol.medusalib.navigator.data.StackItem
Expand Down Expand Up @@ -212,14 +211,23 @@ open class MultipleStackNavigator(
lifecycleOwner: LifecycleOwner,
destinationChangedListener: (Fragment) -> Unit
) {
destinationChangeLiveData.observe(
lifecycleOwner,
Observer { fragment ->
if (fragment != null) {
destinationChangedListener(fragment)
destinationChangeLiveData.observe(lifecycleOwner) { fragment ->
if (fragment != null) {
destinationChangedListener(fragment)
}
}
}

override fun getFragmentIndexInStackBySameType(tag: String?): Int {
if (tag.isNullOrEmpty()) return -1
fragmentStackState.fragmentTagStack.forEach { stack ->
stack.forEachIndexed { index, stackItem ->
if (stackItem.fragmentTag == tag) {
return stack.size - index - 1
}
}
)
}
return -1
}

private fun initializeStackState() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,30 +152,46 @@ interface Navigator {
*/
fun onSaveInstanceState(outState: Bundle)

/*
* Initializes fragment stack state and adds related root fragments to your
* container if savedState is null. Otherwise reads and deserialize
* fragment stack state from given bundle.
* @param outState savedInstanceState parameter of onCreate method in
/**
* Initializes fragment stack state and adds related root fragments to your
* container if savedState is null. Otherwise reads and deserialize
* fragment stack state from given bundle.
*
* @param savedState savedInstanceState parameter of onCreate method in
* your fragments or activities
*/
*/
fun initialize(savedState: Bundle?)

/**
* Listeners
*/

/*
Observes any changes made in fragment back stack with the given lifecycle.
All implementation of Navigator interface must guarantee following points:
- View lifecycle of the fragments that is observed by the listener must be at least in
STARTED state.
/**
* Observes any changes made in fragment back stack with the given lifecycle.
* All implementation of Navigator interface must guarantee following points:
*
* - View lifecycle of the fragments that is observed by the listener must be at least in
* STARTED state.
*
* - destinationChangedListener must be removed when the given lifecycle owner is reached
* DESTROYED state
*/
fun observeDestinationChanges(
lifecycleOwner: LifecycleOwner,
destinationChangedListener: (Fragment) -> Unit,
)

- destinationChangedListener must be removed when the given lifecycle owner is reached
DESTROYED state
/**
* Retrieves the index of a [Fragment] within the fragment stack based on the specified tag.
* If the tag is null or empty, returns -1.
* Iterates through the fragment stack to find the specified tag.
* Returns the index of the [Fragment] relative to the top of its stack if found; otherwise,
* returns -1.
*
* @param tag The tag of the [Fragment] to search for within the stack.
* @return The index of the [Fragment] within its stack if found; otherwise, -1.
*/
fun observeDestinationChanges(lifecycleOwner: LifecycleOwner,
destinationChangedListener: (Fragment) -> Unit)
fun getFragmentIndexInStackBySameType(tag: String?): Int

interface NavigatorListener {

Expand Down Expand Up @@ -214,11 +230,8 @@ interface Navigator {
* fragment.
* @return NavigatorTransaction type (ATTACH_DETACH or SHOW_HIDE)
*
* @see https://github.com/Trendyol/medusa/wiki/Fragment-Lifecycle
* @see <a href="https://github.com/Trendyol/medusa/wiki/Fragment-Lifecycle">Fragment Lifecycle</a>
*/
fun getNavigatorTransaction(): NavigatorTransaction
}
}



Original file line number Diff line number Diff line change
@@ -1,31 +1,7 @@
package com.trendyol.medusalib.navigator.data

import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parcelize

data class StackItem(val fragmentTag: String, val groupName: String = "") : Parcelable {
constructor(parcel: Parcel) : this(
requireNotNull(parcel.readString()),
requireNotNull(parcel.readString())
)

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(fragmentTag)
parcel.writeString(groupName)
}

override fun describeContents(): Int {
return 0
}

companion object CREATOR : Parcelable.Creator<StackItem> {
override fun createFromParcel(parcel: Parcel): StackItem {
return StackItem(parcel)
}

override fun newArray(size: Int): Array<StackItem?> {
return arrayOfNulls(size)
}
}

}
@Parcelize
data class StackItem(val fragmentTag: String, val groupName: String = "") : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.trendyol.medusalib.navigator

import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.launchFragmentInContainer
import com.google.common.truth.Truth.assertThat
import com.trendyol.medusalib.TestChildFragment
import com.trendyol.medusalib.TestParentFragment
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class MultipleStackNavigatorBackstackOrderTest {

@Test
fun `given MultipleStackNavigator with empty stack and null as tag, when calling getFragmentIndexInStackBySameType, should return -1`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ TestChildFragment.newInstance("root 1") }),
)
sut.initialize(null)

// When
val actual = sut.getFragmentIndexInStackBySameType(null)

// Then
assertThat(actual).isEqualTo(-1)
}
}

@Test
fun `given MultipleStackNavigator with empty stack and nonnull tag, when calling getFragmentIndexInStackBySameType, should return -1`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ TestChildFragment.newInstance("root 1") }),
)
sut.initialize(null)

// When
val actual = sut.getFragmentIndexInStackBySameType("random-tag")

// Then
assertThat(actual).isEqualTo(-1)
}
}

@Test
fun `given MultipleStackNavigator with stack with single fragment and nonnull tag, when calling getFragmentIndexInStackBySameType for current fragment, should return 0`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ TestChildFragment.newInstance("root 1") }),
)
sut.initialize(null)

sut.start(TestChildFragment.newInstance("child fragment"))

fragment.childFragmentManager.executePendingTransactions()

// When
val actual = sut.getFragmentIndexInStackBySameType(sut.getCurrentFragment()?.tag)

// Then
assertThat(actual).isEqualTo(0)
}
}

@Test
fun `given MultipleStackNavigator with stack with multiple fragment and nonnull tag, when calling getFragmentIndexInStackBySameType for first child fragment, should return 2`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ TestChildFragment.newInstance("root 1") }),
)
sut.initialize(null)

val fragments = mutableListOf<Fragment>()
sut.observeDestinationChanges(fragment.viewLifecycleOwner) {
fragments.add(it)
}

sut.start(TestChildFragment.newInstance("child fragment 1"))
sut.start(TestChildFragment.newInstance("child fragment 2"))
sut.start(TestChildFragment.newInstance("child fragment 3"))
fragment.childFragmentManager.executePendingTransactions()

// When
val actual = sut.getFragmentIndexInStackBySameType(fragments[1].tag)

// Then
assertThat(actual).isEqualTo(2)
}
}

@Test
fun `given MultipleStackNavigator with stack with multiple fragment and nonnull tag, when calling getFragmentIndexInStackBySameType for last child fragment, should return 0`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ TestChildFragment.newInstance("root 1") }),
)
sut.initialize(null)

val fragments = mutableListOf<Fragment>()
sut.observeDestinationChanges(fragment.viewLifecycleOwner) {
fragments.add(it)
}

sut.start(TestChildFragment.newInstance("child fragment 1"))
sut.start(TestChildFragment.newInstance("child fragment 2"))
fragment.childFragmentManager.executePendingTransactions()

// When
val actual = sut.getFragmentIndexInStackBySameType(fragments[2].tag)

// Then
assertThat(actual).isEqualTo(0)
}
}

@Test
fun `given MultipleStackNavigator with stack with multiple root fragments and nonnull tag and switch tab, when calling getFragmentIndexInStackBySameType for first child in switched tab, should return 1`() {
launchFragmentInContainer<TestParentFragment>().onFragment { fragment ->
// Given
val sut = MultipleStackNavigator(
fragmentManager = fragment.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf(
{ TestChildFragment.newInstance("root 1") },
{ TestChildFragment.newInstance("root 2") },
),
)
sut.initialize(null)

val fragments = mutableListOf<Fragment>()
sut.observeDestinationChanges(fragment.viewLifecycleOwner) {
fragments.add(it)
}

sut.start(TestChildFragment.newInstance("child fragment 1"))
sut.start(TestChildFragment.newInstance("child fragment 2"))
sut.switchTab(1)
sut.start(TestChildFragment.newInstance("child fragment 1"))
sut.start(TestChildFragment.newInstance("child fragment 1"))

fragment.childFragmentManager.executePendingTransactions()

// When
val actual = sut.getFragmentIndexInStackBySameType(fragments[4].tag)

// Then
assertThat(actual).isEqualTo(1)
}
}
}

0 comments on commit 22f05b2

Please sign in to comment.