Skip to content

Commit

Permalink
Created a Hilt example with a view model scoped custom component (#503)
Browse files Browse the repository at this point in the history
This PR is a heavily modified #495

MavericksViewModels no longer extend Jetpack ViewModels. However, we want to make sure that we have a story for how to use it with Hilt and its new @ViewModelScoped scope.

I couldn't figure out how to integrate Mavericks with its corresponding ViewModelComponent. However, I was able to create the same setup with a custom component instead. It achieves the same thing and I can't think of any downsides.
  • Loading branch information
gpeal authored Feb 2, 2021
1 parent 5ffc235 commit e16c8d3
Show file tree
Hide file tree
Showing 42 changed files with 669 additions and 14 deletions.
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
}

companion object : MavericksViewModelFactory<HelloHiltViewModel, HelloHiltState> by hiltMavericksViewModelFactory()
}
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

0 comments on commit e16c8d3

Please sign in to comment.