0

For every Fragment class I make, I add something like this:

companion object {
    private const val PARAMETER_1 = "parameter1"
    private const val PARAMETER_2 = "parameter2"

    fun newInstance(parameter1: String, parameter2: Int) = MyDialog().apply {
        arguments = bundleOf(
            PARAMETER_1 to parameter1,
            PARAMETER_2 to parameter2)
    }
}

And then I add:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val args = arguments ?: return

    property1 = args[PARAMETER_1]
    property2 = args[PARAMETER_2]
}

This isn't horrific. But it is boilerplate that it would be great to get rid of.

Here's my attempt so far:

abstract class BaseFragment : Fragment() {
  abstract val constructorArguments: List<KMutableProperty<*>>

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val args = arguments ?: return

    constructorArguments.forEach {
        val key = keyPrefix + it.name
        val argument = args.get(key)
        val clazz = it.javaClass
        val typedArgument = clazz.cast(argument)
        it.setter.call(typedArgument)
    }
  }

  companion object {
    const val keyPrefix = "ARGUMENT_"

    fun newInstance(fragment: BaseFragment, vararg parameters: Any): BaseFragment {
        val constructorArguments = fragment.constructorArguments
        val parameterMap = mutableListOf<Pair<String, Any?>>()
        constructorArguments.forEachIndexed { index, kMutableProperty ->
            val key = keyPrefix + kMutableProperty.name
            val parameter = parameters[index]
            parameterMap.add(Pair(key, parameter))
        }
        val args = bundleOf(*parameterMap.toTypedArray())
        fragment.arguments = args
        return fragment
    }
  }
}

And then, in the actual fragment I can just have:

class MyFragment : BaseFragment() {

  lateinit var myProperty: String

  override val constructorArguments = listOf<KMutableProperty<*>>(
    ::myProperty
  )

  companion object {
    fun newInstance(argument: String) = BaseFragment.newInstance(MyFragment(), argument)
  }
}

This approach is far from perfect - especially the:

val parameter = parameters[index]

Does anyone know a better way to do this? Do you have some suggestions for how my approach can be improved? Or is this whole idea doomed to fail, and have I wasted a morning?

Roland
  • 22,259
  • 4
  • 57
  • 84
Luke Needham
  • 3,373
  • 1
  • 24
  • 41
  • what isn't "beautiful" about this? ie what parameter of code quality do you want to optimize for? make it more generic? make it faster? passing via bundle is a pretty established way of doing things. my question would be why are you passing so many arguments to your fragment that you need a mutablelist instead of just accessing data from your fragment itself... – Karan Harsh Wardhan Jan 31 '19 at 12:00
  • I'd like a generic way to pass arguments to the static constructor, without having to manually set up the Bundle in each Fragment class. If we can make each Fragment sub-class Bundle-agnostic, that would be ideal. – Luke Needham Jan 31 '19 at 12:41

2 Answers2

1

An 'answer' to this question is to use the Android Jetpack Navigation library. It provides SafeArgs, which greatly simplifies passing arguments to Fragments. See:

https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args

Luke Needham
  • 3,373
  • 1
  • 24
  • 41
1

You can have a base fragment that defines a common args parameter

abstract class BaseFragment : Fragment() {
    companion object {
        const val ARGS_KEY = "__ARGS__"
    }

    fun <T: Parcelable> getArgs(): T = requireArguments().getParcelable(ARGS_KEY)

    fun putArgs(args: Parcelable): Bundle = (arguments ?: Bundle()).apply {
        putParcelable(ARGS_KEY, args)
    }
}

Then

@Parcelize data class Args(val parameter1: String, val parameter2: Int)

companion object {
    fun newInstance(args: Args) = MyDialog().apply {
        putArgs(args)
    }
}

And now you can do it like

class MyFragment: BaseFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val args: Args = getArgs()
        args.parameter2
    }
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • This is nice, and exactly what I was looking for at the time of asking. Now I use the Navigation Component, which works in a similar way, with the additional benefit of auto-generating the Args classes, without the need for the base Fragment. – Luke Needham Jun 29 '20 at 14:16
  • On the other hand, now you're writing the same XML ` – EpicPandaForce Jun 29 '20 at 14:35
  • @EpicPandaForce how can we make arg key dynamic? in your example, all fragments will use same key and they will overwrite it. – Mücahid Kambur Jul 06 '20 at 11:05
  • @MücahidKambur the point of this approach is to make anything "dynamic" be part of `Args` class – EpicPandaForce Jul 06 '20 at 11:42