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()
Related
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
}
There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?
// ParentFragment
private fun save() {
val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
dialog.show(parentFragmentManager, "is_save_dialog")
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
}
class ContinueDialogFragment(
#StringRes private val titleStringId: Int,
#StringRes private val messageStringId: Int? = null
) : DialogFragment() {
private val _resultSharedFlow = MutableSharedFlow<Int>(1)
val resultSharedFlow = _resultSharedFlow.asSharedFlow()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { context ->
AlertDialog.Builder(context)
.setTitle(getString(titleStringId))
.setMessage(messageStringId?.let { getString(it) })
.setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_YES)
}
.setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_NO)
}
.setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_CONTINUE)
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
companion object {
const val RESULT_YES = 1
const val RESULT_NO = 0
const val RESULT_CONTINUE = 2
}
}
When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.
A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.
Or more simply, you could use first() instead of collect { }.
Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.
It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.
DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.
I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
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.
I have to use this peice of code twice in two different places in two different activites. No good programmer would willingly want to use same code in multiple places without reusing it.
//when back key is pressed
override fun onBackPressed() {
dialog.setContentView(twoBtnDialog.root)
twoBtnDialog.title.text = getString(R.string.warning)
twoBtnDialog.msgDialog.text = getString(R.string.backPressWarning)
twoBtnDialog.ok.text = getString(R.string.exit)
twoBtnDialog.cancel.text = getString(R.string.cancel)
twoBtnDialog.ok.setOnClickListener {
//do nav back
finish()
dialog.dismiss()
}
twoBtnDialog.cancel.setOnClickListener {
dialog.dismiss() //just do nothing
}
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
}
I can move it to one place, but the problem is I have to pass in the finish() function from Activity Class to close the calling activity.
My simple question is how can I resue it ? Or How can I pass this function (finish()) to a different class (which is in some other file).
Take a function type parameter in your method.
fun doBackPress(finish: () -> Unit) {
// you need to invoke the finish method when necessary
finish.invoke()
}
Then you need to call the method and have to pass the finish() method from any other activity or fragment method like bellow.
override fun onBackPressed() {
doBackPress { finish() }
}
You could make an interface and extension function, which I think is less messy than trying to pass everything you need as parameters to a function, because it communicates intent better and makes it harder to do something wrong.
interface MyDialogOwner {
val dialog: Dialog
val twoBtnDialog: MyDialogBinding
fun Activity.handleBackPress() {
//the exact same content you have in your function now.
}
}
// In Activity:
override fun onBackPressed() = handleBackPress()
Your Activities should implement the interface, using your existing properties for dialog and twoBtnDialog (just add override in front of their declarations).
I'm assuming twoBtnDialog is a view binding.
I want to test DialogFragment using androidx.fragment:fragment-testing lib.
I call launchFragmentInContainer and moveToState(Lifecycle.State.RESUMED), but onCreateDialog is not called in this fragment.
#Test
fun `submit search - presenter state is changed`() {
val p: PinCatsPresenter = F.presenter(PinCatsPresenter.COMPONENT_ID)!!
launchFragmentInContainer<PinCatsDialog>().let { scenario ->
scenario
.moveToState(Lifecycle.State.RESUMED)
.onFragment { fragment ->
assertFalse(p.state.isFiltered)
fragment.dialog!!.findViewById<SearchView>(R.id.search_field).let {
it.isIconified = false
it.setQuery("ea", true)
}
awaitUi()
assertTrue(p.state.isFiltered)
assertEquals(3, p.state.count)
}
}
}
I debug the app, and ensured that onCreateDialog is called earlier than onResume, but in this test scenario onCreateDialog is not called, so fragment.dialog is null.
What should I call onFragmentScenario so my dialog would be created?
This is described in the official documentation. We need to call launchFragment instead of launchFragmentInContainer:
launchFragment<PinCatsDialog>().let { scenario ->
scenario
.moveToState(Lifecycle.State.RESUMED)
.onFragment { fragment ->
// Code here
}
}