Skip to content

Commit 7be4270

Browse files
authored
Merge pull request #58 from Trendyol/PreloadedFragment
Preloaded fragment
2 parents 99db321 + 6889f37 commit 7be4270

File tree

10 files changed

+277
-1
lines changed

10 files changed

+277
-1
lines changed

app/src/main/java/com/trendyol/medusa/MainActivity.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ class MainActivity : AppCompatActivity(), Navigator.NavigatorListener {
6464

6565
navigation = findViewById<View>(R.id.navigation) as BottomNavigationView
6666

67+
multipleStackNavigator.preloadFragment(
68+
fragment = SamplePreloadFragment.newInstance(),
69+
fragmentTag = SamplePreloadFragment.TAG
70+
)
71+
6772
multipleStackNavigator.initialize(savedInstanceState)
6873
val restartRootFragmentCheckBox = findViewById<View>(R.id.restartSwitch) as SwitchCompat
6974
findViewById<Button>(R.id.resetCurrentTab).setOnClickListener {
@@ -84,6 +89,9 @@ class MainActivity : AppCompatActivity(), Navigator.NavigatorListener {
8489
findViewById<Button>(R.id.resetGroup).setOnClickListener {
8590
multipleStackNavigator.clearGroup("group1")
8691
}
92+
findViewById<Button>(R.id.startPreloadedFragment).setOnClickListener {
93+
multipleStackNavigator.startPreloadedFragment(SamplePreloadFragment.newInstance(), SamplePreloadFragment.TAG)
94+
}
8795
multipleStackNavigator.observeDestinationChanges(this) {
8896
Log.d("Destination Changed", "${it.javaClass.name} - ${it.tag}")
8997
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.trendyol.medusa
2+
3+
import android.os.Bundle
4+
import android.os.SystemClock
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import android.widget.Chronometer
9+
10+
class SamplePreloadFragment : BaseFragment() {
11+
12+
override fun onCreateView(
13+
inflater: LayoutInflater,
14+
container: ViewGroup?,
15+
savedInstanceState: Bundle?,
16+
): View {
17+
return inflater.inflate(R.layout.fragment_sample_preload, container, false)
18+
}
19+
20+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
21+
super.onViewCreated(view, savedInstanceState)
22+
startTimer()
23+
}
24+
25+
private fun startTimer() {
26+
getChronometer().apply {
27+
base = SystemClock.elapsedRealtime()
28+
start()
29+
}
30+
}
31+
32+
private fun stopTimer() {
33+
getChronometer().stop()
34+
}
35+
36+
override fun onDestroyView() {
37+
stopTimer()
38+
super.onDestroyView()
39+
}
40+
41+
private fun getChronometer(): Chronometer = requireView().findViewById(R.id.chronometer)
42+
43+
companion object {
44+
const val TAG = "SamplePreloadFragmentTag"
45+
fun newInstance(): SamplePreloadFragment = SamplePreloadFragment()
46+
}
47+
}

app/src/main/res/layout/activity_main.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@
107107
android:text="Reset Group" />
108108
</LinearLayout>
109109

110+
<LinearLayout
111+
android:layout_width="match_parent"
112+
android:layout_height="wrap_content">
113+
114+
<Button
115+
android:id="@+id/startPreloadedFragment"
116+
android:layout_width="0dp"
117+
android:layout_height="wrap_content"
118+
android:layout_weight="1"
119+
android:singleLine="true"
120+
android:text="Start Preloaded Fragment" />
121+
</LinearLayout>
122+
110123
</LinearLayout>
111124
</RelativeLayout>
112125

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent">
5+
6+
<Chronometer
7+
android:id="@+id/chronometer"
8+
android:layout_width="match_parent"
9+
android:layout_height="wrap_content"
10+
android:layout_gravity="center"
11+
android:textAlignment="center"
12+
android:textSize="16sp" />
13+
14+
</LinearLayout>

medusalib/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ android {
4343

4444
ext {
4545
PUBLISH_GROUP_ID = 'com.trendyol'
46-
PUBLISH_VERSION = '0.12.1'
46+
PUBLISH_VERSION = '0.12.2'
4747
PUBLISH_ARTIFACT_ID = 'medusa'
4848
PUBLISH_DESCRIPTION = "Android Fragment Stack Controller"
4949
PUBLISH_URL = "https://github.com/Trendyol/medusa"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.trendyol.medusalib.navigator
2+
3+
import androidx.fragment.app.testing.launchFragmentInContainer
4+
import androidx.lifecycle.Lifecycle
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import com.google.common.truth.Truth
7+
import com.trendyol.medusalib.TestChildFragment
8+
import com.trendyol.medusalib.TestParentWithNavigatorFragment
9+
import com.trendyol.medusalib.navigator.controller.PreloadedFragmentResult
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
13+
@RunWith(AndroidJUnit4::class)
14+
class PreloadFragmentTest {
15+
16+
@Test
17+
fun testPreloadFragment() {
18+
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
19+
scenario.moveToState(Lifecycle.State.RESUMED)
20+
21+
scenario.onFragment { parentFragment ->
22+
val navigator = parentFragment.navigator
23+
24+
val fragmentTag = "preloaded_test_fragment"
25+
val testChildFragment = TestChildFragment.newInstance("Preload Fragment")
26+
27+
navigator.preloadFragment(testChildFragment, fragmentTag)
28+
parentFragment.childFragmentManager.executePendingTransactions()
29+
30+
val preloadedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
31+
Truth.assertThat(preloadedFragment).isNotNull()
32+
Truth.assertThat(preloadedFragment?.isVisible).isFalse()
33+
Truth.assertThat(parentFragment.childFragmentManager.fragments.any { it.isVisible }).isTrue()
34+
}
35+
}
36+
37+
@Test
38+
fun testStartPreloadedFragment() {
39+
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
40+
scenario.moveToState(Lifecycle.State.RESUMED)
41+
scenario.onFragment { parentFragment ->
42+
val fragmentTag = "preloaded_test_fragment"
43+
val navigator = parentFragment.navigator
44+
val testChildFragment = TestChildFragment.newInstance("Preload Fragment")
45+
46+
navigator.preloadFragment(testChildFragment, fragmentTag)
47+
parentFragment.childFragmentManager.executePendingTransactions()
48+
49+
val result = navigator.startPreloadedFragment(null, fragmentTag)
50+
parentFragment.childFragmentManager.executePendingTransactions()
51+
52+
Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.Success)
53+
val startedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
54+
Truth.assertThat(startedFragment).isNotNull()
55+
Truth.assertThat(startedFragment?.isVisible).isTrue()
56+
57+
val fragmentsWithoutPreloaded = parentFragment.childFragmentManager.fragments - startedFragment
58+
Truth.assertThat(fragmentsWithoutPreloaded.all { it?.isVisible == false }).isTrue()
59+
}
60+
}
61+
62+
@Test
63+
fun testStartPreloadedFragmentWithFallback() {
64+
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
65+
scenario.moveToState(Lifecycle.State.RESUMED)
66+
scenario.onFragment { parentFragment ->
67+
val fragmentTag = "non_existent_fragment"
68+
val navigator = parentFragment.navigator
69+
val fallbackFragment = TestChildFragment.newInstance("Fallback Fragment")
70+
71+
val result = navigator.startPreloadedFragment(fallbackFragment, fragmentTag)
72+
parentFragment.childFragmentManager.executePendingTransactions()
73+
74+
val startedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
75+
Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.FallbackSuccess)
76+
Truth.assertThat(startedFragment).isNotNull()
77+
Truth.assertThat(startedFragment?.isVisible).isTrue()
78+
79+
val fragmentsWithoutPreloaded = parentFragment.childFragmentManager.fragments - startedFragment
80+
Truth.assertThat(fragmentsWithoutPreloaded.all { it?.isVisible == false }).isTrue()
81+
}
82+
}
83+
84+
@Test
85+
fun testStartPreloadedFragmentNotFound() {
86+
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
87+
scenario.moveToState(Lifecycle.State.RESUMED)
88+
scenario.onFragment { parentFragment ->
89+
val fragmentTag = "non_existent_fragment"
90+
val navigator = parentFragment.navigator
91+
92+
val result = navigator.startPreloadedFragment(null, fragmentTag)
93+
parentFragment.childFragmentManager.executePendingTransactions()
94+
95+
val fragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
96+
Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.NotFound)
97+
Truth.assertThat(fragment).isNull()
98+
Truth.assertThat(parentFragment.childFragmentManager.fragments.any { it.isVisible }).isTrue()
99+
}
100+
}
101+
}

medusalib/src/main/java/com/trendyol/medusalib/navigator/MultipleStackNavigator.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
77
import androidx.lifecycle.LifecycleOwner
88
import androidx.lifecycle.MutableLiveData
99
import com.trendyol.medusalib.navigator.controller.FragmentManagerController
10+
import com.trendyol.medusalib.navigator.controller.PreloadedFragmentResult
1011
import com.trendyol.medusalib.navigator.controller.StagedFragmentHolder
1112
import com.trendyol.medusalib.navigator.data.FragmentData
1213
import com.trendyol.medusalib.navigator.data.StackItem
@@ -58,6 +59,19 @@ open class MultipleStackNavigator(
5859
start(fragment, DEFAULT_GROUP_NAME, transitionAnimation)
5960
}
6061

62+
override fun preloadFragment(fragment: Fragment, fragmentTag: String) {
63+
fragmentManagerController.preloadFragment(FragmentData(fragment, fragmentTag))
64+
}
65+
66+
override fun startPreloadedFragment(fallbackFragment: Fragment?, fragmentTag: String): PreloadedFragmentResult {
67+
val currentFragmentTag = getCurrentFragmentTag()
68+
val result = fragmentManagerController.showPreloadedFragment(currentFragmentTag, fragmentTag, fallbackFragment)
69+
if (result !is PreloadedFragmentResult.NotFound) {
70+
fragmentStackState.notifyStackItemAddToCurrentTab(StackItem(fragmentTag = fragmentTag))
71+
}
72+
return result
73+
}
74+
6175
override fun start(fragment: Fragment, fragmentGroupName: String, transitionAnimation: TransitionAnimationType?) {
6276

6377
val createdTag = tagCreator.create(fragment)

medusalib/src/main/java/com/trendyol/medusalib/navigator/Navigator.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.trendyol.medusalib.navigator
33
import android.os.Bundle
44
import androidx.fragment.app.Fragment
55
import androidx.lifecycle.LifecycleOwner
6+
import com.trendyol.medusalib.navigator.controller.PreloadedFragmentResult
67
import com.trendyol.medusalib.navigator.transaction.NavigatorTransaction
78
import com.trendyol.medusalib.navigator.transitionanimation.TransitionAnimationType
89

@@ -69,6 +70,36 @@ interface Navigator {
6970
*/
7071
fun start(fragment: Fragment, transitionAnimation: TransitionAnimationType)
7172

73+
/**
74+
* Preloads a fragment into the current navigation stack without immediately displaying it.
75+
*
76+
* This method attaches the given fragment to the fragment manager and prepares it in a hidden.
77+
* The fragment will not be visible to the user until it is started later using
78+
* [startPreloadedFragment].
79+
*
80+
* @param fragment The fragment instance to preload.
81+
* @param fragmentTag The unique tag of fragment.
82+
*/
83+
fun preloadFragment(fragment: Fragment, fragmentTag: String)
84+
85+
/**
86+
* Starts a fragment that was previously preloaded, making it visible.
87+
*
88+
* This method takes a previously preloaded fragment identified by its [fragmentTag] and brings it
89+
* to the foreground. If the fragment was successfully preloaded, it will be shown immediately. If
90+
* the fragment was not found or not preloaded, the optionally provided [fallbackFragment] will be added
91+
* and displayed as a fallback.
92+
*
93+
* @param fallbackFragment Fragment instance to display if the preloaded fragment cannot be found.
94+
* @param fragmentTag The unique tag of the previously preloaded fragment to start.
95+
*
96+
* @return [PreloadedFragmentResult]
97+
* - [PreloadedFragmentResult.Success] if the preloaded fragment was found and displayed.
98+
* - [PreloadedFragmentResult.FallbackSuccess] if the fallback fragment was used instead.
99+
* - [PreloadedFragmentResult.NotFound] if no suitable fragment was found and no fallback was provided.
100+
*/
101+
fun startPreloadedFragment(fallbackFragment: Fragment?, fragmentTag: String): PreloadedFragmentResult
102+
72103
/**
73104
* Modifies fragment stack. Pops current fragment from
74105
* fragment stack and detaches it. Peeks from fragment stack

medusalib/src/main/java/com/trendyol/medusalib/navigator/controller/FragmentManagerController.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ internal class FragmentManagerController(
7878
commitAllowingStateLoss()
7979
}
8080

81+
fun preloadFragment(fragmentData: FragmentData) {
82+
checkAndCreateTransaction()
83+
stageFragment(fragmentData)
84+
currentTransaction?.add(containerId, fragmentData.fragment, fragmentData.fragmentTag)?.hide(fragmentData.fragment)
85+
commitAllowingStateLoss()
86+
}
87+
88+
fun showPreloadedFragment(
89+
currentFragmentTag: String,
90+
fragmentTag: String,
91+
fallbackFragment: Fragment?,
92+
): PreloadedFragmentResult {
93+
val fragment = getFragmentWithExecutingPendingTransactionsIfNeeded(fragmentTag) ?: fallbackFragment
94+
if (fragment == null) return PreloadedFragmentResult.NotFound
95+
96+
disableFragment(currentFragmentTag)
97+
98+
return if (fragment != fallbackFragment) {
99+
enableFragment(fragmentTag)
100+
PreloadedFragmentResult.Success
101+
} else {
102+
addFragment(FragmentData(fallbackFragment, fragmentTag))
103+
PreloadedFragmentResult.FallbackSuccess
104+
}
105+
}
106+
81107
fun disableAndStartFragment(disableFragmentTag: String, vararg fragmentDataArgs: FragmentData) {
82108
val disabledFragment = getFragmentWithExecutingPendingTransactionsIfNeeded(disableFragmentTag)
83109

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.trendyol.medusalib.navigator.controller
2+
3+
/**
4+
* Represents the result of attempting to show a preloaded fragment.
5+
*/
6+
sealed interface PreloadedFragmentResult {
7+
8+
/**
9+
* Indicates that the preloaded fragment was successfully shown.
10+
*/
11+
data object Success : PreloadedFragmentResult
12+
13+
/**
14+
* Indicates that a fallback fragment was used and successfully shown.
15+
*/
16+
data object FallbackSuccess : PreloadedFragmentResult
17+
18+
/**
19+
* Indicates that neither the preloaded fragment nor a fallback fragment was found.
20+
*/
21+
data object NotFound : PreloadedFragmentResult
22+
}

0 commit comments

Comments
 (0)