Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active April 29, 2024 21:56
Show Gist options
  • Save ElianFabian/19205699597b279e63266a5b05632236 to your computer and use it in GitHub Desktop.
Save ElianFabian/19205699597b279e63266a5b05632236 to your computer and use it in GitHub Desktop.
An interface that allows to work with Fragment arguments and events in a type-safe and process-death-safe way using extension functions.
<?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"
android:gravity="center">
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|top"
android:orientation="vertical"
android:paddingTop="50dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="Title:"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="A regular title" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="Message:"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="A regular message" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="50dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccept"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#00C853"
android:text="Accept" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnReject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#FF1744"
android:text="Reject" />
</LinearLayout>
</LinearLayout>
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sampleFragment = SampleFragment.newInstance(
args = SampleFragment.Args(
title = "A regular title",
message = "A regular message",
)
)
sampleFragment.setFragmentEventListener(this@MainActivity) { event ->
when (event) {
is SampleFragment.Event.OnAccept -> {
Toast.makeText(applicationContext, "Accepted! Data = ${event.data}", Toast.LENGTH_SHORT).show()
}
is SampleFragment.Event.OnReject -> {
Toast.makeText(applicationContext, "Rejected!", Toast.LENGTH_SHORT).show()
}
}
}
supportFragmentManager.beginTransaction()
.add(R.id.fragmentContainer, sampleFragment, sampleFragment.typedInstanceId)
.commit()
}
}
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.elian.typedfragment.databinding.LayoutSampleBinding
import kotlinx.parcelize.Parcelize
class SampleFragment : Fragment(),
TypedArgEvent<SampleFragment.Args, SampleFragment.Event> {
private lateinit var binding: LayoutSampleBinding
override var typedInstanceId: String? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
typedInstanceId = typedInstanceId ?: savedInstanceState?.getString(EXTRA_FRAGMENT_ID) ?: this::class.qualifiedName
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_FRAGMENT_ID, typedInstanceId)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = LayoutSampleBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args = getFragmentArgs() ?: return
binding.apply {
tvTitle.text = args.title
tvMessage.text = args.message
btnAccept.setOnClickListener {
sendFragmentEvent(
Event.OnAccept(
data = "This is your title: ${args.title} and this is your message = ${args.message}"
)
)
}
btnReject.setOnClickListener {
sendFragmentEvent(Event.OnReject)
}
}
}
@Parcelize
data class Args(
val title: String,
val message: String,
) : Parcelable
sealed interface Event : Parcelable {
@Parcelize
class OnAccept(val data: String) : Event
@Parcelize
object OnReject : Event
}
companion object {
private const val EXTRA_FRAGMENT_ID = "EXTRA_FRAGMENT_ID"
fun newInstance(id: String? = null, args: Args? = null) = SampleFragment().apply {
if (args != null) {
arguments = createArgsBundle(args)
}
this.typedInstanceId = id ?: SampleFragment::class.qualifiedName
}
}
}
/**
* Interface mostly meant for Fragment classes to provide a type-safe and process-death-safe way of receiving arguments and sending events.
*
* The interface requires indicating the types of arguments [TArgs] and events [TEvent] needed, as well as an ID to identify the fragment.
*
* This interface is intended to be extended with extension functions to fit specific needs.
* Refer to the TypedArgEventExt file for some base functions that extends this interface.
*
*
* Example usage:
*
* ```
* class MyFragment : TypedArgEvent<MyFragment.Args, MyFragment.Event>() {
*
* [...]
*
* override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
*
* val args = getFragmentArgs() ?: return
*
* tvTitle.text = args.title
* tvMessage.text = args.message
*
* btnAccept.setOnClickListener {
* sendFragmentEvent(Event.OnAccept(data = "important data"))
* }
* btnReject.setOnClickListener {
* sendFragmentEvent(Event.OnReject)
* }
* }
*
*
* @Parcelize
* data class Args(
* val title: String,
* val message: Int,
* ) : Parcelable
*
* sealed interface Event : Parcelable {
* @Parcelize
* class OnAccept(val data: String) : Event
*
* @Parcelize
* object OnReject : Event
* }
*
*
* companion object {
* fun newInstance(id: String? = null, args: Args? = null) = MyFragment().apply {
* if (args != null) {
* arguments = createArgsBundle(args)
* }
*
* typedInstanceId = id ?: MyFragment::class.qualifiedName
* }
* }
* }
*
*
* // In another fragment or activity create a new instance
* val myFragment = MyFragment.newInstance(
* args = MyFragment.Args(
* firstArg = "some data",
* secondArg = "more data",
* )
* )
*
* // Set the listener
* myFragment.setFragmentEventListener(this@SomeFragment) { event ->
* when (event) {
* is MyFragment.Event.OnAccept -> {
* // Do something
* }
* is MyFragment.Event.OnReject -> {
* // Do something
* }
* }
* }
* ```
*/
interface TypedArgEvent<TArgs, TEvent> {
/**
* An ID that identifies a single instance.
*
* For fragments if there is only one instance, it's fine to use "this::class.qualifiedName" as the ID value. For multiple instances,
* provide different keys for each instance.
*
* To persist this ID after process death, you need to manually save and restore it. Example:
*
* ```
* override var typedInstanceId: String? = null
* private set
*
* override fun onCreate(savedInstanceState: Bundle?) {
* super.onCreate(savedInstanceState)
*
* typedInstanceId = typedInstanceId ?: savedInstanceState?.getString("id") ?: this::class.qualifiedName
* }
*
* @CallSuper
* override fun onSaveInstanceState(outState: Bundle) {
* super.onSaveInstanceState(outState)
*
* outState.putString("id", typedInstanceId)
* }
* ```
*/
val typedInstanceId: String?
companion object {
const val EXTRA_ARGS = "TypedArgEvent.EXTRA_ARGS"
const val EXTRA_EVENT = "TypedArgEvent.EXTRA_EVENT"
}
}
@file:Suppress("NOTHING_TO_INLINE")
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
/**
* Retrieves the fragment arguments casted to the specified type [TArgs].
*
* @return The fragment arguments, or null if not found.
*/
@Suppress("Deprecation")
inline fun <T, TArgs : Parcelable> T.getFragmentArgs(): TArgs?
where T : Fragment, T : TypedArgEvent<TArgs, *> {
return arguments?.getParcelable(TypedArgEvent.EXTRA_ARGS)
}
/**
* Null implementation of the [getFragmentArgs] function.
*
* @return Always returns null.
*/
inline fun <T> T.getFragmentArgs(): Nothing?
where T : Fragment, T : TypedArgEvent<Nothing?, *> {
return null
}
/**
* Creates a bundle containing the fragment arguments.
*
* @param args The arguments to be put in the bundle.
* @return The created bundle.
*/
inline fun <T, TArgs : Parcelable> T.createArgsBundle(args: TArgs): Bundle
where T : Fragment, T : TypedArgEvent<TArgs, *> {
return bundleOf(TypedArgEvent.EXTRA_ARGS to args)
}
/**
* Sends a fragment event to the parent fragment or activity.
*
* @param event The event to send.
*/
inline fun <T, TEvent : Parcelable> T.sendFragmentEvent(event: TEvent)
where T : Fragment, T : TypedArgEvent<*, TEvent> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot send event without a argEventId. Please set the argEventId property."
}
parentFragmentManager.setFragmentResult(argEventId, bundleOf(TypedArgEvent.EXTRA_EVENT to event))
}
/**
* Sets the event listener for the specified fragment manager and lifecycle owner, using a lambda to handle events.
*
* @param fragmentManager The fragment manager to set the event listener on.
* @param lifecycleOwner The lifecycle owner (fragment or activity) associated with the event listener.
* @param onEvent The lambda function to handle events.
*
* @throws IllegalArgumentException if the argEventId is not set.
*/
@Suppress("Deprecation")
inline fun <T, TEvent : Parcelable> T.setFragmentEventListener(
fragmentManager: FragmentManager,
lifecycleOwner: LifecycleOwner,
crossinline onEvent: (event: TEvent) -> Unit,
) where T : Fragment, T : TypedArgEvent<*, TEvent> {
fragmentManager.setFragmentResultListener(
typedInstanceId ?: return,
lifecycleOwner
) { _, bundle ->
val event = bundle.getParcelable<TEvent>(TypedArgEvent.EXTRA_EVENT)
check(event != null) {
"Couldn't retrieve the event from bundle. Please check the sendFragmentEvent() function."
}
onEvent(event)
}
}
/**
* Sets the event listener for the specified fragment, using a lambda to handle events.
*
* @param fragment The fragment to set the event listener on.
* @param onEvent The lambda function to handle events.
*
* @throws IllegalArgumentException if the argEventId is not set or if the provided fragment is the same as the current fragment.
*/
inline fun <T, TEvent : Parcelable> T.setFragmentEventListener(
fragment: Fragment,
crossinline onEvent: (event: TEvent) -> Unit,
) where T : Fragment, T : TypedArgEvent<*, TEvent> {
require(fragment != this) {
"Cannot set the event listener on the same fragment instance: ${fragment::class.qualifiedName}."
}
setFragmentEventListener(fragment.childFragmentManager, fragment.viewLifecycleOwner, onEvent)
}
/**
* Sets the event listener for the specified activity, using a lambda to handle events.
*
* @param activity The activity to set the event listener on.
* @param onEvent The lambda function to handle events.
*
* @throws IllegalArgumentException if the argEventId is not set.
*/
inline fun <T, TEvent : Parcelable> T.setFragmentEventListener(
activity: FragmentActivity,
crossinline onEvent: (event: TEvent) -> Unit,
) where T : Fragment, T : TypedArgEvent<*, TEvent> {
setFragmentEventListener(activity.supportFragmentManager, activity, onEvent)
}
/**
* Clears the stored event.
*/
inline fun <T> T.clearFragmentEvent() where T : Fragment, T : TypedArgEvent<*, *> {
parentFragmentManager.clearFragmentResult(typedInstanceId ?: return)
}
/**
* Clears the fragment stored event listener.
*/
inline fun <T> T.clearFragmentEventListener() where T : Fragment, T : TypedArgEvent<*, *> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot clear event listener without a argEventId. Please set the argEventId property."
}
parentFragmentManager.clearFragmentResultListener(argEventId)
}
/**
* Shows the dialog fragment.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param tag The tag for this fragment, as per [androidx.fragment.app.FragmentTransaction.add].
*/
inline fun <T> T.showDialog(
fragmentManager: FragmentManager,
tag: String?,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
show(fragmentManager, tag)
}
/**
* Shows the dialog fragments.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
*/
inline fun <T> T.showDialog(
fragmentManager: FragmentManager,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot show dialog without a argEventId. Please set the argEventId property."
}
showDialog(fragmentManager, argEventId)
}
/**
* Shows the dialog fragment.
*
* @param fragment The fragment from which the dialog fragment it's going to be shown.
*/
inline fun <T> T.showDialog(
fragment: Fragment,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
require(fragment != this) {
"Cannot set event listener on the same dialog fragment instance: ${fragment::class.qualifiedName}."
}
showDialog(fragment.childFragmentManager)
}
/**
* Shows the dialog fragment.
*
* @param activity The activity from which the dialog fragment it's going to be shown.
*/
inline fun <T> T.showDialog(
activity: FragmentActivity,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
showDialog(activity.supportFragmentManager)
}
/**
* Shows the dialog fragment with arguments.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param tag The tag for this fragment, as per [androidx.fragment.app.FragmentTransaction.add].
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialog(
fragmentManager: FragmentManager,
tag: String?,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
arguments = createArgsBundle(args)
show(fragmentManager, tag)
}
/**
* Shows the dialog fragment with arguments.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialog(
fragmentManager: FragmentManager,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot show dialog without a argEventId. Please set the argEventId property."
}
showDialog(fragmentManager, argEventId, args)
}
/**
* Shows the dialog fragment with arguments.
*
* @param fragment The fragment from which the dialog fragment it's going to be shown.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialog(
fragment: Fragment,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
require(fragment != this) {
"Cannot set event listener on the same dialog fragment instance: ${fragment::class.qualifiedName}."
}
showDialog(fragment.childFragmentManager, args)
}
/**
* Shows the dialog fragment with arguments.
*
* @param activity The activity from which the dialog fragment it's going to be shown.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialog(
activity: FragmentActivity,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
showDialog(activity.supportFragmentManager, args)
}
/**
* Ensures the dialog fragment is shown only once.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param tag The tag for this fragment, as per [androidx.fragment.app.FragmentTransaction.add].
*/
inline fun <T> T.showDialogOnce(
fragmentManager: FragmentManager,
tag: String,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
val self = fragmentManager.findFragmentByTag(typedInstanceId) as? DialogFragment
if (self == null || dialog?.isShowing == false) {
showDialog(fragmentManager, tag)
}
}
/**
* Ensures the dialog fragment is shown only once.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
*/
inline fun <T> T.showDialogOnce(
fragmentManager: FragmentManager,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot show dialog without a argEventId. Please set the argEventId property."
}
showDialogOnce(fragmentManager, argEventId)
}
/**
* Ensures the dialog fragment is shown only once.
*
* @param fragment The fragment from which the dialog fragment it's going to be shown.
*/
inline fun <T> T.showDialogOnce(
fragment: Fragment,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
require(fragment != this) {
"Cannot set event listener on the same dialog fragment instance: ${fragment::class.qualifiedName}."
}
showDialogOnce(fragment.childFragmentManager)
}
/**
* Ensures the dialog fragment is shown only once.
*
* @param activity The activity from which the dialog fragment it's going to be shown.
*/
inline fun <T> T.showDialogOnce(
activity: FragmentActivity,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
showDialogOnce(activity.supportFragmentManager)
}
/**
* Ensures the dialog fragment is shown only once with arguments.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param tag The tag for this fragment, as per [androidx.fragment.app.FragmentTransaction.add].
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialogOnce(
fragmentManager: FragmentManager,
tag: String,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
val self = fragmentManager.findFragmentByTag(typedInstanceId) as? DialogFragment
if (self == null || dialog?.isShowing == false) {
showDialog(fragmentManager, tag, args)
}
}
/**
* Ensures the dialog fragment is shown only once with arguments.
*
* @param fragmentManager The FragmentManager this fragment will be added to.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialogOnce(
fragmentManager: FragmentManager,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
val argEventId = typedInstanceId
check(argEventId != null) {
"Cannot show dialog without a argEventId. Please set the argEventId property."
}
showDialogOnce(fragmentManager, argEventId, args)
}
/**
* Ensures the dialog fragment is shown only once with arguments.
*
* @param fragment The fragment from which the dialog fragment it's going to be shown.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialogOnce(
fragment: Fragment,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
require(fragment != this) {
"Cannot set event listener on the same dialog fragment instance: ${fragment::class.qualifiedName}."
}
showDialogOnce(fragment.childFragmentManager, args)
}
/**
* Ensures the dialog fragment is shown only once with arguments.
*
* @param activity The activity from which the dialog fragment it's going to be shown.
* @param args The arguments that can passed to the dialog fragment.
*/
inline fun <T, TArgs : Parcelable> T.showDialogOnce(
activity: FragmentActivity,
args: TArgs,
) where T : DialogFragment, T : TypedArgEvent<TArgs, *> {
showDialogOnce(activity.supportFragmentManager, args)
}
/**
* Dismisses the dialog fragment safely, even outside the dialog fragment itself.
*
* @param fragmentManager The fragment manager associated with the dialog fragment.
*/
inline fun <T> T.dismissDialog(
fragmentManager: FragmentManager,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
val self = fragmentManager.findFragmentByTag(typedInstanceId) as? DialogFragment
if (self?.isStateSaved == true) {
self.dismissAllowingStateLoss()
}
else self?.dismiss()
}
/**
* Dismisses the dialog fragment safely, even outside the dialog fragment itself.
*
* @param fragment The fragment associated with the dialog fragment.
*/
inline fun <T> T.dismissDialog(
fragment: Fragment,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
dismissDialog(fragment.childFragmentManager)
}
/**
* Dismisses the dialog fragment safely, even outside the dialog fragment itself.
*
* @param activity The activity associated with the dialog fragment.
*/
inline fun <T> T.dismissDialog(
activity: FragmentActivity,
) where T : DialogFragment, T : TypedArgEvent<*, *> {
dismissDialog(activity.supportFragmentManager)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment