-
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
Created a Hilt example with a view model scoped custom component #503
Changes from all commits
f157868
49ada08
6710edf
ab809b4
0f37ae2
1941ec1
1be286d
5eaef2e
71f64a6
18f27a8
774a616
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,8 @@ android { | |
versionName "1.0" | ||
} | ||
|
||
dataBinding { | ||
enabled = true | ||
buildFeatures { | ||
dataBinding true | ||
} | ||
} | ||
|
||
|
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,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. |
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") | ||
} |
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() | ||
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 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 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. @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 | ||
} |
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.
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 setupThere 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 don't think so because the return type has to match the
@AssistedInject
constructor