How do i properly initialize Room (AndroidViewModel)? - android

I am using a Room Database with AndroidViewModel (without Factory)
class RoomModel(app: Application) : AndroidViewModel(app) {
// ....
}
and not sure about how to correctly initialize that and also, if I could use one-time initilization for whole app.
I have two ways to deal with initializations which seem to work ok:
Initializing in onViewCreated():
...
private lateinit var database: RoomModel
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
Initializing by activityViewModels():
...
private val mData: RoomModel by activityViewModels()
...
The both seem to work, but not sure which one would never lead to an app crash at some point.
Also, I would like to know, if it's a good practce using one shared reference variable of my RoomModel declared and initialized in a base Fragment that is used by other fragments like this:
class BaseFragment : Fragment() {
...
lateinit var database: RoomModel
// Or
val database: RoomModel by activityViewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
database = ViewModelProvider(this)[RoomModel::class.java]
...
}
}
Some other fragments extended by BaseFragment() like this:
class FragmentA : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference variable database to observe or add or edit data
}
}
and
class FragmentB : BaseFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// use the reference database to observe or add or edit data
}
}
and so on...
In this way I have only to init the Room once and use it without reinitializing in each fragmet I need to access to. Is that a goog idea or I rather need to use individual declaration and initialization of RoomModel in each Fragment I need access to?
Many thanks in advance.

Your Room Database instance should be a singleton. The instance should live as long as the application itself. It is costly to initialize it every time you need to use it. You should initialize Room Database in the application and each ViewModel that needs Room Database instance will get it from the application as a dependency. Or you can provide the Room Database as a dependency to your ViewModel, but then you need to create a factory for your ViewModel. You can also use a Dependency Injection library to manage it for you, but it can be overwhelming at first.
Without using any dependency injection library, you can see the example below.
Override the Application, and don't forget to declare it in the AndroidManifest.xml file.
AndroidApp.kt
class AndroidApp : Application() {
lateinit var myRoomDatabase: MyRoomDatabase
override fun onCreate() {
super.onCreate()
myRoomDatabase = Room
.databaseBuilder(applicationContext, MyRoomDatabase::class.java, "my-room-database")
.build()
}
}
AndroidManifest.xml
...
<application
android:name=".AndroidApp"
...
>
...
When you extends the AndroidViewModel you have the Application instance as a dependency. Cast it to AndroidApp and now you can get myRoomDatabase.
MainViewModel.kt
class MainViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentAViewModel.kt
class FragmentAViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}
FragmentBViewModel.kt
class FragmentBViewModel(app:Application) : AndroidViewModel(app) {
private val myRoomDatabase = (app as AndroidApp).myRoomDatabase
...
}

Related

How to optimize fragments without DI with single core fragment?

What I would like to do is to remove at each fragment such lines:
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SomeFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
all these lines can be placed in onCreateView or some other lifecycle methods. I have a lot of fragment, so as a result a lot of such code scopes. I think it is not logical to have similar code inside every file. At first I thought about DI, but as I got to know (maybe I'm wrong) that it is not possible to use DI with ViewModel where I have some constructor parameters. So, I started thinking about creating some single CoreFragment or something like that for having such scope and then make implementation of this CoreFragment inside all other fragment. Let see some example: we have such SampleFragment with lines which I would like to optimize:
class SomeSampleFrg : Fragment(R.layout.fragment_sample) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = FragmentSampleBinding.bind(view)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#SampleFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
....
}
...
}
then I created CoreFragment:
open class CoreFragment: Fragment() {
lateinit var viewModel: AppViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val retrofitService = RetrofitService.getInstance(requireContext())
val mainRepository = MainRepository(retrofitService)
viewModel = ViewModelProvider(
this#CoreFragment,
AppVMFactory(mainRepository)
)[AppViewModel::class.java]
}
}
and then the field viewModel will be accessible after implementation of this fragment class. Is it possible to do? Or it is very stupid question?)) The only problem which I can not even imagine how to do in addition is how to work with DataBinding in such situation) I mean that I will need to make some more fields and also pass layout to this CoreFragment. Maybe someone has any ideas how to do it?

Unable to inject dependency in fragment using hilt

I have a fragment class as follows:
class MainFragment : Fragment(R.layout.main_fragment) {
#Inject lateinit var runner: Runner
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
runner.runProcess()
}
}
Here is my Runner.java file:
public class Runner {
#Inject
public Runner(#ApplicationContext Context applicationContext) {
// some setup code
}
public runProcess() {...}
}
I am getting an error at the runner.runProcess() line saying that lateinit property runner is not initialized. I am using Hilt in my Android app. How can I go about fixing this?
MainActivity.kt:
#AndroidEntryPoint
class MainActivity #Inject constructor() : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MainFragment>(R.id.fragment_container_view);
}
}
}
}
Found the issue. I have to add the #AndroidEntryPoint annotation to my fragment as well, despite the fragment being added to the main activity which has this annotation.
For future reference, the #AndroidEntryPoint annotation should be provided for every android class which requires dependency injection. It marks the entry points for hilt to inject modules. It is not to be confused with the entry point of the app i.e., the launch activity.
Source

Koin's getStateViewModel is deprecated - what is the alternative?

I have the following implementation on my Fragment -
class HeroesDetailsFragment : Fragment() {
private val navArgs: HeroesDetailsFragmentArgs by navArgs()
private val heroesDetailsViewModel: HeroesDetailsViewModel by stateViewModel(state = { navArgs.toBundle() })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHeroDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListeners()
observeUiState()
observeUiAction()
}
}
My HeroesDetailsViewModel looks like this -
class HeroesDetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val heroesDetailsRepository: HeroesDetailsRepository
) : ViewModel() {
private fun getArgsModel() = HeroesDetailsFragmentArgs.fromSavedStateHandle(savedStateHandle)
init {
val navArgs = getArgsModel()
getAdditionalHeroDetails(navArgs.heroModel.id)
observeUiEvents()
}
}
And in my ViewModelModule I declare the following
val viewModelModule = module {
// ...
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
As you can see, I utilized the stateViewModel extension for Fragments that allows me to create a StateViewModel. The issue is that when trying to use the same functionality in Compose:
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = getStateViewModel() //provides deprecation error
) {
}
I get the following deprecation message -
getStateViewModel will be merged to sharedViewModel - no need anymore of state parameter
I did not find any good references on this topic, and it seems weird for me because the Fragment extension stateViewModel is completely fine and not deprecated so I am missing information on what should I do to replace it.
My goal is to inject a ViewModel with state parameters that will initialize the SavedStateHandle object. Currently I am using Koin DI, will switch in the future to Dagger-Hilt so it would be also a nice bonus to see the solution both in Koin and in Dagger-Hilt.
So I finally found a way to inject the ViewModel with dynamic information coming from the Fragment. I was looking at the old Fragment / Activity way which includes handling bundles, but in Compose it's much easier as we don't need to use the SavedStateHandle object because we can handle process death by the rememberSaveable { } block, which decouples the need to inject a ViewModel with dynamic information and the need to save information for process death.
This leaves the ViewModel to only ask for the relevant model and not bother handling process death. Just pure information.
class HeroesDetailsViewModel(
heroListModel : HeroesListModel,
private val heroesDetailsRepositoryImpl: HeroesDetailsRepositoryImpl
) : ViewModel() {
init {
getAdditionalHeroDetails(heroListModel.id)
observeUiEvents()
}
}
So I added a model that will be injected when needed via the parameters field -
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = koinViewModel(parameters = { ParametersHolder(mutableListOf(model)) })
) {
}
And in my DI module the implementation actually is left the same -
val viewModelModule = module {
viewModelOf(::HeroesViewModel)
viewModelOf(::HeroesListItemViewModel)
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
Hopefully this saves some time for other people in the future 💪🙂

How to use viewmodel + room with fragments?

I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
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")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.

Sharing one view model with multiple fragments

I am having some common logic which I currently have it in a Util class. Now, I want to move this logic to ViewModel class. As this util method is used in different fragments, is it a good practice to create a common view model (feature based view model) for multiple fragments. I know Google recommended to use 1 view model for 1 view. Please suggest.
If you've got common code, you could have several viewModels that inherit from a baseViewModel, which contains the shared code.
The advantage of this over a Util class is that the shared code is only visible to ViewModels that derive from the base, and can't get confused with anything else.
It's better to create a viewmodel per each fragment, but it is possible to create a single viewmodel for several fragments. According to the official documents:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}

Categories

Resources