I've an host activity with 4 tabs, one fragment for each tab.
In the host activity I collect the user's location through FusedLocationProviderClient.
I want to pass that location to the child fragments.
It seems that I've to use a ViewModel like:
class LocationViewModel : ViewModel() {
lateinit var location: Location
}
How can I inform the fragments when the location has been set by the host Activity?
Thanks.
EDIT - Solution
Using MutableLiveData as suggested did the trick.
class LocationViewModel : ViewModel()
{
private val mutableLiveLocation = MutableLiveData<Location>()
val liveLocation: LiveData<Location> get() = mutableLiveLocation
fun setLocation(location: Location)
{
mutableLiveLocation.value = location
}
}
Related
I have tried to simplify my problem as much as possible, I hope it is clear what I am asking.
I would like to obtain in the fragment the value of the variable pto1 after modifying it through the onClickEvent function of class B.
I understood that doing so is not possible because when I have
val classA = A ()
in the fragment,a new instance of class A is recreated and a new pto1 it is recreated and set to 0.0 .
how can i access pto1 from fragment after modifying it with class B?
class A {
var pto1 = 0.0
fun changeValue(a: Double){
pto1 = a
}
}
--------------------------------
class B {
val classA = A()
fun onClickEvent(b:Double){
classA.changeValue(b)
}
}
--------------------------------
fragment D {
val classA = A()
onCreateView( ... ){
val botton = view.findViewById<Button>(R.id.button)
button.setOnClickListner{
val f = classA.pto1
}
}
}
There is a couple of options here
Make it static
class A {
companion object {
var pto1 = 0.0
}
}
Is this a View Model problem?
Use the navgraph viewmodel or a viewmodel from the activity.
Make it persistent: you can use the SharedPreferences or ROOM to save it.
Make the host of both fragments have it and then fragments can acces it. If the host is the activity:
class MyActivity {
val a = A()
fun getA() = a
}
(requireActivity() as? MyActivity).?getA()
Is this a navigation result?
Take a look here How to get a result from fragment using Navigation Architecture Component?
I use Koin and Fragment + ViewModel per screen.
In my HomeFragment I have list with post.
When user selects post I navigate user to PosDetailsFragment and I want to display info about post.
class HomeFragment : Fragment() {
private val homeViewModel by viewModel<HomeViewModel>()
//when user select post I set that value to LeadViewModel (I want to make that viewModel as common for some fragment)
leadViewModel.state.selectedPost.value = action.post
}
class PostDetailsFragment : Fragment() {
private val leadViewModel by sharedViewModel<LeadViewModel>()
//always null
val post = leadViewModel.state.selectedPost.value
}
My Koin module:
viewModel { LeadViewModel() }
viewModel { HomeViewModel(get(bottomNavigationCommander), get()) }
viewModel { AddPostViewModel() }
What is wrong? It looks like instance of LeadViewModel in PostDetailsFragment is completely different than in HomeFragment?
Try making the val as backing field
val post
get() = leadViewModel.state.selectedPost.value
The FusedLocationProviderClient that I'm using to get location information needs a reference to an activity or context. When I try to instantiate my UserLocationService - which implements FusedLocationProviderClient - inside a viewmodel, i have to pass a reference to an activity.
class UserLocationService {
public val locationUpdates: MutableLiveData<Location> = MutableLiveData()
private var fusedLocationClient: FusedLocationProviderClient
constructor(activity: Activity) {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
...
I don't want to pass any reference to the viewmodel. What is the right approach in this case? I could use the UserLocationService directly in the fragment but my understanding is, that the viewmodel should do the instantiation und initialization.
You should use an AndroidViewModel.
Edit:
So what you can do is have the view model class.
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val context = application.applicationContext
}
And then use the context for what you need.
To initialize the view model you can do something like this in your activity.
private val myViewModel: MyViewModel by lazy {
ViewModelProviders.of(this).get(MyViewModel::class.java)
}
And then use it after the onCreate()
I have my Activity MainActivity.kt .
And and one ViewModel MainActivityViewModel.kt
And I want to observe my live data to my 3 different fragments.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
But my observer only work on activity not on the fragments.
Hey there you are doing everything greate except one thing you should use requireActivity() instead on this in your fragment class.
Make sure your all fragment are attached to your viewModel holding Activity.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(MainActivityViewModel::class.java)
}
This will help you solve your issue.
For further detail view this.
The ViewModelProviders.of has 2 different constructors:
of(Fragment fragment, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given fragment is alive.
of(FragmentActivity activity, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given Activity is alive.
Basically when you used this as the first parameter in your activity, you passed the context of the activity and created a viewmodel that will be alive in the scope of the activity, however your second this is the context to your fragment, meaning that the second ViewModel will be alive as long as your fragment is alive (only one fragment).
What instead you should be doing in your fragment is using the context of the activity, since activity is always alive when fragments are attached and swapped. You should change your fragments to:
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MainActivityViewModel::class.java)
}
or you can use the requireActivity() method that was the previous answer.
To achieve what you are trying to do, you need three things. An activity/fragment that will post the value to the ViewModel, a ViewModel, and an activity/fragment that will retrieve the data from the ViewModel. Lets say your data is stored in an ArrayList, and you want to update and retrieve it from different fragments.
First, we have to implement a ViewModel. It contains the data you want to share between your activities/fragments. You declare the MutableLiveData as an ArrayList then initialize it.
class testviewmodel : ViewModel() {
val list: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
list.value = arrayListOf()
}
}
Our next step is to access and update the ArrayList using your activity:
val viewmodel = ViewModelProviders.of(this).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
If you are using a Fragment to update it, use this:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
}
Finally, to retrieve the data from the ViewModel in a fragment, put this inside your onViewCreated:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(Dbviewmodel::class.java)
observeInput(viewmodel)
}
Put this outside of your onViewCreated:
private fun observeInput(viewmodel: testviewmodel ) {
viewmodel.list.observe(viewLifecycleOwner, Observer {
it?.let {
if (it.size > 5) {
pos = it[5]
//grab it
Toast.makeText(context,pos,Toast.LENGTH_LONG).show()
//display grabbed data
}
}
})
}
Take a look at this docs for more information about ViewModels
Good Luck! I hope this helps :)
That's because you are using the fragment 'this' instance, and not the activity.
Replace
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
With
activity?.let { mainActivityViewModel = ViewModelProviders.of(it, viewModelFactory).get(MainActivityViewModel::class.java) }
I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.