Skip to content

Instantly share code, notes, and snippets.

@mrblrrd
Created August 26, 2019 10:20
Show Gist options
  • Save mrblrrd/a7b509016a6729b9ac87633f058f5882 to your computer and use it in GitHub Desktop.
Save mrblrrd/a7b509016a6729b9ac87633f058f5882 to your computer and use it in GitHub Desktop.
Demonstration of the Fragment Plugin pattern.
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
}
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
}
}
}
}
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()
}
package com.redmadrobot.ui.common.plugin
interface FragmentPlugin {
fun onFragmentEvent(event: FragmentEvent)
}
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"
}
}
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