Skip to content

Commit

Permalink
Created an Anvil sample (#652)
Browse files Browse the repository at this point in the history
This PR creates a sample app that uses Anvil and has a `@ContributesViewModel` code generator to let Anvil do all of the wiring necessary to do constructor injection for ViewModels. It also demonstrates how to handle Dagger subcomponents including those owned by Fragments/features.
  • Loading branch information
gpeal authored Sep 23, 2022
1 parent 86b8983 commit 34c6624
Show file tree
Hide file tree
Showing 46 changed files with 1,049 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ buildscript {
}
dependencies {
classpath libs.androidGradle
classpath libs.anvilGradle
classpath libs.kotlinGradle
classpath libs.hiltGradle

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ksp = { id = "com.google.devtools.ksp", version = "1.7.10-1.0.6" }

[libraries]
androidGradle = "com.android.tools.build:gradle:_"
anvilGradle = "com.squareup.anvil:gradle-plugin:_"
autoValue = "com.google.auto.value:auto-value:_"
autoValueAnnotations = "com.google.auto.value:auto-value-annotations:_"
dagger = "com.google.dagger:dagger:_"
Expand Down
1 change: 1 addition & 0 deletions sample-anvil/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
13 changes: 13 additions & 0 deletions sample-anvil/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Anvil Sample

The Anvil sample consists of 3 modules:
* :sample-anvil: The actual application that demonstrates an example features.
* :sample-anvilcodegen: The [anvil-compiler code generator](https://github.com/square/anvil/blob/main/compiler-api/README.md) that allows `@ContributesViewModel` to setup all of the wiring necessary for constructor dependency injection.
* :sample-anvilannotations: Shared annotations between the sample and codegen.

The Anvil sample demonstrates how to setup a `@ContributesViewModel` annotation that allows you to do constructor injection from Dagger without any additional wiring such as the creation of a Dagger module. The setup requires a bit of infrastructure which is all included in these three modules. However, once it is set up, it dramatically simplifies the usage of ViewModels and Dagger.

A good starting point is ExampleFeatureFragment. ExampleFeatureFragment and the other classes in that folder demonstrate:
* How to use `@ContributesViewModel` to do constructor injection.
* How to create feature/Fragment scoped Dagger Components that are supported by `@ContributesViewModel`.
* How to set up App/User/Feature Dagger component hierarchies that are commonly used in real world apps.
53 changes: 53 additions & 0 deletions sample-anvil/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
apply plugin: "com.android.application"
apply plugin: "com.squareup.anvil"
apply plugin: "kotlin-android"
apply plugin: "kotlin-kapt"

android {

defaultConfig {
applicationId "com.airbnb.mvrx.sample.anvil"
versionCode 1
versionName "0.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}

buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.debug
}
}

signingConfigs {
debug {
storeFile file("debug.keystore")
storePassword "testing"
keyAlias "helloDagger"
keyPassword "testing"
}
}

buildFeatures {
viewBinding true
}
}

dependencies {
implementation project(":mvrx")
implementation project(":utils-view-binding")
implementation project(":sample-anvilannotations")

anvil project(':sample-anvilcodegen')

kapt libs.daggerCompiler

implementation libs.appcompat
implementation libs.constraintlayout
implementation libs.coreKtx
implementation libs.dagger
implementation libs.fragmentKtx
implementation libs.viewModelKtx
implementation libs.multidex
}
25 changes: 25 additions & 0 deletions sample-anvil/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.airbnb.mvrx.sample.anvil">

<application
android:name=".AnvilSampleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.airbnb.mvrx.sample.anvil

import android.app.Application
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.sample.anvil.di.DaggerComponentOwner
import com.airbnb.mvrx.sample.anvil.di.bindings

class AnvilSampleApplication : Application(), DaggerComponentOwner {

lateinit var appComponent: AppComponent
// This can be set or unset as users log in and out.
var userComponent: UserComponent? = null

override val daggerComponent get() = listOfNotNull(appComponent, userComponent)

override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.create()
// Simulate a logged in user
userComponent = bindings<UserComponent.ParentBindings>().userComponentBuilder().user(User("Gabriel Peal")).build()
Mavericks.initialize(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.airbnb.mvrx.sample.anvil

import com.airbnb.mvrx.sample.anvil.di.SingleIn
import com.squareup.anvil.annotations.MergeComponent

interface AppScope

@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.airbnb.mvrx.sample.anvil

import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.airbnb.mvrx.sample.anvil

import com.airbnb.mvrx.sample.anvil.di.SingleIn
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import dagger.BindsInstance
import dagger.Subcomponent

data class User(val name: String)

interface UserScope

@SingleIn(UserScope::class)
@MergeSubcomponent(UserScope::class)
interface UserComponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance
fun user(user: User): Builder
fun build(): UserComponent
}

/**
* This is a subcomponent of [AppComponent]. This tells [AppComponent] that it needs to be able to
* provide the builder for [UserComponent].
*/
@ContributesTo(AppScope::class)
interface ParentBindings {
fun userComponentBuilder(): Builder
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.airbnb.mvrx.sample.anvil

import com.airbnb.mvrx.sample.anvil.di.SingleIn
import kotlinx.coroutines.delay
import javax.inject.Inject

/**
* This doesn't need to be a singleton in the user component but is done just to demonstrate
* how to create singletons with [SingleIn].
*/
@SingleIn(UserScope::class)
class UserScopedRepository @Inject constructor(
private val user: User,
) {

suspend operator fun invoke(): String {
delay(2000)
return "Hello World, ${user.name}!"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.airbnb.mvrx.sample.anvil.di

import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel

/**
* You do not need to use this type directly. It is used by DaggerMavericksViewModelFactory.
*
* Serves as a supertype for AssistedInject factories in ViewModels.
*
* We use this interface as a marker in a Multibinding Dagger setup to populate a Map
* of ViewModel classes with their AssistedInject factories.
*
* First we define our ViewModel with an @AssistedInject annotated constructor, and a Factory interface
* implementing AssistedViewModelFactory.
*
* class MyViewModel @AssistedInject constructor(
* @Assisted initialState: MyState,
* …
* ): MavericksViewModel<MyState>(...) {
* @AssistedFactory
* interface Factory : AssistedViewModelFactory<MyViewModel, MyState> {
* override fun create(state: MyState): MyViewModel
* }
* }
*
* Then we need to create a Dagger Module, which contains methods that @Binds @IntoMap all of our
* AssistedViewModelFactories using a [ViewModelKey]. Notice that the input to these methods is
* the exact type of our AssistedInject factory, but the return type is an AssistedViewModelFactory.
*
* @Module
* interface MyAppModule {
* @Binds
* @IntoMap
* @ViewModelKey(MyViewModel::class)
* fun myViewModelFactory(factory: MyViewModel.Factory): AssistedViewModelFactory<*, *>
* }
*
* This Module tells Dagger to include MyViewModel.Factory class in the Multibinding map using
* MyViewModel::class as the key. Such a method should be added for **every ViewModel Factory**
* so that they can be identified by Dagger and used for populating the Map.
*
* The generated map can then be injected wherever it is required.
*
* interface AppComponent {
* fun viewModelFactories(): Map<Class<out BaseViewModel<*>>, AssistedViewModelFactory<*, *>>
* }
*
* class SomeClass @Inject constructor(
* val viewModelFactories: Map<Class<out BaseViewModel<*>>, AssistedViewModelFactory<*, *>>
* )
*/
interface AssistedViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState> {
fun create(state: S): VM
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.airbnb.mvrx.sample.anvil.di

import android.content.Context
import android.content.ContextWrapper
import androidx.fragment.app.Fragment

/**
* Use this to get the Dagger "Bindings" for your module. Bindings are used if you need to directly interact with a dagger component such as:
* * an inject function: `inject(MyFragment frag)`
* * an explicit getter: `fun myClass(): MyClass`
*
* Anvil will make your Dagger component implement these bindings so that you can call any of these functions on an instance of your component.
*
* [bindings] will walk up the Fragment/Activity hierarchy and check for [DaggerComponentOwner] to see if any of its components implement the
* specified bindings. Most of the time this will "just work" and you don't have to think about it.
*
* For example, if your class has @Inject properties:
* 1) Create an bindings interface such as `YourModuleBindings`
* 1) Add an inject function like `fun inject(yourClass: YourClass)`
* 2) Contribute your interface to the correct component via `@ContributesTo(AppScope::class)`.
* 3) Call bindings<YourModuleBindings>().inject(this).
*/
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)

/**
* @see bindings
*/
inline fun <reified T : Any> Fragment.bindings() = bindings(T::class.java)

/** Use no-arg extension function instead: [Context.bindings] */
fun <T : Any> Context.bindings(klass: Class<T>): T {
// search dagger components in the context hierarchy
return generateSequence(this) { (it as? ContextWrapper)?.baseContext }
.plus(applicationContext)
.filterIsInstance<DaggerComponentOwner>()
.map { it.daggerComponent }
.flatMap { if (it is Collection<*>) it else listOf(it) }
.filterIsInstance(klass)
.firstOrNull()
?: error("Unable to find bindings for ${klass.name}")
}

/** Use no-arg extension function instead: [Fragment.bindings] */
fun <T : Any> Fragment.bindings(klass: Class<T>): T {
// search dagger components in fragment hierarchy, then fallback to activity and application
return generateSequence(this, Fragment::getParentFragment)
.filterIsInstance<DaggerComponentOwner>()
.map { it.daggerComponent }
.flatMap { if (it is Collection<*>) it else listOf(it) }
.filterIsInstance(klass)
.firstOrNull()
?: requireActivity().bindings(klass)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.airbnb.mvrx.sample.anvil.di

/**
* A [DaggerComponentOwner] is anything that "owns" a Dagger Component. The owner can be a Application, Activity, or Fragment.
*
* When using [bindings] or when creating a ViewModel, the infrastructure provided will walk up the Fragment tree, then check
* the Activity, then the Application for any [DaggerComponentOwner] that can provide ViewModels.
*
* In this sample:
* * [com.airbnb.mvrx.sample.anvil.AppComponent] is owned by the Application class.
* * [com.airbnb.mvrx.sample.anvil.UserComponent] is subcomponent of AppComponent and injected with the current logged in user.
* It is set/cleared in the Application class.
* * [com.airbnb.mvrx.sample.anvil.ExampleFeatureComponent] is a subcomponent of UserComponent and
* owned by [com.airbnb.mvrx.sample.anvil.ExampleFeatureFragment].
*/
interface DaggerComponentOwner {
/** This is either a component, or a list of components. */
val daggerComponent: Any
}
Loading

0 comments on commit 34c6624

Please sign in to comment.