Skip to content

Instantly share code, notes, and snippets.

@thapld
Created June 20, 2019 09:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thapld/e4e187a2442dc69ca60d31542b8bde0b to your computer and use it in GitHub Desktop.
Save thapld/e4e187a2442dc69ca60d31542b8bde0b to your computer and use it in GitHub Desktop.
abstract class MvvmFragment<VDB : ViewDataBinding, VM : BaseViewModel> : Fragment(), CanFetchExtras, CanHandleNewIntent,
CanHandleBackPressEvents {
/**
* The [Cyanea] instance used for styling.
*/
open val cyanea: Cyanea get() = (activity as? BaseCyaneaActivity)?.cyanea ?: Cyanea.instance
/**
* The content [View] of the current [MvvmFragment].
* (might be null if the current [MvvmFragment] hasn't been initialized yet.)
*/
var rootView: View? = null
private set
private var viewDataBinding: VDB? = null
private var viewModel: VM? = null
private val eventConsumerDisposables = CompositeDisposable()
private val registeredObservables = HashSet<Pair<Observable.OnPropertyChangedCallback, Observable>>()
/**
* Indicates whether the current [MvvmFragment]'s content view is initialized or not.
*/
var isViewCreated = false
private set
/**
* Indicates whether the current [MvvmFragment] is being animated or not.
*/
var isViewAnimating = false
private set
/**
* Hint provided by the app that this fragment is currently visible to the user, as well as "active".
* (This is usually set manually (e.g. when using the [androidx.viewpager.widget.ViewPager]) to indicate that the "Page" is active
* and ready to load data or do something useful)
*/
var isActive = true
private set
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
applyMenuTint(menu)
super.onCreateOptionsMenu(menu, inflater)
}
protected open fun applyMenuTint(menu: Menu) = cyanea.tint(menu, requireActivity())
final override fun onCreate(savedInstanceState: Bundle?) {
// dependencies will be injected only once (based on the state of the content view)
if (!isViewCreated) {
injectDependencies()
}
super.onCreate(savedInstanceState)
// the overall initialization, extras fetching and post initialization will be performed only once, too
if (!isViewCreated) {
initViewModel()
arguments?.let(::fetchExtras)
preInit()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (!isViewCreated) {
viewDataBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
rootView = viewDataBinding?.root
}
viewDataBinding?.setVariable(getBindingVariable(), viewModel)
viewDataBinding?.lifecycleOwner = this
return rootView
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val wasViewCreated = isViewCreated
isViewCreated = true
// performing the initialization only in cases when the view was created for the first time
if (!wasViewCreated) {
init(savedInstanceState)
postInit()
}
performDataBinding()
// performing the state restoring only in cases when the view was created for the first time
// (otherwise there's no need to restore the state, as the current view already holds the most recent state)
if (!wasViewCreated) {
savedInstanceState?.let(::onRestoreStateInternal)
}
}
/**
* Gets called when it's the right time for you to inject the dependencies.
*/
open fun injectDependencies() {
//
}
/**
* Gets called right before the pre-initialization stage ([preInit] method call),
* if the [Bundle] received from the [onViewCreated] is not null.
*
* @param extras the bundle of arguments
*/
@CallSuper
override fun fetchExtras(extras: Bundle) {
//
}
@CallSuper
override fun handleNewIntent(intent: Intent) {
//
}
/**
* Gets called right before the UI initialization.
*/
protected open fun preInit() {
//
}
private fun initViewModel() {
viewModel = getViewModel()
}
/**
* Get's called when it's the right time for you to initialize the UI elements.
*
* @param savedInstanceState the state bundle brought from the [Fragment.onViewCreated]
*/
protected open fun init(savedInstanceState: Bundle?) {
//
}
/**
* Gets called right after the UI initialization.
*/
protected open fun postInit() {
//
}
/**
* Executes the pending Data Binding operations.
*/
@CallSuper
protected open fun performDataBinding() {
viewDataBinding?.executePendingBindings()
}
/**
* Looks up the [View] for the specified viewId within the current view hierarchy.
*
* @throws IllegalStateException if the [MvvmFragment]'s root [View] hasn't been created yet.
*/
protected fun <T : View> findViewById(@IdRes viewId: Int): T {
return (rootView?.findViewById(viewId)
?: throw IllegalStateException("The Fragment View hasn't been created yet."))
}
/**
* Shows the system software keyboard.
*
* @param requestFocus whether the target view should request the focus or not
*/
protected fun showKeyboard(requestFocus: Boolean = true) {
rootView?.let { showKeyboard(it, requestFocus) }
}
/**
* Shows the system software keyboard.
*
* @param targetView the view that's requesting the keyboard to be shown
* @param requestFocus whether the target view should request the focus or not
*/
protected fun showKeyboard(targetView: View, requestFocus: Boolean = true) {
targetView.showKeyboard(requestFocus)
}
/**
* Hides the system software keyboard.
*
* @param clearFocus whether the focus should be cleared from the target view or not
*/
protected fun hideKeyboard(clearFocus: Boolean = true) {
rootView?.let { hideKeyboard(it, clearFocus) }
}
/**
* Hides the system software keyboard.
*
* @param targetView the view that's requesting the keyboard to be hidden
* @param clearFocus whether the focus should be cleared from the target view or not
*/
protected fun hideKeyboard(targetView: View, clearFocus: Boolean = true) {
targetView.hideKeyboard(clearFocus)
}
/**
* Performs the Back Press Action (see: [android.app.Activity.onBackPressed]).
*/
protected fun performBackPress() {
activity?.onBackPressed()
}
/**
* Finishes the host [android.app.Activity] (see: [android.app.Activity.finish]).
*/
protected fun finishActivity() {
activity?.finish()
}
/**
* Finishes the host [android.app.Activity] affinity (see: [android.app.Activity.finishAffinity]).
*/
protected fun finishActivityAffinity() {
activity?.finishAffinity()
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation {
if (nextAnim == 0) {
return AnimationUtils.loadAnimation(context!!, R.anim.no_animation)
}
// enabling the hardware acceleration for the time of the animation (to smoothen the things up)
view?.useHardwareLayer()
view?.cancelActiveAnimations()
return AnimationUtils.loadAnimation(context!!, nextAnim).apply {
setAnimationListener(object : AnimationListenerAdapter() {
override fun onAnimationStart(animation: Animation?) {
onAnimationStarted()
}
override fun onAnimationEnd(animation: Animation?) {
view?.useNoLayer()
onAnimationEnded()
}
})
}
}
/**
* Gets called whenever the [Fragment]-related transition animation starts.
*/
@CallSuper
protected open fun onAnimationStarted() {
isViewAnimating = true
}
/**
* Gets called whenever the [Fragment]-related transition animation ends.
*/
@CallSuper
protected open fun onAnimationEnded() {
isViewAnimating = false
}
@CallSuper
override fun onResume() {
super.onResume()
subscribeEventConsumers()
onRegisterObservables()
viewModel?.onStart()
}
/**
* Gets called when it's the right time to register the [ObservableField]s of your [androidx.lifecycle.ViewModel].
*/
protected open fun onRegisterObservables() {
//
}
@CallSuper
override fun onPause() {
super.onPause()
viewModel?.onStop()
unsubscribeEventConsumers()
unregisterFields()
}
@CallSuper
override fun onBackPressed(): Boolean {
hideKeyboard()
val isConsumedByViewModel = (viewModel?.onBackPressed() ?: false)
return (handleBackPressEvent() || isConsumedByViewModel)
}
private fun handleBackPressEvent(): Boolean {
return childFragmentManager.fragments.handleBackPressEvent()
}
private fun onRestoreStateInternal(stateBundle: Bundle) {
viewModel?.onRestoreState(stateBundle)
onRestoreState(stateBundle)
}
/**
* Gets called whenever it's the right time to restore the previously stored state.
*
* @param stateBundle the previously store state
*/
open fun onRestoreState(stateBundle: Bundle) {
//
}
final override fun onSaveInstanceState(outState: Bundle) {
viewModel?.onSaveState(outState)
onSaveState(outState)
super.onSaveInstanceState(outState)
}
/**
* Gets called whenever it's the right time to save the state.
*
* @param stateBundle the bundle the state is to be saved into
*/
open fun onSaveState(stateBundle: Bundle) {
//
}
override fun onDestroy() {
onRecycle()
recycleInternal()
super.onDestroy()
}
private fun recycleInternal() {
rootView = null
viewDataBinding = null
viewModel = null
isViewAnimating = false
isViewCreated = false
}
/**
* Gets called right before the destruction of the [Fragment] (see: [Fragment.onDestroy]).
*/
protected open fun onRecycle() {
//
}
/**
* Gets called whenever the new [BaseViewModel] event arrives.
*
* @param event the newly arrived [BaseViewModel] event
*/
@CallSuper
protected open fun onViewModelEvent(event: ViewModelEvent<*>) {
when (event) {
is GeneralViewModelEvents.HideKeyboard -> event.data?.let { hideKeyboard(clearFocus = it) }
is GeneralViewModelEvents.ConfirmBackButtonPress -> performBackPress()
is GeneralViewModelEvents.FinishActivity -> activity?.finish()
}
}
/**
* Gets called whenever the [MvvmFragment] becomes "active".
* (see: [MvvmFragment.setActive])
*/
protected open fun onBecameActive() {
//
}
/**
* Gets called whenever the [MvvmFragment] becomes "inactive".
* (see: [MvvmFragment.setActive])
*/
protected open fun onBecameInactive() {
//
}
private fun subscribeEventConsumers() {
viewModel?.subscribe(Consumer(::onViewModelEvent))
?.manageLifecycle()
}
private fun unsubscribeEventConsumers() {
eventConsumerDisposables.clear()
}
private fun unregisterFields() {
registeredObservables.forEach { (callback, field) -> field.removeOnPropertyChangedCallback(callback) }
registeredObservables.clear()
}
/**
* Set a hint about whether this fragment is currently "active".
* (This hint defaults to true)
* (It's mostly used in conjunction with the [androidx.viewpager.widget.ViewPager])
*
* (See: [isActive])
*
* @param isActive true if this fragment is currently "active" (default).
*/
fun setActive(isActive: Boolean) {
val wasChanged = (this.isActive != isActive)
this.isActive = isActive
if (isActive) {
if (wasChanged) {
onBecameActive()
}
} else {
if (wasChanged) {
onBecameInactive()
}
}
}
/**
* Retrieves the resource id of the layout which will be used
* as a content view of the [Fragment].
*
* @return a valid layout resource id
*/
@LayoutRes
protected abstract fun getLayoutId(): Int
/**
* Retrieves the id of the Data Binding variable.
* (This id should correspond to the id of the ViewModel
* variable defined in your xml layout file)
*
* @return the binding variable id
*/
protected abstract fun getBindingVariable(): Int
/**
* Used to retrieve the concrete version of the
* initialized [BaseViewModel].
*
* @return the initialized [BaseViewModel]
*/
protected abstract fun getViewModel(): VM
private fun Disposable.manageLifecycle() {
eventConsumerDisposables.add(this)
}
/**
* Adds the specified [Observable.OnPropertyChangedCallback] to the registry of Lifecycle-aware Callbacks.
* <br>
* [Observable.OnPropertyChangedCallback]s are automatically disposed whenever the
* [Fragment.onPause] method is called.
*
* @param observable the [Observable] the [Observable.OnPropertyChangedCallback] is registered to
*/
protected fun Observable.OnPropertyChangedCallback.manageLifecycle(observable: Observable) {
registeredObservables.add(Pair(this, observable))
}
/**
* Registers the value change callback to the specified [ObservableField].
* <br>
* The lifecycle of the registered callbacks is managed internally (see: [manageLifecycle]),
* so you don't have to do any manual unregistering yourself.
*
* @param callback value change callback
*/
protected inline fun <T : ObservableField<R>, R : Any> T.register(crossinline callback: (R) -> Unit) {
this.onPropertyChanged { it.get()?.let(callback) }.manageLifecycle(this)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment