So I have a bit of a complex flow. For navigation, we're using Jetpack navigation component.
So I have a Fragment A that lives in module A. When a button is hit it likes to Fragment B in Module B.
From there Fragment B will open up a webview that will deep link to a bottom sheet fragment C in module B.
I need to send the result from Fragment C back to Fragment A.
Fragment C will pass a value to Fragment B
Fragment A emission:
findNavController().previousBackStackEntry?.savedStateHandle?.set<Boolean>(AppsAndDevicesFragment.CONNECTED_CLOUD_DEVICE, true)
Fragment B watches:
override fun onAttach(context: Context) {
super.onAttach(context)
findNavControllerSafely()?.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>(
CONNECTED_CLOUD_DEVICE
)?.observeForever { result ->
findNavController().previousBackStackEntry?.savedStateHandle?.set(
WEARABLE_CONNECTION_OBSERVABLE, result)
}
}
Fragment B does properly trigger the observer.
However in Fragment A, it never fires, and every time I try and fetch the value, it's null
Fragment A listener:
override fun onAttach(context: Context) {
super.onAttach(context)
val hasConnected = findNavControllerSafely()?.currentBackStackEntry?.savedStateHandle?.get<Boolean>(AppsAndDevicesFragment.WEARABLE_CONNECTION_OBSERVABLE)
if (hasConnected == true) {
viewModel.addProgramToJourney(CampaignMode.AUTOMATIC)
}
findNavControllerSafely()?.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>(
AppsAndDevicesFragment.WEARABLE_CONNECTION_OBSERVABLE
)?.observeForever { result ->
if (result) {
viewModel.addProgramToJourney(CampaignMode.AUTOMATIC)
}
findNavControllerSafely()?.currentBackStackEntry?.savedStateHandle?.remove<Boolean>(
AppsAndDevicesFragment.WEARABLE_CONNECTION_OBSERVABLE
)
}
}
I don't understand why Fragment A never gets the value.
Related
I want to show some common snackbar by context.
When the context is Activity, Fragment or DialogFragment, I want to handle by them.
So I wrote codes like below, but it doesn't work at Fragment or DialogFragment.
I can only check that the context is activty.
I don't understand why context is fragment is not working.
fun Context.showSnackBar(text: String) {
when(this) {
is Fragment, // not working
is DialogFragment -> { // not working
showSnackBar(text)
}
is Activity -> {
showSnackBar(text)
}
}
}
usecase updated
1. RootApplication.context.showSnackBar()
2. passed paramter context -> fun foo(context: Context){context.showSnackBar()}
3. binding View's context -> binding.text.context.showSnackBar()
Let's assume a fragment has this ActivityResultLauncher:
class MyFragment : Fragment(R.layout.my_fragment_layout) {
companion object {
private const val EXTRA_ID = "ExtraId"
fun newInstance(id: String) = MyFragment().apply {
arguments = putString(EXTRA_ID, id)
}
}
private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
Timber.i("Callback successful")
}
}
...
This Fragment a wrapped in an Activity for temporary architectural reasons, it will eventually be moved into an existing coordinator pattern.
class FragmentWrapperActivity : AppCompatActivity() {
private lateinit var fragment: MyFragment
private lateinit var binding: ActivityFragmentWrapperBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFragmentWrapperBinding.inflate(this)
setContentView(binding.root)
fragment = MyFragment.newInstance("blah")
supportFragmentManager.transact {
replace(R.id.fragment_container, fragment)
}
}
}
And we use that launcher to start an Activity, expecting a result:
fun launchMe() {
val intent = Intent(requireContext(), MyResultActivity::class.java)
launcher.launch(intent)
}
On a normal device with plenty of available memory, this works fine. MyResultActivity finishes with RESULT_OK, the callback is called and I see the log line.
However, where memory is an issue and the calling fragment is destroyed, the launcher (and its callback) is destroyed along with it. Therefore when MyResultActivity finishes, a new instance of my fragment is created which is completely unaware of what's just happened. This can be reproduced by destroying activities as soon as they no longer have focus (System -> Developer options -> Don't keep activities).
My question is, if my fragment is reliant on the status of a launched activity in order to process some information, if that fragment is destroyed then how will the new instance of this fragment know where to pick up where the old fragment left off?
Your minimal fragment is unconditionally replacing the existing fragment with a brand new fragment everytime it is created, thus causing the previous fragment, which has had its state restored, to be removed.
As per the Create a Fragment guide, you always need to wrap your code to create a fragment in onCreate in a check for if (savedInstanceState == null):
In the previous example, note that the fragment transaction is only created when savedInstanceState is null. This is to ensure that the fragment is added only once, when the activity is first created. When a configuration change occurs and the activity is recreated, savedInstanceState is no longer null, and the fragment does not need to be added a second time, as the fragment is automatically restored from the savedInstanceState.
So your code should actually look like:
fragment = if (savedInstanceState == null) {
// Create a new Fragment and add it to
// the FragmentManager
MyFragment.newInstance("blah").also { newFragment ->
supportFragmentManager.transact {
replace(R.id.fragment_container, newFragment)
}
}
} else {
// The fragment already exists, so
// get it from the FragmentManager
supportFragmentManager.findFragmentById(R.id.fragment_container) as MyFragment
}
I attached bottom navigation to my app and call changeFragment. When I call changeFragment, for example, A fragment to B fragment, I expected to be called onStop at A fragment. However, Any lifecycle callback isn't called.. Why is happened?
THIS IS MY CODE..
private fun setBottomNavigation() {
binding.bottomNavigation.run {
setOnItemSelectedListener { menu ->
when (menu.itemId) {
R.id.navigation_menu_main ->
changeFragment(mainFragment, MainFragment.TAG, menu.itemId)
R.id.navigation_menu_money ->
changeFragment(moneyFragment, MoneyFragment.TAG, menu.itemId)
R.id.navigation_menu_life ->
changeFragment(lifeFragment, LifeFragment.TAG, menu.itemId)
R.id.navigation_menu_food ->
changeFragment(foodFragment, FoodFragment.TAG, menu.itemId)
R.id.navigation_menu_menu ->
changeFragment(menuFragment, MenuFragment.TAG, menu.itemId)
else -> false
}
}
selectedItemId = R.id.navigation_menu_main
}
}
private fun changeFragment(fragment: Fragment, tag: String, menuId: Int): Boolean {
supportFragmentManager.commit {
hide(currentFragment)
showFragment(fragment, tag)
}
currentFragment = fragment
return true
}
private fun FragmentTransaction.showFragment(fragment: Fragment, tag: String) {
supportFragmentManager.executePendingTransactions()
if (fragment.isAdded) {
show(fragment)
} else {
add(binding.container.id, fragment, tag).show(fragment)
}
}
You can use add() and replace() to change fragments in your activity. And the difference between add and replace is that:
add simply add another fragment to the fragment container and does not destroy the existing fragments so they remain active and lifecycle events for those existing fragments are not called.
replace removes the existing fragments to add a new fragment which means lifecycle events like onPause, onStop, and onCreateView will be invoked.
I have a fragment A which sends a search query to the network, and if the result is positive uses Android navigation component to navigate to fragment B, and its done using observers.
After navigation to fragment B, i click on "<-" arrow on the top of the screen, but instead of navigating back to fragment A it reloads fragment B again. And if using the native "back" button on the device, the app crashes with "illegalArgumentException navigation destination unknown" error.
I check the internet for clues on this issue, but all i learned is that this happens because i am using .observe in onViewCreated() and when i go back, it gets called again, and because livedata has something in it already, it just navigates me back to B.
I have tried observing in onActivityCreated(), and using getViewLifeCycleOwner, but no success... the only thing that helped is checking if livedata has observers and returning if true, before using .observe, but it seems incorrect.
This is the viewModel:
private val getAssetResult = MutableLiveData<GeneralResponse<Asset>>()
private val updateAssetResult = MutableLiveData<GeneralResponse<Int>>()
private val deleteAssetResult = MutableLiveData<GeneralResponse<Int>>()
init {
state.value = ViewState(false)
Log.d(TAG, "State in init: $state")
}
fun getAssetResult(): LiveData<GeneralResponse<Asset>>{
return getAssetResult
}
fun findAsset(req: GetAssetRequest) {
scope.launch {
setProgressIndicator(true)
val result = repository.getAsset(req)
getAssetResult.postValue(result)
setProgressIndicator(false)
}
}
This is the fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(EditAssetViewModel::class.java)
setupViewModel()
initFields()
}
private fun setupViewModel() {
if (viewModel.getAssetResult().hasObservers()) // <- This is the part that prevents the app from crashing.
return
viewModel.getAssetResult().observe(this, Observer {
if (it == null) return#Observer
handleSearchResult(it)
})
if (viewModel.getState().hasObservers())
return
viewModel.getState().observe(this, Observer { handleState(it) })
}
private fun handleSearchResult(response: GeneralResponse<Asset>) {
if (response.singleValue == null) {
Toast.makeText(context!!, response.errorMessage, Toast.LENGTH_SHORT).show()
return
}
targetFragment?.let { it ->
val bundle = bundleOf("asset" to response.singleValue)
when(it) {
"UpdateLocation" ->
Navigation.findNavController(view!!).navigate(R.id.updateLocation, bundle)
"EditAsset" -> {
Navigation.findNavController(view!!).navigate(R.id.editAsset, bundle)
}
}
}
}
if i remove this part from the setupViewModel function:
if (viewModel.getAssetResult().hasObservers())
return
the app will either crash when clicked "back" using the device button or go back to fragment A, just to be navigated back to fragment B because of the .observe function.
Override the method onBackPressed() to handle the "<-" arrow
Seems like the LiveData that you use to signal to fragment A that it should navigate to fragment B is actually an event. An event happens only once and once it is consumed (navigation event is done), it is gone. Therefore, after navigating you need to send a message to the viewmodel that the navigation took place and that the corresponding data holder should be (e.g.) null again. In Fragment A you check that the new value is unequal to null, and only if this is the case, you issue the navigation event. This would prevent fragment A to immediatelly switch to B again in the back scenario.
If you want to learn more about ways to use live data for events, please refer to this article.
I have an activity with two fragments.
The second one is called when I click on something to the first.
What I want is this : if i click on "back" button, I want to go back to the first fragment (that is working), but I want to set the visibility to VISIBLE on an element (if the first fragment is called with back press only)
How do I do that ?
I tried something like this (in my main fragment), I've found the idea in another topic, but this is trigger always in my main activity :
override fun onResume() {
super.onResume()
view?.isFocusableInTouchMode = true
view?.requestFocus()
view?.setOnKeyListener { v, keyCode, event ->
if(event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
Log.i(TAG, "backpress pressed")
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
Temporary solution :
I've created a companion object with a value true or false and I change it everytime I need it, but it's temporary only.
Assuming your second Fragment replaces the first (i.e. using FragmentTransaction#replace), your first Fragment (we'll call them FragmentA and FragmentB) will be paused (i.e. onPause() will be called on FragmentA).
When you press the back button, the backstack will be popped, and FragmentA will be resumed (i.e. onResume() will be called).
What I would recommend, is to save a boolean flag in FragmentA, and set it to true when you show FragmentB. Then, in FragmentA#onResume, you can check if the flag is set to true, and set it back to false while handing the case that you wanted.
For example, something like:
private const val STATE_WAITING_FOR_FRAGMENT_B = "state_waiting_b"
class FragmentA : Fragment() {
private var isWaitingForFragmentB: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
isWaitingForFragmentB = savedInstanceState.getBoolean(STATE_WAITING_FOR_FRAGMENT_B)
}
}
override fun onResume() {
super.onResume()
if (isWaitingForFragmentB) {
isWaitingForFragmentB = false
// handle your view state here
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(
STATE_WAITING_FOR_FRAGMENT_B,
isWaitingForFragmentB
)
}
private fun showFragmentB() {
isWaitingForFragmentB = true
// do fragment transaction here
}
}
I'm not good at grammar.
First fragment do not call resume function when returning.
You must create callback with interface.
A good approach should be passing some flag, on the second fragment, by activity intent and to capture it on the first Fragment on onResume()
If you need extra info, just let me know