Skip to content

Instantly share code, notes, and snippets.

@elihart
Created December 5, 2019 04:31
Show Gist options
  • Select an option

  • Save elihart/be43575bf7e7447f67f08467a5604740 to your computer and use it in GitHub Desktop.

Select an option

Save elihart/be43575bf7e7447f67f08467a5604740 to your computer and use it in GitHub Desktop.

Revisions

  1. elihart created this gist Dec 5, 2019.
    158 changes: 158 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,158 @@
    // 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()
    }