Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A: 3 - Created the App, BaseActivity, BaseFragment, and custom PerActivity, PerFragment, and PerChildFragment Scopes. Closes #3 #20

Merged
merged 1 commit into from
Jul 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/src/main/java/com/vestrel00/daggerbutterknifemvp/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp;

import android.app.Activity;
import android.app.Application;

import javax.inject.Inject;

import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector;

/**
* The Android {@link Application}.
*/
Copy link
Owner Author

@vestrel00 vestrel00 Jul 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation:

 * <b>DEPENDENCY INJECTION</b>
 * We could extend {@link dagger.android.DaggerApplication} so we can get the boilerplate
 * dagger code for free. However, we want to avoid inheritance (if possible and it is in this case)
 * so that we have to option to inherit from something else later on if needed 
 * (e.g. if we need to override MultidexApplication). 

public class App extends Application implements HasActivityInjector {

@Inject
DispatchingAndroidInjector<Activity> activityInjector;

@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.create().inject(this);
}

@Override
public AndroidInjector<Activity> activityInjector() {
return activityInjector;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp;

import javax.inject.Singleton;

import dagger.Component;

/**
* Injects application dependencies.
*/
@Singleton
@Component(modules = AppModule.class)
interface AppComponent {
void inject(App app);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp;

import dagger.Module;
import dagger.android.AndroidInjectionModule;

/**
* Provides application-wide dependencies.
*/
@Module(includes = AndroidInjectionModule.class)
abstract class AppModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp.inject;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import javax.inject.Scope;

/**
* A custom scoping annotation that specifies that the lifespan of a dependency be the same as that
* of an Activity.
*
* This is used to annotate dependencies that behave like a singleton within the lifespan of an
* Activity, Fragment, and child Fragments instead of the entire Application.
*/
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp.inject;

import android.app.Fragment;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import javax.inject.Scope;

/**
* A custom scoping annotation that specifies that the lifespan of a dependency be the same as that
* of a child Fragment (a fragment inside a fragment that is added using
* {@link Fragment#getChildFragmentManager()}).
* <p>
* This is used to annotate dependencies that behave like a singleton within the lifespan of a
* child Fragment instead of the entire Application, Activity, or parent Fragment.
* <p>
* Note that this does not support a child fragment within a child fragment as conflicting scopes
* will occur. Child fragments within child fragments should usually be avoided. However, if
* another level of child fragment is required, then another scope would need to be created
* (perhaps PerGrandChild custom scope annotation).
*/
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerChildFragment {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp.inject;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import javax.inject.Scope;

/**
* A custom scoping annotation that specifies that the lifespan of a dependency be the same as that
* of a Fragment.
*
* This is used to annotate dependencies that behave like a singleton within the lifespan of a
* Fragment and child Fragments instead of the entire Application or Activity.
*/
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerFragment {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Contains the different custom scopes used.
* <p>
* Note that the standard {@link javax.inject.Singleton} scope is used as the application scope
* (there is no PerApplication scope annotation). This is done in order to support singleton
* scoped classes from other libraries (which do not have access to this project's custom scopes).
*/
package com.vestrel00.daggerbutterknifemvp.inject;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Contains the {@link android.app.Application}.
*/
package com.vestrel00.daggerbutterknifemvp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2017 Vandolf Estrellado
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vestrel00.daggerbutterknifemvp.ui.common;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.support.annotation.Nullable;

import javax.inject.Inject;
import javax.inject.Named;

import dagger.android.AndroidInjection;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasFragmentInjector;

/**
* Abstract Activity for all Activities to extend.
* <p>
* <b>DEPENDENCY INJECTION</b>
* We could extend {@link dagger.android.DaggerActivity} so we can get the boilerplate
* dagger code for free. However, we want to avoid inheritance (if possible and it is in this case)
* so that we have to option to inherit from something else later on if needed.
*/
public abstract class BaseActivity extends Activity implements HasFragmentInjector {

@Inject
@Named(BaseActivityModule.ACTIVITY_FRAGMENT_MANAGER)
Copy link
Owner Author

@vestrel00 vestrel00 Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use @Named vs @Qualifier?

Short answer is preference and lack of better naming ideas.

Instead of using @Named, we can also use a custom Qualifier in order to distinguish between the FragmentManager type from the activity versus the fragment.

E.G.

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityFragmentManager {
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentFragmentManager {
}

@Module
public abstract class BaseActivityModule {

    @Provides
    @ActivityFragmentManager
    @PerActivity
    static FragmentManager activityFragmentManager(Activity activity) {
        return activity.getFragmentManager();
     }
}

@Module
public abstract class BaseFragmentModule {

    @Provides
    @FragmentFragmentManager
    @PerFragment
    static FragmentManager childFragmentManager(@Named(FRAGMENT) Fragment fragment) {
        return fragment.getChildFragmentManager();
    }
}

public abstract class BaseActivity extends Activity implements HasFragmentInjector {

    @Inject
    @ActivityFragmentManager
    protected FragmentManager fragmentManager;
    ...
}

public abstract class BaseFragment extends Fragment implements HasFragmentInjector {

    @Inject
    @FragmentFragmentManager
    protected FragmentManager fragmentManager;
    ...
}

I'm not a fan of the name FragmentFragmentManager. It looks weird (though doesn't sound as weird). I can't think of a more appropriate name... Besides, using @Named is common practice. If @Qualifiers were used every time multiple instances of the same type had to be provided in the same graph, then we would end up with large quantities of qualifiers (not that its bad or anything). It certainly has its advantages over using @Named, which on a side note is just another custom qualifier;

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    String value() default "";
}

Why use @Qualifier over @Named?

There are 2 concrete advantages of using @Qualifier over @Named, though be it a bit "elitist".

  1. Solid @interface references instead of possibly brittle Strings.
  2. @Named Strings may "leak" a reference to the module / component via an import.

We could easily mitigate these @Named issues though;

  1. Using @Named requires the use of Strings, which can be misspelled. So a module may provide a "peach" but a variable may be injected with a "peech". This will never happen when using @Qualifier. However, a simple way to fix this flaw is to simply define the String (i.e. "peach") as a static final field and use that instead of typing the string over and over.

  2. We mostly come to the immediate conclusion that we should place the static final String in the @Module that uses/provides it. At a glance, there seems to be no issues with this approach. However, if we really think hard and adopt a super strict policy, we'll notice that this will lead to a violation of a core principle of dependency injection.

    That principle is that a class should not know anything about how it is injected or anything related to the injection process. Take the setup shown in this repository,

    import com.vestrel00.daggerbutterknifemvp.ui.common.view.BaseFragmentModule;
    
    @PerFragment
    public final class PerFragmentUtil {
    
        @Inject
        PerFragmentUtil(@Named(BaseFragmentModule.FRAGMENT) Fragment fragment) {
        }
    }

    Note that PerFragmentUitl appears in the next PR.

    Notice the import for com.vestrel00.daggerbutterknifemvp.ui.common.view.BaseFragmentModule;, which is required to access the FRAGMENT String used for @Named. In turn, PerFragmentUtil now references the BaseFragmentModule, which it shouldn't. Classes being injected should have no visibility of @Modules and @Components. This is clearly stated here: https://google.github.io/dagger/android.html#daggerandroid. It is one of the main motivations of why the dagger.android extension was created.

    Official Dagger & Android snippet

    In practice, this violation is bad because not PerFragmentUtil can only be created / provided when BaseFragmentModule is a part of the injection graph. What if we want to use a different module other than BaseFragmentModule? We can't. Well we can as long as the String values are the same but still doesn't look right when we look at the code. We won't really encounter any issues in this particular example (BaseActivityModule, after all, is a module that is included in all of our modules so it is guaranteed to be in the injection graph), this was just an illustration of why this practice is bad.

    A simple solution is to move the static final String FRAGMENT outside of the BaseActivityModule into a separate class. This way, the BaseActivityModule need not be referenced. Then any @Module can be used in the injection graph to provide the @Named(FRAGMENT). The caveat to this solution is that a separate class would be required. Furthermore, the static final String seems like it should belong to BaseActivityModule.

Conclusion

This has been a long "elitist" rant. I recognize that these are really nit-picky details and won't really pose any issues during development. Therefore, we ignore these "issues" in this repo as they are far from pragmatic. We want to be pragmatic programmers, not elitists / over-engineerists!

protected FragmentManager fragmentManager;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Why is FragmentManager injected into BaseActivity? Why not just use getFragmentManager() method?" See #52


@Inject
DispatchingAndroidInjector<Fragment> fragmentInjector;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
}

@Override
public final AndroidInjector<Fragment> fragmentInjector() {
return fragmentInjector;
}

protected final void addFragment(int containerViewId, Fragment fragment) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can annotate it with (@idres id containerViewId,...)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Thanks.

Fixed: 88f9f5b

I also added a quick tip about the support annotations in the article.

fragmentManager.beginTransaction()
.add(containerViewId, fragment)
.commit();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.vestrel00.daggerbutterknifemvp.ui.common;

import android.app.Activity;
import android.app.FragmentManager;
import android.content.Context;

import com.vestrel00.daggerbutterknifemvp.inject.PerActivity;

import javax.inject.Named;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;

/**
* Provides base activity dependencies. This must be included in all activity modules, which must
* provide a concrete implementation of {@link Activity}.
*/
@Module
public abstract class BaseActivityModule {

static final String ACTIVITY_FRAGMENT_MANAGER = "BaseActivityModule.activityFragmentManager";

@Binds
@PerActivity
abstract Context activityContext(Activity activity);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoping the Context activityContext(Activity activity) with @PerActivity is not necessary since the Activity instance will always be unique (new instances of it will not be created even without any scope). In general, providing Application, Activity, Fragment, Service, etc does not require scoped annotations since they are the components being injected and their instance is unique.

The same thing applies to static FragmentManager activityFragmentManager(Activity activity). The Activity instance is unique so the FragmentManager it returns always come from the same Activity. Thus, the @PerActivity is not necessary here as the scope is implicitly per activity (literally).

However, using scope annotations in these cases makes the module easier to read. We wouldn’t have to look at what is being provided in order to understand its scope. I choose readability here over (negligible) “performance/optimization”.


@Provides
@Named(ACTIVITY_FRAGMENT_MANAGER)
@PerActivity
static FragmentManager activityFragmentManager(Activity activity) {
return activity.getFragmentManager();
}
}
Loading