Dependency injection is a design pattern of object-oriented programming designed to reduce program coupling, which is caused by dependencies between classes.

  • Consistent with the single blame principle and the open closed principle

What is dependency injection

Classes often need to refer to other classes. Eg: Car may need People classes. These classes are called dependencies

In the past, dependency injection was not used very much in Android development. Until recently, I have been looking at several projects, including Java projects for several weeks, and noticed the status of injection in the existing technology. In Java Spring Boot, dependency injection was almost used to the extreme, reducing too much work

Advantage:

  • Reuse code
  • Easy to reconstruct
  • Easy to test

1. Injection mode

  1. Injection through interface
public interface ClassBInterface {
    void setClassB(ClassB classB);
}

class ClassB {
    public void doSomething(a) {}}class ClassA implements ClassBInterface {
    ClassB classB;

    @Override
    public void setClassB(ClassB classB) {
        this.classB = classB; }}Copy the code
  1. Injection through the set method
  2. Injection through the constructor
  3. Injection via Java annotations

Dependency injection in Android

In general, Android mainly uses constructor injection or set method injection

Dagger2 is the best dependency injection framework available for Android. Google does the injection at static compile time. It has no impact on performance, is good for maintenance, and can reduce OOM problems caused by object references.

2.1 Dagger2

DI(dependency injection), divided into three parts:

  • Dependent provider
  • Dependent demand side
  • Dependency Injection (Bridge)

Explain what is dependency

A class has two variables, and those two variables are its dependencies. There are two ways to initialize a dependency, self initialization, and external initialization is called dependency injection.

We can’t use a component without knowing what it provides. Second, what are the requirements that are relevant to our business? And finally, how do we use it

In the Dagger

  • The @moudle annotation is generally used as a dependency provider
  • @Component acts as a bridge between dependencies
  • @inject annotates variables as demanders (can also be used for dependency providers)

2.1.1 to use

Add it to build.gradle of your Android project

apply plugin: 'kotlin-kapt'
// ...


implementation 'com. Google. Dagger: a dagger: 2.11'
    kapt 'com. Google. Dagger: a dagger - compiler: 2.11'
    annotationProcessor 'com. Google. Dagger: a dagger - compiler: 2.11'
    / / Java annotations
    implementation 'org. Anyone: javax.mail. Annotation: 10.0 - b28'
Copy the code

@Inject

It is generally used to inject attributes, methods, and constructors. The injection constructors are used mostly in projects and have two functions

  • As a provider of dependencies
// Annotate the constructor
class Navigator @Inject constructor() {
    fun navigator(a) {
        println("navigator method")}}Copy the code
  • As the demand side of dependence
class MainActivity : AppCompatActivity() {
// Inject the Navigator into MainActivty so that MainActivity has a reference to the Navigator
    @Inject lateinit var navigator: Navigator
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        navigator.navigator()
    }
}
Copy the code

The two @inject annotations form a dependency,

what?? Isn’t it said that @inject since the supplier of dependency is also the demander of dependency? Am I using it wrong?

We’re not saying that the @Component in the dependency is a bridge between the demand side and the provider side.

@Component

Annotation interface

The Dagger2 container, as it were, acts as a bridge between injected dependencies and provided dependencies, injecting provided dependencies into the required dependencies

  1. Declare an interface and annotate it with @Component
@Component
interface ApplicationComponent {
// Provide a method for annotations
    fun inject(application: Dagger2Application)
}
Copy the code
  1. Rebuild project, generates a file called DaggerApplicationComponent, and realize the ApplicationComponent, obviously it is the Dagger generated for us,

@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class DaggerApplicationComponent implements ApplicationComponent {
  private MembersInjector<MainActivity> mainActivityMembersInjector;

  private DaggerApplicationComponent(Builder builder) {
  / /...
Copy the code
  1. Declare, initialize, and provide references to annotations in the ApplicationComponent to global books
class Dagger2Application: Application() {

    val appComponent: ApplicationComponent by lazy(mode = LazyThreadSafetyMode.NONE) {
        DaggerApplicationComponent
            .builder()
            .build()
    }
    override fun onCreate(a) {
        super.onCreate()
        appComponent.inject(this)}}Copy the code
  1. In the MainActivity
class MainActivity : AppCompatActivity() {
    private val appComponent: ApplicationComponent by lazy(mode = LazyThreadSafetyMode.NONE) {
        (application as Dagger2Application).appComponent
    }
    @Inject lateinit var navigator: Navigator
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // This is the real injection
        appComponent.inject(this)
        navigator.navigator()
    }
}
Copy the code

Don’t forget to configure Application in AndroidMianfest.xml

Of course, with a framework in a project, we need to consider whether we can cover as many business scenarios as possible, so there is a problem right now, Dagger to provide annotations to the third party library

@ the Module and @ Provides

@Module and @Provides dependencies for Dagger2. This is a supplement to @inject (3). It is used in cases where @inject cannot be used to provide dependencies, such as classes provided by third-party libraries, basic data types, etc. @Provides can annotate only methods, and the class in which the method is provided has the @Module annotation. The annotated method indicates that Dagger2 can provide dependencies on the method instance object. By convention, the @provides method is prefixed with provide for easy reading and management.

eg:

  1. Create @ the Module
@Module
class ApplicationModule {}Copy the code
  1. Combine with @Provides to provide dependencies
// This simply provides a Retrofit (third-party library) dependency
@Module
class ApplicationModule {
    @Provides
    fun provideRetrofit(a): Retrofit {
        return Retrofit.Builder().build()
    }
}
Copy the code

After @ Inject lesson, we should first think of is and how to DaggerApplicationComponent this interface

  1. Associate the ApplicationModule with @Component

@Component(modules = [ApplicationModule::class])
interface ApplicationComponent {
    fun inject(application: Dagger2Application)
    fun inject(activity: MainActivity)
}
Copy the code
  1. Add a line of code to Application
val appComponent: ApplicationComponent by lazy(mode = LazyThreadSafetyMode.NONE) {
        DaggerApplicationComponent
            .builder()
            / / is this line
            .applicationModule(ApplicationModule())
            .build()
    }
Copy the code

conclusion

The @Component annotation contains a Module class, indicating that Dagger2 can look for dependencies from the Module class. Dagger2 will automatically look for dependencies on the Module class with the @Provides annotation, and then complete the injection


This is the end of Dagger2’s simple annotations, which can also be used in the project. Of course, there are many Dagger2 annotations, you can refer to:

Android | finish don’t forget the series, “dagger

Read so much, my original intention is Dagger2 under a more simple, less code amount of network request framework, a few days ago summed up a set of framework and friends talk about it is relatively large, some bloated

3. Kotlin Coroutines + Jetpack + Retrofit + okHttp3 + Dagger Network request

The idea is to put the fixed parts in Dagger2 and then minimize the variable code

Obviously the initialization of Retrofit will have to be in the Dagger2 annotation

3.1 Declare a dependency on Retrofit in @Module

@Module
class ApplicationModule(private val application: Dagger2Application) {

    @Provides
    @Singleton fun provideApplicationContext(a): Context = application

    @Provides
    @Singleton
    fun provideRetrofit(a): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://gank.io/api/v2/data/category/Girl/type/Girl/")
            .client(createClient())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    private fun createClient(a): OkHttpClient {
        val builder = OkHttpClient.Builder()
        if (BuildConfig.DEBUG) {
            val loggingInterceptor =
                HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)
            builder.addInterceptor(loggingInterceptor)
        }
        return builder.build()
    }
}

Copy the code

There is an @singleton annotation, which is literally a Singleton, but based on the way Dagger2 is compiled, @singleton is just a reminder because each compilation checks to see if an annotation has been compiled, and if it has, it will not be compiled again. So it is impossible to have a reference that is not a unified object

3.1.1 Error Handling

The network request framework chosen for your application should be tailored to your needs, but with any framework we need to handle exceptions and other unexpected error situations

Sealed classes are similar to enumerations, but are more flexible than enumerations, can carry parameters, and so on
sealed class Failure {
    object NetworkConnection : Failure()
    object ServerError : Failure()
    abstract class FeatureFailure : Failure()}Copy the code

3.1.2 Return Value Processing

There is also the handling of the return value, which is divided into two states, success and failure

sealed class Either<out L, out R> {

    data class Error<out L>(val a: L) : Either<L, Nothing> ()data class Success<out R>(val b: R) : Either<Nothing, R>()

    val isRight get() = this is Success<R>

    val isLeft get() = this is Error<L>

    fun <L> left(a: L) = Error(a)

    fun <R> right(b: R) = Success(b)

    fun fold(fnL: (L) - >Any, fnR: (R) - >Any): Any =
        when(this) {
            is Error -> fnL(a)
            is Success -> fnR(b)
        }
}
Copy the code

Anything into a system is not have a specific order, because open the refrigerator is put in the elephant has a variety of ways, but the objective is the elephant in the refrigerator, so we don’t need to worry about how to build up a framework, we just according to their own ideas to write out the things we want, and then improve it. So don’t think about what do I do after this step

3.2 Declaring apis and Services

interface ImageApi {
    @GET("page/{page}/count/{size}")
    fun images(@Path("page") page: Int.@Path("size") size: Int): Call<ImageEntry>
}
Copy the code
@Singleton
class ImageService @Inject constructor(retrofit: Retrofit) : ImageApi {
    private val imageApi by lazy { retrofit.create(ImageApi::class.java) }
    override fun images(page: Int, size: Int) = imageApi.images(page, size)
}
Copy the code

This links Retrofit to the API, so where do we use ImageService next? This is obviously the class for the network request service,

It is recommended that network requests or data sources be stored in a warehouse for centralized processing and data cache design

3.3 Establish a data warehouse

interface ImageRepository { fun images(page: Int, size: Int): Either<Failure, ImageEntry> class NetWork @Inject constructor( private val networkHandler: NetworkHandler, private val imageService: ImageService ) : ImageRepository { override fun images(page: Int, size: Int): Either<Failure, ImageEntry> { return when (networkHandler.isConnected) { true -> request( imageService.images(page, size), { it }, ImageEntry.empty() ) false, null -> Either.Error(Failure.NetworkConnection) } } private fun <T, R> request( call: Call<T>, transform: (T) -> R, default: T ): Either<Failure, R> { return try { val response = call.execute() when (response.isSuccessful) { true -> Either.Success(transform((response.body() ? : default))) false -> Either.Error(Failure.ServerError) } } catch (e: Throwable) { Either.Error(Failure.ServerError) } } } }Copy the code

After the above two operations, our answer framework is out. Now it is to write how to perform a network request and how to display the requested data to the page

3.4 Create a request use case

We summarize the request directly into a use case, which is the class dedicated to the request


abstract class UseCase<out Type, in Params> where Type : Any {
    abstract suspend fun run(params: Params): Either<Failure, Type>

    operator fun invoke(params: Params, onResult: (Either<Failure.Type- > >)Unit = {}) {
        val job = GlobalScope.async(Dispatchers.IO) { run(params) }
        GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
    }

    class None
}
Copy the code

Here we use coroutines and expose a run(params) method that implements the specific request

The specific interface request method

class GetImage @Inject constructor(private val imageRepository: ImageRepository) :
    UseCase<ImageEntry, GetImage.Params>() {
    override suspend fun run(params: Params): Either<Failure, ImageEntry> = imageRepository.images(params.page, params.size)

    data class Params(val page: Int.val size: Int)}Copy the code

3.5 create ViewModel

class ImageViewModel @Inject constructor(private val getImage: GetImage) : BaseViewModel() {
    private val _image: MutableLiveData<List<Image>> = MutableLiveData()
    val image: LiveData<List<Image>> = _image

    fun loadImage(page: Int, size: Int) = getImage(GetImage.Params(page, size)) {
        it.fold(::handleFailure, ::handleImageList)
    }

    private fun handleImageList(imageEntry: ImageEntry) {
        _image.value = imageEntry.toImage()
    }
}

Copy the code

The double colon operator in “::” kotlin means that one method is passed as an argument to another method for use

Reference project: Android-CleanArchitecture-Kotlin

Interpretation item: github.com/kongxiaoan/…

Download the Demo

Four, the purpose,

Some projects for foreigners are particularly excellent, but because their English is not easy to understand for people with weak English, I will see this project is split, and realized again, from which to obtain some knowledge, is very worthwhile