I'm starting bottomSheetDialogFragment from a fragment A.
I want to select the date from that bottomSheetDialogFragment then set it in the fragment A.
The select date is already done, I just want to get it in the fragment A to set it in some fields.
How can I get the value?
Any suggestions how to do it?
Create an interface class like this
public interface CustomInterface {
public void callbackMethod(String date);
}
Implement this interface in your Activity or Fragment. and make an object of this Interface.
private CustomInterface callback;
Initialize it in onCreate or onCreateView
callback=this;
Now pass this callback in your BottomSheetDialogFragment constructor when you call it.
yourBottomSheetObject = new YourBottomSheet(callback);
yourBottomSheetObject.show(getSupportFragmentManager()," string");
Now in your BottomSheetFragment's constructor
private CustomInterface callback;
public SelectStartTimeSheet(CustomInterface callback){
this.callback=callback;
}
And at last use this callback object to set your date
callback.callbackMethod("your date");
and yout will recieve this date in your Fragment or Your Activity in callbackMethod function.
override the constructor of a fragment is a bad practice as the document said:
Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state.
if you using another constructor that passing a callback as the param, when the fragment is resotored by the framework, your app crash
the recommend way is using viewModel and livedata.
Android navigation architecture component
eg:
Suppose you open Fragment B from Fragment A using navController.
and you want some data from fragment B to Fragment A.
class B :BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.your_layout, container, false)
root.sampleButton.setOnClickListener {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("your_key", "your_value")
dismiss()
}
}
and in your Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("your_key")
?.observe(viewLifecycleOwner) {
if (it == "your_value") {
//your code
}
}
you can use do as below:
Select Account Fragment code
class SelectAccountFragment(val clickListener: OnOptionCLickListener) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_fragment_accounts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = DataProcessorApp(context).allUsers
val rvAccounts = view.findViewById<RecyclerView>(R.id.rvAccounts)
rvAccounts.layoutManager = LinearLayoutManager(context)
rvAccounts.adapter = AccountsAdapter(context, list)
Log.e(tag,"Accounts "+list.size);
tvAccountAdd.setOnClickListener {
val intent = Intent(context,LoginActivity::class.java)
startActivity(intent)
}
tvManageAccounts.setOnClickListener {
Log.e(tag,"Manage Click")
clickListener.onManageClick()
}
}
interface OnOptionCLickListener{
fun onManageClick()
}
}
Now show and get call back into another fragment /activity as below
SelectAccountFragment accountFragment = new SelectAccountFragment(() -> {
//get fragment by tag and dismiss it
BottomSheetDialogFragment fragment = (BottomSheetDialogFragment) getChildFragmentManager().findFragmentByTag(SelectAccountFragment.class.getSimpleName();
if (fragment!=null){
fragment.dismiss();
}
});
accountFragment.show(getChildFragmentManager(),SelectAccountFragment.class.getSimpleName());
If you are using BottomSheetDialogFragment , since it's a fragment, you should create your interface and bind to it at onAttach lifecycle method of the fragment , doing the appropriate cast of activity reference to your listener/callback type.
Implement this interface in your activity and dispatch change when someone click in a item of fragment's inner recyclerview, for instance
It's a well known pattern and are explained better at here
One big advice is rethink your app architecture, since the best approach is to always pass primitive/simple/tiny data between Android components through Bundle, and your components are able to retrieve the required state with their dependencies later on.
For example, you should never pass along large Objects like Bitmaps, Data Classes , DTO's or View References.
first there is some serialization process going on regarding Parcel which impacts in app responsiveness
second it can lead you to TransactionTooLarge type of error.
Hope that helps!
You can also use LocalBroadcastManager. And as hglf said, it is better to keep the empty constructor for your fragment and use newInstance(Type value) instead to instantiate your fragment if you still want to use the interface callBack way.
You can use the benefit of Navigation library:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController();
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")) {
val result = navBackStackEntry.savedStateHandle.get<String>("key");
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
For more info, read the document.
The accepted answer is wrong.
What you can do is just user Fragment A's childFragmentManager when calling show().
like this:
val childFragmentManager = fragmentA.childFragmentManager
bottomSheetDialogFragment.show(childFragmentManager, "dialog")
Related
I have a custom dialog with X button, that dialog is called in a Fragment and here i have to manage all the clicks from the Alert.
Which is the best way to do so?
I actually was going to set the click listeners in the DialogFragment but i have to change some layout stuff and set variables from my Fragment so it will be better if i manage it from the fragment directly.
Here is my code now:
class ElencoDialog(private val testata: Testata, private val elimina: Boolean): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
}
}
}
And here is my fragment where i show the dialog:
class ElencoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false).show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true).show(parentFragmentManager, "ElencoDialog")
})
}
}
So instead of managing the click from the DialogFragment how can i manage the clicks directly from my Fragment?
First of all it is bad practice to have anything apart from the default constructor in a DialogFragment (or any Fragment). Although this might work initially, the system might need to recreate the fragment for various reasons, rotation, low memory etc. and it will attempt to use an empty constructor to do so. You should instead be using fragment arguments to pass simple data (covered in another question), a ViewModel for more complex data (I prefer this method anyway) or the new fragment results API, which I've outlined below.
But in answer to your specific question, to interact between your dialog fragment and main fragment, you have a few options:
Target fragment
You can set your original fragment as a target of your dialog but using setTargetFragment(Fragment). The fragment can then be retrieved safely from your dialog using getTargetFragment. It would probably be best practice to have your fragment implement an interface which you can cast to has the relevant callback methods.
Fragment results API
This is a relatively new API that attempts to replace the above, you can read more about it here: Communicating between fragments.
ViewModel
You can use a shared ViewModel scoped to the activity or parent fragment and keep your state in there. This would also solve the problem of having to pass your initial state through your fragment constructor. I won't explain how they work here as that's another question, but I would take a look here: ViewModel overview.
Pass callBack from fragment to your Dialog Fragment
class ElencoFragment() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
})
}
}
Your Dialog Fragment
class ElencoDialog(private val testata: Testata, private val elimina: Boolean, block : () -> Unit): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
block()
}
}
}
I'm trying to develop an app where I have spinner in fragment(first fragment), to update spinner options user has to open a dialog window(second fragment) and put input in there. Data is passed through interface as a bundle and then into first fragment arguments. My spinner is in first fragment view so I couldn't figure out how to call a first argument view function from second argument, instead inside first argument view I constantly check if the arguments are updated, something like this
if(!displayedText.isNullOrEmpty()) {
updateListSpinner()
arguments?.clear()
}
so in fragment view I constantly check if arguments were updated and then I clear them, everything works as it should but I just wonder what's better way of doing it.
You can use Fragment Result Listeners. More documentation: Communicating between fragments. So in your instance, whenever the user interacts with the dialog fragment, send a result to the first fragment, which will be registered to use the data and you can update your spinner.
Here is some working code that controls navigation in my app:
In second fragment:
open fun navigate(idOfDestination: Int, bundle: Bundle?) {
val tempBundle = bundle ?: Bundle()
tempBundle.putInt(NAVIGATION_DESTINATION, idOfDestination)
activity?.supportFragmentManager?.setFragmentResult(NAVIGATION_RESULT, tempBundle)
}
In target fragment (which in your case would be the first fragment), register the listener:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.supportFragmentManager?.setFragmentResultListener(NAVIGATION_RESULT, this, FragmentResultListener { _, bundle ->
val destination = bundle.getInt(NAVIGATION_DESTINATION)
bundle.remove(NAVIGATION_DESTINATION)
navigationHandler(bundle,destination)
})
}
private fun navigationHandler(bundle: Bundle?, idOfDestination: Int) {
navController = Navigation.findNavController(binding.root)
if (!FeatureControlManager.isDestinationAllowedToGo(idOfDestination)) {
navController = childFragmentManager.fragments.first().findNavController()
}
navController.validateAndNavigate(navController, idOfDestination, childFragmentManager, bundle)
}
Is there any way to make a shared transition between fragment1 inside activity1 and fragment2 inside activity2?
I have tried achieving this like so:
val intent = Intent(this, RecipeActivity::class.java)
intent.putExtra("recipeId", recipeId)
val elem1 =
Pair<View, String>(itemView.findViewById(R.id.recipe_preview), "preview")
val elem2 =
Pair<View, String>(itemView.findViewById(R.id.recipe_title), "title")
val elem3 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating_stars), "rating_stars")
val elem4 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating), "rating")
val elem5 =
Pair<View, String>(itemView.findViewById(R.id.recipe_description), "description")
val elem6 =
Pair<View, String>(itemView.findViewById(R.id.author_avatar), "avatar")
val options =
ActivityOptionsCompat.makeSceneTransitionAnimation(
this, elem1, elem2, elem3, elem4, elem5, elem6
)
startActivity(intent, options.toBundle())
But that didn't work so well. Do I have to redesign my app so those two fragments will be inside a single activity or is there any workaround? Thank you
The idea: Pause the transaction until the targed fragment is fully loaded, created and is about to be drawn. Then continue the transaction.
The code:
Everything you do in your first activity is ok and we won't touch it.
The first thing your activity has to is to stop the transaction. Therefore you need to call supportPostponeEnterTransition() in onCreate() of your second activity. This will tell android to wait with the transaction until you tell it to start it.
Secondly you need to know when the fragment is about to be drawn. In my use case I display some fragments in a ViewPager what makes things a lot easier, as you can add an ViewTreeObserver to it and wait until the ViewPager is loaded because you know that at this point the fragments are already created and basically drawn even if you can't see them. When using frgaments the normal way with transaction you need some trickery.
Important: Everything from now on is not tested but in theory it should work.
In your fragment you do something like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
Instead you to do it like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
return view
}
You need the root view of your fragment because we will add the ViewTreeObserver to it.
But before you to that you need to create an interface in your FragmentClass or add the method to your preexisting one:
interface FragmentListener {
fun resumeTransaction()
}
You need to implement it in your activity:
override fun resumeTransaction() {
supportStartPostponedEnterTransition()
}
In your fragment we need to get the Activity as listener. In the onAttach do following:
try {
// Instantiate the FragmentListener so we can send the event to the host
listener = context as FragmentInterface
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() + " must implement FragmentInterface"))
}
Now we get back to the ViewTreeObserver. In the onCreateView you do this:
viewPager.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
listener.resumeTransaction()
}
}
)
If I didn't forget anything this should work. If not please tell me, I will then try to make an example app later this day as I don't have more time now.
I'm using Navigation Components, I have Fragment A and Fragment B, from Fragment A I send an object to Fragment B with safe args and navigate to it.
override fun onSelectableItemClick(position:Int,product:Product) {
val action = StoreFragmentDirections.actionNavigationStoreToOptionsSelectFragment(product,position)
findNavController().navigate(action)
}
Now, after some logic in my Fragment B , I want to deliver that data to Fragment A again, which I use
btn_add_to_cart.setOnClickListener {button ->
findNavController().previousBackStackEntry?.savedStateHandle?.set("optionList",Pair(result,product_position))
findNavController().popBackStack()
}
Then in Fragment A, I catch up this data with
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Pair<MutableList<ProductOptions>,Int>>("optionList")
?.observe(viewLifecycleOwner, Observer {
storeAdapter.updateProductOptions(it.second,it.first)
})
Now, this is working fine, but if I go from Fragment A to Fragment B and press the back button, the observer above fires again duplicating my current data, is there a way to just fire this observer when I only press the btn_add_to_cart button from Fragment B ?
You use this extenstion:
fun <T> Fragment.getResult(key: String = "key") =
findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)
fun <T> Fragment.getResultLiveData(key: String = "key"): MutableLiveData<T>? {
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
findNavController().previousBackStackEntry?.savedStateHandle?.remove<T>(key)
}
})
return findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
}
fun <T> Fragment.setResult(key: String = "key", result: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
Example:
FragmentA -> FragmentB
Fragment B need to set the result of the TestModel.class
ResultTestModel.class
data class ResultTestModel(val id:String?, val name:String?)
Fragment A:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// ...
getNavigationResultLiveData<PassengerFragmentResultNavigationModel>(
"UNIQUE_KEY")?.observe(viewLifecycleOwner) { result ->
Log.i("-->","${result.id} and ${result.name}")
}
//...
}
Fragment B: set data and call popBackStack.
ResultTestModel(id = "xyz", name = "Rasoul")
setNavigationResult(key = "UNIQUE_KEY", result = resultNavigation)
findNavController().popBackStack()
Facing same issue
Resolve this by removing old data from savedStateHandle live data
Inside Fragment B :
button?.setOnClickListener {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
findNavController().popBackStack()
}
Inside Fragment A:
Here is key to remove the old data by using live data remove method and it should be after view created like in onViewCreated method of fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)?.observe(viewLifecycleOwner) {
result(it)
findNavController().currentBackStackEntry?.savedStateHandle?.remove<String>(key)
}
}
Update :
I have created Extension for this for better usage
fun <T> Fragment.setBackStackData(key: String, data: T, doBack: Boolean = false) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
if (doBack)
findNavController().popBackStack()
}
fun <T> Fragment.getBackStackData(key: String, singleCall : Boolean= true , result: (T) -> (Unit)) {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
?.observe(viewLifecycleOwner) {
result(it)
//if not removed then when click back without set data it will return previous data
if(singleCall) findNavController().currentBackStackEntry?.savedStateHandle?.remove<T>(key)
}
}
Calling inside fragment be like
While setting data in fragment B
var user : User = User(data) // Make sure this is parcelable or serializable
setBackStackData("key",user,true)
While getting data inside fragment A
getBackStackData<User>("key",true) { it ->
}
Thanks to This Guy
It is not clear from your code where your last piece of code is called - where you add an Observer to LiveData. I am guessing it is inside one of the methods onResume() or onViewStateRestored() or any other lifecycle callback which is called again whenever you return to Fragment A from Fragment B. If that is the case, then you are adding a new observer to the LiveData and any observer of a LiveData receives an instant update for the current value.
Move that piece of code to one of the callbacks methods which is called only once during the lifecycle of a fragment.
https://stackoverflow.com/a/66111168/8354145
This answer should help in this case. Use SingleLiveEvent.
Otherwise in these cases, maybe using a shared view model (might be scoped to the nav graph) however you won't need to use savedStateHandle.
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