How can a BottomSheetDialogFragment communicate with its host fragment? - android

I have a button in my fragment which opens a BottomSheetDialogFragment. I want to notify the host fragment if the user selected an item on the BottomSheetDialogFragment. In order to achieve this, I have made an interface in my BottomSheetDialogFragment. However, that interface only communicates with the host activity, not the fragment. How can I send the information from the dialog to the fragment?
This is my interface:
public interface BottomSheetListener {
void onButtonClicked(int index);
}
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
try {
mListener = (BottomSheetListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement BottomSheetListener");
}
}

getParentFragment will return the parent fragment, if the current fragment is attached to a fragment else it will return null if it is attached directly to an Activity
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
try {
mListener = (BottomSheetListener) getParentFragment();
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement BottomSheetListener");
}
}

When you use a lot of fragments, nested fragments or dialogfragments it becomes messy for communicate between them. I am suggesting to use ViewModel with LiveData for passing and updating data.
first add this to build gradle :
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
then create ViewModel class :
public class YourViewModel extends AndroidViewModel {
private MutableLiveData<Integer> yourMutableLiveData=new MutableLiveData<>();
public YourViewModel(#NonNull Application application) {
super(application);
}
public MutableLiveData<Integer> getYourMutableLiveData() {
return yourMutableLiveData;
}
}
This the fragment you want set value :
public class FragmentA extends Fragment{
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
YourViewModel yourViewModel =new ViewModelProvider(getActivity()).get(YourViewModel.class);
yourViewModel.getYourMutableLiveData().setValue(0);
}
}
And this is the fragment you want to get value when updated :
public class FragmentB extends Fragment{
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
YourViewModel yourViewModel =new ViewModelProvider(getActivity()).get(YourViewModel.class);
yourViewModel.getYourMutableLiveData().observe(getViewLifecycleOwner(), new Observer<Integer>() {
#Override
public void onChanged(Integer integer) {
}
});
}
}
It can work on dialog fragment as well as I tested.
Notes :
-Do not pass context or any view into view model.
-Remember that onActivityCreated comes after onCreateView.
-Do not set this key to
YourViewModel yourViewModel =new ViewModelProvider(this).get(YourViewModel.class);
in fragment if you want to pass data fragment to fragment but you can pass in activity.
-You can set more than one observer to the data.

Related

Extend BottomSheetDialogFragment from a class instantiated in a fragment in Android

I have a fragment (FragmentSearchResults) that contains results retrieved from a database, in which there is a button "filters". When the user taps on such a button, a class (FiltersDialog) extending a BottomSheetDialogFragment is instantiated, so that the user can set his filters. When the user closes the FiltersDialog activity, the values are passed from FiltersDialog to FragmentSearchResults.
public class FragmentSearchResults extends Fragment implements FiltersDialog.FilterListener {
/* code */
ImageButton btnFilter = myFragment.findViewById(R.id.btn_filters);
btnFilter.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
showFilters();
}
});
}
private void showFilters() {
FiltersDialog filtersDialog = new FiltersDialog();
filtersDialog.show(((FragmentActivity) mContext).getSupportFragmentManager(), "argument");
}
#Override
public void onAttach(#NotNull Context context) {
super.onAttach(context);
mContext = context;
}
#Override
public void onFiltersSet(Map filters) {
// apply filters selected by user
}
public interface FilterListener {
void onFiltersSet(Map filters);
}
}
public class FiltersDialog extends BottomSheetDialogFragment {
private FilterListener mListener;
private Map<String, Object> mFilters;
public FiltersDialog() {
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.layout_filters_dialog, container, false);
TextView txtSave = v.findViewById(R.id.txt_save_filters);
mTxtSave.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mListener.onFiltersSet(mFilters);
}
});
return v;
}
public interface FilterListener {
void onFiltersSet(Map filters);
}
#Override
public void onAttach(#NotNull Context context) {
super.onAttach(context);
if (context instanceof FilterListener) {
mListener = (FilterListener) context;
}
else {
// Here's the error, as the activity Home.java containing FragmentSearchResults
// does not implement FilterListener, FragmentSearchResults does
throw new RuntimeException(context.toString() + " must implement FilterListener");
}
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
}
The problem is that FilterListener needs to be implemented in FragmentSearchResults, but I am passing the activity Home context.
How can I implement FilterListener in the fragment?
Why don't you create method inside your FiltersDialog, like
public void setFiltersListener(FiltersDialog.FilterListener listener) {
mListener = listener;
}
and simply call it after you instantiate the dialog.
FiltersDialog filtersDialog = new FiltersDialog();
filtersDialog.setFiltersListener(this);
filtersDialog.show(((FragmentActivity) mContext).getSupportFragmentManager(), "argument");
Then you can use the listener inside dialog. something like this
if (mListener != null) {
mListener.onFiltersSet(mFilters);
}
How can I setup listener to the dialog?
parameter of onAttach in Fragment is FragmentHost(Activity). thus, it can't typecast to FilterListener.
I suggest a simple way to implement FilterListener setter in FragmentDialog as below code.
... in FiltersDialog
public void setListener(FilterListener listener) {
mListener = listener;
}
...
... in FragmentSearchResults
private void showFilters() {
FiltersDialog filtersDialog = new FiltersDialog();
filtersDialog.setListener(this);
filtersDialog.show(((FragmentActivity) mContext).getSupportFragmentManager(), "argument");
}
...
//When FragmentSearchResults recreated, FiltersDialog must also need to be recreated.
A better approach will be to use LiveData, ViewModel in this case. Use Shared ViewModel Approach, An Activity Level ViewModel can be accessed via all the fragments lying in its environment.
Make an Activity Level ViewModel
Define a LiveData in ViewModel
When your "FragmentSearchResults" opens for the first time, start
observing it.
When You open "FiltersDialog" screen and click save button, Then post
to LiveData changes in the filter (You have activity context here,
You can fetch ActivityViewModel here, get LiveData from it, post
changes to this LiveData)
Now As "FragmentSearchResults" is already observing changes in the
LiveData, You will get callback here, make changes accordingly. This way your code will be completely decoupled. You will be escaped from
hustles of Interfaces.

Accessing to other Fragment variables or elements from Fragment

For example I have 2 fragments including 1 integer variable and 1 TextView for each. One of them has a button. I want this button to change all Integers and TextViews including the other fragment. How can I access to Variable and TextView of the other fragment ? Please explain with example code.
Fragment to Fragment Communication basically happens via an activity which generally hosts the Fragments, define an interface in your Fragment A, and let your Activity implement that Interface. Now you can call the interface method in your Fragment, and your Activity will receive the event. Now in your activity, you can call your second Fragment to update the textview(For example) with the received value:
// You Activity implements your interface which is defined in FragmentA
public class YourActivity implements FragmentA.TextClicked{
#Override
public void sendText(String text){
// Get instance of Fragment B using FragmentManager
FraB frag = (FragB)
getSupportFragmentManager().findFragmentById(R.id.fragment_b);
frag.updateText(text);
}
}
// Fragment A defines an Interface, and calls the method when needed
public class FragA extends Fragment{
TextClicked mCallback;
public interface TextClicked{
public void sendText(String text);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (TextClicked) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement TextClicked");
}
}
public void someMethod(){
mCallback.sendText("YOUR TEXT");
}
#Override
public void onDetach() {
mCallback = null; // => avoid leaking
super.onDetach();
}
}
// Fragment B has a public method to do something with the text
public class FragB extends Fragment{
public void updateText(String text){
// Here you have it
}
}

How to inject dependency to nested android fragment?

For an ordinary (non-nested fragment) I use the following approach
1) create dependencies(...) method for setting fragment's dependencies
class MyFragment extends MyFragment {
void dependencies(Deps deps);
}
2) in MyFragment parent's activity onAttachFragment() method I just provide dependencies for fragment
class MyActivity{
void onAttachFragment(Fragment f){
((MyFragment)f).dependencies(deps);
}
}
For nested fragment there is no more onAttachFragment fragment called.
Providing dependencies for fragment just for providing dependencies for nested fragment seems to be very cumbersome. So how could I provide dependencies for it?
Just do it off the context which will be an activity. Create a getter for the dependencies on your activity. Fragments have access to the parent activity whether nested or not. Cast the context and then call the getter to get the dependencies in the nested activity.
If MyFragment depends upon MyNestedFragment, and MyNestedFragment depends upon Deps; it follows that MyFragment also depends upon Deps. Of course, no instance of MyNestedFragment exists when Activity.onAttachFragment() is called, so you will have to wait until after you have inflated the layout in MyFragment.onCreateView() before supplying MyNestedFragment with its dependencies.
public class MyActivity {
...
void onAttachFragment(Fragment f){
((MyFragment)f).dependencies(deps);
}
public static class MyFragment extends Fragment {
private Deps deps;
void dependencies(Deps deps) {
this.deps = deps;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// <fragment> element in fragment_main layout has
// android:tag set to nested_fragment
((MyNestedFragment)getChildFragmentManager()
.findFragmentByTag("nested_fragment"))
.dependencies(this.deps);
return rootView;
}
}
public static class MyNestedFragment extends Fragment {
void dependencies(Deps deps) {
...
}
}
...
}
If all of this seems a bit messy, that's because Fragments are not POJOs you can just wire up in some arbitrary manner. Their lifecycles must be managed by nested FragmentManagers. If you create your fragments programmatically rather than using the <fragment> element, you will have a bit more control over their lifecycle at the cost of more complexity.
If you want to treat Android like an IoC container, then RoboGuice may be what you are looking for:
public class MyActivity extends roboguice.activity.RoboFragmentActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
// This only needs to be called once for the whole app, so it could
// be in the onCreate() method of a custom Application subclass
RoboGuice.setUseAnnotationDatabases(false);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public static class MyNestedFragment extends Fragment {
#Inject
private Deps deps;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// this isn't necessary if you extend RoboFragment
roboguice.RoboGuice.getInjector(activity).injectMembers(this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//This would not even be possible in the previous example
// because onCreateView() is called before dependencies()
// can be called.
deps.method();
View rootView = inflater.inflate(R.layout.fragment_nested, container, false);
return rootView;
}
}
}
#Singleton
public class Deps {
public void method() {
System.out.println("Deps.method()");
}
}
You try to set the dependecies, when the fragments are attached. Instead of this, try to get the dependencies from the fragment when needed. There is an example:
public class MyActivity extends Activity {
public Deps getDepsForFragment(Fragment fragment) {
if (fragment instanceof MyFragment) {
return depsForMyFragment;
} else if (fragment instanceof MyNestedFragment) {
return depsForMyNestedFragment;
} else {
return null;
}
}
}
public class MyFragment extends Fragment {
private Deps deps;
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
MyActivtiy myActivity = (MyActivtiy) context;
deps = myActivity.getDepsForFragment(this);
} catch (ClassCastException e) {
throw new ClassCastException("This fragment attached to an activity which can't provide the required dependencies.");
}
}
}
// this is the same as the MyFragment
public class MyNestedFragment extends Fragment {
private Deps deps;
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
MyActivtiy myActivity = (MyActivtiy) context;
deps = myActivity.getDepsForFragment(this);
} catch (ClassCastException e) {
throw new ClassCastException("This fragment attached to an activity which can't provide the required dependencies.");
}
}
}
Of course, you can make separated method for get deps in the activity (like getDepsForMyFragment and getDepsForMyNestedFragment).
Just keep the hierarchy logics, and it should be something like this:
class MyActivity{
void onAttachFragment(Fragment f){
((MyFragment)f).dependencies(deps);
}
}
class MyFragment extends MyFragment {
void dependencies(Deps deps) {
//TODO: do dependencies of my fragment before
((MyNestedFragment)childF).nestedDependencies(deps);
//TODO: do dependencies of my fragment after
}
}
class MyNestedFragment extends MyNestedFragment {
void nestedDependencies(Deps deps);
}

Android detect when fragment is detached

I can easily detect when Fragments are attached to Activity via Activity.onAttachFragment()
But how can I detect in Activity that some Fragment is detached from activity?
There is no Activity.onDetachFragment()
Is subcclasing Fragment and write some code to notify Activity about that state is the only solution?
you can use interface for communicating between Fragment and Activity
something like this :
public Class MyFragment extends Fragment {
FragmentCommunicator communicator;
public void setCommunicator(FragmentCommunicator communicator) {
this.communicator = communicator;
}
#Override
public void OnDetach() {
communicator.fragmentDetached();
}
...
public Interface FragmentCommunicator {
public void fragmentDetached();
}
}
and in your activity :
public Class MyActivity extends Activity Implements FragmentCommunicator {
...
MyFragment fragment = new MyFragment();
fragment.setCommunicator(this);
...
#Override
public void fragmentDetached() {
//Do what you want!
}
}
Edit:
the new approach is setting interface instance in onAttach.
public void onAttach(Activity activity) {
if (activity instanceof FragmentCommunicator) {
communicator = activity;
} else {
throw new RuntimeException("activity must implement FragmentCommunicator");
}
}
now there is no need to have setCommunicator method.
Mohammad's original answer is close to I would do. He has since updated it to leverage a mechanism provided by Android - Fragment.onAttach(Context context).In that approach, the fragment grabs components (ie, the activity) from the system and calls into it. This breaks inversion of control.
Here is my preferred approach:
public class MyActivity extends AppCompatActivity {
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment instanceof MyFragment) {
((MyFragment) fragment).setListener(mMyFragmentListener);
}
}
private final MyFragment.Listener mMyFragmentListener = new MyFragment.Listener() {
#Override
public void onDetached(MyFragment fragment) {
fragment.setListener(null);
}
// implement other worker methods.
};
}
public class MyFragment extends Fragment {
#Nullable
private Listener mListener;
public void setListener(#Nullable Listener listener) {
mListener = listener;
}
public interface Listener {
void onDetached(MyFragment fragment);
// declare more worker methods here that leverage the connection.
}
#Override
public void onDetach() {
super.onDetach();
if (mListener != null) {
mListener.onDetached(this);
}
}
}
In this solution, the fragment doesn't dictate it's surroundings. Some control is given the to fragment in that it breaks the connection itself. We also already don't own the detaching of the fragment anyways, so clearing the listener is really just cleanup.
Here is an alternative approach that is more explicit, less prone to developer error, but also creates extra boiler plate (I prefer the previous approach because the goodbye handshake feels like an unnecessary distraction):
public static class MyActivity extends AppCompatActivity {
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment instanceof MyFragment) {
((MyFragment) fragment).setListener(mMyFragmentListener);
}
}
private final MyFragment.Listener mMyFragmentListener = new MyFragment.Listener() {
#Override
public void onDetached(MyFragment fragment) {
fragment.setListener(null);
}
// implement other worker methods.
};
}
public static class MyFragment extends Fragment {
#Nullable
private Listener mListener;
public void setListener(#Nullable Listener listener) {
mListener = listener;
}
public interface Listener {
void onDetached(MyFragment fragment);
// declare more worker methods here that leverage the connection.
}
#Override
public void onDetach() {
super.onDetach();
if (mListener != null) {
mListener.onDetached(this);
}
}
}
You have a callback in the fragment life cycle. onDetach() is called when fragment is no longer attached to activity.
An alternative would be:
mFragmentManager.findFragmentByTag("Tag").getView()
If the view is null the fragment must be detached.
You can use ViewModel for update host activity. Shared ViewModel could be better choice than across the old listener based polymorphism model. You can follow the official documentation.
Data, fragment lifecyle etc. can be observable with shared viewmodel.
sealed class FragmentStates {
object Attached : FragmentStates()
object Started : FragmentStates()
object Stopped : FragmentStates()
object DeAtached : FragmentStates()
}
class FragmentStateViewModel : ViewModel() {
private val _fragmentState = MutableLiveData<FragmentStates>()
val fragmentStates: LiveData<FragmentStates> get() = _fragmentState
fun fragmentAttached() {
_fragmentState.value = FragmentStates.Attached
}
fun fragmentDeAtached() {
_fragmentState.value = FragmentStates.DeAtached
}
}
class HostActivity : AppCompatActivity() {
private val fragmentStateViewModel: FragmentStateViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentStateViewModel.fragmentStates.observe(this, Observer {
when(it) {
FragmentStates.Attached -> {}
FragmentStates.Started -> {}
FragmentStates.Stopped -> {}
FragmentStates.DeAtached -> {}
}
})
}
}
class MyFragment: Fragment() {
private val fragmentStateViewModel: FragmentStateViewModel by activityViewModels()
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentStateViewModel.fragmentAttached()
}
override fun onDetach() {
super.onDetach()
fragmentStateViewModel.fragmentDeAtached()
}
}

Passing interface to Fragment

Let's consider a case where I have Fragment A and Fragment B.
B declares:
public interface MyInterface {
public void onTrigger(int position);
}
A implements this interface.
When pushing Fragment B into stack, how should I pass reference of Fragment A for it in Bundle so A can get the onTrigger callback when needed.
My use case scenario is that A has ListView with items and B has ViewPager with items. Both contain same items and when user goes from B -> A before popping B it should trigger the callback for A to update it's ListView position to match with B pager position.
Thanks.
Passing interface to Fragment
I think you are communicating between two Fragment
In order to do so, you can have a look into Communicating with Other Fragments
public class FragmentB extends Fragment{
MyInterface mCallback;
// Container Activity must implement this interface
public interface MyInterface {
public void onTrigger();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (MyInterface ) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement MyInterface ");
}
}
...
}
For Kotlin 1.0.0-beta-3595
interface SomeCallback {}
class SomeFragment() : Fragment(){
var callback : SomeCallback? = null //some might want late init, but I think this way is safer
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
callback = activity as? SomeCallback //returns null if not type 'SomeCallback'
return inflater!!.inflate(R.layout.frag_some_view, container, false);
}
}
It is optimal for two fragments to only communicate through an activity. So you can define an interface in Fragment B that is implemented in the activity. Then in the activity, define in the interface method what you want to happen in fragment A.
In Fragment B,
MyInterface mCallback;
public interface MyInterface {
void onTrigger(int position);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (MyInterface) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement MyInterface");
}
}
Method for determining if user goes from B to A
public void onChangeFragment(int position){
//other logic here
mCallback.onTrigger(position);
}
In Activity,
public void onTrigger(int position) {
//Find listview in fragment A
listView.smoothScrollToPosition(position);
}
Goodluck!
Using #Amit's answer, and adapting to the OPs question, here is all the relevant code:
public class FragmentA extends BaseFragment implements MyInterface {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// THIS IS JUST AN EXAMPLE OF WHERE YOU MIGHT CREATE FragmentB
FragmentB myFragmentB = new FragmentB();
}
void onTrigger(int position){
// My Callback Happens Here!
}
}
...
public class FragmentB extends BaseFragment {
private MyInterface callback;
public interface MyInterface {
void onTrigger(int position);
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
callback = (MyInterface ) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement MyInterface");
}
}
}
I think you should use communication, as I've written below. This code comes from this Android Dev page of communication between Fragments:
HeadlinesFragment
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
public void setOnHeadlineSelectedListener(Activity activity) {
mCallback = activity;
}
// Container Activity must implement this interface
public interface OnHeadlineSelectedListener {
public void onArticleSelected(int position);
}
// ...
}
MainActivity
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener{
// ...
#Override
public void onAttachFragment(Fragment fragment) {
if (fragment instanceof HeadlinesFragment) {
HeadlinesFragment headlinesFragment = (HeadlinesFragment) fragment;
headlinesFragment.setOnHeadlineSelectedListener(this);
}
}
public static class MainActivity extends Activity
implements HeadlinesFragment.OnHeadlineSelectedListener {
...
public void onArticleSelected(int position) {
// The user selected the headline of an article from the HeadlinesFragment
// Do something here to display that article
}
You may create call back interface by this way.
var screenVisibility=activity as YourActivity
screenVisibility.setScreenVisibility("which screen you want")

Categories

Resources