Skip to content

Commit

Permalink
Merge pull request #56 from Trendyol/FixIllegalStateExceptionConcurre…
Browse files Browse the repository at this point in the history
…ntTransaction

FixIllegalStateExceptionConcurrentTransaction
  • Loading branch information
MertNYuksel authored Jun 25, 2024
2 parents 3a0ccf9 + 7b253f3 commit 1690abe
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 31 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- This changelog file
- Fix a bug where a fragment that we want to hide is not added to the fragment manager,
causing an IllegalStateException due to multiple fragment transactions occurring simultaneously
because of the executePendingTransactions() method call

### Changed

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ androidXArchCoreTesting = "androidx.arch.core:core-testing:2.2.0"
androidXFragmentTesting = "androidx.fragment:fragment-testing:1.8.0"
androidXTestCoreKtx = "androidx.test:core-ktx:1.4.0"
androidXTestExtJunit = "androidx.test.ext:junit:1.1.3"
androidXTestRunner = "androidx.test:runner:1.5.2"
junit = "junit:junit:4.13.2"
robolectric = "org.robolectric:robolectric:4.12.2"
truth = "com.google.truth:truth:1.4.2"
espressoTest = "androidx.test.espresso:espresso-core:3.6.0"

[versions]
kotlin = "2.0.0"
8 changes: 8 additions & 0 deletions medusalib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ dependencies {
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.truth

androidTestImplementation libs.androidXTestExtJunit
androidTestImplementation libs.androidXTestRunner
debugImplementation libs.androidXFragmentTesting
androidTestImplementation libs.truth
androidTestImplementation libs.junit
androidTestImplementation libs.espressoTest

}

repositories {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.trendyol.medusalib

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment

private const val KEY_TITLE = "title"

class TestChildFragment : Fragment() {

var onFragmentVisibleAgain: (() -> Unit)? = null

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return TextView(requireContext()).apply {
text = requireArguments().getString(KEY_TITLE)
}
}

override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden.not() && view != null) {
onFragmentVisibleAgain?.invoke()
}
}

companion object {
fun newInstance(title: String): TestChildFragment {
return TestChildFragment().apply {
arguments = Bundle().apply { putString(KEY_TITLE, title) }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.trendyol.medusalib

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment

class TestParentFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return FrameLayout(requireContext()).apply { id = CONTAINER_ID }
}

companion object {
const val CONTAINER_ID = 1_000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.trendyol.medusalib.navigator

import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.fragment.app.testing.withFragment
import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.trendyol.medusalib.TestChildFragment
import com.trendyol.medusalib.TestParentFragment
import com.trendyol.medusalib.navigator.transaction.NavigatorTransaction
import com.trendyol.medusalib.navigator.transaction.TransactionType
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch


@RunWith(AndroidJUnit4::class)
class ConcurrentTransactionTest {

@Test
fun givenNavigatorWithShowAndHideWhenFragmentResetsTheCurrentTabAndStartsAnotherFragmentThenItMustNotThrowAnyExceptions() {
// Given
val transaction = CountDownLatch(1)
var caughtException: Throwable? = null
var navigator: Navigator? = null
val scenario = launchFragmentInContainer<TestParentFragment>(
initialState = Lifecycle.State.INITIALIZED
)
scenario.moveToState(Lifecycle.State.RESUMED)
val rootFragment = TestChildFragment.newInstance("Root")
val expectedFragment = TestChildFragment.newInstance("ExpectedFragment")

scenario.withFragment { navigator = createNavigator(rootFragment = rootFragment) }
scenario.moveToState(Lifecycle.State.RESUMED)

// When
rootFragment.onFragmentVisibleAgain = {
resetTabAndStartFragment(navigator!!, expectedFragment)
.onSuccess { transaction.countDown() }
.onFailure {
caughtException = it
transaction.countDown()
}
}
scenario.startAndDismissAFragment(navigator!!)

transaction.await()
caughtException?.let { throw it }
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withText("ExpectedFragment")).check(matches(isDisplayed()))
}

private fun FragmentScenario<TestParentFragment>.startAndDismissAFragment(navigator: Navigator) {
onFragment { navigator.start(TestChildFragment.newInstance("SecondFragment")) }
moveToState(Lifecycle.State.RESUMED)
onFragment { navigator.goBack() }
}

private fun resetTabAndStartFragment(
navigator: Navigator,
expectedFragment: TestChildFragment
): Result<Unit> {
return runCatching {
navigator.resetCurrentTab(true)
navigator.start(expectedFragment)
}
}

private fun TestParentFragment.createNavigator(rootFragment: TestChildFragment): MultipleStackNavigator {
return MultipleStackNavigator(
fragmentManager = this.childFragmentManager,
containerId = TestParentFragment.CONTAINER_ID,
rootFragmentProvider = listOf({ rootFragment }),
navigatorConfiguration = NavigatorConfiguration(
defaultNavigatorTransaction = NavigatorTransaction(TransactionType.SHOW_HIDE)
)
).apply { this.initialize(null) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.trendyol.medusalib.navigator.controller.FragmentManagerController
import com.trendyol.medusalib.navigator.controller.StagedFragmentHolder
import com.trendyol.medusalib.navigator.data.FragmentData
import com.trendyol.medusalib.navigator.data.StackItem
import com.trendyol.medusalib.navigator.tag.TagCreator
Expand All @@ -28,7 +29,8 @@ open class MultipleStackNavigator(
private val fragmentManagerController = FragmentManagerController(
fragmentManager,
containerId,
navigatorConfiguration.defaultNavigatorTransaction
navigatorConfiguration.defaultNavigatorTransaction,
StagedFragmentHolder(mutableMapOf())
)

private val fragmentStackStateMapper = FragmentStackStateMapper()
Expand Down Expand Up @@ -77,10 +79,13 @@ open class MultipleStackNavigator(
fragmentStackState.notifyStackItemAddToCurrentTab(
StackItem(
fragmentTag = createdTag,
groupName = fragmentGroupName
)
groupName = fragmentGroupName,
),
)
fragment.observeFragmentLifecycle(
::onFragmentViewCreated,
::onFragmentDestroy
)
notifyFragmentDestinationChange(fragment)
}

override fun goBack() {
Expand Down Expand Up @@ -144,17 +149,27 @@ open class MultipleStackNavigator(
val createdTag = tagCreator.create(rootFragment)
val rootFragmentData = FragmentData(rootFragment, createdTag)
fragmentStackState.switchTab(currentTabIndex)
fragmentStackState.notifyStackItemAdd(currentTabIndex, StackItem(fragmentTag = createdTag))
fragmentStackState.notifyStackItemAdd(
currentTabIndex,
StackItem(fragmentTag = createdTag),
)
fragmentManagerController.addFragment(rootFragmentData)
notifyFragmentDestinationChange(rootFragment)

rootFragment.observeFragmentLifecycle(
onViewCreated = ::onFragmentViewCreated,
onFragmentDestroy = ::onFragmentDestroy
)
} else {
val upperFragmentTag: String = getCurrentFragmentTag()
val upperFragment: Fragment? = fragmentManagerController.getFragment(upperFragmentTag)

val newDestination: Fragment = upperFragment ?: getRootFragment(currentTabIndex)
val newDestinationTag: String = tagCreator.create(newDestination)

notifyFragmentDestinationChange(newDestination)
newDestination.observeFragmentLifecycle(
onViewCreated = ::onFragmentViewCreated,
onFragmentDestroy = ::onFragmentDestroy
)
fragmentManagerController.enableFragment(newDestinationTag)
}
}
Expand All @@ -163,6 +178,7 @@ open class MultipleStackNavigator(
clearAllFragments()
fragmentStackState.clear()
initializeStackState()

}

override fun resetWithFragmentProvider(rootFragmentProvider: List<() -> Fragment>) {
Expand Down Expand Up @@ -237,14 +253,20 @@ open class MultipleStackNavigator(
val stackItem = StackItem(fragmentTag = createdTag)

fragmentStackState.setStackCount(rootFragmentProvider.size)
fragmentStackState.notifyStackItemAdd(tabIndex = initialTabIndex, stackItem = stackItem)
fragmentStackState.notifyStackItemAdd(
tabIndex = initialTabIndex,
stackItem = stackItem,
)
fragmentStackState.switchTab(initialTabIndex)

val rootFragmentTag = fragmentStackState.peekItem(initialTabIndex).fragmentTag
val rootFragmentData = FragmentData(rootFragment, rootFragmentTag)
fragmentManagerController.addFragment(rootFragmentData)
navigatorListener?.onTabChanged(navigatorConfiguration.initialTabIndex)
notifyFragmentDestinationChange(rootFragment)
rootFragment.observeFragmentLifecycle(
::onFragmentViewCreated,
::onFragmentDestroy
)
}

private fun loadStackStateFromSavedState(savedState: Bundle) {
Expand All @@ -271,13 +293,29 @@ open class MultipleStackNavigator(
val rootFragmentData = FragmentData(rootFragment, createdTag)
fragmentStackState.notifyStackItemAdd(
fragmentStackState.getSelectedTabIndex(),
StackItem(createdTag)
StackItem(createdTag),
)
fragmentManagerController.addFragment(rootFragmentData)
notifyFragmentDestinationChange(rootFragment)
rootFragment.observeFragmentLifecycle(
onViewCreated = ::onFragmentViewCreated,
onFragmentDestroy = ::onFragmentDestroy
)
} else {
fragmentManagerController.enableFragment(upperFragmentTag)
notifyFragmentDestinationChange(upperFragment)
upperFragment.observeFragmentLifecycle(
onViewCreated = ::onFragmentViewCreated,
onFragmentDestroy = ::onFragmentDestroy
)
}
}

private fun onFragmentViewCreated(fragment: Fragment) {
destinationChangeLiveData.value = fragment
}

private fun onFragmentDestroy(fragment: Fragment) {
if (destinationChangeLiveData.value == fragment) {
destinationChangeLiveData.value = null
}
}

Expand Down Expand Up @@ -321,28 +359,34 @@ open class MultipleStackNavigator(
return true
}

private fun notifyFragmentDestinationChange(fragment: Fragment) {
fragment.lifecycle.addObserver(object: DefaultLifecycleObserver {
private fun Fragment.observeFragmentLifecycle(
onViewCreated: (Fragment) -> Unit,
onFragmentDestroy: (Fragment) -> Unit
) {
this.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
owner.lifecycle.removeObserver(this)
fragment.viewLifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
destinationChangeLiveData.value = fragment
}

override fun onDestroy(owner: LifecycleOwner) {
if (destinationChangeLiveData.value == fragment) {
destinationChangeLiveData.value = null
val fragment = this@observeFragmentLifecycle
fragment
.viewLifecycleOwner
.lifecycle
.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
onViewCreated(fragment)
}
override fun onDestroy(owner: LifecycleOwner) {
onFragmentDestroy(fragment)
owner.lifecycle.removeObserver(this)
}
owner.lifecycle.removeObserver(this)
}
}
)
)
}
})
}


override fun onSaveInstanceState(outState: Bundle) {
outState.putBundle(MEDUSA_STACK_STATE_KEY, fragmentStackStateMapper.toBundle(fragmentStackState))
}
Expand Down
Loading

0 comments on commit 1690abe

Please sign in to comment.