Skip to content

Commit

Permalink
Merge pull request #58 from Trendyol/PreloadedFragment
Browse files Browse the repository at this point in the history
Preloaded fragment
  • Loading branch information
erolaksoy authored Dec 19, 2024
2 parents 99db321 + 6889f37 commit 7be4270
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 1 deletion.
8 changes: 8 additions & 0 deletions app/src/main/java/com/trendyol/medusa/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class MainActivity : AppCompatActivity(), Navigator.NavigatorListener {

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

multipleStackNavigator.preloadFragment(
fragment = SamplePreloadFragment.newInstance(),
fragmentTag = SamplePreloadFragment.TAG
)

multipleStackNavigator.initialize(savedInstanceState)
val restartRootFragmentCheckBox = findViewById<View>(R.id.restartSwitch) as SwitchCompat
findViewById<Button>(R.id.resetCurrentTab).setOnClickListener {
Expand All @@ -84,6 +89,9 @@ class MainActivity : AppCompatActivity(), Navigator.NavigatorListener {
findViewById<Button>(R.id.resetGroup).setOnClickListener {
multipleStackNavigator.clearGroup("group1")
}
findViewById<Button>(R.id.startPreloadedFragment).setOnClickListener {
multipleStackNavigator.startPreloadedFragment(SamplePreloadFragment.newInstance(), SamplePreloadFragment.TAG)
}
multipleStackNavigator.observeDestinationChanges(this) {
Log.d("Destination Changed", "${it.javaClass.name} - ${it.tag}")
}
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/com/trendyol/medusa/SamplePreloadFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.trendyol.medusa

import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Chronometer

class SamplePreloadFragment : BaseFragment() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return inflater.inflate(R.layout.fragment_sample_preload, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startTimer()
}

private fun startTimer() {
getChronometer().apply {
base = SystemClock.elapsedRealtime()
start()
}
}

private fun stopTimer() {
getChronometer().stop()
}

override fun onDestroyView() {
stopTimer()
super.onDestroyView()
}

private fun getChronometer(): Chronometer = requireView().findViewById(R.id.chronometer)

companion object {
const val TAG = "SamplePreloadFragmentTag"
fun newInstance(): SamplePreloadFragment = SamplePreloadFragment()
}
}
13 changes: 13 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@
android:text="Reset Group" />
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/startPreloadedFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="Start Preloaded Fragment" />
</LinearLayout>

</LinearLayout>
</RelativeLayout>

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/res/layout/fragment_sample_preload.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Chronometer
android:id="@+id/chronometer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAlignment="center"
android:textSize="16sp" />

</LinearLayout>
2 changes: 1 addition & 1 deletion medusalib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ android {

ext {
PUBLISH_GROUP_ID = 'com.trendyol'
PUBLISH_VERSION = '0.12.1'
PUBLISH_VERSION = '0.12.2'
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
@@ -0,0 +1,101 @@
package com.trendyol.medusalib.navigator

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth
import com.trendyol.medusalib.TestChildFragment
import com.trendyol.medusalib.TestParentWithNavigatorFragment
import com.trendyol.medusalib.navigator.controller.PreloadedFragmentResult
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PreloadFragmentTest {

@Test
fun testPreloadFragment() {
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
scenario.moveToState(Lifecycle.State.RESUMED)

scenario.onFragment { parentFragment ->
val navigator = parentFragment.navigator

val fragmentTag = "preloaded_test_fragment"
val testChildFragment = TestChildFragment.newInstance("Preload Fragment")

navigator.preloadFragment(testChildFragment, fragmentTag)
parentFragment.childFragmentManager.executePendingTransactions()

val preloadedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
Truth.assertThat(preloadedFragment).isNotNull()
Truth.assertThat(preloadedFragment?.isVisible).isFalse()
Truth.assertThat(parentFragment.childFragmentManager.fragments.any { it.isVisible }).isTrue()
}
}

@Test
fun testStartPreloadedFragment() {
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.onFragment { parentFragment ->
val fragmentTag = "preloaded_test_fragment"
val navigator = parentFragment.navigator
val testChildFragment = TestChildFragment.newInstance("Preload Fragment")

navigator.preloadFragment(testChildFragment, fragmentTag)
parentFragment.childFragmentManager.executePendingTransactions()

val result = navigator.startPreloadedFragment(null, fragmentTag)
parentFragment.childFragmentManager.executePendingTransactions()

Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.Success)
val startedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
Truth.assertThat(startedFragment).isNotNull()
Truth.assertThat(startedFragment?.isVisible).isTrue()

val fragmentsWithoutPreloaded = parentFragment.childFragmentManager.fragments - startedFragment
Truth.assertThat(fragmentsWithoutPreloaded.all { it?.isVisible == false }).isTrue()
}
}

@Test
fun testStartPreloadedFragmentWithFallback() {
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.onFragment { parentFragment ->
val fragmentTag = "non_existent_fragment"
val navigator = parentFragment.navigator
val fallbackFragment = TestChildFragment.newInstance("Fallback Fragment")

val result = navigator.startPreloadedFragment(fallbackFragment, fragmentTag)
parentFragment.childFragmentManager.executePendingTransactions()

val startedFragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.FallbackSuccess)
Truth.assertThat(startedFragment).isNotNull()
Truth.assertThat(startedFragment?.isVisible).isTrue()

val fragmentsWithoutPreloaded = parentFragment.childFragmentManager.fragments - startedFragment
Truth.assertThat(fragmentsWithoutPreloaded.all { it?.isVisible == false }).isTrue()
}
}

@Test
fun testStartPreloadedFragmentNotFound() {
val scenario = launchFragmentInContainer<TestParentWithNavigatorFragment>()
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.onFragment { parentFragment ->
val fragmentTag = "non_existent_fragment"
val navigator = parentFragment.navigator

val result = navigator.startPreloadedFragment(null, fragmentTag)
parentFragment.childFragmentManager.executePendingTransactions()

val fragment = parentFragment.childFragmentManager.findFragmentByTag(fragmentTag)
Truth.assertThat(result).isEqualTo(PreloadedFragmentResult.NotFound)
Truth.assertThat(fragment).isNull()
Truth.assertThat(parentFragment.childFragmentManager.fragments.any { it.isVisible }).isTrue()
}
}
}
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.PreloadedFragmentResult
import com.trendyol.medusalib.navigator.controller.StagedFragmentHolder
import com.trendyol.medusalib.navigator.data.FragmentData
import com.trendyol.medusalib.navigator.data.StackItem
Expand Down Expand Up @@ -58,6 +59,19 @@ open class MultipleStackNavigator(
start(fragment, DEFAULT_GROUP_NAME, transitionAnimation)
}

override fun preloadFragment(fragment: Fragment, fragmentTag: String) {
fragmentManagerController.preloadFragment(FragmentData(fragment, fragmentTag))
}

override fun startPreloadedFragment(fallbackFragment: Fragment?, fragmentTag: String): PreloadedFragmentResult {
val currentFragmentTag = getCurrentFragmentTag()
val result = fragmentManagerController.showPreloadedFragment(currentFragmentTag, fragmentTag, fallbackFragment)
if (result !is PreloadedFragmentResult.NotFound) {
fragmentStackState.notifyStackItemAddToCurrentTab(StackItem(fragmentTag = fragmentTag))
}
return result
}

override fun start(fragment: Fragment, fragmentGroupName: String, transitionAnimation: TransitionAnimationType?) {

val createdTag = tagCreator.create(fragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.trendyol.medusalib.navigator
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import com.trendyol.medusalib.navigator.controller.PreloadedFragmentResult
import com.trendyol.medusalib.navigator.transaction.NavigatorTransaction
import com.trendyol.medusalib.navigator.transitionanimation.TransitionAnimationType

Expand Down Expand Up @@ -69,6 +70,36 @@ interface Navigator {
*/
fun start(fragment: Fragment, transitionAnimation: TransitionAnimationType)

/**
* Preloads a fragment into the current navigation stack without immediately displaying it.
*
* This method attaches the given fragment to the fragment manager and prepares it in a hidden.
* The fragment will not be visible to the user until it is started later using
* [startPreloadedFragment].
*
* @param fragment The fragment instance to preload.
* @param fragmentTag The unique tag of fragment.
*/
fun preloadFragment(fragment: Fragment, fragmentTag: String)

/**
* Starts a fragment that was previously preloaded, making it visible.
*
* This method takes a previously preloaded fragment identified by its [fragmentTag] and brings it
* to the foreground. If the fragment was successfully preloaded, it will be shown immediately. If
* the fragment was not found or not preloaded, the optionally provided [fallbackFragment] will be added
* and displayed as a fallback.
*
* @param fallbackFragment Fragment instance to display if the preloaded fragment cannot be found.
* @param fragmentTag The unique tag of the previously preloaded fragment to start.
*
* @return [PreloadedFragmentResult]
* - [PreloadedFragmentResult.Success] if the preloaded fragment was found and displayed.
* - [PreloadedFragmentResult.FallbackSuccess] if the fallback fragment was used instead.
* - [PreloadedFragmentResult.NotFound] if no suitable fragment was found and no fallback was provided.
*/
fun startPreloadedFragment(fallbackFragment: Fragment?, fragmentTag: String): PreloadedFragmentResult

/**
* Modifies fragment stack. Pops current fragment from
* fragment stack and detaches it. Peeks from fragment stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ internal class FragmentManagerController(
commitAllowingStateLoss()
}

fun preloadFragment(fragmentData: FragmentData) {
checkAndCreateTransaction()
stageFragment(fragmentData)
currentTransaction?.add(containerId, fragmentData.fragment, fragmentData.fragmentTag)?.hide(fragmentData.fragment)
commitAllowingStateLoss()
}

fun showPreloadedFragment(
currentFragmentTag: String,
fragmentTag: String,
fallbackFragment: Fragment?,
): PreloadedFragmentResult {
val fragment = getFragmentWithExecutingPendingTransactionsIfNeeded(fragmentTag) ?: fallbackFragment
if (fragment == null) return PreloadedFragmentResult.NotFound

disableFragment(currentFragmentTag)

return if (fragment != fallbackFragment) {
enableFragment(fragmentTag)
PreloadedFragmentResult.Success
} else {
addFragment(FragmentData(fallbackFragment, fragmentTag))
PreloadedFragmentResult.FallbackSuccess
}
}

fun disableAndStartFragment(disableFragmentTag: String, vararg fragmentDataArgs: FragmentData) {
val disabledFragment = getFragmentWithExecutingPendingTransactionsIfNeeded(disableFragmentTag)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.trendyol.medusalib.navigator.controller

/**
* Represents the result of attempting to show a preloaded fragment.
*/
sealed interface PreloadedFragmentResult {

/**
* Indicates that the preloaded fragment was successfully shown.
*/
data object Success : PreloadedFragmentResult

/**
* Indicates that a fallback fragment was used and successfully shown.
*/
data object FallbackSuccess : PreloadedFragmentResult

/**
* Indicates that neither the preloaded fragment nor a fallback fragment was found.
*/
data object NotFound : PreloadedFragmentResult
}

0 comments on commit 7be4270

Please sign in to comment.