Retaining Int after view gets destroyed - android

I am using a gridlayout in a fragment on which the user can switch the amount of items shown per line (by pressing a button he can switch currently between 6 and 4 but might extend on adding a third option).
When the user leaves the the fragment (switches to another fragment) and comes back later, I want to retain the information of how many items per line are shown.
After having tried different options (savedInstanceState: doesn't work as the activity is never recreated, getArguments: not a feasible option afaik, as I have to pass the information several times) I am using a sharedViewModel that is implemented anyways as some of information is observed.
I feel like it's an overkill, as it is "just" an int (or even a bool, if there are just 2 states) and there might be a more simple ("built in"?) solution which might be similiar to activity's savedInstanceState.
Here is some code:
Fragment with GridLayout
class LibraryFragment : Fragment() {
private val model: SharedViewModel by activityViewModels()
private lateinit var gridlayoutManager: GridLayoutManager
private lateinit var thumbnailAdapter: ThumbnailAdapter
private lateinit var thumbnailRecyclerView: RecyclerView
private var booksPerLine = 4
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLibraryViewerBinding.inflate(layoutInflater)
val view = libraryFragmentBinding.root
initRecyclerView()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.booksPerLine.observe(viewLifecycleOwner, Observer {
booksPerLine = it
switchNoOfBooksPerLine()
})
}
private fun initRecyclerView() {
thumbnailRecyclerView = libraryFragmentBinding.thumbnailRecyclerView
switchNoOfBooksPerLine()
thumbnailAdapter = ThumbnailAdapter { selectedBook: Book -> displaySelectedBook(selectedBook) }
thumbnailAdapter.setThumbnailList(listOf())
thumbnailRecyclerView.adapter = thumbnailAdapter
}
private fun switchNoOfBooksPerLine() {
gridlayoutManager = GridLayoutManager(requireContext(), booksPerLine, LinearLayoutManager.VERTICAL, false)
thumbnailRecyclerView.layoutManager = gridlayoutManager
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.change_grid -> {
when (booksPerLine) {
4 -> model.setBooksPerLine(6)
6 -> model.setBooksPerLine(4)
}
true
}
}
}
}
SharedViewModel with grid information
class SharedViewModel : ViewModel() {
val booksPerLine = MutableLiveData<Int>()
internal fun setBooksPerLine(_booksPerLine: Int) {
booksPerLine.value = _booksPerLine
}
}
So the questions is, if there is a more simple/efficient way of retaining the Int? (bonus question: how efficient (in terms of ressource usage like memory) is a SharedViewModel actually?
Is there a built in method like savedInstanceState for activities for fragments, too?

I would suggest to keep the data in the activity. And in fragment get the data from activity in onViewCreated method like-
val data = (activity as MyActivity)?.data
Also, put back the data into the activity the other way when data changed-
(activity as MyActivity)?.data = changedData
This is the manual way. Keep in mind the data is nullable.

Related

Should I inflate the layout in onCreateView or onViewCreated?

I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)

LiveData binding observer being registered multiple times when changing device orientation even when using viewLifeCycleOwner

I have a view model that is data binded to a fragment. The view model is shared with the main activity.
I've button is binded to the view as follows:
<Button
android:id="#+id/startStopBtn"
android:text="#{dashboardViewModel.startStopText == null ? #string/startBtn : dashboardViewModel.startStopText}"
android:onClick = "#{() -> dashboardViewModel.onStartStopButton(context)}"
android:layout_width="83dp"
android:layout_height="84dp"
android:layout_gravity="center_horizontal|center_vertical"
android:backgroundTint="#{dashboardViewModel.isRecStarted == false ? #color/startYellow : #color/stopRed}"
tools:backgroundTint="#color/startYellow"
android:duplicateParentState="false"
tools:text="START"
android:textColor="#FFFFFF" />
What I expect to happen is that every time I press the button the function onStartStopButton(context) runs. This works fine as long as I don't rotate the device. When I rotate the device the function is run twice, if I rotate again the function is run 3 times and so on. This is not a problem if I go to another fragment and then back to the dashboard fragment. It looks like the live data observer is getting registered every time I rotate my screen, but not every time I detach and reattach the fragment.
This is true for all the elements in that fragment, whether they are data binded or I manually observe them.
Fragment code:
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val dashboardViewModel: DashboardViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
binding.dashboardViewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
dashboardViewModel.bleSwitchState.observe(viewLifecycleOwner, Observer { switchState -> handleBleSwitch(switchState) })
dashboardViewModel.yLims.observe(viewLifecycleOwner, Observer { yLims ->
updatePlotWithNewData(yLims.first, yLims.second)
})
Timber.i("Dahsboard on create: DashboardViewModel in fragment: $dashboardViewModel")
return root
}
}
The view model:
class DashboardViewModel : ViewModel() {
//region live data
private var _isRecStarted = MutableLiveData<Boolean>()
val isRecStarted: LiveData<Boolean> get() = _isRecStarted
//private var _bleSwitchState = MutableLiveData<Boolean>()
val bleSwitchState = MutableLiveData<Boolean>()
private var _startStopText = MutableLiveData<String>()
val startStopText: LiveData<String> get() = _startStopText
private var _yLims = MutableLiveData<Pair<kotlin.Float,kotlin.Float>>()
val yLims: LiveData<Pair<kotlin.Float,kotlin.Float>> get() = _yLims
//endregion
init {
Timber.d("DashboardViewModel created!")
bleSwitchState.value = true
}
//region start stop button
fun onStartStopButton(context: Context){
Timber.i("Start stop button pressed, recording data size: ${recordingRawData.size}, is started: ${isRecStarted.value}")
isRecStarted.value?.let{ isRecStarted ->
if (!isRecStarted){ // starting recording
_isRecStarted.postValue(true)
_startStopText.postValue(context.getString(R.string.stopBtn))
startDurationTimer()
}else{ // stopping recording
_isRecStarted.postValue(false)
_startStopText.postValue(context.getString(R.string.startBtn))
stopDurationTimer()
}
} ?: run{
Timber.e("Error! Is rec started is not there for some reason")
}
}
}
The view model is created the first time from the MainActivity as follows:
class MainActivity : AppCompatActivity() {
private val dashboardViewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("DashboardViewModel in main activity: $dashboardViewModel")
}
}
Edit explaining why the MainActivity is tided to the ViewModel:
The reason why the ViewModel is linked to the main activity is that the main activity handles some Bluetooth stuff for a stream of data, when a new sample arrives then the logic to handle it and update the UI of the dashboard fragment is on the DashboardViewModel. The data still needs to be handled even if the dashboard fragment is not there.
So I need to pass the new sample to the DashboardViewModel from the main activity as that is where I receive it. Any suggestions to make this work?
As you know, when you instantiate the ViewModel of a Fragment with activityViewModels, it means that the ViewModel will follow the lifecycle of the Activity containing that Fragment. Specifically here is MainActivity.
So what does ViewModel tied to Activity lifecycle mean in your case?
When you return to the Fragment, normally LiveData (with ViewModel attached to Fragment lifcycler) will trigger again.
But when that ViewModel is attached to the Activity's lifecycle, the LiveData will not be triggered when returning to the Fragment.
That leads to when you return to the Fragment, your LiveData doesn't trigger again.
And that LiveData only triggers according to the life cycle of the activity. That is, when you rotate the screen, the Activity re-initializes, now your LiveData is triggered.
EDIT:
Here, I will give you one way. Maybe my code below doesn't work completely for your case, but I think it will help you in how to control LiveData and ViewModel when you bind ViewModel to Activity.
First, I recommend that each Fragment should have its own ViewModel and it should not depend on any other Fragment or Activity. Here you should rename the DashboardViewModel initialized by activityViewModels() as ShareViewModel or whatever you feel it is related to this being the ShareViewModel between your Activity and Fragment.
class DashboardFragment : Fragment() {
// Change this `DashboardViewModel` to another class name. Could be `ShareViewModel`.
private val shareViewModel: ShareViewModel by activityViewModels()
// This is the ViewModel attached to the DashboardFragment lifecycle.
private val viewModel: DashboardViewModel by viewModels()
private lateinit var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.dashboardViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
Next, when there is data triggered by the ShareViewModel's LiveData, you will set the value for the LiveData in the ViewModel associated with your Fragment. As follows:
DashboardViewModel.kt
class DashboardViewModel: ViewModel() {
private val _blueToothSwitchState = MutableLiveData<YourType>()
val blueToothSwitchState: LiveData<YourType> = _blueToothSwitchState
private val _yLims = MutableLiveData<Pair<YourType, YourType>>()
val yLims: LiveData<Pair<YourType, YourType>> = _blueToothSwitchState
fun setBlueToothSwitchState(data: YourType) {
_blueToothSwitchState.value = data
}
fun setYLims(data: Pair<YourType, YourType>) {
_yLims.value = data
}
}
DashboardFragment.kt
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.run {
bleSwitchState.observe(viewLifeCycleOwner) {
viewModel.setBlueToothSwitchState(it)
}
yLims.observe(viewLifeCycleOwner) {
viewModel.setYLims(it)
}
}
viewModel.run {
// Here, LiveData fires observe according to the life cycle of `DashboardFragment`.
// So when you go back to `DashboardFragment`, the LiveData is re-triggered and you still get the observation of that LiveData.
blueToothSwitchState.observe(viewLifeCycleOwner, ::handleBleSwitch)
yLims.observe(viewLifeCycleOwner) {
updatePlotWithNewData(it.first, it.second)
}
}
}
...
}
Edit 2:
In case you rotate the device, the Activity and Fragment will be re-initialized. At that time, LiveData will fire observe. To prevent that, use Event. It will keep your LiveData from observing the value until you set the value again for LiveData.
First, let's create a class Event.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? = if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
fun peekContent(): T = content
}
Next, modify the return type of the LiveData that you want to trigger once.
ShareViewModel.kt
class ShareViewModel: ViewModel() {
private val _test = MutableLiveData<Event<YourType>>()
val test: LiveData<Event<YourType>> = _test
fun setTest(value: YourType) {
_test.value = Event(value)
}
}
Add this extension to easily get LiveData's observations.
LiveDataExt.kt
fun <T> LiveData<Event<T>>.eventObserve(owner: LifecycleOwner, observer: (t: T) -> Unit) {
this.observe(owner) { it?.getContentIfNotHandled()?.let(observer) }
}
Finally in the view, you get the data observed by LiveDatat.
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.test.eventObserve(viewLifeCycleOwner) {
Timber.d("This is test")
}
}
...
}
Note: When using LiveData with Event, make sure that LiveData is not reset when rotating the device. If LiveData is set to value again, LiveData will still trigger even if you use Event.

Android:SaveState, Fragments and ViewModel: what am I doing wrong?

I have the single activity with several fragments on top, as Google recommends. In one fragment I wish to place a switch, and I wish to still know it's state when I come back from other fragments. Example: I am in fragment one, then I turn on the switch, navigate to fragment two or three, go back to fragment one and I wish to load that fragment with that switch in the on position as I left it.
I have tried to copy the examples provided by google advocates, just to see the code to fail hard and do nothing.
/////////////////////////////////////////////////////////////////
//Inside the first fragment:
class myFragment : Fragment() {
companion object {
fun newInstance() = myFragment()
}
private lateinit var viewModel: myViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
**viewModel = ViewModelProvider(this, SavedStateVMFactory(this)).get(myViewModel::class.java)
//Here I was hoping to read the state when I come back.
switch_on_off.isChecked = viewModel.getSwRoundTimerInit()**
subscribeToLiveData() //To read liveData
switch_on_off.setOnCheckedChangeListener { _, isChecked ->
viewModel.setOnOff(isChecked)
}
}//End of onActivityCreated
//other code...
/////////////////////////////////////////////////////////////////////////
//On the fragment ViewModel
class myViewModel(private val **mState: SavedStateHandle**) : ViewModel() {
//SavedStateHandle Keys to save and restore states in the App
private val swStateKey = "SW_STATE_KEY"
private var otherSwitch:Boolean //other internal states.
//Init for the other internal states
init {
otherSwitch = false
}
fun getSwRoundTimerInit():Boolean{
val state = mState[swStateKey] ?: "false"
return state.toBoolean()
}
fun setOnOff(swValue:Boolean){
mState.set(swStateKey, swValue.toString())
}
}
This does not work. It always loads the default (off) value, as if the savedState is null all the time.
change
//fragment scope
viewModel = ViewModelProvider(thisSavedStateVMFactory(this)).get(myViewModel::class.java)
to
//activity scope
viewModel = activity?.let { ViewModelProviders.of(it,SavedStateVMFactory(this)).get(myViewModel::class.java) }
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing

RecyclerView within a ViewPager unpopulated despite Adapter containing data

I'm currently using a ViewPager and TabLayout for a couple of similar fragments. The fragments hosted within the ViewPager all have a RecyclerView. When I swipe more than one page over, the Fragment (I think?) is destroyed. When I swipe back it's recreated.
Upon debugging, I found that the adapter within the Fragment is non null and populated with the data from before. However, once the fragment is visible it no longer displays any entries.
Here's a video of what's going on.
This is a fragment within the ViewPager
class ArtistListFragment : Fragment(), ListView {
override val title: String
get() = "Artists"
private val artistList: RecyclerView by lazy { artist_list }
#Inject lateinit var api: SaddleAPIManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_list_artist)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
artistList.apply {
setHasFixedSize(true)
if (adapter == null) {
adapter = ViewTypeAdapter(mapOf(AdapterConstants.ARTIST to ArtistDelegateAdapter()))
}
layoutManager = LinearLayoutManager(context)
if (savedInstanceState != null && savedInstanceState.containsKey("artist")) {
(adapter as ViewTypeAdapter).setItems(savedInstanceState.getParcelableArrayList<Artist>("artists"))
}
}
(activity?.application as SaddleApplication).apiComponent.inject(this)
refreshData()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(
"artists",
(artistList.adapter as ViewTypeAdapter).items as ArrayList<out Parcelable>
)
}
private fun refreshData() = launch(UI) {
val result = withContext(CommonPool) { api.getArtists() }
when(result) {
is Success -> (artistList.adapter as ViewTypeAdapter).setItems(result.data.results)
is Failure -> Snackbar.make(artistList, result.error.message ?: "Error", Snackbar.LENGTH_LONG).show()
}
}
}
This is the Fragment hosting the ViewPager
class NavigationFragment : Fragment() {
private val viewPager: ViewPager by lazy { pager }
private val tabLayout: TabLayout by lazy { tab_layout }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_navigation)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager.apply {
adapter = NavigationPageAdapter(childFragmentManager)
}
tabLayout.apply {
setupWithViewPager(viewPager)
}
}
}
The adapter I'm using for paging
class NavigationPageAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {
companion object {
const val NUM_PAGES = 4
}
private val pages: List<Fragment> = (0 until NUM_PAGES).map {
when (it) {
0 -> ArtistListFragment()
1 -> AlbumListFragment()
2 -> TrackListFragment()
else -> PlaylistListFragment()
} as Fragment
}
override fun getItem(position: Int): Fragment = pages[position]
override fun getCount(): Int = NUM_PAGES
override fun getPageTitle(position: Int): CharSequence? = (pages[position] as ListView).title
}
I've tried overriding onSaveInstanceState and reading the information from the bundle. It doesn't seem to do anything. The problem seems to actually be the RecyclerView displaying? It's populated with data which is why I'm stumped.
Try to use setOffscreenPageLimit for ViewPager to keep containing fragments as below:
viewPager.setOffscreenPageLimit(NavigationPageAdapter.NUM_PAGES)
I've figured out the problem. While setOffScreenPageLimit(NavigationAdapter.NUM_PAGES) did work, I am cautious to use it because of memory consumption concerns.
As it turns out, storing references to views using lazy is bad practice. Since onCreateView gets called many times in the lifecycle of the ArtistListFragment the same view wasn't being referenced that was currently inflated on the screen.
Removing all lazy instantiated views and accessing them directly with the Android Extensions solved my problem.

LiveData Observer not Called

I have an activity, TabBarActivity that hosts a fragment, EquipmentRecyclerViewFragment. The fragment receives the LiveData callback but the Activity does not (as proofed with breakpoints in debugging mode). What's weird is the Activity callback does trigger if I call the ViewModel's initData method. Below are the pertinent sections of the mentioned components:
TabBarActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initVM()
setContentView(R.layout.activity_nav)
val equipmentRecyclerViewFragment = EquipmentRecyclerViewFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frameLayout, equipmentRecyclerViewFragment, equipmentRecyclerViewFragment.TAG)
.commit()
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, loadingObserver)// eVM?.initData() //TODO: Not calling this causes Activity to never receive the observed ∆
}
val loadingObserver = Observer<List<Gun>> { equipment ->
...}
EquipmentRecyclerViewFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
columnCount = 2
initVM()
}
//MARK: ViewModel Methods
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, equipmentObserver)
eVM?.initData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_equipment_list, container, false)
if (view is RecyclerView) { // Set the adapter
val context = view.getContext()
view.layoutManager = GridLayoutManager(context, columnCount)
view.adapter = adapter
}
return view
}
EquipmentViewModel
class EquipmentViewModel(application: Application) : AndroidViewModel(application), LifecycleObserver {
var equipment = MutableLiveData<List<Gun>>()
var isLoading = MutableLiveData<Boolean>()
fun initData() {
isLoading.setValue(true)
thread { Thread.sleep(5000) //Simulates async network call
var gunList = ArrayList<Gun>()
for (i in 0..100){
gunList.add(Gun("Gun "+i.toString()))
}
equipment.postValue(gunList)
isLoading.postValue(false)
}
}
The ultimate aim is to have the activity just observe the isLoading MutableLiveData boolean, but since that wasn't working I changed the activity to observe just the equipment LiveData to minimize the number of variables at play.
To get same reference of ViewModel of your Activity you need to pass the same Activity instance, you should use ViewModelProviders.of(getActivity). When you pass this as argument, you receive instance of ViewModel that associates with your Fragment.
There are two overloaded methods:
ViewModelProvider.of(Fragment fragment)
ViewModelProvider.of(FragmentActivity activity)
For more info Share data between fragments
I put this code inside the onActivityCreated fragment, don't underestimate getActivity ;)
if (activity != null) {
globalViewModel = ViewModelProvider(activity!!).get(GlobalViewModel::class.java)
}
globalViewModel.onStop.observe(viewLifecycleOwner, Observer { status ->
Log.d("Parent Viewmodel", status.toString())
})
This code helps me to listening Parent ViewModel changes in fragment.
Just for those who are confused between definitions of SharedViewModel vs Making two fragments use one View Model:
SharedViewModel is used to share 'DATA' (Imagine two new instances being created and data from view model is being send to two fragments) where it is not used for observables since observables look for 'SAME' instance to take action. This means you need to have one viewmodel instance being created for two fragments.
IMO: Google should somehow mention this in their documentation since I myself thought that under the hood they are same instance where it is basically not and it actually now makes sense.
EDIT : Solution in Kotlin: 11/25/2021
In Your activity -> val viewModel : YourViewModel by viewModels()
In Fragment 1 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
In Fragment 2 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
This Way 2 fragments share one instance of Activity viewmodel and both fragments can use listeners to observe changes between themselves.
When you create fragment instead of getting viewModel object by viewModels() get it from activityViewModels()
import androidx.fragment.app.activityViewModels
class WeatherFragment : Fragment(R.layout.fragment_weather) {
private lateinit var binding: FragmentWeatherBinding
private val viewModel: WeatherViewModel by activityViewModels() // Do not use viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentWeatherBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
// Observing for testing & Logging
viewModel.cityName.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onCreateView() | City name changed $it")
})
return binding.root
}
}
Kotlin Answer
Remove these two points in your function if you are using:
= viewModelScope.launch { }
suspend

Categories

Resources