diff --git a/medusalib/build.gradle b/medusalib/build.gradle index 66bae21..4dd938f 100755 --- a/medusalib/build.gradle +++ b/medusalib/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' android { namespace 'com.trendyol.medusalib' @@ -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" diff --git a/medusalib/src/main/java/com/trendyol/medusalib/navigator/MultipleStackNavigator.kt b/medusalib/src/main/java/com/trendyol/medusalib/navigator/MultipleStackNavigator.kt index f6f78d2..3c122e6 100755 --- a/medusalib/src/main/java/com/trendyol/medusalib/navigator/MultipleStackNavigator.kt +++ b/medusalib/src/main/java/com/trendyol/medusalib/navigator/MultipleStackNavigator.kt @@ -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 @@ -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() { diff --git a/medusalib/src/main/java/com/trendyol/medusalib/navigator/Navigator.kt b/medusalib/src/main/java/com/trendyol/medusalib/navigator/Navigator.kt index 940b1a4..ff5515e 100755 --- a/medusalib/src/main/java/com/trendyol/medusalib/navigator/Navigator.kt +++ b/medusalib/src/main/java/com/trendyol/medusalib/navigator/Navigator.kt @@ -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 { @@ -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 Fragment Lifecycle */ fun getNavigatorTransaction(): NavigatorTransaction } } - - - diff --git a/medusalib/src/main/java/com/trendyol/medusalib/navigator/data/StackItem.kt b/medusalib/src/main/java/com/trendyol/medusalib/navigator/data/StackItem.kt index 2c142b9..02b47ef 100755 --- a/medusalib/src/main/java/com/trendyol/medusalib/navigator/data/StackItem.kt +++ b/medusalib/src/main/java/com/trendyol/medusalib/navigator/data/StackItem.kt @@ -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 { - override fun createFromParcel(parcel: Parcel): StackItem { - return StackItem(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - -} \ No newline at end of file +@Parcelize +data class StackItem(val fragmentTag: String, val groupName: String = "") : Parcelable diff --git a/medusalib/src/test/java/com/trendyol/medusalib/navigator/MultipleStackNavigatorBackstackOrderTest.kt b/medusalib/src/test/java/com/trendyol/medusalib/navigator/MultipleStackNavigatorBackstackOrderTest.kt new file mode 100644 index 0000000..2481ab0 --- /dev/null +++ b/medusalib/src/test/java/com/trendyol/medusalib/navigator/MultipleStackNavigatorBackstackOrderTest.kt @@ -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().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().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().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().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() + 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().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() + 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().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() + 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) + } + } +}