I have a problem with New Architecture components in Kotlin, when I create ViewModel component in recomended way (in onCreate() method) the result is as suposed:
after activity orientation changes, I got the same instance of ViewModel as before
Here is the way i create this
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
val arrayMap = ArrayMap<Class<out ViewModel>, ViewModel>()
arrayMap.put(ListViewModel::class.java, ListViewModel(webApi, repoDao))
val factory = ViewModelFactory(arrayMap)
listViewModel = ViewModelProviders.of(this, factory).get(ListViewModel::class.java)
listViewModel.items.observe({ this.lifecycle }) {
Toast.makeText(this, it?.joinToString { it + " " } ?: "null", Toast.LENGTH_SHORT).show()
}
But when I have used Dagger for inject ListViewModel I got new instance of ListViewModel every time Activity was recreated. Here is a code of Dagger ListActivityModel.
#Module #ListActivityScopeclass ListActivityModule {
#Provides
#ListActivityScope
fun provideListViewModel(webApi: WebApi, repoDao: RepoDao, listActivity: ListActivity): ListViewModel {
val arrayMap = ArrayMap<Class<out ViewModel>, ViewModel>()
arrayMap.put(ListViewModel::class.java, ListViewModel(webApi, repoDao))
val factory = ViewModelFactory(arrayMap)
val result = ViewModelProviders.of(listActivity, factory).get(ListViewModel::class.java)
return result
}
}
Then ListActivity onCreate() method looks like:
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
listViewModel.items.observe({ this.lifecycle }) {
Toast.makeText(this, it?.joinToString { it + " " } ?: "null", Toast.LENGTH_SHORT).show()
}
}
And there is what I have notice after logging:
D/ListActivity: ---> onCreate() ListActivity: = [com.example.dom.app.new_arch.ListActivity#a0f2778]
D/ListActivity: ---> onCreate() listViewModel: = [com.example.dom.app.new_arch.ListViewModel#54a8e51]
//Activity orientation changes
E/ViewModelStores: Failed to save a ViewModel for com.example.dom.app.new_arch.ListActivity#a0f2778
D/ListActivity: ---> onCreate() ListActivity: = [com.example.dom.app.new_arch.ListActivity#6813433]
D/ListActivity: ---> onCreate() listViewModel: = [com.example.dom.app.new_arch.ListViewModel#55cf3f0]
The error I have received :
ViewModelStores: Failed to save a ViewModel for
comes from Android class HolderFragment with package android.arch.lifecycle.
There is something what I missed working with Dagger and new arch components?
The issue has to do with the order of dagger injection and activity creation. The view model implementation relies on a non-visual fragment for identity. By injecting the viewModelProvider before the activity has completed onCreate it is unable to complete this association.
Since super.onCreate does not likely depend on things you are injecting try injecting after the call to super.onCreate and you should be fine.
I had this exact same issue and solved it by this change in order.
Specifically from your code instead of:
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
go with:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_list)
JP
The way I do this is by only providing the ViewModelFactory using Dagger. Then it gets injected in the activity and you call ViewModelProviders.of(listActivity, factory).get(ListViewModel::class.java) from there. The reason your approach doesn't work is that AndroidInjection.inject() will create the ViewModel before onCreate, which leads to undefined behavior.
Also see: https://github.com/googlesamples/android-architecture-components/issues/202
I don't use AndroidInjection.inject() because it creates a new Dagger component. I create an Dagger Component in the Application class and I use that component instance to call inject in all other places of the app. This way your singletons are initialized only one time.
Related
I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}
I defined my own LayoutInflater.Factory2 class in a separate module. I want to inject it into each activity in my App, but the point is that I have to set this factory before activity's super.onCreate() method.
When I using Hilt it makes an injection right after super.onCreate(). So I have an UninitializedPropertyAccessException.
Is there any opportunity to have an injection before super.onCreate with Hilt?
Below is my example of module's di.
#Module
#InstallIn(SingletonComponent::class)
object DynamicThemeModule {
#FlowPreview
#Singleton
#Provides
fun provideDynamicThemeConfigurator(
repository: AttrRepository
): DynamicTheme<AttrInfo> {
return DynamicThemeConfigurator(repository)
}
}
You can inject the class before onCreate by using Entry Points like this.
#AndroidEntryPoint
class MainActivity: AppCompatActivity() {
#EntryPoint
#InstallIn(SingletonComponent::class)
interface DynamicThemeFactory {
fun getDynamicTheme() : DynamicTheme<AttrInfo>
}
override fun onCreate(savedInstanceState: Bundle?) {
val factory = EntryPointAccessors.fromApplication(this, DynamicThemeFactory::class.java)
val dynamicTheme = factory.getDynamicTheme()
super.onCreate(savedInstanceState)
}
}
If you need something like this a lot Id recommend creating an instance of it in the companion object of your Application class when your application starts (onCreate). That is before any of your views are created. So you don´t need to jump threw those hoops all the time, but can just access the instance that already exists. This code above won´t be available in attachBaseContext, when you need it there you have to create it in your application class I think.
Probably it has a simple solution that I cant see. I have a fragment with a ViewModel, The Viewmodel has a method inside of it that I want to call from my fragment and supply the arguments for. but when I try to call the method it shows an error "Unsolved Reference"
class DetailFragmentViewModel : ViewModel() {
private val repo = Crepository.get()
private val itemIdlivedata = MutableLiveData<UUID>()
var crimeLiveDate: LiveData<Crime?> = Transformations.switchMap(itemIdlivedata){ it ->
repo.getitem(it) }
fun LoadItem(itemuuid:UUID){
itemIdlivedata.value = itemuuid
}
}
Fragment Class:
private val crimeDetailVM : ViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
crimeDetailVM.LoadItem <- Unsolved Reference
}
Thanks for the help!
EDIT:IT HAS A SIMPLE SOLUTION, I DID NOT CAST THE VIEW MODEL TO THE VIEW MODEL CLASS,THANKS FOR THE HELP EVERYONE
You are doing downcasting DetailFragmentViewModel to ViewModel. That is why you are not accessing to DetailFragmentViewModel methods.
Use
private val crimeDetailVM : DetailFragmentViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
Instead of
private val crimeDetailVM : ViewModel by lazy {
ViewModelProvider(this).get(DetailFragmentViewModel::class.java)
}
Also this way is not idiomatic i suggest you to use kotlin extension
val viewModel by viewModels<DetailFragmentViewModel>()
But before do that you need to add the dependency which is Fragment KTX to your app gradle file.
https://developer.android.com/kotlin/ktx
You need activity context
try:
ViewModelProvider(requireActivity()).get(DetailFragmentViewModel::class.java)
you can use also extend view model by ActivityViewModel
eg.-> class DetailFragmentViewModel(application:Application) : AndroidViewModel(applivation){}
I have a single activity application.
My MainActivity is referenced in a number of dependency injection modules, as the implementer of these interfaces. I currently have a work around, which is less than ideal.
class MainActivity : TransaktActivity(), RegistrationNavigator, IAuthPresenter,
IAuthButtonNavigator {
override fun navigateAwayFromAuth() {
navController.navigate(R.id.homeFragment)
}
override fun navigateToAuthPin(buttonId: Int) {
//todo navigate to auth with pin fragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_mainActivity = this
setContentView(R.layout.activity_main)
}
companion object {
private var _mainActivity: MainActivity? = null
fun getInstance() = _mainActivity
}
}
interface RegistrationNavigator {
fun navigateToCardDetails()
fun navigateToOtpCapture()
fun navigateToLoading()
fun navigateOutOfCardRegistration()
}
The appModule is a Koin Module
val appModule = module {
viewModel { SharedViewModel() }
single { MainActivity.getInstance() as RegistrationNavigator }
}
What is the preferred way of achieving this?
Android-lifecycled components such as activities should not be in koin modules.
For example you will have issues with e.g. configuration changes since the koin module would be serving references to stale activity after the activity is recreated.
I haven't really worked with NavController but rather rolled up my own navigation solution. As a generic approach I would refactor the RegistrationNavigator implementation to a separate class the instance of which you can provide from your koin module. If lifecycle-dependent params such as Context (or NavController) are needed, supply them as function args.
NullPointerException in textview when I try to change text by dagger 2 instance object. Note: Using a common instance (new Myobjcet(this)) works.
Activity
class MainActivity : AppCompatActivity(), MyCallBack{
#Inject
lateinit var myObject: MyObject
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mytextview.text = "first change"
val myComponent = (application as App).myComponent
myComponent.inject(this)
}
override fun callBack(string: String) {
try {
mytextview.text = string
} catch (e: Exception) {
Log.d("xxx", e.toString())
}
}
}
Object
class MyObject(var res: MyCallBack) {
init {
res.callBack("second change")
}
}
Component
#Component(modules = MyModule.class)
public interface MyComponent {
void inject(MainActivity mainActivity);
}
Module
#Module
class MyModule {
#Provides
fun proviesMyObject(): MyObject = MyObject(MainActivity())
}
the problem stems from the fact that you're manually instantiating MainActivity when constructing MyObject in your module. because it is manually constructed, it isn't managed by the framework and is therefore not run through it's expected lifecycle (e.g. onCreate(), onStart(), onResume(), etc).
(side note - never do this in production code).
since onCreate() never runs for that manually constructed instance, the layout for that instance isn't inflated, so MyObject is referring to an Activity (as an implementation of MyCallBack) that has no awareness of any Views.
if you want to involve a valid, framework-managed instance of MyActivity in your object graph, one solution is to add it as a required constructor parameter to your module, like so:
#Module
class MyModule(private val myCallBack: MyCallBack) {
#Provides
fun providesMyObject(): MyObject = MyObject(myCallBack)
}
...then, in MainActivity construct the component and perform self-injection, like so:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
mytextview.text = "first change"
DaggerMyComponent.builder()
.myModule(MyModule(this))
.build()
.inject(this)
}
hope that helps clear things up!
The problem is that in your Module, you are instantiating a new MainActivity that is different from the one instantiated by the framework. You need to realize that if you manually instantiated an activity, its views will not be inflated, hence the NullPointerException. I suggest you pass the instance of your activity in your module instead.
#Module
class MyModule(val res: MyCallBack) {
#Provides
fun providesMyObject(): MyObject = MyObject(res)
}
Instantiate your component in MainActivity like
val myComponent = DaggerMyComponent.builder()
.myModule(MyModule(this))
.build()
myComponent.inject(this)