aleksandrbel
aleksandrbel

Reputation: 1490

Dagger2 + MVP on Kotlin

I am studying Dagger2 + MVP and doing it on Kotlin. And I have a problem in understanding either Dagger2 or MVP or there combination.

Construction of an application and idea how it should work are very simple. The app consists of MenuActivity with left-side navigation and several Fragments (let's say 3) that should be changed in the FrameLayout in activity_menu.xml.

I have read several articles and spend already couple of days studying Dagger2. This article I use as tutorial to build my example: https://proandroiddev.com/dagger-2-part-ii-custom-scopes-component-dependencies-subcomponents-697c1fa1cfc

In my idea, Dagger architecture should consist of three @Components : (1) AppComponent, (2) MenuActivityComponent and (3) AccountFragmentComponent. And from my understanding and the picture of an architecture in the article my architecture can be like this: (3) depends on -> (2) depends on -> (1)

Each @Component has a @Module : (1) AppModule, (2) MenuActivityModule and (3) AccountFragmentModule respectively. For the cleaner way of MVP dependencies, as far as I understand, both (2) MenuActivityModule and (3) AccountFragmentModule should @Provide Presenters from MVP ideology to be @Inject in MenuActivity and other Fragments, such as AccountFragment.

AppModule

@Module
class AppModule(val app : App){

    @Provides @Singleton
    fun provideApp() = app

}

AppComponent

@Singleton @Component(modules = arrayOf(AppModule::class))
interface AppComponent{

    fun inject(app : App)

    fun plus(menuActivityModule: MenuActivityModule): MenuActivityComponent
}

MenuActivityModule

@Module
class MenuActivityModule(val activity : MenuActivity) {

    @Provides
    @ActivityScope
    fun provideMenuActivityPresenter() =
        MenuActivityPresenter(activity)

    @Provides
    fun provideActivity() = activity
}

MenuActivityComponent

@ActivityScope
@Subcomponent(modules = arrayOf(MenuActivityModule::class))
interface MenuActivityComponent {

    fun inject(activity: MenuActivity)

    fun plus(accountsModule : AccountsFragmentModule) : AccountsFragmentComponent
}

AccountsFragmentModule

@Module
class AccountsFragmentModule(val fragment: AccountsFragment){

    @FragmentScope
    @Provides
    fun provideAccountsFragmentPresenter() =
        AccountsFragmentPresenter(fragment)
}

AccountsFragmentComponent

@FragmentScope
@Subcomponent(modules = arrayOf(AccountsFragmentModule::class))
interface AccountsFragmentComponent {

    fun inject(fragment: AccountsFragment)
}

Also I have two @Scopes: ActivityScope and FragmentScope, so as I understand that will guarantee the existence of only one Component for the time every component is need in the application.

ActivityScope

@Scope
annotation class ActivityScope

FragmentScope

@Scope
annotation class FragmentScope

In Application class I create a graph of @Singleton dependencies.

class App : Application(){

    val component : AppComponent by lazy {
        DaggerAppComponent
            .builder()
            .appModule(AppModule(this))
            .build()
    }

    companion object {
        lateinit var instance : App
            private set
    }

    override fun onCreate() {
        super.onCreate()
        component.inject(this)
    }

}

In MenuActivity:

class MenuActivity: AppCompatActivity()

    @Inject lateinit var presenter : MenuActivityPresenter

    val Activity.app : App
    get() = application as App

    val component by lazy {
        app.component.plus(MenuActivityModule(this))
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_menu)
        /* setup dependency injection */
        component.inject(this)
        /* setup UI */
        setupMenu()
        presenter.init()
    }

private fun setupMenu(){
    navigationView.setNavigationItemSelectedListener({
        menuItem: MenuItem -> selectDrawerItem(menuItem)
        true
    })

    /* Hamburger icon for left-side menu */
    supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp)
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

    drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.drawer_open,  R.string.drawer_close);
    drawerLayout.addDrawerListener(drawerToggle as ActionBarDrawerToggle)
    }
private fun selectDrawerItem(menuItem: MenuItem){

    presenter.menuItemSelected(menuItem)

    // Highlight the selected item has been done by NavigationView
    menuItem.isChecked = true
    // Set action bar title
    title = menuItem.title
    // Close the navigation drawer
    drawerLayout.closeDrawers()
}

@SuppressLint("CommitTransaction")
override fun showFragment(fragment: Fragment, isReplace: Boolean,
                          backStackTag: String?, isEnabled: Boolean)
{
    /* Defining fragment transaction */
    with(supportFragmentManager.beginTransaction()){

        /* Select if to replace or add a fragment */
        if(isReplace)
            replace(R.id.frameLayoutContent, fragment, backStackTag)
        else
            add(R.id.frameLayoutContent, fragment)

        backStackTag?.let { this.addToBackStack(it) }

        commit()
    }

    enableDrawer(isEnabled)
}

private fun enableDrawer(isEnabled: Boolean) {
    drawerLayout.setDrawerLockMode(if(isEnabled) DrawerLayout.LOCK_MODE_UNLOCKED
                                    else DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
    drawerToggle?.onDrawerStateChanged(if(isEnabled) DrawerLayout.LOCK_MODE_UNLOCKED
                                        else DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
    drawerToggle?.isDrawerIndicatorEnabled = isEnabled
    drawerToggle?.syncState()
}

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
  if (drawerToggle!!.onOptionsItemSelected(item)) {
      return true
  }
  return super.onOptionsItemSelected(item)
}

override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onPostCreate(savedInstanceState, persistentState)
    drawerToggle?.syncState()
}

override fun onConfigurationChanged(newConfig: Configuration?) {
    super.onConfigurationChanged(newConfig)
    drawerToggle?.onConfigurationChanged(newConfig)
}
}

MainActivityPresenter

class MenuActivityPresenter(val menuActivity: MenuActivity){

    fun init(){
        menuActivity.showFragment(AccountsFragment.newInstance(), isReplace = false)
    }

    fun menuItemSelected(menuItem: MenuItem){

        val fragment =  when(menuItem.itemId){
            R.id.nav_accounts_fragment -> {
                AccountsFragment.newInstance()
            }
            R.id.nav_income_fragment -> {
                IncomeFragment.newInstance()
            }
            R.id.nav_settings -> {
                IncomeFragment.newInstance()
            }
            R.id.nav_feedback -> {
                OutcomeFragment.newInstance()
            }
            else -> {
                IncomeFragment.newInstance()
            }
        }

        menuActivity.showFragment(fragment)
    }

}

activity_menu.xml

<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!-- This LinearLayout represents the contents of the screen  -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- The ActionBar displayed at the top -->
    <include
        layout="@layout/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!-- The main content view where fragments are loaded -->
    <FrameLayout
        android:id="@+id/frameLayoutContent"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

<!-- The navigation drawer that comes from the left -->
<!-- Note that `android:layout_gravity` needs to be set to 'start' -->
<android.support.design.widget.NavigationView
    android:id="@+id/navigationView"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:background="@android:color/white"
    app:menu="@menu/main_menu"
    app:headerLayout="@layout/nav_header"
    />

</android.support.v4.widget.DrawerLayout>

And the place where I have a breaking point in my understanding:

class AccountsFragment : Fragment() {

    companion object {
        fun newInstance() = AccountsFragment()
    }

    val Activity.app : App
       get() = application as App

    val component by lazy {
       app.component
           .plus(MenuActivityModule(activity as MenuActivity))
           .plus(AccountsFragmentModule(this))
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
       val view = inflater?.inflate(R.layout.fragment_accounts, container, false)
       setHasOptionsMenu(true)
       component.inject(this)
       return view
   }

}

My misunderstanding in this final part in the component value. I came to the situation that I need to plus Subcomponent of the MenuActivityComponent and give as a constructor variable MenuActivity, but I understand this is wrong and I can't create another instance even if I want that the instance should be only one in the application.

Question: Where I have a mistake? In my code, in architecture, in understanding of dependency injection or in all three? Thank you for your help!

Upvotes: 2

Views: 1495

Answers (3)

aleksandrbel
aleksandrbel

Reputation: 1490

Ok. After weeks of deep studying. I have done github project basics so everybody can look it through and have a working example.

https://github.com/Belka1000867/Dagger2Kotlin

Upvotes: 1

user1643723
user1643723

Reputation: 4212

Also I have two @Scopes: ActivityScope and FragmentScope, so as I understand that will guarantee the existence of only one Component for the time every component is need in the application

Dagger's scopes aren't some magical fairy dust, that will manage lifetimes for you. The use of scopes is just validation aid, that helps you to not mix dependencies with different lifetimes. You still have to manage component and module objects yourself, passing correct parameters to their constructors. The dependency graphs, managed by different component instances, are independent and bound to their @Component objects. Note, that I am saying "bound" in a sense, that they are created by them (via constructor), and optionally stored inside them, there is absolutely no other magic, working behind the scenes. So if you need to share a bunch of dependencies between parts of application, you might need to pass some component and module instances around.

In case of fragments there is a tough dependency with complex lifetime — the Activity. You receive that dependency during onAttach and lose it during onDetach. So if you really want to pass some Activity-dependent stuff to Fragments, you have to pass around those Activity-dependent components/modules, just like you would pass around the Activity instance in absence of MVP.

Likewise, if your Activity receives some dependency via Intent, you will have to deserialize that dependency from it, and create a Module, based on Intent contents…

The complexity of Activity and Fragment lifecycles aggravates issues, commonly met when using Dagger injection. Android framework is built around assumption, that Activities and Fragments receive all their dependencies in serialized form. Finally, a well-written Application should not have too many dependencies between modules to begin with (look up "tight coupling"). Because of those reasons, if you don't follow strict single-Activity paradigm, using Dagger on localized Activity/Fagment scopes might not be worthwhile at all in your project. Many people still do it, even if it is not worthwhile, but IMO, that's just matter of personal preference.

Upvotes: 3

user2836797
user2836797

Reputation:

I would separate the concepts in your mind and work on them individually. Once you become fluent/masterful in both concepts, you can try combining them.

For example, try building a simple multiple activity/fragment application the MVP design pattern. With MVP, you'll be writing two-way interface contracts between the Presenter (an object that contains view logic and controls the view, as well as handles behavior that the view collects and forwards through), and the View (a view object, typically a native component like Fragment or Activity that is responsible for displaying a view and handling user input like touch events).

With Dagger2, you'll be learning the dependency injection design pattern/architectural style. You will build modules that combine to form components and then use those components to inject objects.

Combining the two begins with understanding each concept on it's own.

Check out the Google Architectural Blueprint repository for examples of MVP and Dagger2. https://github.com/googlesamples/android-architecture

Upvotes: 2

Related Questions