Kotlin Dagger 2 Sharing ViewModel between Activity and Fragment - kotlin

As mentioned in the title I've got a problem with Dagger 2 injection. I have a single activity and multiple fragments. I'm trying to share activity's ViewModel with every child fragment. I based my solution on Google's Android Architecture ToDo sample. I've created ViewModelFactory as
ViewModelFactory
If you jump to the link you will see that in their solution there's a separate ViewModel for every activity and fragment. They are not showing how to deal with scenarios like mine. My implementation of ActivityModule looks like:
#Module
abstract class SampleModule {
#ContributesAndroidInjector(
modules = [
ViewModelBuilder::class
]
)
internal abstract fun sampleActivity(): SampleActivity
#Binds
#IntoMap
#ViewModelKey(SampleViewModel::class)
abstract fun bindViewModel(viewModel: SampleViewModel): ViewModel
}
My activity extends DaggerAppCompatActivity and fragment DaggerFragment and as follows my injection of view model looks simple as
class SampleActivity : DaggerAppCompatActivity() {
#Inject
lateinit var viewModel: SampleViewModel
...
I can't find a web solution to my problem. I'm a pretty novice user of Dagger. I've tried to implement Subcomponent but still, it's not working because all the examples I have searched so far didn't use DaggerApplication, DaggerAppCompatActivity and my way of injection. Please suggest any solution or if a subcomponent way is right please show me how to do it if it's possible in my current architecture.
Thank you very much in advance.

#silaros88 I was facing yor same issue, share a ViewModel between multiple fragmnets in a single Activity application, and i solved playing with the ViewmModelStoreOwner.
Steps to fix your issue.
See here TasksFragment.kt how they inject the ViewModelProvider.Factory instead of the ViewModel
Retrieve the desire ViewModel using one of this two options:
viewModels<SharedDesireViewModel> (requireActivity()) { Injected ViewModelProvider.Factory }
ViewModelProvider(requireActivity(), Injected ViewModelProvider.Factory ).get(SharedDesireViewModel::class.java)
Examples:
Option #1:
FragmentA.kt
class FragmentA: DaggerFragment() {
#Inject
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val mainViewModel: MainViewModel by viewModels({requireActivity()}) { viewModelProviderFactory }
......
FragmentB.kt
class FragmentB: DaggerFragment() {
#Inject
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val mainViewModel: MainViewModel by viewModels({requireActivity()}) { viewModelProviderFactory }
......
Option #2:
FragmentA.kt
class FragmentA: DaggerFragment() {
#Inject
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val mainViewModel: MainViewModel by lazy {
ViewModelProvider(requireActivity(), viewModelProviderFactory)
.get(MainViewModel::class.java)
}
......
FragmentB.kt
class FragmentB: DaggerFragment() {
#Inject
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val mainViewModel: MainViewModel by lazy {
ViewModelProvider(requireActivity(), viewModelProviderFactory)
.get(MainViewModel::class.java)
}
......

I solved the problem with a slightly different approach.
Since the fragments and the activity both have dagger modules.
In the ActivityModule I am providing the sharedViemodel as below
#Module
class ActivityModule(private val activity: AppCompatActivity)
{
#Provides
fun provideMainSharedVieModel() : MainSharedViewModel =
ViewModelProvider(activity).get(MainSharedViewModel::class.java)
}
And in my fragment module I am again providing the same viemodel as below:
#Module
class FragmentModule(private val fragment: Fragment)
{
#Provides
fun provideMainSharedVieModel() : MainSharedViewModel =
ViewModelProvider(fragment.activity!!).get(MainSharedViewModel::class.java)
}
Since the ViewModels are stored in a map with the activity or fragments as the key, hence providing the sharedViewModel with "fragment.activity!!" in the Fragment module will not create a new instance of the viewmodel , it will just the return the already instantiated shared viewmodel to the fragment.

Related

Injecting Context into a Broadcast Receiver using Hilt/Dagger?

I am using context() in my GlanceAppWidget() for tasks like retrieving glanceId and updating app widget state. I am having issue with how I inject the context object.
I would like to use the dagger/hilt framework to inject the context into my GlanceAppWidget() constructor. See MyWidget() below.
However by injecting the context into MyWidget, I then need to pass the context as constructor parameter in MyWidgetReceiver() for val glanceAppWidget. Broadcast receivers are not meant to have constructor arguments so this gives me an Instantiation Exception.
How can I inject context into my GlanceAppWidget? Any help will be much appreciated.
Note: I have also tried using default arguments in MyWidget() to avoid providing context in MyWidgetReceiver but this throws "Type may only contain one injected constructor".
#Singleton
class MyWidget #Inject constructor(
#ApplicationContext val context: Context
) : GlanceAppWidget()
#AndroidEntryPoint
#Singleton
class MyWidgetReceiver #Inject constructor(
#ApplicationContext val context: Context /*<-java.lang.InstantiationException when trying to inject into BroadcastReceiver*/
) : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MyWidget(context)
}
onReceive method of the BroadcastReceiver has the context as its argument. You probably want to bind your widget-creating logic to this method
fun onReceive(context: Context, intent: Intent)
EDIT:
I did not noticed that you are using Glance. Since that, I recommend to stop using context in constructor and instead update glanceId and widget state when you will actually have access to context via some kind of method.
override fun onReceive(context: Context, intent: Intent) {
glanceAppWidget.update(context)
}
MyWidget:
fun update(context: Context) {
// do some work
}
In case when you will need update, you will simply send matching intent which will be received by the receiver.
Couple of things:
GlanceAppWidgetReceiver is a BroadcastReceiver thus you can't have constructor parameters. Also it shouldn't be a singleton. BroadcastReceivers are short-term living classes.
You can retrieve the context inside a #Composable function by calling LocalContext.current. Also you can retrieve the glanceId by calling LocalGlanceId.current
Thus you don't need to inject the context in the first place.
class MyWidget: GlanceAppWidget() {
#Composable
override fun Content() {
val context = LocalContext.current
val glanceId = LocalGlanceId.current
//...
}
}
#AndroidEntryPoint
class MyWidgetReceiver: GlanceAppWidgetReceiver() {
override val glanceAppWidget = MyWidget()
}

Why can't the ViewModel object be created automatically when I use Hilt as Dependency Injection in Jetpack Compose?

The following Code A is from the official sample project.
The project use Hilt as Dependency Injection.
In my mind, I needn't create the ViewModel object by myself because Hilt will create it automatically.
But in Code A, it seems that viewModel: MainViewModel = viewModel() must be created manually, why?
Code A
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
...
}
#HiltViewModel
class MainViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
#DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
) : ViewModel() {
...
}
#HiltAndroidApp
class CraneApplication : Application()
By creating manually, I mean calling the view model constructor. And this is not practiced with Hilt, because you have to pass down all the injections.
You can't just declare the viewModel: MainViewModel parameter, because then you have to pass it from the calling view.
The viewModel() does all the injection magic for you. Also, if the view model already exists, the same object will be returned. So it's completely automatic.

Where is the defaultDispatche instanced by Hilt?

The Code A is from the end branch of the official sample project.
The project use Hilt to implement dependency injection.
I searched the the whole project, I find only the code annotation class DefaultDispatcher, but I can't understand what the code mean.
Where is the defaultDispatche instanced by Hilt ?
Code A
#HiltViewModel
class MainViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
#DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
) : ViewModel() {
...
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
...
}
#Retention(AnnotationRetention.BINARY)
#Qualifier
annotation class DefaultDispatcher
You should have a module declared like this
#Module
object DispatcherModule {
#DefaultDispatcher
#Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}
Now you create this class in order to use #DefaultDispatcher as an annotation
#Retention(AnnotationRetention.BINARY)
#Qualifier
annotation class DefaultDispatcher
For more details, visit here
In here on your sample project, that you have shared is perfectly written like this.
This means #DefaultDispatcher will be used as an annotation, where value will be provided by the DispatcherModule

Can I use #Inject lateinit var navigator: AppNavigatorImpl instead of #Binds abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator?

I'm learning dependency injection by the article with the project。
The Code A need instance an object navigator by dependency injection , so the author use Code B to implement it, you can see it here.
I'm very strange that the class AppNavigatorImpl has implemented the class AppNavigator by dependency injection in Code C, so I think Code D will work well.
Can I use #Inject lateinit var navigator: AppNavigatorImpl instead of #Binds abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator ?
Code A
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#Inject lateinit var navigator: AppNavigator
...
}
Code B
#InstallIn(ActivityComponent::class)
#Module
abstract class NavigationModule {
#Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
Code C
class AppNavigatorImpl #Inject constructor(private val activity: FragmentActivity) : AppNavigator {
override fun navigateTo(screen: Screens) {
val fragment = when (screen) {
Screens.BUTTONS -> ButtonsFragment()
Screens.LOGS -> LogsFragment()
}
activity.supportFragmentManager.beginTransaction()
.replace(R.id.main_container, fragment)
.addToBackStack(fragment::class.java.canonicalName)
.commit()
}
}
Code D
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#Inject lateinit var navigator: AppNavigatorImpl
...
}
Technically, you can.
The question is, whether you actually want to do that.
Usually you want to depend on abstractions, not concretions (dependency inversion principle).
If you'd injected the AppNavigatorImpl, then you depend on concretion instead of it's abstraction AppNavigator.
Also, that page in the codelab teaches how to inject an interface (or an abstract class) as those can't have constructor annotated with #Inject.
In your codebase, you usually need to decide whether it's necessary to have this abstraction or not.

Provide instance without interface using Dagger

There is way to provide instance without actual constructor calling.
class ModelImpl #Inject constructor(...): Model{}
#Provides
fun model(inst: ModelImpl): Model = inst
Is there way to do the same if there is no interface? Dagger knows already all dependencies for ModelImpl, so it can create an instance.
This gives obviously dependency cycle:
#Provides
fun model(inst: ModelImpl): ModelImpl = inst
When you use constructor injection Dagger can construct the object for you and you're already using Dagger to create ModelImpl to use it as a binding for Model in your example!
class ModelImpl #Inject constructor(...): Model{}
#Provides
fun model(inst: ModelImpl): Model = inst
// somewhere else...
// both variants would work!
#Inject lateinit var modelImpl : ModelImpl
#Inject lateinit var model : Model
The same would work without the interface
class ModelImpl #Inject constructor(...)
// somewhere else...
#Inject lateinit var model : ModelImpl
If you annotate the constructor then Dagger can create the object for you (if all dependencies can be resolved). This works the same wherever you request the object / dependency,
as a parameter in a #Provides annotated method (as your example)
as a field injection property (#Inject lateinit var)
as a parameter in another objects constructor
as a provision method in a component (fun getFoo() : Foo)
All of the following would work
// foo and bar can both be constructor injected
class Foo #Inject constructor()
class BarImpl #Inject constructor(val foo : Foo) : Bar
#Module
interface BarModule() {
#Binds // you should prefer Binds over Provides if you don't need initialization
// barImpl can be constructor injected, so it can be requested/bound to its interface here
fun bindBar(bar : BarImpl) : Bar
}
#Component(modules = BarModule::class)
interface BarComponent {
fun getBar() : Bar // returns barImpl due to binding
}
#Inject lateinit var bar : BarImpl // but we could as well use the implementation directly
#Inject lateinit var bar : Foo // or just foo
I recommend you to start with a small example, then compile the project and have a look at the generated code. If something is wrong you'll get errors immediately, while you can play around and try different setups!
An addition to David Medenjak anwser. If there is no interface and there is no requirement to group instances into modules, then module could be omitted completely:
class Model #Inject constructor(...){
//...
}
#Component
interface SomeComponent{
fun model(): Model
}
val model = someComponent.model()