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

Created a Hilt example with a view model scoped custom component #503

Merged
merged 11 commits into from
Feb 2, 2021
Merged
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
9 changes: 0 additions & 9 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buildscript {
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
5 changes: 4 additions & 1 deletion buildSrc/src/main/java/dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ object Versions {

// Libraries
const val autoValue = "1.6.6"
const val dagger = "2.27"
const val dagger = "2.31.1"
const val daggerAssisted = "0.5.2"
const val epoxy = "4.0.0"
const val hilt = "2.31.1-alpha"
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
4 changes: 2 additions & 2 deletions dogs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {
versionName "1.0"
}

dataBinding {
enabled = true
buildFeatures {
dataBinding true
}
}

Expand Down
2 changes: 0 additions & 2 deletions hellodagger/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ dependencies {
implementation Libraries.rxJava
implementation Libraries.viewModelKtx
implementation Libraries.multidex

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

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
67 changes: 67 additions & 0 deletions hellohilt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Dagger Usage Sample for MvRx

This module contains a sample app demonstrating how to setup Hilt and AssistedInject in an app using MvRx.

// build.gradle
dependencies {
def hiltVersion = "2.31.0" // or newer.
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, we use the [AssistedInject](https://dagger.dev/dev-guide/assisted-injection).

* **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 `MavericksViewModelFactory`.

```kotlin
// NOTE: unlike using Jetpack ViewModels with Hilt, you do not need to annotate your ViewModel class with @HiltViewModel.
class MyViewModel @AssistedInject constructor(
@Assisted initialState: MyState,
// and other dependencies
) {

@AssistedFactory
interface Factory: AssistedViewModelFactory<MyViewModel, MyState> {
override fun create(initialState: MyState): MyViewModel
}

companion object : MavericksViewModelFactory<MyViewModel, MyState> by hiltMavericksViewModelFactory()
}
```

* Tell Hilt to include your ViewModel's AssistedInject Factory in a Multibinding map.

```kotlin
@Module
@InstallIn(MavericksViewModelComponent::class)
interface ViewModelsModule {
@Binds
@IntoMap
@ViewModelKey(HelloHiltViewModel::class)
fun helloViewModelFactory(factory: HelloHiltViewModel.Factory): AssistedViewModelFactory<*, *>
}

```

* With this setup complete, request your ViewModel in a Fragment as usual, using any of MvRx's ViewModel delegates.

```kotlin
class MyFragment : Fragment(), MavericksView {
val viewModel: MyViewModel by fragmentViewModel()
}
```

## How it works

`HiltMavericksViewModelFactory` will create a custom ViewModelComponent that is a child of ActivityComponent and will create an instance of your ViewModel with it.
33 changes: 33 additions & 0 deletions hellohilt/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-kapt"
apply plugin: 'dagger.hilt.android.plugin'

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
}
}

buildFeatures {
viewBinding true
}
}

dependencies {
implementation Libraries.appcompat
implementation Libraries.constraintlayout
implementation Libraries.fragmentKtx
kapt AnnotationProcessors.hilt
implementation Libraries.hilt
implementation project(":mvrx-rxjava2")
}
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>
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.Mavericks
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HelloHiltApplication : Application() {
override fun onCreate() {
super.onCreate()
Mavericks.initialize(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.airbnb.mvrx.hellohilt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRxView
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.hellohilt.databinding.HelloHiltFragmentBinding
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class HelloHiltFragment : Fragment(R.layout.hello_hilt_fragment), MvRxView {
val viewModel1: HelloHiltViewModel by fragmentViewModel(keyFactory = { "a" })
val viewModel2: HelloHiltViewModel by fragmentViewModel(keyFactory = { "b" })

private var _binding: HelloHiltFragmentBinding? = null
private val binding get() = _binding ?: error("Binding was null!")

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = HelloHiltFragmentBinding.inflate(inflater, container, false)
return binding.root
}

override fun onDestroyView() {
_binding = null
super.onDestroyView()
}

override fun invalidate() = withState(viewModel1, viewModel2) { state1, state2 ->
@Suppress("Detekt.MaxLineLength")
binding.with.text = "@MavericksViewModelScoped: VM1: [${state1.viewModelScopedClassId1},${state1.viewModelScopedClassId2}] VM2: [${state2.viewModelScopedClassId1},${state2.viewModelScopedClassId2}]"
@Suppress("Detekt.MaxLineLength")
binding.without.text = "VM1: [${state1.notViewModelScopedClassId1},${state1.notViewModelScopedClassId2}] VM2: [${state2.notViewModelScopedClassId1},${state2.notViewModelScopedClassId2}]"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.airbnb.mvrx.hellohilt

import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.MvRxState
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 HelloHiltState(
val viewModelScopedClassId1: Int? = null,
val viewModelScopedClassId2: Int? = null,
val notViewModelScopedClassId1: Int? = null,
val notViewModelScopedClassId2: Int? = null,
) : MvRxState

class HelloHiltViewModel @AssistedInject constructor(
@Assisted state: HelloHiltState,
private val repo1: HelloRepository,
private val repo2: HelloRepository,
) : BaseMvRxViewModel<HelloHiltState>(state) {

init {
setState {
copy(
viewModelScopedClassId1 = repo1.viewModelScopedClass.id,
viewModelScopedClassId2 = repo2.viewModelScopedClass.id,
notViewModelScopedClassId1 = repo1.notViewModelScopedClass.id,
notViewModelScopedClassId2 = repo2.notViewModelScopedClass.id,
)
}
}

@AssistedFactory
interface Factory : AssistedViewModelFactory<HelloHiltViewModel, HelloHiltState> {
override fun create(state: HelloHiltState): HelloHiltViewModel
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of needing to override this could be make the parent interface generic, ie AssistedViewModelFactory<*,*>. We convert to that when we multi bind anyway, so does having it be typed here matter? just thinking of ways to simplify the setup

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think so because the return type has to match the @AssistedInject constructor

}

companion object : MavericksViewModelFactory<HelloHiltViewModel, HelloHiltState> by hiltMavericksViewModelFactory()
Copy link
Contributor

Choose a reason for hiding this comment

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

what might be nifty is if we could remove this boilerplate somehow. One idea off the top of my head is to have an empty interface the viewmodel could implement as a flag to mavericks to essentially force this behavior without needing to declare the object

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@elihart Want to fork this and try that out? :)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.airbnb.mvrx.hellohilt

import javax.inject.Inject

class HelloRepository @Inject constructor(
val viewModelScopedClass: ViewModelScopedClass,
val notViewModelScopedClass: NotViewModelScopedClass,
)
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,12 @@
package com.airbnb.mvrx.hellohilt

import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject

class NotViewModelScopedClass @Inject constructor() {
val id = instanceId.incrementAndGet()

companion object {
private val instanceId = AtomicInteger(0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.airbnb.mvrx.hellohilt

import com.airbnb.mvrx.hellohilt.di.MavericksViewModelScoped
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject

@MavericksViewModelScoped
class ViewModelScopedClass @Inject constructor() {
val id = instanceId.incrementAndGet()

companion object {
private val instanceId = AtomicInteger(0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.airbnb.mvrx.hellohilt

import com.airbnb.mvrx.hellohilt.di.AssistedViewModelFactory
import com.airbnb.mvrx.hellohilt.di.MavericksViewModelComponent
import com.airbnb.mvrx.hellohilt.di.ViewModelKey
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.IntoMap

@Module
@InstallIn(MavericksViewModelComponent::class)
interface ViewModelsModule {
@Binds
@IntoMap
@ViewModelKey(HelloHiltViewModel::class)
fun helloViewModelFactory(factory: HelloHiltViewModel.Factory): AssistedViewModelFactory<*, *>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.airbnb.mvrx.hellohilt.di

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

/**
* This factory allows Mavericks to supply the initial or restored [MavericksState] to Hilt.
*
* Add this interface inside of your [MavericksViewModel] class then create the following Hilt module:
*
* @Module
* @InstallIn(MavericksViewModelComponent::class)
* interface ViewModelsModule {
* @Binds
* @IntoMap
* @ViewModelKey(MyViewModel::class)
* fun myViewModelFactory(factory: MyViewModel.Factory): AssistedViewModelFactory<*, *>
* }
*
* If you already have a ViewModelsModule then all you have to do is add the multibinding entry for your new [MavericksViewModel].
*/
interface AssistedViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState> {
fun create(state: S): VM
}
Loading