Designing Modular Apps - Circular Dependency problem in navigation

Viewed 1060

As you know, designing Android app as modules is one of the popular practices nowadays in the Android development world. But this trend comes with some challenges. One of them is Circular Dependency.

For example, I have a navigation module which opens HomeActivity from Home Feature module. Also, I have to open another activity such as ProductListActivity from products module.

Home feature must include navigation module and navigation module should include HomeFeature if i navigate between activities like the following:

val intent = Intent(activity, HomeActivity::class.java)

This'll cause circular dependency problem.

For a fastest solution to figure out this problem is creating intents like the following and build navigation system on this approach.

Intent(Intent.ACTION_VIEW).setClassName(PACKAGE_NAME, className)

So my questions are, what other possible problems we'll face off with this navigation approach? Are there another practises to handle navigation in modular android apps?

2 Answers

Here is my solution for stiuation. This enables the use of explicit intents. You can also apply this approach to single activity application with navigation component with a little modification.

Here is navigation object for module B

object ModuleBNavigator {

    internal lateinit var navigationImpl: ModuleBContract

    fun setNavigationImpl(navigationImpl: ModuleBContract) {
        this.navigationImpl = navigationImpl
    }

    interface ModuleBContract {
        fun navigateModuleA(self: Activity, bundle: Bundle?)
    }
}

And here is module B Activity

class ModuleBActivity : Activity() {

    companion object {
        private const val BUNDLE = "BUNDLE"
        fun newIntent(context: Context, bundle: Bundle?) = Intent(context, ModuleBActivity::class.java).apply {
            putExtra(BUNDLE, bundle)
        }
    }
}

And here is app module class to inject navigation impl to module A navigation object

class ApplicationModuleApp : Application() {

    // Can also inject with a DI library
    override fun onCreate() {
        super.onCreate()
        ModuleBNavigator.setNavigationImpl(object : ModuleBNavigator.ModuleBContract {
            override fun navigateModuleA(self: Activity, bundle: Bundle?) {
                self.startActivity(ModuleBActivity.newIntent(self, bundle))
            }
        })
    }
}

And finally you can navigate from module A -> module B with provided implementation

class ModuleAActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... Some code 
        ModuleBNavigator.navigationImpl.navigateModuleA(this, Bundle())
        // .. Some code
    }
}

This approact avoids circler dependency and you don't have to use implicit intents anymore. Hope this helps.

For a different approach -actually similar which I mentioned in my question- which implementation belongs to sanogueralorenzo

Create a loader which laods the module classes

const val PACKAGE_NAME = "com.example.android"

private val classMap = mutableMapOf<String, Class<*>>()

private inline fun <reified T : Any> Any.castOrReturnNull() = this as? T

internal fun <T> String.loadClassOrReturnNull(): Class<out T>? =
    classMap.getOrPut(this) {
        try {
            Class.forName(this)
        } catch (e: ClassNotFoundException) {
            return null
        }
    }.castOrReturnNull()

Create a String extension function for loading Intents dynamically.

private fun intentTo(className: String): Intent =
    Intent(Intent.ACTION_VIEW).setClassName(BuildConfig.PACKAGE_NAME, className)

internal fun String.loadIntentOrReturnNull(): Intent? =
    try {
        Class.forName(this).run { intentTo(this@loadIntentOrReturnNull) }
    } catch (e: ClassNotFoundException) {
        null
    }

Create another String extension function for loading Fragments dynamically

internal fun String.loadFragmentOrReturnNull(): Fragment? =
    try {
        this.loadClassOrReturnNull<Fragment>()?.newInstance()
    } catch (e: ClassNotFoundException) {
        null
    }

Create an Feature interface for your feature implementations

interface Feature<T> {
    val dynamicStart: T?
}

I assume that you have a Messages feature. Implement your dynamic feature interface

object Messages : Feature<Fragment> {

    private const val MESSAGES = "$PACKAGE_NAME.messages.presentation.MessagesFragment"

    override val dynamicStart: Fragment?
        get() = MESSAGES.loadFragmentOrReturnNull()

}

And finally use it in another module without dependency

 Messages.dynamicStart?.let {
            if (savedInstanceState == null) {
                supportFragmentManager.beginTransaction()
                    .replace(R.id.fl_main, it)
                    .commit()
            }
        }
Related