Last active
October 22, 2021 13:05
-
-
Save adam-hurwitz/964a37c60161d74b80cb8a1e520f2bef to your computer and use it in GitHub Desktop.
Dagger 2 AssistedInject ViewModels
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.squareup.inject.assisted.dagger2.AssistedModule | |
import dagger.Module | |
@AssistedModule | |
@Module(includes = [AssistedInject_AssistedInjectModule::class]) | |
class AssistedInjectModule |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SomeFragment : Fragment() { | |
private lateinit var component: Component | |
private lateinit var someViewModel: SomeViewModel | |
override fun onAttach(context: Context) { | |
super.onAttach(context) | |
component = (context.applicationContext as App).component | |
component.inject(this) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
val someViewModel: SomeViewModel by navGraphSavedStateViewModels(R.id.nav_graph) { handle -> | |
component.someViewModelFactory().create( | |
coroutineDispatcherProvider = DefaultDispatcherProvider(), | |
someStringVal = "Some string value" | |
) | |
} | |
this.someViewModel = someViewModel | |
... | |
return binding.root | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SomeViewModel @AssistedInject constructor( | |
@Assisted private val coroutineScopeProvider: CoroutineScope?, | |
@Assisted private val coroutineDispatcherProvider: DispatcherProvider, | |
@Assisted private val someStringVal: String, | |
private val repository: SomeRepository | |
) : ViewModel() { | |
@AssistedInject.Factory | |
interface Factory { | |
fun create( | |
coroutineScopeProvider: CoroutineScope? = null, | |
coroutineDispatcherProvider: DispatcherProvider, | |
someStringVal: String | |
): FeedViewModel | |
} | |
private val coroutineScope = getViewModelScope(coroutineScopeProvider) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package app.topcafes.dependencyinjection | |
import android.os.Bundle | |
import androidx.annotation.IdRes | |
import androidx.fragment.app.Fragment | |
import androidx.fragment.app.createViewModelLazy | |
import androidx.lifecycle.AbstractSavedStateViewModelFactory | |
import androidx.lifecycle.SavedStateHandle | |
import androidx.lifecycle.ViewModel | |
import androidx.lifecycle.ViewModelProvider | |
import androidx.lifecycle.viewModelScope | |
import androidx.navigation.fragment.findNavController | |
import androidx.savedstate.SavedStateRegistryOwner | |
import kotlinx.coroutines.CoroutineDispatcher | |
import kotlinx.coroutines.CoroutineScope | |
import kotlinx.coroutines.Dispatchers | |
/** | |
* See 'Dagger Tips' > 'Injecting ViewModel + SavedStateHandle' | |
* [https://proandroiddev.com/dagger-tips-leveraging-assistedinjection-to-inject-viewmodels-with-savedstatehandle-and-93fe009ad874#7919] | |
* Original: [https://gist.github.com/Zhuinden/06b86cb35cba0cb5e880505042e18c3d] | |
* Author: Gabor Varadi [https://twitter.com/Zhuinden] | |
*/ | |
/** | |
* Create a single instance of the ViewModel with Saved State enabled. | |
* | |
* @receiver Fragment launches ViewModel | |
* @param creator Function1<SavedStateHandle, T> | |
* @return Lazy<T> | |
*/ | |
inline fun <reified T : ViewModel> Fragment.fragmentSavedStateViewModels( | |
crossinline creator: (SavedStateHandle) -> T | |
): Lazy<T> { | |
return createViewModelLazy( | |
viewModelClass = T::class, | |
storeProducer = { viewModelStore }, | |
factoryProducer = { | |
createAbstractSavedStateViewModelFactory( | |
arguments = arguments ?: Bundle(), | |
creator = creator | |
) | |
}) | |
} | |
/** | |
* Create a shared ViewModel instance across a given navGraphId. | |
* | |
* @receiver Fragment launches ViewModel | |
* @param navGraphId Int defines ViewModel lifecycle | |
* @param creator Function1<SavedStateHandle, T> | |
* @return Lazy<T> | |
*/ | |
inline fun <reified T : ViewModel> Fragment.navGraphSavedStateViewModels( | |
@IdRes navGraphId: Int, | |
crossinline creator: (SavedStateHandle) -> T | |
): Lazy<T> { | |
// Wrapped in lazy to not search the NavController each time we want the backStackEntry | |
val backStackEntry by lazy { findNavController().getBackStackEntry(navGraphId) } | |
return createViewModelLazy( | |
viewModelClass = T::class, | |
storeProducer = { backStackEntry.viewModelStore }, | |
factoryProducer = { | |
backStackEntry.createAbstractSavedStateViewModelFactory( | |
arguments = backStackEntry.arguments ?: Bundle(), | |
creator = creator | |
) | |
}) | |
} | |
/** | |
* Helper factory function to create a single instance of the ViewModel. | |
* | |
* @receiver SavedStateRegistryOwner | |
* @param arguments Bundle? | |
* @param creator Function1<SavedStateHandle, T> | |
* @return ViewModelProvider.Factory | |
*/ | |
inline fun <reified T : ViewModel> SavedStateRegistryOwner.createAbstractSavedStateViewModelFactory( | |
arguments: Bundle, | |
crossinline creator: (SavedStateHandle) -> T | |
): ViewModelProvider.Factory { | |
return object : AbstractSavedStateViewModelFactory(this, arguments) { | |
@Suppress("UNCHECKED_CAST") | |
override fun <T : ViewModel?> create( | |
key: String, | |
modelClass: Class<T>, | |
handle: SavedStateHandle | |
): T = creator(handle) as T | |
} | |
} | |
/** | |
* Configure CoroutineScope injection for production and testing. | |
* | |
* @receiver ViewModel provides viewModelScope for production | |
* @param coroutineScope null for production, injects TestCoroutineScope for unit tests | |
* @return CoroutineScope to launch coroutines on | |
*/ | |
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) = | |
if (coroutineScope == null) this.viewModelScope | |
else coroutineScope | |
/** | |
* Configure Dispatchers injection for production and testing. | |
* | |
* See 'Unit Testing Coroutine Suspend Functions using TestCoroutineDispatcher' > 'DispatcherProvider' | |
* [https://craigrussell.io/2019/11/unit-testing-coroutine-suspend-functions-using-testcoroutinedispatcher/#dispatcherprovider] | |
* Author: Craig Russell [https://twitter.com/trionkidnapper] | |
*/ | |
interface DispatcherProvider { | |
fun main(): CoroutineDispatcher = Dispatchers.Main | |
fun default(): CoroutineDispatcher = Dispatchers.Default | |
fun io(): CoroutineDispatcher = Dispatchers.IO | |
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined | |
} | |
class DefaultDispatcherProvider : DispatcherProvider |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment