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
})
}
}
Related
I have my MainActivity where there are the movie with their poster. If I click on a poster (in the layout there are 4 ImageView) I can read the plot (a TextView) that is in the Fragment. If I open my app with the emulator MainActivity and Fragment are overlapping so I need to set the ViewModel. How to set it?
//MainActivity
class JacksonActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.jackson_activity)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.jackson_film, JacksonFragment.newInstance())
.commitNow()
}
}
//Fragment
class JacksonFragment : Fragment() {
companion object {
fun newInstance() = JacksonFragment()
}
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return inflater.inflate(R.layout.jackson_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
// TODO: Use the ViewModel
} }
ViewModelProvider is deprecated. more info: ViewModelProvider
To implement viewModel simply use val viewModel: MainViewModel by viewmodels()
if you use the same viewmodel in more than one place(acitity/fragment) in fragment use val viewModel: MainViewModel by activityViewModels()
Note: do not forget to update the dependencies. More info: ViewModel / SharedViewModel
i'm making an app and i want to separate my UI logic into multiple UI classes with BaseUi class being lifecycle aware. I'm using Kodein as my DI and i have an issue with fragment.viewLifecycleOwnerLiveData.observe not being called when instance of my ui class is being retrieved by Kodein.
Here is my Fragment class:
class ListFragment : Fragment(), DIAware {
override val di: DI by closestDI()
override val diTrigger: DITrigger = DITrigger()
private var binding: FragmentMoviesBinding? = null
private val fragmentBinding get() = binding
private val kodeinMoviesUi: MoviesUi by instance() //fragment does not observe viewLifecycleOwnerLiveData
private val moviesUi: MoviesUi = MoviesUi(this) //fragment now observe viewLifecycleOwnerLiveData
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoviesBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
diTrigger.trigger()
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
BaseUi class:
abstract class BaseUi<F : Fragment>(private val fragment: F) : LifecycleObserver {
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment, { subscribeToLifecycle() })
}
private fun subscribeToLifecycle() {
fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
onViewCreated()
}
})
}
abstract fun onViewCreated()
}
And UiModule:
val uiModule = DI.Module("uiModule") {
bind<ListFragment>() with provider { ListFragment() }
bind<MoviesUi>() with provider { MoviesUi(instance()) }
}
Cross post from https://github.com/Kodein-Framework/Kodein-DI/issues/353
Here is your problem bind<ListFragment>() with provider { ListFragment() }.
You bound the ListFragment with a provider, meaning every time you ask to the container it will create an instance of ListFragment. So, when you inject MoviesUi with private val kodeinMoviesUi: MoviesUi by instance(), it gets another instance of ListFragment.
I suggest that you define the binding for MoviesUi as a factory, waiting to receive a ListFragment instance:
bind<MoviesUi>() with factory {fragment: ListFragment -> MoviesUi(fragment) }
then you can inject it in the ListFragment like:
private val kodeinMoviesUi: MoviesUi by instance(args = this)
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
Im using ViewModel with states and LiveData with one observer .
I have an activity and a fragment . everything works fine until Im rotating the screen
and then, the states I want to observe , clash.
What can I do in order to prevent it and make it work as expected?
I know I can add more observers but I don't want to solve it in this way , it may lead to problems with the other code.
MainActivity code :
private var appsDetailsHmap = HashMap< String , AppsDetails>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressBar = CircleProgressBarDialog(this)
viewModel.getState().observe(this, Observer {state->
when(state){
is SettingsViewModelStates.GetAppsDetails->initUI(state.list)
is SettingsViewModelStates.ShowDialog->progressBar.showOrHide(state.visibility)
is SettingsViewModelStates.GetCachedData->setCachedSettings(state.appDetailsHmap,state.selectedApp,state.speechResultAppName)
}
})
if (savedInstanceState==null){
viewModel.initSettingsActivityUI(appsDetailsHmap)
}
else{
viewModel.initCachedSettingsActivityUI()
}
Fragment code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view= inflater.inflate(R.layout.fragment_added_apps, container, false)
viewModel.getCachedApplist()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getState().observe(viewLifecycleOwner, Observer {state->
when(state){
is SettingsViewModelStates.PassAppsToFragment->{
initRecyclerView(state.list)
}
}
})
}
ViewModel
private var state=MutableLiveData<SettingsViewModelStates>()
private var appsDetailsHmap=HashMap<String,AppsDetails>()
private var addedApps= HashMap<String,Drawable?>()
fun getState()=state as LiveData<SettingsViewModelStates>
fun getCachedApplist(){
if (addedApps.isEmpty()){
getAppsList()
println("empty")
}
else
state.setValue(SettingsViewModelStates.PassAppsToFragment(addedApps))
}
fun initCachedSettingsActivityUI(){
state.setValue(SettingsViewModelStates.GetCachedData(appsDetailsHmap,selectedApp,speechResultAppName))
}
fun initSettingsActivityUI(list:HashMap<String, AppsDetails>) {
appsDetailsHmap=list
state.setValue( SettingsViewModelStates.GetAppsDetails(list))
}
states:
sealed class SettingsViewModelStates {
data class GetAppsDetails(val list:HashMap<String, AppsDetails>):SettingsViewModelStates()
data class ShowDialog(val visibility: Int) : SettingsViewModelStates()
data class PassAppsToFragment(val list:HashMap<String,Drawable?>) : SettingsViewModelStates()
data class GetCachedData(val appDetailsHmap:HashMap<String,AppsDetails>,
val selectedApp: AppsDetails,
val speechResultAppName:String ) : SettingsViewModelStates()
}
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)