Skip to content

Commit

Permalink
Merge pull request #48 from hotwired/consolidate-fragments
Browse files Browse the repository at this point in the history
Consolidate old Turbo* fragments into new *Hotwire fragments
  • Loading branch information
jayohms authored May 22, 2024
2 parents 0fca5c2 + 82a09c6 commit 1fa7f18
Show file tree
Hide file tree
Showing 14 changed files with 523 additions and 564 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,124 @@
package dev.hotwire.core.navigation.fragments

import dev.hotwire.core.turbo.fragments.TurboBottomSheetDialogFragment
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dev.hotwire.core.R
import dev.hotwire.core.navigation.navigator.Navigator
import dev.hotwire.core.navigation.navigator.NavigatorHost
import dev.hotwire.core.turbo.config.title
import dev.hotwire.core.turbo.nav.HotwireNavDestination
import dev.hotwire.core.turbo.nav.HotwireNavDialogDestination

abstract class HotwireBottomSheetFragment : TurboBottomSheetDialogFragment()
/**
* The base class from which all bottom sheet native fragments in a
* Hotwire app should extend from.
*
* For web bottom sheet fragments, refer to [HotwireWebBottomSheetFragment].
*/
abstract class HotwireBottomSheetFragment : BottomSheetDialogFragment(),
HotwireNavDestination, HotwireNavDialogDestination {
override lateinit var navigator: Navigator
internal lateinit var delegate: HotwireFragmentDelegate

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigator = (parentFragment as NavigatorHost).navigator
delegate = HotwireFragmentDelegate(this)
}

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

if (shouldObserveTitleChanges()) {
observeTitleChanges()
pathProperties.title?.let {
fragmentViewModel.setTitle(it)
}
}
}

/**
* This is marked `final` to prevent further use, as it's now deprecated in
* AndroidX's Fragment implementation.
*
* Use [onViewCreated] for code touching
* the Fragment's view and [onCreate] for other initialization.
*/
@Suppress("DEPRECATION")
final override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}

/**
* This is marked `final` to prevent further use, as it's now deprecated in
* AndroidX's Fragment implementation.
*
* Use [registerForActivityResult] with the appropriate
* [androidx.activity.result.contract.ActivityResultContract] and its callback.
*
* Turbo provides the [HotwireNavDestination.activityResultLauncher] interface
* to obtain registered result launchers from any destination.
*/
@Suppress("DEPRECATION")
final override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
}

override fun onStart() {
super.onStart()
delegate.onStart()
}

override fun onStop() {
super.onStop()
delegate.onStop()
}

override fun onCancel(dialog: DialogInterface) {
delegate.onDialogCancel()
super.onCancel(dialog)
}

override fun onDismiss(dialog: DialogInterface) {
delegate.onDialogDismiss()
super.onDismiss(dialog)
}

override fun closeDialog() {
requireDialog().cancel()
}

override fun onBeforeNavigation() {}

override fun refresh(displayProgress: Boolean) {}

override fun prepareNavigation(onReady: () -> Unit) {
delegate.prepareNavigation(onReady)
}

/**
* Gets the Toolbar instance in your Fragment's view for use with
* navigation. The title in the Toolbar will automatically be
* updated if a title is available. By default, Turbo will look
* for a Toolbar with resource ID `R.id.toolbar`. Override to
* provide a Toolbar instance with a different ID.
*/
override fun toolbarForNavigation(): Toolbar? {
return view?.findViewById(R.id.toolbar)
}

final override fun delegate(): HotwireFragmentDelegate {
return delegate
}

private fun observeTitleChanges() {
fragmentViewModel.title.observe(viewLifecycleOwner) {
toolbarForNavigation()?.title = it
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,172 @@
package dev.hotwire.core.navigation.fragments

import dev.hotwire.core.turbo.fragments.TurboFragment
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import dev.hotwire.core.R
import dev.hotwire.core.navigation.navigator.Navigator
import dev.hotwire.core.navigation.navigator.NavigatorHost
import dev.hotwire.core.turbo.config.context
import dev.hotwire.core.turbo.config.title
import dev.hotwire.core.turbo.nav.HotwireNavDestination
import dev.hotwire.core.turbo.nav.TurboNavPresentationContext
import dev.hotwire.core.turbo.observers.HotwireWindowThemeObserver
import dev.hotwire.core.turbo.session.SessionModalResult

abstract class HotwireFragment : TurboFragment()
/**
* The base class from which all "standard" native Fragments (non-dialogs) in a
* Turbo-driven app should extend from.
*
* For web fragments, refer to [HotwireWebFragment].
*/
abstract class HotwireFragment : Fragment(), HotwireNavDestination {
override lateinit var navigator: Navigator
internal lateinit var delegate: HotwireFragmentDelegate

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigator = (parentFragment as NavigatorHost).navigator
delegate = HotwireFragmentDelegate(this)
}

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

observeModalResult()
observeDialogResult()
observeTheme()

if (shouldObserveTitleChanges()) {
observeTitleChanges()
pathProperties.title?.let {
fragmentViewModel.setTitle(it)
}
}
}

/**
* This is marked `final` to prevent further use, as it's now deprecated in
* AndroidX's Fragment implementation.
*
* Use [onViewCreated] for code touching
* the Fragment's view and [onCreate] for other initialization.
*/
@Suppress("DEPRECATION")
final override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}

/**
* This is marked `final` to prevent further use, as it's now deprecated in
* AndroidX's Fragment implementation.
*
* Use [registerForActivityResult] with the appropriate
* [androidx.activity.result.contract.ActivityResultContract] and its callback.
*
* Turbo provides the [HotwireNavDestination.activityResultLauncher] interface
* to obtain registered result launchers from any destination.
*/
@Suppress("DEPRECATION")
final override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
}

override fun onStart() {
super.onStart()

if (!delegate.sessionViewModel.modalResultExists) {
delegate.onStart()
}
}

override fun onStop() {
super.onStop()
delegate.onStop()
}

override fun prepareNavigation(onReady: () -> Unit) {
delegate.prepareNavigation(onReady)
}

/**
* Called when the Fragment has been started again after receiving a
* modal result. Will navigate if the result indicates it should.
*/
open fun onStartAfterModalResult(result: SessionModalResult) {
delegate.onStartAfterModalResult(result)
}

/**
* Called when the Fragment has been started again after a dialog has
* been dismissed/canceled and no result is passed back.
*/
open fun onStartAfterDialogCancel() {
if (!delegate.sessionViewModel.modalResultExists) {
delegate.onStartAfterDialogCancel()
}
}

override fun onBeforeNavigation() {}

override fun refresh(displayProgress: Boolean) {}

/**
* Gets the Toolbar instance in your Fragment's view for use with
* navigation. The title in the Toolbar will automatically be
* updated if a title is available. By default, Turbo will look
* for a Toolbar with resource ID `R.id.toolbar`. Override to
* provide a Toolbar instance with a different ID.
*/
override fun toolbarForNavigation(): Toolbar? {
return view?.findViewById(R.id.toolbar)
}

final override fun delegate(): HotwireFragmentDelegate {
return delegate
}

private fun observeModalResult() {
if (shouldHandleModalResults()) {
delegate.sessionViewModel.modalResult.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let {
onStartAfterModalResult(it)
}
}
}
}

private fun observeDialogResult() {
delegate.sessionViewModel.dialogResult.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let {
onStartAfterDialogCancel()
}
}
}

private fun observeTitleChanges() {
fragmentViewModel.title.observe(viewLifecycleOwner) {
toolbarForNavigation()?.title = it
}
}

/*
* If a theme is applied directly on the root view, allow the
* system status and navigation bars to inherit the view's theme
* and override the Activity's theme window attributes.
*/
private fun observeTheme() {
val view = view ?: return

if (requireActivity().theme != view.context.theme) {
viewLifecycleOwner.lifecycle.addObserver(HotwireWindowThemeObserver(this))
}
}

private fun shouldHandleModalResults(): Boolean {
// Only handle modal results in non-modal contexts
return pathProperties.context != TurboNavPresentationContext.MODAL
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.hotwire.core.turbo.delegates
package dev.hotwire.core.navigation.fragments

import dev.hotwire.core.lib.logging.logEvent
import dev.hotwire.core.turbo.fragments.TurboFragmentViewModel
import dev.hotwire.core.turbo.nav.HotwireNavDestination
import dev.hotwire.core.turbo.session.SessionModalResult
import dev.hotwire.core.turbo.session.SessionViewModel
Expand All @@ -13,13 +12,13 @@ import dev.hotwire.core.turbo.util.displayBackButtonAsCloseIcon
* to this class. Note: This class should not need to be used directly
* from within your app.
*/
class TurboFragmentDelegate(private val navDestination: HotwireNavDestination) {
class HotwireFragmentDelegate(private val navDestination: HotwireNavDestination) {
private val fragment = navDestination.fragment
private val location = navDestination.location
private val navigator = navDestination.navigator

internal val sessionViewModel = SessionViewModel.get(navigator.session.sessionName, fragment.requireActivity())
internal val fragmentViewModel = TurboFragmentViewModel.get(location, fragment)
internal val fragmentViewModel = HotwireFragmentViewModel.get(location, fragment)

fun prepareNavigation(onReady: () -> Unit) {
onReady()
Expand Down Expand Up @@ -51,15 +50,15 @@ class TurboFragmentDelegate(private val navDestination: HotwireNavDestination) {
}

/**
* Provides a hook to Turbo when the Fragment has been started again after a dialog has
* Provides a hook when the Fragment has been started again after a dialog has
* been dismissed/canceled and no result is passed back.
*/
fun onStartAfterDialogCancel() {
logEvent("fragment.onStartAfterDialogCancel", "location" to location)
}

/**
* Provides a hook to Turbo when a Fragment has been started again after receiving a
* Provides a hook when a Fragment has been started again after receiving a
* modal result. Will navigate if the result indicates it should.
*/
fun onStartAfterModalResult(result: SessionModalResult) {
Expand All @@ -70,7 +69,7 @@ class TurboFragmentDelegate(private val navDestination: HotwireNavDestination) {
}

/**
* Provides a hook to Turbo when the dialog has been canceled. If there is a modal
* Provides a hook when the dialog has been canceled. If there is a modal
* result, an event will be created in [SessionViewModel] that can be observed.
*/
fun onDialogCancel() {
Expand All @@ -81,7 +80,7 @@ class TurboFragmentDelegate(private val navDestination: HotwireNavDestination) {
}

/**
* Provides a hook to Turbo when the dialog has been dismissed.
* Provides a hook when the dialog has been dismissed.
*/
fun onDialogDismiss() {
logEvent("fragment.onDialogDismiss", "location" to location)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.hotwire.core.turbo.fragments
package dev.hotwire.core.navigation.fragments

import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
Expand All @@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModelProvider
/**
* Holds onto fragment-level state data.
*/
class TurboFragmentViewModel : ViewModel() {
class HotwireFragmentViewModel : ViewModel() {
val title: MutableLiveData<String> = MutableLiveData()

/**
Expand All @@ -19,9 +19,9 @@ class TurboFragmentViewModel : ViewModel() {
}

companion object {
fun get(location: String, fragment: Fragment): TurboFragmentViewModel {
fun get(location: String, fragment: Fragment): HotwireFragmentViewModel {
return ViewModelProvider(fragment).get(
location, TurboFragmentViewModel::class.java
location, HotwireFragmentViewModel::class.java
)
}
}
Expand Down
Loading

0 comments on commit 1fa7f18

Please sign in to comment.