Reputation: 1490
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 @Component
s : (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
Presenter
s from MVP ideology to be @Inject
in MenuActivity
and other Fragment
s, 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 @Scope
s: 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
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
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
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