Skip to content

Instantly share code, notes, and snippets.

@elihart
Created December 5, 2019 04:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elihart/74e465676ff6663729f52690661e37e0 to your computer and use it in GitHub Desktop.
Save elihart/74e465676ff6663729f52690661e37e0 to your computer and use it in GitHub Desktop.
Rough example code of a system for declaring routes for Fragments and their argument types. Allows for easily loading the Fragments individually or in a new activity.. Assumes usage with MvRx, but can be used with normal Fragments and Activities with basic modification.
// The tools below can be used to easily start fragments and activities across module boundaries.
// Example usage - a "directory" is declared like this object.
// Then you can use it to:
// Create a new fragment: Search.results().newInstance(fragmentArguments)
// Create an intent for the Fragment: Search.results().newIntent(context, fragmentArguments)
// Start an intent for the Fragment: Search.results().startActivity(context, fragmentArguments)
object Search : Fragments("com.example.android.search") {
fun results() = create<SearchArguments>("SearchResultsFragment")
fun settings() = create("SearchSettingsFragment")
fun mapResults() = create("MapResultsFragment")
}
/**
* Usage: Create an "object" implementation to hold the fragments that live in another directory.
* The fully qualified fragment name is provided as a string, and the fragment is later looked up with reflection.
* This enables fragments to be used when a feature doesn't explicitly depend on them.
*
* If you are using fragments within the same module, prefer [LocalFragments]
*
*/
open class Fragments(val packagePrefix: String) {
/** Combine package name and fragment name, without assuming that they have a period in the correct place at beginning or end. */
@PublishedApi internal fun fqn(name: String) = "${packagePrefix.removeSuffix(".")}.${name.removePrefix(".")}"
protected inline fun <reified A : Parcelable> create(name: String) = MvRxFragmentFactory.create<A>(fqn(name))
protected fun create(name: String) = MvRxFragmentFactory.create(fqn(name))
}
/**
* Similar to [Fragments], but intended for use by fragments that are only used in the module they are declared in.
* This allows the fragments to be referenced via their class instead of a hardcoded string.
*/
open class LocalFragments {
protected inline fun <reified A : Parcelable> create(clazz: KClass<*>) = MvRxFragmentFactory.create<A>(clazz.qualifiedName!!)
protected fun create(clazz: KClass<*>) = MvRxFragmentFactory.create(clazz.qualifiedName!!)
}
/**
* Helps to load MvRx Fragments and Activities.
* Assumes you have a base MvRxFragment that all other fragments extend.
* Also assume you have a common MvRxActivity that hosts MvRx Fragments.
*/
sealed class MvRxFragmentFactory {
abstract val fragmentClassName: String
val fragmentClass by lazy { ClassRegistry.loadClassOrNull<MvRxFragment>(fragmentClassName) }
fun <T : Any> requireClass(ifNotNull: (Class<MvRxFragment>) -> T): T = ifNotNull(ClassRegistry.loadFragmentOrThrow(fragmentClassName))
/**
* Creates an intent that will be started with this MvRxFragment. In debug builds if the MvRx fragment is missing a fragment that automatically
* finishes and toasts the missing fragment will be shown.
*/
protected fun fragment(arg: Parcelable?): MvRxFragment {
val fragmentClass: Class<out MvRxFragment> = ClassRegistry.loadClassOrNull(fragmentClassName) ?: error("Fragment not found $fragmentClassName")
return fragmentClass.newInstance().also { fragment ->
if (arg != null) {
fragment.withArgs { addMvrxArgs(arg) }
}
}
}
/**
* Creates an intent that will be started with this MvRxFragment. In debug builds if the MvRx fragment is missing a fragment that automatically
* finishes and toasts the missing fragment will be shown.
*/
protected fun fragmentIntent(context: Context, arg: Parcelable?): Intent {
val fragmentClass: Class<out Fragment>? = ClassRegistry.loadClassOrNull(fragmentClassName)
return MvRxActivity.newIntent(context, fragmentClass, arg)
}
companion object {
inline fun <reified A : Parcelable> create(name: String) = MvRxFragmentFactoryWithArgs<A>(name)
fun create(name: String) = MvRxFragmentFactoryWithoutArgs(name)
}
}
class MvRxFragmentFactoryWithArgs<A : Parcelable> @PublishedApi internal constructor(override val fragmentClassName: String) : MvRxFragmentFactory() {
fun newInstance(arg: A): MvRxFragment = fragment(arg)
@JvmOverloads
fun newIntent(context: Context, arg: A): Intent = fragmentIntent(context, arg)
@JvmOverloads
fun startActivity(context: Context, arg: A) = context.startActivity(
newIntent(context, arg)
)
@JvmOverloads
fun startActivityForResult(activity: Activity, arg: A, requestCode: Int) = activity.startActivityForResult(
newIntent(activity, arg),
requestCode
)
}
fun Bundle.addMvrxArgs(arg: Parcelable?): Bundle {
putParcelable(MvRx.KEY_ARG, arg)
return this
}
class MvRxFragmentFactoryWithoutArgs @PublishedApi internal constructor(override val fragmentClassName: String) : MvRxFragmentFactory() {
fun newInstance(): Fragment = fragment(null)
@JvmOverloads
fun newIntent(context: Context) = fragmentIntent(context, null,)
@JvmOverloads
fun startActivity(context: Context) = context.startActivity(
newIntent(context)
)
@JvmOverloads
fun startActivityForResult(activity: Activity, requestCode: Int) = activity.startActivityForResult(
newIntent(activity),
requestCode
)
}
/**
* Helps to load a class by fully qualified name, with a cache.
*/
object ClassRegistry {
private val CLASS_MAP = ConcurrentHashMap<String, Class<*>>()
@JvmStatic fun <T : Activity> loadActivityOrThrow(className: String): Class<T> = loadClassOrThrow(className, Activity::class)
@JvmStatic fun <T : Fragment> loadFragmentOrThrow(className: String): Class<T> = loadClassOrThrow(className, Fragment::class)
@JvmStatic fun <T : Service> loadServiceOrThrow(className: String): Class<T> = loadClassOrThrow(className)
@JvmStatic fun <T : BroadcastReceiver> loadReceiverOrThrow(className: String): Class<T> = loadClassOrThrow(className)
fun <T> loadClassOrNull(className: String): Class<T>? {
return CLASS_MAP.getOrPut(className) {
try {
Class.forName(className)
} catch(e: ClassNotFoundException) {
// Can't store a null value in the concurrent map
return null
}
} as? Class<T>
}
@Throws(ClassNotFoundException::class)
private fun <T> loadClassOrThrow(className: String, type: KClass<*>? = null): Class<T> {
return loadClassOrNull(className) ?: throw ClassNotFoundException("Class not found $className")
}
/** Given a FQN class name, returns the simple name. */
fun simpleName(className: String) = className.split(".").lastOrNull()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment