I have a MainActivity in which I am using a few Fragments using Navigation components, but my app is crashing lateinit var viewModel is not initialized error, i am not able to figure out why my viewmodel is not intilized
Error
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property viewModel has not been initialized
at com.choudhary.newsappmvvm.ui.MainActivity.getViewModel(MainActivity.kt:18)
at com.choudhary.newsappmvvm.ui.fragments.BreakingNewsFragment.onViewCreated(BreakingNewsFragment.kt:27)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3138)
My MainActivity
class MainActivity : AppCompatActivity() {
lateinit var viewModel: NewsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val repository = NewsRepository(ArticleDataBase(this))
val viewModelFactory = NewsViewModelFactory(repository)
viewModel = ViewModelProvider(this,viewModelFactory).get(NewsViewModel::class.java)
bottomNavigationView.setupWithNavController(newsNavHostFragment.findNavController())
}
My viewModelFactory Class
class NewsViewModelFactory(private val repository: NewsRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NewsViewModel(repository) as T
}
}
inside Fragment i using like this
class BreakingNewsFragment : Fragment(R.layout.fragment_breaking_news) {
lateinit var viewModel : NewsViewModel
lateinit var newsAdapter : NewsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel =(activity as MainActivity).viewModel
setUpRecylerView()
viewModel.breakingNews.observe(viewLifecycleOwner, Observer { response->
when(response){
is Resource.Success ->{
hideProgress()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles)
}
}
is Resource.Error ->{
hideProgress()
response.messsage?.let {
Log.d("somnath", it)
}
}
is Resource.Loading ->{
showProgress()
}
}
})
}
My dependepencies
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
// Room
implementation "androidx.room:room-runtime:2.2.5"
annotationProcessor "androidx.room:room-compiler:2.2.5"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.2.5"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
// Navigation Components
implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
My NewsRepository
class NewsRepository(
val db : ArticleDataBase
) {
suspend fun getBreakingNews(countryCode : String,pageNum : Int) =
RetrofitInstance.api.getBreakingNews(countryCode, pageNumber = pageNum)
}
My Database class for room
@Database(
entities = [Article::class],
version = 1
)
@TypeConverters(Converters::class)
abstract class ArticleDataBase : RoomDatabase() {
abstract fun getArticlesDao(): ArticleDao
companion object{
@Volatile
private var INSTANCE : ArticleDataBase ? = null
private val LOCK = Any()
operator fun invoke(context : Context) = INSTANCE ?: synchronized(LOCK) {
INSTANCE ?: createDatabase(context).also{ INSTANCE = it}
}
private fun createDatabase(context: Context ) =
Room.databaseBuilder(
context.applicationContext,
ArticleDataBase::class.java,
"article_db.db"
).build()
}
}
CodePudding user response:
You're relying on your Activity
's onCreate
running before your Fragment
's onViewCreated
does, and that's not always the case. That's why this:
viewModel = (activity as MainActivity).viewModel
is accessing your lateinit
viewModel
property before it's been initialised.
You shouldn't really have Fragment
s accessing ViewModel
s through the Activity
itself, because of issues like this. They have separate lifecycles, and it can be tricky to juggle them and ensure things like this. That's part of why the ViewModel
library was created, so you don't need to worry about it because the complexity is handled for you!
So your Fragment
should also be using ViewModelProvider
to grab the VM. Since you want the same copy the Activity
holds, just pass the Activity
to the constructor, so it fetches the VM associated with that
// in the Fragment
lateinit var viewModel: NewsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val repository = NewsRepository(ArticleDataBase(requireContext()))
val viewModelFactory = NewsViewModelFactory(repository)
viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(NewsViewModel::class.java)
This is using requireActivity()
and requireContext()
because you have access to those by the time onCreate
runs. The Fragment
is attached to the Activity
, which has been instantiated, but isn't necessarily in the CREATED
state yet, which is the whole problem here. So you can pass the Activity
to the ViewModelProvider
to fetch its instance of the VM, creating one for it if necessary
Also if this Fragment
is the only thing using this NewsViewModel
, and it's not shared, then the Activity
doesn't need to be involved at all. So all this code could be in the Fragment
instead of duplicated in both (and passing the Fragment
to ViewModelProvider
instead)