Created
August 26, 2019 10:20
-
-
Save mrblrrd/a7b509016a6729b9ac87633f058f5882 to your computer and use it in GitHub Desktop.
Demonstration of the Fragment Plugin pattern.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common | |
import android.os.Bundle | |
import android.support.annotation.CallSuper | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import com.arellomobile.mvp.MvpAppCompatFragment | |
import com.redmadrobot.ui.common.plugin.FragmentEvent | |
import com.redmadrobot.ui.common.plugin.FragmentPlugin | |
import com.redmadrobot.ui.common.plugin.ScopeManagementPlugin | |
import com.redmadrobot.ui.common.plugin.VisibilityManagementPlugin | |
import toothpick.Scope | |
abstract class BaseFragment : | |
MvpAppCompatFragment(), | |
ScopeNameHolder, | |
SavedInstanceStateHolder { | |
abstract val layoutRes: Int | |
override var instanceStateHasBeenSaved: Boolean = false | |
// region Plugin management | |
private val plugins = mutableListOf<FragmentPlugin>() | |
private val scopeManagementPlugin by lazy { ScopeManagementPlugin(this) { onScopeConfigure(it) } } | |
private val visibilityManagementPlugin by lazy { VisibilityManagementPlugin(this) } | |
@CallSuper | |
protected open fun initPlugins() { | |
addPlugin(scopeManagementPlugin) | |
addPlugin(visibilityManagementPlugin) | |
} | |
protected fun addPlugin(plugin: FragmentPlugin) { | |
plugins.add(plugin) | |
} | |
private fun dispatchEventToPlugins(event: FragmentEvent) { | |
plugins.forEach { it.onFragmentEvent(event) } | |
} | |
private fun releasePlugins() { | |
plugins.clear() | |
} | |
// endregion | |
// region System callbacks | |
override fun onCreate(savedInstanceState: Bundle?) { | |
initPlugins() | |
dispatchEventToPlugins(FragmentEvent.BeforeOnCreate(savedInstanceState)) | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View { | |
return inflater.inflate(layoutRes, container, false) | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
dispatchEventToPlugins(FragmentEvent.OnViewCreated(view, savedInstanceState)) | |
} | |
override fun onResume() { | |
super.onResume() | |
instanceStateHasBeenSaved = false | |
dispatchEventToPlugins(FragmentEvent.OnResume) | |
} | |
override fun onPause() { | |
super.onPause() | |
dispatchEventToPlugins(FragmentEvent.OnPause) | |
} | |
override fun onSaveInstanceState(outState: Bundle) { | |
super.onSaveInstanceState(outState) | |
instanceStateHasBeenSaved = true | |
dispatchEventToPlugins(FragmentEvent.OnSaveInstanceState(outState)) | |
} | |
override fun onHiddenChanged(hidden: Boolean) { | |
super.onHiddenChanged(hidden) | |
dispatchEventToPlugins(FragmentEvent.OnHiddenChanged(hidden)) | |
} | |
override fun onDestroyView() { | |
super.onDestroyView() | |
dispatchEventToPlugins(FragmentEvent.OnDestroyView) | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
dispatchEventToPlugins(FragmentEvent.OnDestroy) | |
releasePlugins() | |
} | |
// endregion | |
// region Fragment feature extensions | |
protected val scope: Scope | |
get() = scopeManagementPlugin.scope | |
override val scopeName: String | |
get() = scope.name.toString() | |
open fun onScopeConfigure(scope: Scope) {} | |
/** | |
* Called when fragment become visible/invisible to user including parent visibility. | |
* Currently we implement it as approximation to [isVisible] implementation. Thus we don't take into account [setUserVisibleHint]. | |
*/ | |
open fun onVisibleChanged(visible: Boolean) { | |
if (!visible) { | |
onHidePopupWindows() | |
} | |
} | |
fun dispatchOnParentHiddenChanged(hidden: Boolean) = | |
visibilityManagementPlugin.dispatchOnParentHiddenChanged(hidden) | |
/** | |
* Called when popup windows managed by this fragment must be hidden. | |
* Called when fragment becomes invisible (from [onVisibleChanged]). | |
*/ | |
open fun onHidePopupWindows() {} | |
open fun onBackPressed() {} | |
// endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common.plugin | |
import android.os.Build | |
import android.support.annotation.DimenRes | |
import android.support.annotation.RequiresApi | |
import android.support.v7.widget.RecyclerView | |
import android.view.View | |
import android.view.ViewTreeObserver | |
import com.redmadrobot.R | |
import com.redmadrobot.util.ifNotNull | |
class ElevateViewOnScrollPlugin( | |
private val targetProvider: () -> View, | |
private val scrollableViewProvider: () -> View, | |
@DimenRes private val elevationSizeRes: Int = R.dimen.standard_elevation | |
) : FragmentPlugin { | |
private var target: View? = null | |
private var scrollView: View? = null | |
private val applyElevation: (target: View, isApply: Boolean) -> Unit = { target, isApply -> | |
target.elevation = if (!isApply) 0f else target.resources.getDimension(elevationSizeRes) | |
} | |
@get:RequiresApi(Build.VERSION_CODES.M) | |
private val scrollChangeListener by lazy(LazyThreadSafetyMode.NONE) { | |
View.OnScrollChangeListener { _, _, _, _, _ -> | |
ifNotNull(target, scrollView) { target, scrollView -> | |
applyElevation(target, scrollView.scrollY != 0) | |
} | |
} | |
} | |
private val treeViewOnScrollChangedListener = ViewTreeObserver.OnScrollChangedListener { | |
ifNotNull(target, scrollView) { target, scrollView -> | |
applyElevation(target, scrollView.scrollY != 0) | |
} | |
} | |
private val recyclerViewOnScrollListener = object : RecyclerView.OnScrollListener() { | |
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | |
super.onScrolled(recyclerView, dx, dy) | |
ifNotNull(target, scrollView) { target, _ -> | |
applyElevation(target, recyclerView.computeVerticalScrollOffset() != 0) | |
} | |
} | |
} | |
override fun onFragmentEvent(event: FragmentEvent) { | |
when (event) { | |
is FragmentEvent.OnViewCreated -> { | |
target = targetProvider() | |
scrollView = scrollableViewProvider().apply { | |
when { | |
this is RecyclerView -> addOnScrollListener(recyclerViewOnScrollListener) | |
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> setOnScrollChangeListener(scrollChangeListener) | |
else -> viewTreeObserver.addOnScrollChangedListener(treeViewOnScrollChangedListener) | |
} | |
} | |
} | |
is FragmentEvent.OnDestroyView -> { | |
scrollView?.apply { | |
when { | |
this is RecyclerView -> removeOnScrollListener(recyclerViewOnScrollListener) | |
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> setOnScrollChangeListener(null) | |
else -> viewTreeObserver.removeOnScrollChangedListener(treeViewOnScrollChangedListener) | |
} | |
} | |
target = null | |
scrollView = null | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common.plugin | |
import android.os.Bundle | |
import android.view.View | |
sealed class FragmentEvent { | |
data class BeforeOnCreate(val savedInstanceState: Bundle?) : FragmentEvent() | |
object OnResume : FragmentEvent() | |
object OnPause : FragmentEvent() | |
data class OnSaveInstanceState(val outState: Bundle) : FragmentEvent() | |
object OnDestroy : FragmentEvent() | |
data class OnViewCreated(val view: View, val savedInstanceState: Bundle?) : FragmentEvent() | |
object OnDestroyView : FragmentEvent() | |
data class OnHiddenChanged(val hidden: Boolean) : FragmentEvent() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common.plugin | |
interface FragmentPlugin { | |
fun onFragmentEvent(event: FragmentEvent) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common.plugin | |
import android.support.v4.app.Fragment | |
import com.redmadrobot.ui.common.SavedInstanceStateHolder | |
import com.redmadrobot.ui.common.ScopeNameHolder | |
import com.redmadrobot.util.objectScopeName | |
import timber.log.Timber | |
import toothpick.Scope | |
import toothpick.Toothpick | |
class ScopeManagementPlugin( | |
private val fragment: Fragment, | |
private val onScopeConfigure: (scope: Scope) -> Unit | |
) : FragmentPlugin { | |
lateinit var scope: Scope | |
override fun onFragmentEvent(event: FragmentEvent) { | |
when (event) { | |
is FragmentEvent.BeforeOnCreate -> { | |
val fragmentScopeName = | |
event.savedInstanceState?.getString(STATE_SCOPE_NAME) ?: fragment.objectScopeName | |
if (Toothpick.isScopeOpen(fragmentScopeName)) { | |
Timber.d("Get existing UI scope: $fragmentScopeName") | |
scope = Toothpick.openScopes(fragment.getParentScopeName(), fragmentScopeName) | |
} else { | |
Timber.d("Init new UI scope: $fragmentScopeName, parent: ${fragment.getParentScopeName()}") | |
scope = Toothpick.openScopes(fragment.getParentScopeName(), fragmentScopeName) | |
onScopeConfigure(scope) | |
} | |
} | |
is FragmentEvent.OnSaveInstanceState -> { | |
event.outState.putString(STATE_SCOPE_NAME, scope.name.toString()) | |
} | |
is FragmentEvent.OnDestroy -> { | |
if (fragment.needCloseScope()) { | |
// Destroy this fragment with its scope. | |
Timber.d("Destroy UI scope: ${scope.name}") | |
Toothpick.closeScope(scope.name) | |
} | |
} | |
} | |
} | |
private fun Fragment.getParentScopeName(): String = | |
(parentFragment as? ScopeNameHolder)?.scopeName | |
?: (activity as? ScopeNameHolder)?.scopeName | |
?: throw RuntimeException("Can't provide scope name") | |
// It will be valid only for onDestroy() method. | |
private fun Fragment.needCloseScope(): Boolean = | |
when { | |
activity?.isChangingConfigurations == true -> false | |
activity?.isFinishing == true -> true | |
this is SavedInstanceStateHolder -> isRealRemoving(this) | |
else -> true | |
} | |
// This is Android baby! | |
private fun Fragment.isRealRemoving(savedInstanceStateHolder: SavedInstanceStateHolder): Boolean = | |
// Because isRemoving == true for fragment in back stack on screen rotation. | |
(isRemoving && !savedInstanceStateHolder.instanceStateHasBeenSaved) | |
|| (parentFragment | |
?.let { parentFragment -> | |
return@let if (parentFragment is SavedInstanceStateHolder) | |
parentFragment.isRealRemoving(parentFragment) | |
else null | |
} | |
?: false) | |
companion object { | |
private const val STATE_SCOPE_NAME = "state_scope_name" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.redmadrobot.ui.common.plugin | |
import android.support.v4.app.Fragment | |
import com.redmadrobot.ui.common.BaseFragment | |
class VisibilityManagementPlugin(private val fragment: BaseFragment) : FragmentPlugin { | |
override fun onFragmentEvent(event: FragmentEvent) { | |
when (event) { | |
is FragmentEvent.OnResume -> { | |
dispatchOnVisibleChanged(fragment.isVisibleIncludingParents()) | |
} | |
is FragmentEvent.OnPause -> { | |
dispatchOnVisibleChanged(fragment.isVisibleIncludingParents()) | |
} | |
is FragmentEvent.OnHiddenChanged -> { | |
dispatchOnParentHiddenChanged(event.hidden) | |
} | |
} | |
} | |
/** | |
* Notifies child fragments that this fragment became hidden. | |
*/ | |
fun dispatchOnParentHiddenChanged(hidden: Boolean) { | |
dispatchOnVisibleChanged(if (hidden) false else fragment.isVisibleIncludingParents()) | |
fragment | |
.childFragmentManager | |
.fragments | |
.filterIsInstance(BaseFragment::class.java) | |
.forEach { it.dispatchOnParentHiddenChanged(hidden) } | |
} | |
private var lastDispatchedVisibleFlag = false | |
/** | |
* Calls [BaseFragment.onVisibleChanged] only when visibility changes. | |
*/ | |
private fun dispatchOnVisibleChanged(currentVisibleFlag: Boolean) { | |
if (currentVisibleFlag != lastDispatchedVisibleFlag) { | |
lastDispatchedVisibleFlag = currentVisibleFlag | |
fragment.onVisibleChanged(currentVisibleFlag) | |
} | |
} | |
/** | |
* The same as [Fragment.isVisible] but includes parents [Fragment.isVisible] state. | |
*/ | |
private fun Fragment.isVisibleIncludingParents(): Boolean = | |
parentFragment | |
?.let { parent -> | |
when (parent.isVisible) { | |
false -> false | |
true -> parent.isVisibleIncludingParents() && isVisible | |
} | |
} | |
?: isVisible | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment