Skip to content
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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ buildscript {
repositories {
google()
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
dependencies {
classpath "com.android.tools.build:gradle:${Versions.gradlePlugin}"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:${Versions.kotlin}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
classpath "com.google.dagger:hilt-android-gradle-plugin:${Versions.hilt}"

// Upload with:
// ./gradlew clean assemble uploadArchives --no-daemon --no-parallel
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/java/dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object Versions {
const val dagger = "2.27"
const val daggerAssisted = "0.5.2"
const val epoxy = "4.0.0"
const val hilt = "HEAD-SNAPSHOT" // cause hilt @AssistedInject is not yet published as of time of writing.
const val koin = "2.0.1"
const val kotlinCoroutines = "1.4.1"
const val lottie = "3.4.0"
Expand Down Expand Up @@ -54,6 +55,7 @@ object AnnotationProcessors {
const val dagger = "com.google.dagger:dagger-compiler:${Versions.dagger}"
const val daggerAssisted = "com.squareup.inject:assisted-inject-processor-dagger2:${Versions.daggerAssisted}"
const val epoxy = "com.airbnb.android:epoxy-processor:${Versions.epoxy}"
const val hilt = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
const val lifecycle = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycle}"
const val moshi = "com.squareup.moshi:moshi-kotlin-codegen:${Versions.moshi}"
const val room = "androidx.room:room-compiler:${Versions.room}"
Expand All @@ -73,6 +75,7 @@ object Libraries {
const val fragment = "androidx.fragment:fragment:${Versions.fragment}"
const val fragmentKtx = "androidx.fragment:fragment-ktx:${Versions.fragment}"
const val fragmentTesting = "androidx.fragment:fragment-testing:${Versions.fragment}"
const val hilt = "com.google.dagger:hilt-android:${Versions.hilt}"
const val junit = "junit:junit:${Versions.junit}"
const val koin = "org.koin:koin-android:${Versions.koin}"
const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}"
Expand Down
5 changes: 2 additions & 3 deletions hellodagger/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ dependencies {
implementation Libraries.rxJava
implementation Libraries.viewModelKtx
implementation Libraries.multidex

implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(":mvrx")
implementation project(":mvrx-rxjava2")
implementation project(":mvrx-mocking")

debugImplementation Libraries.fragmentTesting

Expand Down
1 change: 1 addition & 0 deletions hellohilt/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
91 changes: 91 additions & 0 deletions hellohilt/README.md
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 {
Copy link
Collaborator

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:

allprojects {
    repositories {
        ...
    }
}

would be more standard

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can update this doc :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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`
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 @EntryPoint for each scope and adding @Qualifier to the ViewModel's to split them into each scope… I think

and lookup the viewModel factory using the `viewModelClass` key.
51 changes: 51 additions & 0 deletions hellohilt/build.gradle
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you put the kotlin plugins together?


repositories {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
}
24 changes: 24 additions & 0 deletions hellohilt/src/main/AndroidManifest.xml
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>
34 changes: 34 additions & 0 deletions hellohilt/src/main/java/com/airbnb/mvrx/hellohilt/HelloFragment.kt
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() }
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
fun sayHello(): Observable<String> {
return Observable
.just("Hello, world!")
.delay(2, TimeUnit.SECONDS)
}
suspend fun sayHello()String {
delay(2_000)
return "Hello World"
}

}
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) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
repo.sayHello().execute { copy(message = it) }
suspend { repo.sayHello() }.execute { copy(message = it) }

}

@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)
21 changes: 21 additions & 0 deletions hellohilt/src/main/java/com/airbnb/mvrx/hellohilt/di/AppModule.kt
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)]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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<*, *>
}
Loading