I have a viewModel like:
abstract class MyViewModel : ViewModel() {
abstract fun onTextUpdated(text: String)
abstract val liveData: LiveData<String>
}
Which has 2 implementations like:
class MyViewModelOne : MyViewModel() {
override fun onTextUpdated(text: String) { // Do stuff }
override val liveData = MutableLiveData<String>()
}
class MyViewModelTwo : MyViewModel() {
override fun onTextUpdated(text: String) { // Do stuff }
override val liveData = MutableLiveData<String>()
}
Which are each used in their own seperate fragments, let's call them FragmentOne and FragmentTwo for the sake of demonstration.
FragmentOne and FragmentTwo both share the requirement to ask the user to enter some text via a dialog added to the host activity when the user taps a "Add Note" button. Let's call this dialog FragmentThree for the sake of demonstration. The result of this dialog (a string) should be shared with the parent fragment via the onTextUpdated function on either FragmentOne or FragmentTwo respectively, depending upon which route the user took through the app, when the dialog is dismissed.
Obviously, I only want to implement the FragmentThree dialog once, as it will look the same and have the same behaviour for both the FragmentOne and FragmentTwo use cases.
Therefore, I decided in the FragmentThree implementation to make use of Koins by sharedViewModel delegate, to share the viewModel injected in with the parent scope (the activity level). Obviously, because I don't know which implementation of MyViewModel will be there, because either FragmentOne or FragmentTwo could have lead to this point, I am referencing the base abstract class MyViewModel instead, assuming that Koin will be able to resolve this at run time.
However, when I run the code, the app crashes with:
2022-04-12 09:51:52.422 3607-3607/com.example.test E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.test, PID: 3607
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: org.koin.core.error.NoBeanDefFoundException: |- No definition found for class:'com.example.test.MyViewModel'. Check your definitions!
In my Koin module, where I am creating the definitions of what interfaces/abstract classes map to which implementations, I have correctly setup definitions for both of the implementations of MyViewModel, and the FragmentOne and FragmentTwo code which uses this view model via the by sharedViewModel delegate works fine as I also stipulate a qualifier for each of them, in both the module definitions and the fragments themselves, like so:
val appModule = module {
viewModel<MyViewModel>(
named("myViewModelOne")
) {
MyViewModelOne()
}
viewModel<MyViewModel>(
named("myViewModelTwo")
) {
MyViewModelTwo()
}
}
And then, in FragmentOne and FragmentTwo where the implementations are used, similarly:
class FragmentOne : Fragment() {
private val viewModel by sharedViewModel<MyViewModel>(
named("myViewModelOne")
)
...
}
class FragmentTwo : Fragment() {
private val viewModel by sharedViewModel<MyViewModel>(
named("myViewModelTwo")
)
...
}
And finally, in FragmentThree where I am wanting Koin to determine the implementation dynamically based upon the parent fragments view model implementation:
class FragmentThree : DialogFragment() {
private val viewModel by sharedViewModel<MyViewModel>()
...
}
So, my question is; is there someway I can share the view model from either FragmentOne or FragmentTwo via referencing it's base class in the by sharedViewModel delegate in FragmentThree without also qualifying which implementation to use (because I want it to use whichever implementation is currently active on it's parent fragment).
Related
I'm trying to inject some dependency to both activity and fragment using Koin and I expect it to live as long as activity lives, but it turned out a headache for me.
I managed to create a module that resolves MainRouter, inject it into an activity, but it doesn't work for a fragment.
val appModule = module {
scope<MainActivity> {
scoped { MainRouter() }
}
}
MainActivity extends ScopeActivity, MyFragment extends ScopeFragment.
in MainActivity private val router : MainRouter by inject() works fine, but in MyFragment it throws org.koin.core.error.NoBeanDefFoundException: No definition found for class:'com.example.app.MainRouter'. Check your definitions!
Finally I managed to inject, but it doesn't look pretty
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scopeId = scopeActivity!!.getScopeId()
scope.linkTo(getKoin().getScope(scopeId))
mainRouter = get()
...
I also don't like that scopeActivity can't be accessed in the init method. Does this mean that activity scoped dependencies cannot be resolved in fragment using by inject()?
As I can see in your code, you have to declare a Fragment instance, just declare it as a fragment in your Koin module and use constructor injection. Like below:
val appModule = module {
single { MyService() }
fragment { MyFragment(get()) }
}
Please refer link for more details.
I have a Fragment flow scoped with a navigation graph and want to scope each Fragment's ViewModel accordingly. However, I don't want each of the Fragments to have access to all methods and variables in the ViewModel, therefore each Fragment's ViewModel is an interface implemented in the base ViewModel.
I am using by navGraphViewModels() delegation in each of the Fragments but it seems to be unable to cast the interface to the base class.
The trace error is:
java.lang.ClassCastException: java.lang.Object cannot be cast to
androidx.lifecycle.ViewModel
Any advice on how to approach this problem??
In my Fragment it is defined as follows:
#AndroidEntryPoint
class ExampleFragment : Fragment() {
private val viewModel: ExampleViewModelController by
navGraphViewModels(R.id.nav_graph_example){defaultViewModelProviderFactory}
///
And the ViewModel is defined by:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val handle: SavedStateHandle,
private val useCases: ExampleUseCases,
) : ViewModel(), ExampleViewModelController {
override fun validateExampleInputs() {
// TODO("Not yet implemented")
}
}
And lastly, the interface:
interface ExampleViewModelController {
fun validateExampleInputs()
}
The ClassCastException happens because there's no type parameter passed to the delegate like by navGraphViewModels<ExampleViewModel>(). Thus, the delegate is wrongly trying to create a new instance of the interface ExampleViewModelController instead of ExampleViewModel.
in my project I am using Dagger2 to inject ViewModels into fragments.
override val viewModel: AllStockListTabViewModel by viewModels ({this}, {viewModelFactory})
To briefly explain my situation, I have a fragment that uses fragment state adapter which contains two fragments. For convinience, I'll call the parent fragment fragment A and child fragments in fragment state adapter fragment B and fragment C.
Typically, when testing the app user spends time in fragment B that contains a recyclerview. When user taps one of the items, it leads to a different fragment with some detailed information. When user enters that detail fragment, fragment B holding that item goes through onPause() and onStop(). At the same time, onStop() is called in fragment C.
The point is, if user spends enough time in fragment B (contained by fragment A), fragment C is destroyed and this is not by surprise because I know that is intended by fragment state adapter. It is supposed to get rid of some fragments when not visible.
My problem is that when fragment C gets destroyed, viewmodel associated with it does not get destroyed. This is bad because now when user goes to fragment C, which still has reference to old viewmodel, app doesn't supply any data to the fragment because when onDestroy() is called, viewmodel of fragment C is cleared and thus viewmodelscope.launch is not working.
I also thought of not using viewmodelscope (use coroutinescope instead) but that is not the issue. What I am curious and eager to know is why viewmodel of fragment C, scoped to lifecycle of fragment C is not destroyed.(I want to get rid of old viewmodel at the demise of fragment C and get new viewmodel instance)
Please understand my clumsy wording and my lack of knowledge that might give out some confusion. I am new to dagger. Please see my code below for better understanding.
AppComponent.kt
#Singleton
#Component(
modules = [
AndroidSupportInjectionModule::class,
ActivityBindingModule::class,
RepositoryModule::class,
DataSourceModule::class,
ServiceModule::class,
DaoModule::class,
ViewModelModule::class,
]
)
ViewModelModule.kt
#MapKey
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(AllStockListTabViewModel::class)
abstract fun bindAllStockListTabViewModel(allStockListTabViewModel: AllStockListTabViewModel): ViewModel
}
ViewModelFactory
#Singleton
class ViewModelFactory #Inject constructor(
private val viewModelMap: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return viewModelMap[modelClass]?.get() as T
}
}
Fragment
class AllStockListTabFragment #Inject constructor() :
ViewModelFragment<FragmentAllStockListBinding>(R.layout.fragment_all_stock_list) {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override val viewModel: AllStockListTabViewModel by viewModels ({this}, {viewModelFactory})
}
Adapter
tradingTabAdapter = TradingTabAdapter(
this.childFragmentManager,
this.lifecycle,
tradingStateTabFragment,
allStockListTabFragment
)
class TradingTabAdapter #Inject constructor(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val tradingStateTabFragment: TradingStateTabFragment,
private val allStockListTabFragment: AllStockListTabFragment
) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun createFragment(position: Int): Fragment =
when (position) {
0 -> tradingStateTabFragment
else -> allStockListTabFragment
}
override fun getItemCount(): Int = 2
}
SubComponent
#FragmentScope
#Subcomponent(
modules = [
TradingTabBindingModule::class,
TradingTabModule::class,
EventModule::class,
UseCaseModule::class
]
)
AdapterModule
#Module
class TradingTabModule {
#Provides
fun provideTradingTabAdapter(
fragment: TradingTabFragment,
allStockListTabFragment: AllStockListTabFragment,
tradingStateTabFragment: TradingStateTabFragment
) = TradingTabAdapter(
fragment.childFragmentManager,
fragment.lifecycle,
tradingStateTabFragment,
allStockListTabFragment
)
I found that create method of ViewModelFactory is not called when fragment C is destroyed and created again. I think this is because I am using lazy initialization of viewmodel and that is how ViewModelLazy works. It caches viewmodel and invokes factory's create method only when cache is null. I guess what's happening is old viewmodel of fragment C is still referencing the dead viewmodel(which survived viewModelStore.onclear). I put a log statement in the init block of viewmodel of fragment C and I can see that it is called only for the very frist time fragment C is created and never called again even when fragment C is destroyed and created again.
Thank you so much for your patience reading all this haha. So I need help from any expereienced Android gurus who might be able to give some insight.
My goal: make viewmodel destroyed and recreated with the lifecycle of fragment. I want to avoid memory leak due to unused zombie viewmodels.
Current situation: viewmodel never gets destroyed and reborn fragment still references old viewmodel and thus lazy initialisation keeps the cache of old viewmodel, not triggering create method of ViewModelFactory.
--Edit--
version of dagger im using
"com.google.dagger:dagger-android:2.37"
Since your ViewModel is tied to your Activity, it is not getting destroyed when Fragment is destroyed.
#ViewModelKey(MainActivityViewModel::class)
abstract fun bindMainActivityViewModel(mainActivityViewModel: MainActivityViewModel): ViewModel
You can check this answer which explains How to use ViewModel with Fragment.
How to use ViewModel in a fragment?
I'm trying to implement Hilt on an Android App, while It's pretty easy to implement and removes a lot of the boilerplate code when comparing with Dagger, there are some things I miss, Like building my own components and scoping them myself so i'll have my own hirerchy.
To the point: Example: let's say I have a simple App with a RecyclerView, Adapter, Acitivity, and a Callback nested in my Adapter that I pass into my Adapter constructor in order to detect clicks or whatever, and I'm letting my activity implement that Callback, and of course I want to inject the adapter.
class #Inject constructor (callBack: Callback): RecyclerView.Adapter...
When I let Hilt know that I want to inject my adapter I need to let Hilt know how to provide all the Adapter dependencies - the Callback.
In Dagger I was able to achieve this by just binding the Activity to the Callback in one of my modules:
#Binds fun bindCallback(activity: MyActivity): Adapter.Callback
Dagger knew how to bind the Activity(or any Activity/Fragment) and then it was linked to that Callback, but with Hilt it does'nt work.
How can I achieve this? How can I provide or Bind Activity or Fragment with Hilt?
The solution is quite simple.
So a few days ago I came back to look at my question only to see that there was still no new solution, so I tried Bartek solution and wasn't able to make it work, and even if it did work, the clean Hilt code was becoming too messy, so I did a little investigation and played a little and discovered that the solution is actually stupidly easy.
It goes like this:
App:
#HiltAndroidApp
class MyApp: Application()
Activity: (implements callback)
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), SomeClass.Callback {
#Inject
lateinit var someClass: SomeClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onWhatEver() {
// implement
}
}
SomeClass: (with inner callback)
class SomeClass #Inject constructor(
private val callback: Callback
) {
fun activateCallback(){
callback.onWhatEver()
}
interface Callback{
fun onWhatEver()
}
}
SomeModule: (providing/binding the activity to the callback)
#Module
#InstallIn(ActivityComponent::class)
object SomeModule{
#Provides
fun provideCallback(activity: Activity) =
activity as SomeClass.Callback
}
And that's all we need.
We cannot bind the activity to the callback with #Bind because it needs to be explicitly provided and cast to the callback so that the app can build.
The module is installed in ActivityComponent and is aware of a generic 'activity', if we cast it to the callback, Hilt is content and the activity is bound to the callback, and Hilt will know how to provide the callback as long as its in the specific activity scope.
Multiple Activities/Fragments
App:
#HiltAndroidApp
class MyApp: Application()
BooksActivity:
#AndroidEntryPoint
class BooksActivity : AppCompatActivity(), BooksAdapter.Callback{
#Inject
lateinit var adapter: BooksAdapter
...
override fun onItemClicked(book: Book) {...}
}
}
AuthorsActivity:
#AndroidEntryPoint
class AuthorsActivity : AppCompatActivity(), AuthorsAdapter.Callback{
#Inject
lateinit var adapter: AuthorsAdapter
...
override fun onItemClicked(author: Author) {...}
}
BooksAdapter
class BooksAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(book: Book)
}
}
AuthorsAdapter
class AuthorsAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(auhtor: Auhtor)
}
}
AuhtorsModule
#Module
#InstallIn(ActivityComponent::class)
object AuthorsModule {
#Provides
fun provideCallback(activity: Activity) =
activity as AuthorsAdapter.Callback
}
BooksModule
#Module
#InstallIn(ActivityComponent::class)
object BooksModule {
#Provides
fun provideCallback(activity: Activity) =
activity as BooksAdapter.Callback
}
The Modules can be joined to one module with no problem, just change the names of the functions.
This is offcourse applicable for more activities and/or multiple fragments.. for all logical cases.
Hilt can provide an instance of the generic Activity (and Fragment for that matter) as a dependency within the ActivityComponent (and FragmentComponent respectively). It's just not able to provide an instance of your specific MyActivity.
You can still create your own Component in Hilt. You will just have to manage the component instance on your own. Add the MyActivity as the seed data for the component builder and you should be able to #Bind your Callback with no problem.
I have the following LoginFragment that uses Dagger to inject its fields:
class LoginFragment : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(LoginViewModel::class.java)
}
I also have a corresponding test that mocks the LoginViewModel according to the documentation from Google: "You can create the fragment and provide it a mock ViewModel."
#MediumTest
#RunWith(AndroidJUnit4::class)
class LoginFragmentTest {
#Mock
private lateinit var viewModel: LoginViewModel
#Before
fun setUp() {
loginFragment = LoginFragment()
loginFragment.viewModelFactory = createMockViewModelFactory(viewModel)
activityRule.activity.setFragment(loginFragment)
}
}
The problem is that when the onAttached method of the fragment is invoked, Dagger overrides the viewModelFactory with its own object, thus replacing my mock.
How can I prevent Dagger from overriding my mock object?
In the android-architecture-components samples on Github Google have an interesting solution.
They inject the activities trough ActivityLifecycleCallbacks. For instrumented tests they use a TestApp that does not register ActivityLifecycleCallbacks so it injects nothing.
Just like in your example the ViewModel.Factory is package private so in the test you can assign it yourself.
For Fragments there is FragmentManager.FragmentLifecycleCallbacks class which can be used. Instead of the Fragment injecting itself in onActivityCreated your production activity injects the Fragment using FragmentLifecycleCallbacks. You can create a testing activity which does not inject the fragment and you can create a mock factory yourself.