Android ViewModel - "by activityViewModels" called before "by viewModels" - android

After some time away from android development I'm trying to start again with a simple project.
I've created a new project picking the "basic activity" option which resulted in a MainActivity and two fragments. Starting from this, since the main functionality requires a database, I've followed the "Room with a view" codelab, which however has a single activity. In my project I set an observer in the activity and all worked fine but, as soon as I moved the observer in the first fragment and "retrieved" the ViewModel with "by activityViewModels", the app started throwing an Instantiation exception. Reason: MyViewModel has no zero argument constructor.
After some debugging, I've noticed that the "by activityViewModel" property in the fragment is called before the "by viewModel" in the activity.
The ViewModel has a factory and I would like it scoped to the activity and later would be accessed from the second fragment.
ViewModel:
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class MyViewModel(private val repository: MyRepository): ViewModel() {
val list: LiveData<List<Item>> = repository.allItems.asLiveData()
}
Activity
...more imports
import androidx.activity.viewModels
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
val myViewModel: MyViewModel by viewModels {
MyViewModelFactory((application as MyApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
myViewModel.list.observe(this) { list ->
print(list.size)
}
}
}
Fragment
...more imports
import androidx.fragment.app.activityViewModels
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
val sharedViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.list.observe(viewLifecycleOwner) { list ->
print(list.size)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Dependencies
def room_version = "2.4.2"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
// Room components
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//same result enabling these dependencies
//implementation 'androidx.activity:activity-ktx:1.4.0'
//implementation 'androidx.fragment:fragment-ktx:1.4.1'
//implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
For what I understand, the "by viewModels { //factory method }" property in the activity should instantiate the viewModel using the factory, then the ":viewModelType by activityViewModel" property in the fragment (which has no factory option) retrieve a ViewModel of the defined type, if already instantiated by the parent activity.
If I have understood correctly, why "by activityViewModels" is called before "by viewModels"? Shouldn't be the other way around? How can I fix it?

You should modify your viewmodel, activity and fragment.
First, for your ViewModel, the ViewModelProvider.Factory is deprecated, so use this instead :
class MyViewModel(application: Application): AndroidViewModel(application) {
private val repository by lazy { MyRepository.newInstance(application) }
val list: LiveData<List<Item>> = repository.allItems.asLiveData()
}
Then, in your activity class:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
val myViewModel by viewModels<MyViewModel>()
And for your fragment:
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
val sharedViewModel by activityViewModels<MyViewModel>()

Related

Activity-Fragment Communication w/ Hilt

I have an app with a single activity but with many Fragments. I am using ViewModel for my Activity-Fragment communication. Lately, I am using Hilt, and I am having a problem now communicating between my activity and fragments.
My Viewmodel
#HiltViewModel
class AppViewModel #Inject internal constructor(
): ViewModel() {
private var _data = MutableLiveData<String>()
val data: LiveData<String>
get() = _data
fun insertData(dataStr: String) {
_data.value = dataStr
}
}
My MainActivity
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mViewModel: AppViewModel by viewModels()
private var dataString: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel.data.observe(this, {
dataString = it
})
}
}
One of my Fragments
#AndroidEntryPoint
class ReportFragment : Fragment() {
private val reportViewModel: ReportViewModel by viewModels()
private val appViewModel: AppViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
...
appViewModel.insertData("Hello")
...
}
}
When I run the app, I am getting null as a result of data. Any solution to solve this?
Not sure if this is the exact issue, but you get the ViewModel inside your fragment using by activityViewModels<AppViewModel> and not by viewModels
EDIT:
Also, I just noticed you are using an internal constructor. Try using only inject constructor once and let me know if it fixed it for you :)

Cannot create an instance of class ViewModel class

I am trying to follow MVVM pattern in my Android app but getting error while creating an instance of ViewModel.
Error: Cannot create an instance of class DemoViewModel class.
Here is my code:
DemoFragment.kt:
class DemoFragment : Fragment(R.layout.fragment_demo) {
lateinit var mViewModel: DemoViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel=ViewModelProvider(this).get(DemoViewModel::class.java)
mViewModel.getSomeData()
}
}
DemoViewModel.kt:
class DemoViewModel(val demoRepository: DemoRepository) : ViewModel() {
fun getSomeData() {
Log.d("DemoViewModel", "${demoRepository.getData()}")
}
}
DemoRepository.kt:
interface DemoRepository {
fun getData(): Boolean
}
class DemoImpl : DemoRepository {
override fun getData() = false
}
You need to use ViewModelFactory. Because there is "demoRepository" in your primary builder.
class DemoViewModelFactory constructor(private val repository:DemoImpl): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(DemoViewModel::class.java!!)) {
DemoViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Usage
viewModel = ViewModelProvider(this, DemoViewModelFactory(repositoryObject)).get(DemoViewModel::class.java)
I would encourage you to use "by viewModels()" extension function to create viewModel instance easily. Note that you should add following dependency to use it:
implementation 'androidx.fragment:fragment-ktx:1.2.5'
Sample Fragment Implementation:
class DemoFragment : Fragment() {
// Use the 'by ViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: DemoViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Then you can inject an instance of your repository via constructor injection by Dagger or Hilt etc.

Cannot get the same instance of a scoped component - Dagger 2 Clean architecture

I'm using Dagger 2 in clean architecture project, I have 2 fragments. These 2 fragments should be scoped together to share the same instances, but unfortunately, I got empty object in the second fragment.
Application Component
#ApplicationScope
#Component(modules = [ContextModule::class, RetrofitModule::class])
interface ApplicationComponent {
fun exposeRetrofit(): Retrofit
fun exposeContext(): Context
}
Data layer - Repository
class MoviesParsableImpl #Inject constructor(var moviesLocalResult: MoviesLocalResult): MoviesParsable {
private val TAG = javaClass.simpleName
private val fileUtils = FileUtils()
override fun parseMovies() {
Log.d(TAG,"current thread is ".plus(Thread.currentThread().name))
val gson = Gson()
val fileName = "movies.json"
val jsonAsString = MyApplication.appContext.assets.open(fileName).bufferedReader().use{
it.readText()
}
val listType: Type = object : TypeToken<MoviesLocalResult>() {}.type
moviesLocalResult = gson.fromJson(jsonAsString,listType)
Log.d(TAG,"result size ".plus(moviesLocalResult.movies?.size))
}
override fun getParsedMovies(): Results<MoviesLocalResult> {
return Results.Success(moviesLocalResult)
}
}
Repo Module
#Module
interface RepoModule {
#DataComponentScope
#Binds
fun bindsMoviesParsable(moviesParsableImpl: MoviesParsableImpl): MoviesParsable
}
MoviesLocalResultsModule(the result need its instance across different fragments)
#Module
class MoviesLocalResultModule {
#DataComponentScope
#Provides
fun provideMovieLocalResults(): MoviesLocalResult{
return MoviesLocalResult()
}
}
Use case
class AllMoviesUseCase #Inject constructor(private val moviesParsable: MoviesParsable){
fun parseMovies(){
moviesParsable.parseMovies()
}
fun getMovies(): Results<MoviesLocalResult> {
return moviesParsable.getParsedMovies()
}
}
Presentation Component
#PresentationScope
#Component(modules = [ViewModelFactoryModule::class],dependencies = [DataComponent::class])
interface PresentationComponent {
fun exposeViewModel(): ViewModelFactory
}
First ViewModel, where I got the result to be shared with the other fragment when needed
class AllMoviesViewModel #Inject constructor(private val useCase: AllMoviesUseCase):ViewModel() {
private val moviesMutableLiveData = MutableLiveData<Results<MoviesLocalResult>>()
init {
moviesMutableLiveData.postValue(Results.Loading())
}
fun parseJson(){
viewModelScope.launch(Dispatchers.Default){
useCase.parseMovies()
moviesMutableLiveData.postValue(useCase.getMovies())
}
}
fun readMovies(): LiveData<Results<MoviesLocalResult>> {
return moviesMutableLiveData
}
}
Second ViewModel where no need to request data again as it's expected to be scoped
class MovieDetailsViewModel #Inject constructor(private val useCase: AllMoviesUseCase): ViewModel() {
var readMovies = liveData(Dispatchers.IO){
emit(Results.Loading())
val result = useCase.getMovies()
emit(result)
}
}
First Fragment, where data should be requested:
class AllMoviesFragment : Fragment() {
private val TAG = javaClass.simpleName
private lateinit var viewModel: AllMoviesViewModel
private lateinit var adapter: AllMoviesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var ascendingOrder = true
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build()
)
.build()
).build()inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(AllMoviesViewModel::class.java)
startMoviesParsing()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_all_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupRecyclerView()
viewModel.readMovies().observe(viewLifecycleOwner, Observer {
if (it != null) {
when (it) {
is Loading -> {
showResults(false)
}
is Success -> {
showResults(true)
Log.d(TAG, "Data observed ".plus(it.data))
addMoviesList(it.data)
}
is Error -> {
moviesList.snack(getString(R.string.error_fetch_movies))
}
}
}
})
}
Second Fragment, where I expect to get the same instance request in First Fragment as they are scoped.
class MovieDetailsFragment: Fragment() {
val TAG = javaClass.simpleName
#Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: MovieDetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val depend = DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build())
.build()
).build()
depend.inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(MovieDetailsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel.readMovies.observe(this, Observer {
if (it!=null){
Log.d(TAG,"Movies returned successfully")
}
})
return super.onCreateView(inflater, container, savedInstanceState)
}
}
Scopes tell a component to cache the results of a binding. It has nothing to do with caching instances of any components. As such, you are always creating a new DataComponent, PresentationComponent, and AllMoviesComponent in your fragments' onCreate methods.
In order to reuse the same AllMoviesComponent instance, you need to store it somewhere. Where you store it can depend on your app architecture, but some options include MyApplication itself, the hosting Activity, or in your navigation graph somehow.
Even after fixing this, you can't guarantee that parseMovies has already been called. The Android system could kill your app at any time, including when MoviesDetailFragment is the current fragment. If that happens and the user navigates back to your app later, any active fragments will be recreated, and you'll still get null.

how to initialize viewmodel in activity using dagger?

I'm dev-ing activity using dagger. In my fragment, I can use this code as below. but when I use this code in activity, I cannot use this code.
private val viewModel by viewModels<NoticeViewModel> { viewModelFactory }
As result I can't initialize viewmodel. how can I initialize activity using dagger?
fragment
class NoticeFragment : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<NoticeViewModel> { viewModelFactory }
private lateinit var viewDataBinding: FragmentNoticeBinding
private var notice = ""
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_notice, container, false)
viewDataBinding = FragmentNoticeBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
init()
viewModel.getNotice()
}
private fun init(){
viewModel.notice.observe(this, Observer{
noticeMain.text = it
})
}
}
activity
class ScheduleDialog : DaggerActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<ScheduleDialogViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_schedule_dialog)
//viewDataBinding = DataBindingUtil.setContentView( this,R.layout.activity_schedule_dialog)
viewModel.getScheduleById(5)
}
}
MyComponent component = DaggerMyComponent.builder().build();
component.inject(this);
For dependency injection you should have the following code, also in the component code should be
void inject(YourActivity activity);

how to inject View model using koin? (for specific use case)

I am having a shared View Model for activity and it's fragment.
My view model need's argument to be passed when instantiating from the activity(onCreate only once)
viewModel =ViewModelProviders.of(this,
NoteViewModelFactory(application!!,
uid = intent!!.getStringExtra("uid")!!))
.get(NoteViewModel::class.java)
But from fragment i don't need to pass the argument as i am sure the i have the argument's passed once.
viewModel = ViewModelProviders.of(activity!!).get(NoteViewModel::class.java)
In Koin i tried doing below.
val noteModule = module(override = true) {
viewModel { (id: String) -> NoteViewModel(androidApplication(), id) }
}
in Activity:
private val viewModel: NoteViewModel by viewModel { parametersOf(intent!!.getStringExtra("uid")!!) }
in Fragment:
private val viewModel: NoteViewModel by sharedViewModel()
Application Crashed with below error:
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.andor.navigate.notepad/com.andor.navigate.notepad.listing.NotesActivity}:
org.koin.core.error.InstanceCreationException: Could not create
instance for
[type:Factory,primary_type:'com.andor.navigate.notepad.core.NoteViewModel']
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: org.koin.core.error.InstanceCreationException: Could not create instance for
[type:Factory,primary_type:'com.andor.navigate.notepad.core.NoteViewModel']
at org.koin.core.instance.DefinitionInstance.create(DefinitionInstance.kt:61)
at org.koin.core.instance.FactoryDefinitionInstance.get(FactoryDefinitionInstance.kt:37)
at org.koin.core.definition.BeanDefinition.resolveInstance(BeanDefinition.kt:70)
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:165)
I am not able to understand how to solve this using KOIN.
P.S:i am new to koin DI.
Is there anything wrong when you init koin in application class? I tried your code without any issuses. I'm using koin version 2.0.1
class App : Application() {
override fun onCreate() {
super.onCreate()
val noteModule = module(override = true) {
viewModel { (id: String) -> NoteViewModel(androidApplication(), id) }
}
startKoin {
androidContext(this#App)
modules(
noteModule
)
}
}
}
Activity and fragment:
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModel { parametersOf(intent!!.getStringExtra("uid")) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("NoteViewModel", "id: ${viewModel.id}")
supportFragmentManager.beginTransaction().replace(R.id.main_root, Frag()).commit()
}
}
class Frag : Fragment() {
private val viewModel: NoteViewModel by sharedViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("NoteViewModel", "id: ${viewModel.id}")
return inflater.inflate(R.layout.activity_main, container, false)
}
}
View model class:
class NoteViewModel (application: Application, val id: String) : AndroidViewModel(application)
You shouldn't pass these type of arguments in ViewModel constructor. Instead what you can do is on your Activity's onCreate(), you set that passed value to the ViewModel. So when you will access that ViewModel in your fragment, you'll surely have that value already set.
class NoteViewModel (application: Application) : AndroidViewModel(application)
{
var id:String = ""
}
Your koin Module:
val noteModule = module(override = true) {
viewModel { NoteViewModel(androidApplication()) }
}
Activity:
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.id = intent?.getStringExtra("uid")?: ""
supportFragmentManager.beginTransaction().replace(R.id.container, MyFrag()).commit()
}
}
Fragment:
class MyFrag : Fragment() {
private val viewModel: NoteViewModel by sharedViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// your value will be available here.
return inflater.inflate(R.layout.activity_main, container, false)
}
}

Categories

Resources