I want to use CastStateListener in a fragment to check if casting devices are available or not.
Code used in Fragment is
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mCastContext = CastContext.getSharedInstance(mContext)
mCastStateListener = CastStateListener { newState ->
if (newState != CastState.NO_DEVICES_AVAILABLE) {
castDevicesAvailable = true
}
}
}
override fun onResume() {
super.onResume()
mCastContext?.addCastStateListener(mCastStateListener)
}
override fun onPause() {
super.onPause()
mCastContext?.removeCastStateListener(mCastStateListener)
}
This code does not give me a call back inside CastListner when used in Fragment but it works fine when I use it in an Activity or Fragment.
I am using custom view
<androidx.mediarouter.app.MediaRouteButton
android:id="#+id/media_route_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:actionButtonStyle="#android:style/Widget.Holo.Light.MediaRouteButton"/>
I want to hide/show the view based on cast devices available
I don't think it is a good idea to use CastStateListener in a fragment. Because an activity can host multiple fragments. When a activity is paused, all fragments in it are paused, so are resumed. In the code, if you add CastStateListener in fragment onResume and remove CastStateListener in fragment onPause. If the activity hosts multiple fragments, it is very easy to mess up the add/remove CastStateListener. So I think it is better to add/remove the CastStateListener to the activity life cycle
Related
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
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 am trying to follow single activity pattern with android navigation component and my %99 of fragment are portrait but I need to make a new fragment can be portrait or landscape without adding new activity how can I achieve. I could't find any resource. is it possible ? if it is how ?
You can add NavController.OnDestinationChangedListener and set orientation according to the current fragment.
Add this in your activity's onCreate:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.your_nav_host_fragment) as NavHostFragment
navHostFragment.navController..addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.destination_with_orientation) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
The following steps could be useful for you:
Don't lock screen orientation from AndroidManifest.xml.
Register a listener inside Activity on the childFragmentManager of the NavHostFragment, that will execute callbacks on fragment lifecycle change
override fun onCreate(savedInstanceState: Bundle?) {
//...
val fragments = supportFragmentManager.fragments
check(fragments.size == 1) {
val baseMessage = "Expected 1 fragment to host the app's navigation. Instead found ${fragments.size}."
if (fragments.size == 0) {
val suggestion = "(Make sure you specify `android:name=\"androidx.navigation.fragment." +
"NavHostFragment\"` or override setUpNavigationGraph)"
"$baseMessage $suggestion"
} else baseMessage
}
with(fragments[0].childFragmentManager) {
registerFragmentLifecycleCallbacks(CustomFragmentLifecycleCallbacks(), false)
}
}
private inner class CustomFragmentLifecycleCallbacks : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {}
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {}
}
Follow this guide to lock/unlock screen orientation depending upon which Fragment is visible, from the above callback.
NOTE
Fragment tags or instance type could be used for writing conditional statements inside the lifecycle callbacks, based on app's navigation design.
Don't forget to unregisterFragmentLifecycleCallbacks from Activity.onDestroy()
Cheers 🍻
I use navigation component for navigating between my fragments. My app is simple. It has two fragmentsm the first one is list of items and the second one shows details of that Item. When user click on an item, I call
view.findNavController()
.navigate(R.id.action_photosFragment_to_photoDetailsFragment, bundle)
but problem is when I press back button, the first fragment reloads and make a network call again.
override fun onActivityCreated(savedInstanceState: Bundle?) {
photosViewModel.getPhotos()
photosViewModel.photosLiveData.observe(viewLifecycleOwner, photosObserver)
photosViewModel.loadingLiveData.observe(viewLifecycleOwner, loadingObserver)
super.onActivityCreated(savedInstanceState)
}
this block of code calls again! How can I stop reloading?
Use onCreate() in your fragment to call the network:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
photosViewModel.getPhotos()
}
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