-
Notifications
You must be signed in to change notification settings - Fork 499
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
Adds Hilt Example (Dagger @AssistedInject edition) #495
Changes from all commits
f157868
49ada08
6710edf
ab809b4
0f37ae2
1941ec1
1be286d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Dagger Usage Sample for MvRx | ||
|
||
This module contains a sample app demonstrating how to setup Hilt and AssistedInject in an app using MvRx. | ||
|
||
**NOTE** Hilts bundled implementation of AssistedInject is as time of writing not released. | ||
So, you will need to use the Snapshot release until Dagger 2.x is released with AssistedInject. | ||
|
||
// build.gradle | ||
buildscript { | ||
repositories { | ||
google() | ||
jcenter() | ||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" } | ||
} | ||
dependencies { | ||
classpath "com.google.dagger:hilt-android-gradle-plugin:HEAD-SNAPSHOT" | ||
// rest of classpath(s)… | ||
} | ||
} | ||
|
||
// module/build.gradle | ||
```groovy | ||
repositories { | ||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" } | ||
} | ||
|
||
dependencies { | ||
def hiltVersion = "HEAD-SNAPSHOT" | ||
kapt "com.google.dagger:hilt-android-compiler:${hiltVersion}" | ||
implementation "com.google.dagger:hilt-android:${hiltVersion}" | ||
} | ||
``` | ||
|
||
## Key Features | ||
|
||
* **Injecting state into ViewModels with AssistedInject** | ||
|
||
Since the `initialState` parameter is only available at runtime, Dagger can not provide this dependency for us. We need the [AssistedInject](https://github.com/square/AssistedInject) library for this purpose. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can update this doc :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh dam, totally missed this. Yep |
||
|
||
* **Multibinding setup for AssistedInject Factories** | ||
|
||
Every ViewModel using AssistedInject needs a Factory interface annotated with `@AssistedFactory`. These factories are grouped together under a common parent type [AssistedViewModelFactory](src/main/java/com/airbnb/mvrx/hellohilt/di/AssistedViewModelFactory.kt) to enable a Multibinding Dagger setup. | ||
|
||
## Example | ||
|
||
* Create your ViewModel with an ``@AssistedInject` constructor, an `@AssistedFactory` implementing `AssistedViewModelFactory`, and a companion object implementing `DaggerMvRxViewModelFactory`. | ||
|
||
```kotlin | ||
class MyViewModel @AssistedInject constructor( | ||
@Assisted initialState: MyState, | ||
// and other dependencies | ||
) { | ||
|
||
@AssistedFactory | ||
interface Factory: AssistedViewModelFactory<MyViewModel, MyState> { | ||
override fun create(initialState: MyState): MyViewModel | ||
} | ||
|
||
companion object: HiltMavericksViewModelFactory<MyViewModel, MyState>(MyViewModel::class.java) | ||
} | ||
``` | ||
|
||
* Tell Dagger to include your ViewModel's AssistedInject Factory in a Multibinding map. | ||
|
||
```kotlin | ||
interface AppModule { | ||
|
||
@[Binds IntoMap ViewModelKey(MyViewModel::class)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting syntax. I've never seen the annotation array like this before! Good to know it's possible but I think it'll be easier for people to understand if you split it into 3 lines because that syntax is more common. |
||
fun myViewModelFactory(factory: MyViewModel.Factory): AssistedViewModelFactory<*, *> | ||
|
||
} | ||
``` | ||
|
||
* Add a provision for the Multibinding map in your Dagger component: | ||
|
||
```kotlin | ||
fun viewModelFactories(): Map<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>> | ||
``` | ||
|
||
* With this setup complete, request your ViewModel in a Fragment as usual, using any of MvRx's ViewModel delegates. | ||
|
||
```kotlin | ||
class MyFragment : BaseMvRxFragment() { | ||
val viewModel: MyViewModel by fragmentViewModel() | ||
} | ||
``` | ||
|
||
## How it works | ||
|
||
`HiltMavericksViewModelFactory` will try loading the custom entry point from the `SingletonComponent` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if you wanted this to be scoped to a different component? This should theoretically be able to be scoped by Fragment, Activity, ActivityRetained, or Singleton right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about that. It should be as simple as creating a custom |
||
and lookup the viewModel factory using the `viewModelClass` key. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
apply plugin: "com.android.application" | ||
apply plugin: "kotlin-android" | ||
apply plugin: "kotlin-android-extensions" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kotlin-android-extensions is deprecated now. Let's move to the parcelize plugin for all new things and we can remove it from the rest soon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yep. Did not see that. |
||
apply plugin: 'dagger.hilt.android.plugin' | ||
apply plugin: "kotlin-kapt" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can you put the kotlin plugins together? |
||
|
||
repositories { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per my comment above, let's move this to the top level build.gradle |
||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" } | ||
} | ||
|
||
android { | ||
|
||
defaultConfig { | ||
applicationId "com.airbnb.mvrx.helloHilt" | ||
versionCode 1 | ||
versionName "0.0.1" | ||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
|
||
buildTypes { | ||
release { | ||
minifyEnabled true | ||
signingConfig signingConfigs.debug | ||
} | ||
} | ||
} | ||
|
||
dependencies { | ||
kapt AnnotationProcessors.hilt | ||
|
||
implementation Libraries.appcompat | ||
implementation Libraries.constraintlayout | ||
implementation Libraries.coreKtx | ||
implementation Libraries.fragmentKtx | ||
implementation Libraries.hilt | ||
implementation Libraries.rxJava | ||
implementation Libraries.viewModelKtx | ||
implementation Libraries.multidex | ||
implementation project(":mvrx-rxjava2") | ||
implementation project(":mvrx-mocking") | ||
|
||
debugImplementation Libraries.fragmentTesting | ||
|
||
androidTestImplementation InstrumentedTestLibraries.core | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you use these |
||
androidTestImplementation InstrumentedTestLibraries.espresso | ||
androidTestImplementation InstrumentedTestLibraries.junit | ||
|
||
testImplementation project(":testing") | ||
testImplementation TestLibraries.junit | ||
testImplementation TestLibraries.mockk | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?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.hellohilt"> | ||
|
||
<application | ||
android:name="com.airbnb.mvrx.hellohilt.HelloHiltApplication" | ||
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="com.airbnb.mvrx.hellohilt.MainActivity"> | ||
<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,34 @@ | ||
package com.airbnb.mvrx.hellohilt | ||
|
||
import android.os.Bundle | ||
import android.view.View | ||
import androidx.fragment.app.Fragment | ||
import com.airbnb.mvrx.Fail | ||
import com.airbnb.mvrx.Loading | ||
import com.airbnb.mvrx.MvRxView | ||
import com.airbnb.mvrx.Success | ||
import com.airbnb.mvrx.Uninitialized | ||
import com.airbnb.mvrx.fragmentViewModel | ||
import com.airbnb.mvrx.withState | ||
import dagger.hilt.android.AndroidEntryPoint | ||
import kotlinx.android.synthetic.main.fragment_hello.helloButton | ||
import kotlinx.android.synthetic.main.fragment_hello.messageTextView | ||
|
||
@AndroidEntryPoint | ||
class HelloFragment : Fragment(R.layout.fragment_hello), MvRxView { | ||
|
||
val viewModel: HelloViewModel by fragmentViewModel() | ||
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
helloButton.setOnClickListener { viewModel.sayHello() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you migrate this to view binding like the sample app? You can copy in FragmentViewBindingDelegate |
||
} | ||
|
||
override fun invalidate() = withState(viewModel) { state -> | ||
helloButton.isEnabled = state.message !is Loading | ||
messageTextView.text = when (state.message) { | ||
is Uninitialized, is Loading -> getString(R.string.hello_fragment_loading_text) | ||
is Success -> state.message() | ||
is Fail -> getString(R.string.hello_fragment_failure_text) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.airbnb.mvrx.hellohilt | ||
|
||
import android.app.Application | ||
import com.airbnb.mvrx.mocking.MockableMavericks | ||
import dagger.hilt.android.HiltAndroidApp | ||
|
||
@HiltAndroidApp | ||
class HelloHiltApplication : Application() { | ||
override fun onCreate() { | ||
super.onCreate() | ||
MockableMavericks.initialize(this) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,14 @@ | ||||||||||||||||||||
package com.airbnb.mvrx.hellohilt | ||||||||||||||||||||
|
||||||||||||||||||||
import io.reactivex.Observable | ||||||||||||||||||||
import java.util.concurrent.TimeUnit | ||||||||||||||||||||
import javax.inject.Inject | ||||||||||||||||||||
|
||||||||||||||||||||
class HelloRepository @Inject constructor() { | ||||||||||||||||||||
|
||||||||||||||||||||
fun sayHello(): Observable<String> { | ||||||||||||||||||||
return Observable | ||||||||||||||||||||
.just("Hello, world!") | ||||||||||||||||||||
.delay(2, TimeUnit.SECONDS) | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+9
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you make this coroutines since most new things will be using that? It can be:
Suggested change
|
||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,34 @@ | ||||||
package com.airbnb.mvrx.hellohilt | ||||||
|
||||||
import com.airbnb.mvrx.Async | ||||||
import com.airbnb.mvrx.BaseMvRxViewModel | ||||||
import com.airbnb.mvrx.MvRxState | ||||||
import com.airbnb.mvrx.Uninitialized | ||||||
import com.airbnb.mvrx.hellohilt.di.AssistedViewModelFactory | ||||||
import com.airbnb.mvrx.hellohilt.di.HiltMavericksViewModelFactory | ||||||
import dagger.assisted.Assisted | ||||||
import dagger.assisted.AssistedFactory | ||||||
import dagger.assisted.AssistedInject | ||||||
|
||||||
data class HelloState(val message: Async<String> = Uninitialized) : MvRxState | ||||||
|
||||||
class HelloViewModel @AssistedInject constructor( | ||||||
@Assisted state: HelloState, | ||||||
private val repo: HelloRepository | ||||||
) : BaseMvRxViewModel<HelloState>(state) { | ||||||
|
||||||
init { | ||||||
sayHello() | ||||||
} | ||||||
|
||||||
fun sayHello() { | ||||||
repo.sayHello().execute { copy(message = it) } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
@AssistedFactory | ||||||
interface Factory : AssistedViewModelFactory<HelloViewModel, HelloState> { | ||||||
override fun create(state: HelloState): HelloViewModel | ||||||
} | ||||||
|
||||||
companion object : HiltMavericksViewModelFactory<HelloViewModel, HelloState>(HelloViewModel::class.java) | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.airbnb.mvrx.hellohilt | ||
|
||
import androidx.appcompat.app.AppCompatActivity | ||
import dagger.hilt.android.AndroidEntryPoint | ||
|
||
@AndroidEntryPoint | ||
class MainActivity : AppCompatActivity(R.layout.activity_main) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.airbnb.mvrx.hellohilt.di | ||
|
||
import com.airbnb.mvrx.hellohilt.HelloViewModel | ||
import dagger.Binds | ||
import dagger.Module | ||
import dagger.hilt.InstallIn | ||
import dagger.hilt.components.SingletonComponent | ||
import dagger.multibindings.IntoMap | ||
|
||
/** | ||
* The InstallIn SingletonComponent scope must match the context scope used in [HiltMavericksViewModelFactory] | ||
* | ||
* If you want an Activity or Fragment scope | ||
*/ | ||
@Module | ||
@InstallIn(SingletonComponent::class) | ||
interface AppModule { | ||
|
||
@[Binds IntoMap ViewModelKey(HelloViewModel::class)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you split this into 3 lines (see comment in README) |
||
fun helloViewModelFactory(factory: HelloViewModel.Factory): AssistedViewModelFactory<*, *> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe putting this in the top level build.gradle file like:
would be more standard