I have following logic in my Activity :
protected abstract val fragment: BaseSearchFragment<T>
override fun onCreate(savedInstanceState: Bundle?) {
...
replaceFragmentInActivity(fragment, R.id.fragment_container)
}
Now when I rotate the device fragment Callbacks get called twice such as onCreate in the fragment, since I replaced the fragment in Activity.
I want to use onSaveInstanceState in the fragment, but since it is called twice the logic will not work as expected since for the 2nd fragment creation, savedInstanceState will be null.
If I use following in the Activity :
if(savedInstanceState == null) {
addFragmentToActivity(fragment, R.id.fragment_container)
}
I will face with another problem since I have following in the activity :
searchView.setOnQueryTextListener(object : OnQueryTextListener {
override fun onQueryTextChange(query: String): Boolean {
fragment.search(query)
return true
}
}
And here is fragment search method :
fun search(query: String) {
if (searchViewModel.showQuery(query)) {
binding.recyclerView.scrollToPosition(0)
tmdbAdapter.submitList(null)
}
}
Here when I am rotating the device I receive following exception :
java.lang.IllegalStateException: Can't access ViewModels from detached fragment
So, is there any solution that when I rotate the device fragment Callbacks get called just once?
You can check the source code here : https://github.com/alirezaeiii/TMDb-Paging
Related
All the Fragment inherits from BaseFragment.
And I'd like to give back press event respectively to each fragment. But BaseFragment has default back press event like this.
override fun onResume() {
super.onResume()
logd("onResume() BaseFragment")
requireActivity().onBackPressedDispatcher.addCallback(this, backPressCallback)
}
And also the child fragments have
override fun onResume() {
super.onResume()
logd("onResume() ChildFragment")
requireActivity().onBackPressedDispatcher.addCallback(this, backPressCallback)
}
It will print,
onResume() BaseFragment
onResume() ChildFragment
So, the ChildFragment override and when I press back button, ChildFragment's backPressCallback is called.
However, when I go out and come back, the order is different.
onResume() ChildFragment
onResume() BaseFragment
So, the user sees ChildFragment but BaseFragment's backPressCallback is called. And it behaves different from what I expected. (ex. I want popBackStack but close the app)
How can I solve this problem? Or is there any method that called after the parent fragment is called?
According to this article, BaseFragment must be called before ChildFragment. But it doesn't seem so.
activity?.onBackPressedDispatcher?.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
try {
if (!isChildFragmentOpen)
{
val trans: FragmentTransaction =
parentFragmentManager.beginTransaction()
trans.remove(Fragment())
trans.commit()
}else{
parentFragmentManager.popBackStack()
}
} catch (e: Exception) {
e.message
}
}
})
add this to your fragment.
I have one MainActivity with -> FragmentA which contains -> ViewPager with (Fragment1,Fragment2,Fragment3)
Now in FragmentA I have one spinner and any selection must reflect the changes inside viewpager's currently visible fragment.
How can I achieve that? I don't want to follow ViewModel or EventBus approach for now as I am working on very old project. I want to use interface to communicate between them.
Create an interface inside your FragmentA
interface OnSpinnerValue{
fun onSpinnerValueChanged()
}
Create a WeakReference for the current selected fragment
private var _currentPage: WeakReference<OnSpinnerValue>? = null
private val currentPage
get() = _currentPage?.get()
fun setCurrentPage(page: OnSpinnerValue) {
_currentPage = WeakReference(page)
}
Now implement this interface in every child fragment of ViewPager
class Fragment1() : Fragment(), OnAddEvent {
override fun onSpinnerValueChanged() {
// implement your method
}
}
And, update currentPage value of the FragmentA, according to the selected fragment, and update it in onResume() of each child fragment
override fun onResume() {
super.onResume()
(parentFragment as FragmentA).setCurrentPage(this)
}
Now, trigger onSpinnerValueChanged from your spinner's onItemSelected methods
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
currentPage?.onSpinnerValueChanged()
}
It is strange if you don't want to use ViewModel ,
I know may be it is not best solutions but you can :
you can create function itIsYourFunctionToUpdate() inside your fragment and update him on FragmentA() , calling function with her object like fragmentB.itIsYourFunctionToUpdate()
or
if (ViewPager.getCurrentItem() == 0 && page != null) {
((FragmentClass1)page).itIsYourFunctionToUpdate("new item");
}
Also you can update viewPager like this mAdapter.notifyDataSetChanged() and all fragment inside viewPager must be updated
In short: when Observe is active it works correctly when I do notify, but when I go back to the previous fragment (I use the navigation component) and again navigate to the current fragment, there is a creation of the fragment, and for some reason the Observe is called.
Why is the Observe not deleted when going back? It should behave according to the fragment's lifecycle.
I tried removing on onStop and still the observe called.
More detail:
Each of my project fragments is divided into 3 parts: model, viewModel, view
In the view section, I first set the viewModel.
class EmergencyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
emergencyFragmentViewModel = ViewModelProviders.of(this).get(EmergencyFragmentViewModel::class.java)
}
And in onViewCreated I set the Observer object so that any changes made in LiveData I get a change notification here:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.observe(viewLifecycleOwner, Observer {
Log.d("sendEmergencyEvent", "isEmergencyEventMediaLDSuccess observer called")
}
})
}
In the ViewModel class, I set the LiveData parameter as follows:
EmergencyFragmentViewModel: ViewModel() {
var isEmergencyEventMediaLDSuccess: LiveData<Boolean> = MutableLiveData()
private set
private val observerEventMedia = Observer<Boolean> { (isEmergencyEventMediaLDSuccess as MutableLiveData).value = it}
And in the init I set an observer:
init {
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.observeForever(observerEventMedia)
}
And of course removes when needed
override fun onCleared() {
super.onCleared()
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.removeObserver(observerEventMedia)
}
The part of the model is defined as follows:
class EmergencyFragmentModel {
companion object{
val isEmergencyEventMediaLDSuccessModel: LiveData<Boolean> = MutableLiveData()
And I do request network and when a reply comes back I perform a notify
override fun onResponse(call: Call<Int>, response: Response<Int>) {
if(response.isSuccessful) {
(isEmergencyEventLDModelSuccess as MutableLiveData).postValue(true)
Log.d("succeed", "sendEmergencyEvent success: ${response.body().toString()}")
}
Can anyone say what I'm missing? Why when there is an active Observe and I go back to the previous fragment (I use the navigation component) and navigate to the current fragment again, the Observe is called? I can understand that when a ViewModel instance is created and it executes setValue for the LiveData parameter, then it is notified. But Why is the observe not removed when I go back? I tried removing the Observe on the onStop and it keeps happening.
override fun onStop() {
super.onStop()
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObservers(viewLifecycleOwner)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObserver(observeEmergencyEventLDSuccess)
}
#Pawel is right. LiveData stores the value and everytime you observe it (in your onViewCreated, in this case), it'll emit the last value stored.
Maybe you want something like SingleLiveEvent, which clean its value after someone reads it.
So when you go back and forth, it won't emit that last value (once it was cleaned).
As I understand your question, you only want to run the observer, when the new value differs from the old one. That can be done by retaining the value in another variable in the viewModel.
if (newValue == viewModel.retainedValue) return#observe
viewModel.retainedValue = newValue
I fixed this by creating an extension in kotlin by checkin the lifecycle state.
fun <T> LiveData<T>.observeOnResumedState(viewLifecycleOwner: LifecycleOwner, observer: Observer<T>) {
this.observe(viewLifecycleOwner) {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
observer.onChanged(it)
}
}
}
And here is how i observe
viewModel.result.observeOnResumedState(viewLifecycleOwner) {
// TODO
}
I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working
I am just not able to figure out what is wrong in this code and why the observer is not called when the value is updated. I am using Fragement with livedata and here is the complete code. When app starts fragment gets it value from default data which in this case is 100. But after the value is updated using queueChannelId(channelId) method the observer is not called. I put a print statement and I can see method is executed in main thread. Please help
Fragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel =
ViewModelProviders.of(this).get(SomeViewModel::class.java)
viewModel.getChannelId().observe(this, Observer {
// Only called with default value of mutablelivedata
})
}
I can assure that onDestroyView and onDestroy have not been called anytime.
ViewModel:
fun getChannelId() : MutableLiveData<Int> {
return repository.getChannelId()
}
Repository:
var channelIdObservable = MutableLiveData(100)
fun queueChannelId(channelId: Int) {
channelIdObservable.value = channelId
}
fun getChannelId() : MutableLiveData<Int> = channelIdObservable
if you are calling queueChannelId from some other Thread try
channelIdObservable.postValue (channelId)
P.S: I cant see any other issue here.Share your code of how are u calling queueChannelId.