-
Notifications
You must be signed in to change notification settings - Fork 500
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
46 changed files
with
1,049 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
23 changes: 23 additions & 0 deletions
23
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/AnvilSampleApplication.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/AppComponent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
5 changes: 5 additions & 0 deletions
5
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
31 changes: 31 additions & 0 deletions
31
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/UserComponent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/UserScopedRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}!" | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/di/AssistedViewModelFactory.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
53 changes: 53 additions & 0 deletions
53
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/di/Bindings.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
19 changes: 19 additions & 0 deletions
19
sample-anvil/src/main/java/com/airbnb/mvrx/sample/anvil/di/DaggerComponentOwner.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.