Android ViewModel reference Fragment - android

I'm a beginner android dev and am looking to write a fully jetpack app. I'm wondering the best way to communicate with the fragment from the ViewModel to say, show a dialog, or navigate to another fragment.
I had something like this:
class Foo: ViewModel() {
interface Callbacks {
fun doSomethingWithUI()
fun showBaz()
}
var callbacks: Callbacks? = null
}
And then in the fragments onCreate it would set itself as the viewmodels callbacks object.
Looking at the documentation it states that the ViewModel shouldn't hold onto anything related view or lifecycle, or that has a reference to the activity. Thinking on this the ViewModel is referencing the fragment that has the activity.
I'm wondering, why is this bad. Everything seems to be working correctly and if there only ever was one activity to begin with, what would we ever be leaking? Why does google make it very clear not to hold onto these things but never really says why not to.
Also, since what I am doing is not ok, what is the "correct" way for having the ViewModel tell the fragment to say, "show a dialog with this error"

You could use Livedata with an Observer. For example you can use a
Mutablelivedata<Boolean> and attach an Observer to it.
You get the Viewmodel in the Fragment and attach an Observer to the Mutablelivedata<Boolean> which Shows a Dialog when the Mutablelivedata<Boolean> changes to true. An Observer is called when you attach it the first time to the Livedata and when the data changes.

This is very broad topic and you should definitely check architecture samples from google, but in short here is the idea. Let's say you'd like to have some notification channel to report various errors. Then:
Under your view model, you can define private val errorsLiveData: MutableLiveData<Exception> = MutableLiveData()
Under your fragment, you can subscribe to the stream like this:
myViewModel.getErrorsLiveData().observe(viewLifecycleOwner, Observer {
showError(it)
})
How view model can report errors: getErrorsLiveData().value = MyException(it)

You can first create the live data into two parts Mutable and Immutable. The mutable live data will be used inside the viewModel only and the Immutable one can be observed from the fragment or the activity.
class CarViewModel: ViewModel() {
private val repo: EngineRepo() // an instance of your repository inside ViewModel
private val _engine = MutableLiveData<List<Engines>>() //mutable live data to be used inside the viewModel
val : LiveData<List<Engines>> = _engine //this is the observable live data
Now,
fun fetchEngineDetails() {
viewModelScope.launch(Dispatchers.IO){
_engine.postValue(repo.getEngine()) // calling the function inside your repo
}
}
Now you are done with your viewModel
So, to call this ViewModel inside your fragment let's say you can create an instance of ViewModel class.
class CarFragment: Fragment(){
private val carViewModel: CarViewModel by viewModels()
Now inside your onCreate method you can call the function using ViewModel like this:
varViewModel.fetchEngineDetails()
And inside onCreateView you can observe the livedata like this:
carViewModel.engine.observe {(viewlifeCycleOwner)} {
Toast.makeText(requireContext, "Number of engines is {it.size}, Toast.LENGTH_SHORT).show()
}
Please note that to use or create instance of ViewModel class inside the fragment the way I have written here you have to put some dependencies in your gradles
So in you build.gradle (Project)
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
and in your build.gradle(App Module)
inside plugins add:
id 'androidx.navigation.safeargs.kotlin'
and inside dependencies add :
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation "android.arch.lifecycle:extensions:1.1.1"

Related

Initiating a ViewModel with Input parameters in Jetpack Compose -> Composable function

I have a ViewModel that takes a string as an argument
class ComplimentIdeasViewModel(ideaCategory : String) : ViewModel() {
//some code here
}
What is the best way to initiate this ViewModel inside a composable fun without using a ViewModel factory and Hilt? A simple statement seems to achieve this inside a composable fun
#Composable
fun SampleComposableFun() {
val compIdeasViewModel = remember { ComplimentIdeasViewModel("someCategory") }
}
There is no warning in Android studio when I try to do this, but this seems too easy to be true, I am able to do this without Dependency Injection and with a ViewModelFactory class. Am I missing something here?
I've tried how you have written yours out and I had issues with screen rotation resetting the view model. I suspect you may too.
I was able to fix it by utilizing the the factory parameter on viewModel() for this, which worked well for me. See this answer on a similar question with example on how to use it: jetpack compose pass parameter to viewModel
This will not provide you the correct instance if viewmodel. See if you store some state in the viewmodel, then using the factory to initialise it is necessary to ensure that you get the same and latest copy of the viewmodel currently present. There is no error since the syntactic implementation is correct. I do not know of any way to do this because most of the times, you don't need to. Why don't you initialise it in the top-level container, like the activity? Then pass it down wherever necessary.
Create a CompositionLocal for your ViewModel.
val YourViewModel = compositionLocalOf { YourViewModel(...) }
Then initialise it (You'd likely use the ViewModelProvider.Factory here). And then provide that to your app.
CompositionLocalProvider(
YourViewModel provides yourInitialisedViewModel,
) {
YourApp()
}
Then reference it in the composable.
#Composable
fun SampleComposableFun(
compIdeasViewModel = YourViewModel.current
) {
...
}
Note, the docs say that ViewModels are not a good fit for CompositionLocals because they will make your composable harder to test, make your composables tied to this app and make it harder to use #Preview.
Some get pretty angry about this. However, if you manage to mock out the ViewModel, so you can test the app and use #Preview and your composables are tied to the app and not generic, then I see no problem.
You can mock a ViewModel fairly simply, providing its dependencies are included as parameters (which is good practice anyway).
open class MockedViewModel : MyViewModel(
app = Application(),
someOtherDependeny = MockedDependecy(),
)
The more dependencies your ViewModel has the more mocking you'll need to do. But I've not found it prohibitive and including the ViewModel as a default parameter has massively sped up development.

Correct implementation of the View in android MVVM

so in MVVM architecture even in google samples we can see things like this:
class CharacterListActivity :BaseActivity() {
val ViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getData() // Bad!!!
...
viewModel.state.observe(this) { state ->
when(state) { // handling state is not views job
Success -> { navigatetoNextPage() } // navigating is not views job
Progress -> { showProgress() }
NetworkError -> { ShowSnackbar(viewModel.error) } // I,m not sure about this one either
Error -> { showErrorDialog(viewModel.error)
}
}
We know that any architecture has its own rules that makes the code testable, maintainable, and scalable over time.
in MVVM pattern according to both Wikipedia and Microsoft docs this is the View:
the view is the structure, layout, and appearance of what a user sees on the screen.[6] It displays a representation of the model and receives the user's interaction with the view (clicks, keyboard, gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.
each view is defined in XAML, with a limited code-behind that does not contain business logic. However, in some cases, the code-behind might contain UI logic that implements visual behavior such as animations.
XAML is a Xamarin thing, so now let's get back to our code:
here, since activity decides what to do with the state, the activity works as Controller like in MVC but, activity supposed to be the View ,view is just supposed to do the UI logic.
the activity even tells the ViewModel to get data. this is again not the View's job.
please note that telling what to do to the other modules in the code is not the View's job. this is making the view act as controller. view is supposed to handle its state via callbacks from the ViewModel.
the View is supposed to just tell the ViewModel about events like onClick().
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
so what is an alternative approach to do this without violation of architecture rules? should I have a function for any lif cycle event in ViewModel, like viewModel.onCreate? or viewModel.onStart? what about navigation or showing dialogs?
For The Record I'm not mixing Up mvc and mvvm, I'm saying that this pattern does which is recommended buy google.
This is not opinion-based, surely anyone can have their own implementation of any architecture but the rules must always be followed to achieve overtime maintainability.
I can name the violations in this code one by one for you:
1) UI is not responsible for getting data, UI just needs to tell ViewModel about events.
2) UI is not responsible for handling state which is exactly what it does here. more general, UI shouldn't contain any non-UI logic.
3) UI is not responsible for navigating between screens
the activity even tells the ViewModel to get data. this is again not the View's job.
Correct. The data fetch should be triggered either by ViewModel.init, or more accurately the activation of a reactive data source (modeled by LiveData, wrapping said reactive source with onActive/onInactive).
If the fetch MUST happen as a result of create, which is unlikely, then it could be done using the DefaultLifecycleObserver using the Jetpack Lifecycle API to create a custom lifecycle-aware component.
Refer to https://stackoverflow.com/a/59109512/2413303
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
You can use a custom lifecycle aware component such as EventEmitter (or here) to send one-off events from the ViewModel to the View.
You can also refer to a slightly more advanced technique where rather than just an event, an actual command is sent down in the form of a lambda expression sent as an event, which will be handled by the Activity when it becomes available.
Refer to https://medium.com/#Zhuinden/simplifying-jetpack-navigation-between-top-level-destinations-using-dagger-hilt-3d918721d91e
typealias NavigationCommand = NavController.() -> Unit
#ActivityRetainedScoped
class NavigationDispatcher #Inject constructor() {
private val navigationEmitter: EventEmitter<NavigationCommand> = EventEmitter()
val navigationCommands: EventSource<NavigationCommand> = navigationEmitter
fun emit(navigationCommand: NavigationCommand) {
navigationEmitter.emit(navigationCommand)
}
}
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#Inject
lateinit var navigationDispatcher: NavigationDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigationDispatcher.navigationCommands.observe(this) { command ->
command.invoke(Navigation.findNavController(this, R.id.nav_host))
}
}
}
class LoginViewModel #ViewModelInject constructor(
private val navigationDispatcher: NavigationDispatcher,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
fun onRegisterClicked() {
navigationDispatcher.emit {
navigate(R.id.logged_out_to_registration)
}
}
}
If Hilt is not used, the equivalent can be done using Activity-scoped ViewModel and custom AbstractSavedStateViewModelFactory subclasses.

How to use livedata with Transformations.switchMap correctly to get initial data?

right now I am starting to use LiveData for the first time. First I put all of my code in the viewModel including the code to start a search in the server.
I used LiveData like this:
Fragment onViewCreated()
viewModel.changeNotifierContacts.observe(this, androidx.lifecycle.Observer { value -> value?.let {
recyclerViewAdapter.setData(value)
} })
This was working as expected. Now I add a repository layer following MVVM pattern. (For this I moved my contact search functionality to repository class)
First I implemented the connection between ViewModel and repository like this:
ViewModel code:
fun getContacts(): MutableLiveData<ContactGroup> {
return contactSearchRepository.changeNotifierContacts;
}
fun search(newSearchInput: String) {
contactSearchRepository.searchInRepository(newSearchInput)
}
Now I read this article that told us to not use LiveData like this: https://developer.android.com/topic/libraries/architecture/livedata#merge_livedata
Example from this page:
class MyViewModel(private val repository: PostalCodeRepository) : ViewModel() {
private fun getPostalCode(address: String): LiveData<String> {
// DON'T DO THIS
return repository.getPostCode(address)
}
}
Instead we should use something like this:
var changeNotifierContacts : LiveData<ContactGroup> = Transformations.switchMap(searchInput) {
address -> contactSearchRepository.getPostCode(address) }
Questions:
Did I understand this article correctly or can I use my first implementation?
In my constructor of the viewModel I am creating an instance of my repository object that is starting to observe server data and it is getting initial data. (For example I am getting a list of all my friends). I am getting this initial data if I am using my first implementation. If I am using Transformations.switchMap implementation I am not getting this initial data. I first have to start a search here to get updated data then. This is not what I want, I also need to display "my friends" list without doing a search.
Is there another approach I can use here? Maybe LiveData is not the best solution to connect ViewModel with Repository?
Thanks for responses and suggestions!
Did I understand this article correctly or can I use my first implementation?
I think you did, but I believe you have expanded the concept too much.
If you are expecting the user to enter a search to receive an answer, you should do like they said:
class MyViewModel(private val repository: PostalCodeRepository) : ViewModel() {
private val addressInput = MutableLiveData<String>()
val postalCode: LiveData<String> = Transformations.switchMap(addressInput) {
address -> repository.getPostCode(address) }
fun setInput(address: String) {
addressInput.value = address
}
}
However, if you are loading a default list, you should do it like you did in your first example:
val getContact = contactSearchRepository.changeNotifierContacts
In this case you will have to observe getContact and postalCode.
In my constructor of the viewModel I am creating an instance of my repository object that is starting to observe server data and it is getting initial data. (For example I am getting a list of all my friends). I am getting this initial data if I am using my first implementation. If I am using Transformations.switchMap implementation I am not getting this initial data. I first have to start a search here to get updated data then. This is not what I want, I also need to display "my friends" list without doing a search.
You can start your fragment/activity with a default search, like this:
MyViewModel.setInput("Friends")
This way you do not need to observe two objects as postalCode will provide all answers.
Is there another approach I can use here? Maybe LiveData is not the best solution to connect ViewModel with Repository?
I think live data is your answer. After done with the learning curve it becomes easier to deal with!
I hope it helps!

How to get ViewModel in Activity in Android SunFlower Project?

I am studying MVVM of Google Android SunFlower project.
For Fragment, it gets viewmodel like the following
private val plantDetailViewModel: PlantDetailViewModel by viewModels {
InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
}
I want to try the same method to get viewmodel in Activity. but the requireActivity()show unresolved reference...
And the data binding is not working when I replace it to this.
Does it has other pattern can be use for providePlantDetailViewModelFactory()
Thanks in advance.
You have to add these to your build.gradle
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.2.5"
Usage in Activity
val usersViewModel: UsersViewModel by viewModels()
Usage in Fragment
// Get a reference to the ViewModel scoped to this Fragment
val viewModel by viewModels<MyViewModel>()
// Get a reference to the ViewModel scoped to its Activity
val viewModel by activityViewModels<MyViewModel>()
You need a gradle dependency on "androidx.activity:activity-ktx:$version".
Then you can import androidx.activity.viewModels in your Activity.
I don't think so that you can do similar for your Activity that's done for Fragment.
Why?
The delegate function viewModels is available from fragment-ktx library (Refer here for reference) as extension for getting ViewModel instance lazily on your value objects using by keyword.
And there's no such thing available for Activities, for an instance.
So, better is to create your own or find some other way like basic lazy{} function from Kotlin standard library and providing logic to it.

android MVVM without Databinding

Question : Can I implement android app with MVVM without using Databinding.
Problem I am trying to solve is pretty simple:
read a list of items from backend API and show them in a Recylerview.
How I am implementing:
In the View - I have Activity and RecyclerViewAdapter
Model : ApiResponse and data models
network - retrofit API service, RxJava2
for ViewModel part - I have a ViewModel class(that doesn't derive from anything) that basically calls Retrofit Service and gets data using RxJava calls.
ViewModel has calls such as :
void getItems();
void addItemData();
void removeItem();
which call service with RXJava2 as
ObServable<ApiResponse> getItems();
ObServable<ApiResponse> addItemData();
ObServable<ApiResponse> removeItem();
View instantiates ViewModel object.
ViewModel gets the instance of Adapter object during creation.
In the View, clicking a button calls a ClickHandler in the Activity which calls a ViewModel#getItems() method. Since ViewModel has link to Adapter, the viewModel updates the items in the adapter so that RecyclerView is automatically updated.
I am not sure if this is right approach for MVVM.
Databinding seems a bit like spaghetti to me.
Again, can we implement MVVM in android without DataBinding ?
Is the approach OK?
Yes! You can. But i think your approach can be better.
Remember that the view model must not have a reference to your view.
ViewModel expose observables, and in your view, you should observe those observables and react over changes.
You can have something like this:
Note: This example is with Kotlin and LiveData because, why not? But you can take this and use it with Java & Rx
ItemsViewModel : ViewModel() {
private val items = MutableLiveData<List<Items>>()
fun getAllItems() : LiveData<List<Items>> {
return items
}
//..
}
ItemsActivity : Activity() {
private var itemsAdapter: ItemsAdapter? = null
private var viewModel: ItemsViewModel? = null
override fun onCreate(savedInstance: Bundle) {
// ...
// Create your Adapter
itemsAdapter = ItemsAdapter()
recyclerView.adapter = itemsAdapter
// Create and observe your view model
viewModel = ViewModelProviders.of(this).get(ItemsViewModel::class.java)
viewModel.getAllItems().observe(this, Observer {
it?.let {
adapter?.datasource = it
}
}
In this case, the view observes view model, and notify the adapter. Then in your adapter, you do the bind as usual, without databinding.
Definitely possible, it's totally up to you how you interpret the "binding" part of MVVM. In our team, we use MVVM with RxJava instead of Android Data Binding.
Your ViewModel has an interface with outputs and inputs like this:
interface TasksViewModel {
// inputs
Observer<Task> taskAddedTrigger();
Observer<Task> taskClickedTrigger();
Observer<Task> taskCompletedTrigger();
// outputs
Observable<Boolean> isLoading();
Observable<List<Task>> tasks();
}
Your ViewModel then just uses RxJava to map inputs to outputs in a very functional style.
You Fragment supplies Inputs to the ViewModel whenever User input is received. It subscribes to Outputs and updates the user interface accordingly when the ViewModel's Output changes.
Here is a blog post which covers the topic in detail (Disclaimer: I wrote it)
The distinguishing characteristic of MVVM is that the ViewModel is not directly coupled to a View (indeed, you could bind your ViewModel to different layouts). This also has implications on the ease of unit testing. By having a reference to the Adapter, it is technically more like MVC. You don't have to use databinding, but for true MVVM, I think you would need another Observer Pattern mechanism for the View to be notified of changes so that it could pull the data it needs.
Your saying Since ViewModel has a link to Adapter and that is the problem because ViewModel should not have reference to view and In your adapter, you have views so by doing this your not following MVVM at all!!
You can still use MVVM without data binding but you need some way to notify the view about data changes, It can be LiveData (preferred way), Java Observable, Rx or even a custom implementation. The view will get notified about the changes and updates itself, in your case, view will update the adapter.
see my answer here for an example Are actions allowed in MVVM? Android
I think you should use data binding to notify the data changed from network or database, your viewmodel should expose methods for requiring or updating data, when the data arrived you can do some operation on your data, and post them to your container(activity or fragment), in there you can update your RecyclerView and its adapter

Categories

Resources