I have 2 fragments that i navigate throught navigation component from Jetpack. When i go back from fragment 2 to fragment 1, i fire an event throught livedata from fragment 2 and fragment1 should be observing. Althought, fragment 1 didnt observed and when i sste a breakpoint to check how many observers liveData has, it say 0.
Code below
class Fragment1 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
findNavController().navigate(
Fragment1Directions.actionToFragment2()
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
}
private lateinit var myViewModel: MyViewModel
}
class Fragment2 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myButton.setOnClickListener{
MyViewModel().selectItem(true)
findNavController().navigateUp()
}
}
}
class MyViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Boolean>()
val selectedItem: LiveData<Boolean> get() = mutableSelectedItem
fun selectItem(value: Boolean) {
mutableSelectedItem.value = value
}
}
It's strange to not have observer for
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
when it's called onViewCreated and with viewLifecycleOwner, but it looks like you have a mistake for creating common ViewModel for Fragment1 and Fragment2
Both should use
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java) to get same ViewModel for both fragments
Related
LiveData is not observing on child viewpager fragments where the child fragments have one shared viewmodel to access data.
Here FragmentA and FragmentB are part of a viewpager and both of them are sharing one viemodel SharedViewModel.
public class SharedViewModel extends AndroidViewModel { //in Java
private final MutableLiveData<Data> mLiveData = new MutableLiveData<>();
public LiveData<Data> getLiveData() {
return mLiveData;
}
//for updating data through LiveData, using post as and when I get the response from DataSource as shown below.
mLiveData.postValue(response); //getting the response on debugging
}
class FragmentA : Fragment() { //in Kotlin
override fun onCreate(#Nullable savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
....
mViewModel.liveData.observe(viewLifecycleOwner, {
//no call coming in this block so unable to update view
})
}
}
class FragmentB : Fragment() { //in Kotlin
override fun onCreate(#Nullable savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
}
}
Need some help as to why the live data is not able to observe the changes.
Thanks in advance.
Your ViewModel is not shared. you have named it as sharedViewmodel but the way you are getting an instance of it by passing a Unique Owner it will also be a unique instance.
the correct way of sharing view model is
class SharedViewModel : ViewModel() {
private val myLiveData = MutableLiveData<Data>()
fun getMyLiveDta():LiveData {
return myLiveData
}
}
Now in the first fragment
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Get instance of viewmodel in fragment like this
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
And in the second fragment like this
class DetailFragment : Fragment() {
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
})
}
}
Please help me with a problem.
I have the activity_main.xml set up with a score text box, a test button and a fragment container that swaps between 2 fragments.
The fragments are basically containers for buttons.
The "buttonTest" does exactly what "button1" from the fragment does (increments the score) but the test button (located on activity_main.xml) works and the fragment one ... does not
When viewing the logs ...i see that score does update when i click both buttons but with different values.
If i click "buttonTest" it adds 5 to score so score = 5. On another click it adds another 5 so score = 10
If i click in the fragment on "button1" ... calling the same method score is now 1 then 2 then 3.
If i now click "buttonTest" the score is 15.
The problem is that the LiveData keeps separate values depending on where the method was called.
Allso the MainActivity observer does not update on the fragment call... only on the activity one.
Please help.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val scoreSwitch = binding.scoreSwitch
binding.buttonTest.setOnClickListener { viewModel.adaugaTren(5) }
// score observer
viewModel.scor.observe(this, Observer { newScore ->
binding.txtPunctaj.text = newScore.toString()
//todo sterge LOG
Log.d("test","Scor Observer triggers")
})
.......
class MainViewModel: ViewModel(){
private val _score = MutableLiveData(0)
val score: LiveData<Int>
get() = _score
fun adaugaTren(valoare:Int) {
_scor.value = _scor.value?.plus(valoare)
listaTrenuri.add(valoare)
//testing
Log.d("test","Trenuri: ${scor.value}")
updateDisplay()
}
fun updateDisplay(){
// _trenuriDetinute.value = listaTrenuri.toString()
_trenuriDetinute.value = TextUtils.join(", ",listaTrenuri)
//testing
Log.d("test","UpdateDisplay: ${scor.value}")
}
...
}
class TrenuriFragment : Fragment() {
private lateinit var binding: FragmentTrenuriBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_trenuri, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initButtons()
}
fun initButtons(){
binding.button1.setOnClickListener { viewModel.adaugaTren(1)}
....
}
}
In TrenuriFragment use activityViewModels
// viewModels is scoped in fragment but you need activity
private val viewModel: MainViewModel by activityViewModels()
I tried to find an easy way to listen to fragment changes from my activity in order to hide/show the drawer menu button from my LoginFragment and I could not find a good and easy way to implement for my case here in sfo, so I would like to share an easy solution I eventually came up with using ViewModel and a LiveData which saves the fragment class name that is currently displayed and observing it from the activity to listen for changes.
NOTE the solution works in case that your fragments are displayed on the same FragmentContainerView in your layout
Here is an exmaple:
ViewModel class :
class MyViewModel : ViewModel(){
val currentFragment = MutableLiveData<String>()
}
Now set currentFragment value inside your fragments:
class LoginFragment() : Fragment() {
private lateinit var : viewModel : MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
ViewModel.currentFragment.value = this::class.java.name
}
}
class MainFragment() : Fragment() {
private lateinit var : viewModel : MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
ViewModel.currentFragment.value = this::class.java.name
}
}
Now in your Activity you can observe currentFragment and do whatever you want(in my case I wanted to know if the current fragment is LoginFragment and hide the drawer menu button from the toolbar) :
class MainActivity() : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapViewModel.currentFragment.observe(this, {
when (it) {
LoginFragment::class.java.name -> {
//your stuff related to LoginFragment
}
MainFragment::class.java.name -> {
//your stuff related to MainFragment
}
}
})
}
}
Hope this helps anyone ^^
I'm trying to send data from DialogFragment to Fragment using ViewModel but it seems both fragment and Dialog fragment are referencing different instances of ViewModel. so I can't access data . Is there any way I can fix this issue? thanks
Here is my Fragment
#AndroidEntryPoint
class FragmentToReceiveData:BaseFragment(R.layout.fragment_1){
private val viewModel: AddScheduleViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d(TAG, "onViewCreated: $viewModel") // will print ...MyViewModel#62274cc
viewModel.dataFromDialog.observe(viewLifecycleOwner){
//nothing happens
}
}
.
.
.
private fun openDialog(){
val action=FragmentToReceiveDataDirections.actionFragmentToReceiveDataToExampleDialog()
findNavController().navigate(action)
//exampleDialog.show(requireActivity().supportFragmentManager, "alarmDialog") //same issue
}
}
Here is ViewModel:
class MyViewModel #ViewModelInject constructor(){
var dataFromDialog=MutableLiveData<SomeClass>()
fun saveDataFromDialog(data:SomeClass){
dataFromDialog.value=data
}
}
Here is my DialogFragment
#AndroidEntryPoint
class ExampleDialog:DialogFragment() {
val viewModel:MyViewModel by viewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Log.d(TAG, "onCreateDialog: $viewModel") // will print ...MyViewModel#125436
.
.
.
viewMode.saveDataFromDialog(data)
}
}
P.S: I'm using single activity architecture, So I'm not sure if activityViewModels() is a good idea
in order to share ViewModel between fragments, you can use activityViewModels(). for instance,
class SharedViewModel : ViewModel() {
...
}
class MasterFragment : 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)
...
}
}
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)
...
}
}
please read more in the android documentation here: https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
I have one fragment where I update a total integer in my sharedViewModel, this is the shopsFragment
class ShopFragment : Fragment(), AppBarLayout.OnOffsetChangedListener {
private val model: SharedViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.updateTotal(200)
}
}
Now, my other fragment that I need this data to be shared between, is a BottomSheetDialogFragment , in this Fragment I get an instance of the sharedViewModel by doing this
class CartBottomSheet: BottomSheetDialogFragment() {
private val model: SharedViewModel by viewModels ({requireParentFragment()})
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.getTotal().observe(viewLifecycleOwner, Observer { total ->
sheet_total_price.text = "$$total.00"
})
}
Now, when I try to get the 200 that I posted in the other Fragment, it shows 0, and that means that the instance of that sharedViewModel is a new instance because it returns 0 because my viewmodel instance initializes a common shared total with 0
class SharedViewModel: ViewModel() {
private val totalData = MutableLiveData<Int>()
private var sharedTotal = 0
fun updateTotal(total:Int){
sharedTotal = total
totalData.value = sharedTotal
}
fun getTotal():LiveData<Int>{
return totalData
}
Now, my question is, do I need to pass as a bundle to the BottomDialogFragment this instance of the sharedViewmodel to work with, or is there any way to get the same instance to get the value of total
Thanks
You can set ShopFragment as targetFragment for the CartBottomSheet fragment. In this way when you create the Shared VM you will get the same instance. Basically if you put this together you can achieve it by the code below :-
class CartBottomSheet: BottomSheetDialogFragment() {
private val model: SharedViewModel?=null
companion object {
fun show(fragmentManager: FragmentManager, parentFragment: Fragment) {
val sheet = CartBottomSheet()
sheet.setTargetFragment(parentFragment, 13)
sheet.show(fragmentManager, sheet.tag)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
targetFragment?.let {
// Create model here with it
}
}
}
So now for opening sheet you should call
CartBottomSheet.show(fragmentManager!!, this)