Just as humans need to live in groups, processes and threads need to communicate with each other. Their communication depends on the relationships generated between modules and between files. How to figure out and build this relationship quickly, while also reducing dependency, requires serious thinking.

This requirement is referred to as DI (Dependency Injection), and this programming technique has been around for a long time. Before I cover it, I’d like to briefly review the basic concepts of dependencies and relationships.

Dependencies and associations

As shown in the following figure, relationships between modules or classes can be divided into Dependency and Association. Dependencies are typically represented as local parameters, while associations are represented as the holding of attributes.According to the life cycle of the associated object, the association can be divided into Aggregation and Composition. The coupling degree increases in turn.

Graph LR dependency --> association --> aggregation --> composition

Dependency injection

The technique of DEPENDENCY injection programming ultimately builds not the dependencies mentioned above, but also the associations. It’s just that the entry to the build is mostly represented by parameter injection.

We can also manually inject these relationships without relying on the framework.

  • Arguments are passed through the constructor
  • The parameters are passed through the setter function

However, in the face of large projects with deep and complex dependencies, manual injection of these dependencies is cumbersome and error-prone, and the interdependencies are inevitably messy and ugly. Over time, the coupling degree becomes stronger and stronger, and it is difficult to expand. This is when it becomes necessary to automatically inject these dependencies.

Automatic injection can choose a framework, such as Guice, that builds dependencies at run time through reflection. You can also choose a better solution where the dependencies can be built at compile time.

For example, today’s protagonist Dagger2, as its name suggests, is a powerful tool for implementing DI technology.

Dagger and Dagger2

Dagger Dagger is widely known and developed by Square, which open-source okHttp. But some of its functions still rely on reflection to achieve, fly in the ointment.

Github.com/square/dagg…

So Google picked up the baton and improved on it with Dagger2. It uses APT to parse the annotations to generate code during compilation to achieve dependency injection.

Github.com/google/dagg…

Dagger2 simple combat 🔪

Let’s look up the movie interface through Dagger2, ViewModel, and Retrofit to briefly demonstrate how to use Dagger2.

DEMO: github.com/ellisonchan…

1. Import the framework

apply plugin: 'kotlin-kapt'
dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}
Copy the code

2. Create the Dagger interface

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationGraph {
    fun inject(activity: DemoActivity)
}

class MyApplication: Application() {
    val appComponent = DaggerApplicationComponent.create()
}
Copy the code

3. Inject ViewModel into the Activity field

class DemoActivity: Activity() {
    @Inject
    lateinit var movieViewModel: MovieViewModel
    override fun onCreate(savedInstanceState: Bundle?). {
        (applicationContext as MyApplication).appGraph.inject(this)
        super.onCreate(savedInstanceState)
        valbinding = ActivityDaggerBinding.inflate(layoutInflater) ... }}Copy the code

Note: An Activity instantiated by the system can only be injected through fields.

4. Declare that the ViewModel requires the MovieRepository injection

class MovieViewModel @Inject constructor(
        private val movieRepository: MovieRepository
)
Copy the code

5. Declare MovieRepository and Data injections

@Singleton
class MovieRepository @Inject constructor(
        private val localData: MovieLocalData,
        private val remoteData: MovieRemoteData
)

class MovieLocalData @Inject constructor(a)@Singleton
class MovieRepository @Inject constructor(
        private val localData: MovieLocalData,
        private val remoteData: MovieRemoteData
)
Copy the code

6. Add a Network module

@Module
class NetworkModule {...@Singleton
    @Provides
    fun provideLoginService(okHttpClient: OkHttpClient, gsonConverterFactory: GsonConverterFactory): MovieService {
        return Retrofit.Builder()
                .baseUrl("http://omdbapi.com/")
                .addConverterFactory(gsonConverterFactory)
                .client(okHttpClient)
                .build()
                .create(MovieService::class.java)
    }
}
Copy the code

Dependency diagram

The Dagger2 is very powerful, and there are still a number of advanced uses that are not mentioned above, which interested people can try further.

  • @scope for custom scopes
  • Annotate @subComponent for a child component
  • Annotates the abstract method @Binds
  • An interface specifies @named for multiple implementations

.

Dagger2 navigation support

Android Studio has support for Dagger2 navigation, making it easy for developers to quickly backtrack dependencies.

  • Click the upward arrow to see the provider of the instance injection
  • Clicking on the downward tree chart will take you to or expand the locations or lists where the instance is being used as a dependency

Dagger2 is applied on the SystemUI

For small projects, the introduction of the DI framework is overkill and overkill. Moreover, maintenance becomes particularly difficult for late successors who are unfamiliar with the DI framework. Only big projects, it seems, give it free rein.

I looked up the SystemUI code a few years ago while investigating a navigation Bug and was surprised to find that a large number of modules including StatusBar, Recents, Keyguard, etc. were introduced using DI. Although I have heard a little bit of the Dagger, I can still see the Dagger and cannot solve it.

SystemUI is the most core and complex App in The Android system, so it’s not too much of a big project. Now let’s see how Dagger2 helps manage a large number of system components for this large App.

※ Source version: Android 11

The main dependency instances in SystemUI are managed in the Denpency class.

public class Dependency {...@Inject @Background Lazy<Executor> mBackgroundExecutor;
    @Inject Lazy<ClockManager> mClockManager;
    @Inject Lazy<ActivityManagerWrapper> mActivityManagerWrapper;
    @Inject Lazy<DevicePolicyManagerWrapper> mDevicePolicyManagerWrapper;
    @Inject Lazy<PackageManagerWrapper> mPackageManagerWrapper;
    @Inject Lazy<SensorPrivacyController> mSensorPrivacyController;
    @Inject Lazy<DockManager> mDockManager;
    @Inject Lazy<INotificationManager> mINotificationManager;
    @Inject Lazy<SysUiState> mSysUiStateFlagsContainer;
    @Inject Lazy<AlarmManager> mAlarmManager;
    @Inject Lazy<KeyguardSecurityModel> mKeyguardSecurityModel;
    @Inject Lazy<DozeParameters> mDozeParameters;
    @Inject Lazy<IWallpaperManager> mWallpaperManager;
    @Inject Lazy<CommandQueue> mCommandQueue;
    @Inject Lazy<Recents> mRecents;
    @Inject Lazy<StatusBar> mStatusBar;
    @Inject Lazy<DisplayController> mDisplayController;
    @Inject Lazy<SystemWindows> mSystemWindows;
}
Copy the code

After StatusBar instance injection as an example to elaborate the SystemUI Dagger2 injection process.

As SystemServer makes a request to start the SystemUIService, the SystemUI Application is instantiated first. Before instantiation, the specified AppComponentFactory implementation class will receive a callback.

// AndroidManifest.xml
<application
        android:name=".SystemUIApplication"
        .
        tools:replace="android:appComponentFactory"
        android:appComponentFactory=".SystemUIAppComponentFactory">
</Application>
Copy the code

After calling Super to get the Application instance, register the contextready callback with it. This callback performs initialization of the SystemUIFactory and DI components.

public class SystemUIAppComponentFactory extends AppComponentFactory {
    @Inject
    publicContextComponentHelper mComponentHelper; .@Override
    public Application instantiateApplicationCompat(
            @NonNull ClassLoader cl, @NonNull String className)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Application app = super.instantiateApplicationCompat(cl, className);
        if (app instanceof ContextInitializer) {
            // Register the callback obtained successfully by the Context
            ((ContextInitializer) app).setContextAvailableCallback(
                    context -> {
                        SystemUIFactory.createFromConfig(context);
                        SystemUIFactory.getInstance().getRootComponent().inject(
                                SystemUIAppComponentFactory.this); }); }returnapp; }... }Copy the code

The Application’s onCreate() callback means that the Context is ready and then executes the callback above.

public class SystemUIApplication extends Application implements
        SystemUIAppComponentFactory.ContextInitializer {...@Override
    public void setContextAvailableCallback( SystemUIAppComponentFactory.ContextAvailableCallback callback) {
        mContextAvailableCallback = callback;
    }

    @Override
    public void onCreate(a) {... log.traceBegin("DependencyInjection");
        mContextAvailableCallback.onContextAvailable(this); U mRootComponent = SystemUIFactory. GetInstance (). GetRootComponent (); mComponentHelper = mRootComponent.getContextComponentHelper(); . }}Copy the code

The callback will first create the SystemUIFactory instance and initialize the Dagger component of the SystemUI App. The DI child component is then initialized and a Dependency is injected into the Dependency instance.

public class SystemUIFactory {
    public static void createFromConfig(Context context) {...try{ Class<? > cls =null;
            cls = context.getClassLoader().loadClass(clsName);
            // 1. Create SystemUIFactory instancemFactory = (SystemUIFactory) cls.newInstance(); mFactory.init(context); }}private void init(Context context) {
        // 2. Get the Dagger instance of SystemUI
        mRootComponent = buildSystemUIRootComponent(context);
        // 3. Create a Dependency instance and bind it to the DependencyInjector child
        Dependency dependency = new Dependency();
        mRootComponent.createDependency().createSystemUI(dependency);
        // 4. Initialize Dependency
        dependency.start();
    }

    // Initialize the Dagger component
    protected SystemUIRootComponent buildSystemUIRootComponent(Context context) {
        return DaggerSystemUIRootComponent.builder() 
                .dependencyProvider(new DependencyProvider())
                .contextHolder(newContextHolder(context)) .build(); }... }Copy the code

The Dependency class manages various dependencies, and dependent instances are managed through a Map. But they are not cached at initialization time. The lazy load callback for each instance is cached first. Each instance is then acquired and cached through lazy loading of the injection when it is really needed.

public class Dependency {
    // Use class as key to cache the Map of the corresponding instance
    private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
    // Cache instance lazy load callback Map
    private final ArrayMap<Object, LazyDependencyCreator> mProviders = new ArrayMap<>();

    protected void start(a) { mProviders.put(ActivityStarter.class, mActivityStarter::get); mProviders.put(Recents.class, mRecents::get); mProviders.put(StatusBar.class, mStatusBar::get); mProviders.put(NavigationBarController.class, mNavigationBarController::get); . }// Retrieve the injected instance and cache it using the lazy load callback
    private synchronized <T> T getDependencyInner(Object key) {
        T obj = (T) mDependencies.get(key);
        if (obj == null) {
            obj = createDependency(key);
            mDependencies.put(key, obj);
            if (autoRegisterModulesForDump() && obj instanceofDumpable) { mDumpManager.registerDumpable(obj.getClass().getName(), (Dumpable) obj); }}return obj;
    }

    protected <T> T createDependency(Object cls) {
        Preconditions.checkArgument(cls instanceofDependencyKey<? > || clsinstanceofClass<? >); LazyDependencyCreator<T> provider = mProviders.get(cls);return provider.createDependency();
    }

    private interface LazyDependencyCreator<T> {
        T createDependency(a); }}Copy the code

Once the Application is created, the SystemUI main Service is started, and the other services are started one by one.

public class SystemUIService extends Service {...@Override
    public void onCreate(a) {
        super.onCreate();
        // Start all of SystemUI((SystemUIApplication) getApplication()).startServicesIfNeeded(); . }}Copy the code

The default service class name is resolved by ContextComponentHelper to get the instance started.

public class SystemUIApplication { 
   public void startServicesIfNeeded(a) {
        String[] names = SystemUIFactory.getInstance().getSystemUIServiceComponents(getResources());
        startServicesIfNeeded(/* metricsPrefix= */ "StartServices", names);
    }

    private void startServicesIfNeeded(String metricsPrefix, String[] services) {...final int N = services.length;
        for (int i = 0; i < N; i++) {
            String clsName = services[i];
            try {
                // Obtain the corresponding instance from ContextComponentHelper
                SystemUI obj = mComponentHelper.resolveSystemUI(clsName);
                if (obj == null) {
                    Constructor constructor = Class.forName(clsName).getConstructor(Context.class);
                    obj = (SystemUI) constructor.newInstance(this); } mServices[i] = obj; } mServices[i].start(); . } mRootComponent.getInitController().executePostInitTasks(); }}Copy the code

List of configured services.

// config.xml
<string-array name="config_systemUIServiceComponents" translatable="false">.<item>com.android.systemui.recents.Recents</item>
    <item>com.android.systemui.volume.VolumeUI</item>
    <item>com.android.systemui.stackdivider.Divider</item>
    <item>com.android.systemui.statusbar.phone.StatusBar</item>U...</string-array>
Copy the code

The ContextComponentHelper singleton is declared to be provided by the Dagger component.

@Singleton
@Component(modules = {... })
public interface SystemUIRootComponent {.../** * Creates a ContextComponentHelper. */
    @Singleton
    ContextComponentHelper getContextComponentHelper(a);
}
Copy the code

The SystemUIModule module is responsible for injecting ContextComponentHelper instances, but it is actually injecting ContextComponentResolver instances.

@Module(...)
public abstract class SystemUIModule {.../ * * * /
    @Binds
    public abstract ContextComponentHelper bindComponentHelper( ContextComponentResolver componentHelper);
}
Copy the code

ContextComponentResolver Is used to parse Activity and Service instances, and obtain the corresponding Service instance from the Provider obtained by querying the Map through the class instance. Its constructor annotates @inject. It relies on several Map parameters, such as the SystemUI Map that the StatusBar Provider is injected into.

@Singleton
public class ContextComponentResolver implements ContextComponentHelper {
    @InjectContextComponentResolver(Map<Class<? >, Provider<Activity>> activityCreators, Map<Class<? >, Provider<Service>> serviceCreators, Map<Class<? >, Provider<SystemUI>> systemUICreators, Map<Class<? >, Provider<RecentsImplementation>> recentsCreators, Map<Class<? >, Provider<BroadcastReceiver>> broadcastReceiverCreators) { mSystemUICreators = systemUICreators; . }...@Override
    public SystemUI resolveSystemUI(String className) {
        return resolve(className, mSystemUICreators);
    }

    // Query the Provider instance based on the class instance obtained by the name, and then obtain the corresponding SystemUI instance
    private <T> T resolve(String className, Map
       
        , Provider
        
         > creators)
        > {
        try{ Class<? > clazz = Class.forName(className); Provider<T> provider = creators.get(clazz);return provider == null ? null : provider.get();
        } catch (ClassNotFoundException e) {
            return null; }}}Copy the code

The SystemUIBinder Module is declared with ClassKey as statusbar. class, and the value is injected into the Map by the StatusBarPhoneModule Module. The instance of Provider#get() will get the injected instance of provideStatusBar. (The StatusBar constructor has an awful 76 parameters…)

@Module(includes = {RecentsModule.class, StatusBarModule.class... })
public abstract class SystemUIBinder {
    /** Inject into StatusBar. */
    @Binds
    @IntoMap
    @ClassKey(StatusBar.class)
    public abstract SystemUI bindsStatusBar(StatusBar sysui); . }@Module(includes = {StatusBarPhoneModule.class... })
public interface StatusBarModule {}@Module(includes = {StatusBarPhoneDependenciesModule.class})
public interface StatusBarPhoneModule {
    @Provides
    @Singleton
    static StatusBar provideStatusBar( Context context, NotificationsController notificationsController, LightBarController lightBarController, AutoHideController autoHideController, KeyguardUpdateMonitor keyguardUpdateMonitor, StatusBarIconController statusBarIconController, ...) {
        return new StatusBar(...);
    }
}
Copy the code

DI diagram in SystemUI

conclusion

Review the need for dependency injection techniques.

  • Code reuse: Reduce boilerplate code by passing reuse instances as parameters
  • Test convenience: Logic can be tested quickly by injecting simulation parameters
  • Reduced coupling: Classes focus on their own logic and connect to each other only by parameters

Does Dagger2 have to be an automatic option? I think it depends on how much you know about the project.

Whether manual or automatic dependency injection is adopted, we need to clarify the relationship between each module, correctly locate the role of each class, and grasp the scope of each instance.

It is also important to recognize the limitations of such frameworks as Dagger2.

  • It is complicated to use and has certain learning threshold
  • To some extent, it is more suitable for large projects

The footsteps of technical people will never be stagnant, optimization and improvement is their eternal pursuit.

Google has reinvented Dagger2, simplifying the use of DI and strengthening the use of Android. This framework is also included in the Jetpack series, named Hilt.

The interpretation of Hilt has been arranged. Look forward to it.

In this paper, the DEMO

Github.com/ellisonchan…