There are a lot of questions about fragment communication here, but they are normally question about getting data from activity and sending data back to activity, normally starting from fragment.
But I wonder what what is best approach for sending data from activity to fragment, when you cannot do it when creating fragment? For clarification, Lets assume that an app has 2 fragments that can use (can not must) some data to improve user experience, but obtaining this data is costly. So obtain this data in activity using a Loader or AsyncTask in main activity while creating Fragments themselves. Now when data is ready asynchronously in Activity, we need to send this data to Fragments. What is best approach for this? I thought of a way for doing this, and I like to know if there is any problem with this approach.
1-In fragment we use onAttach to send fragment to activity and check if any data is already read:
#Override
public void onAttach (Activity activity) {
MyActivity act = (MyActivity)activity;
act.addFragment(this);
Data data = act.getData();
if (data != null) {
setAdditionData(data)
}
}
2-and in activity store a WeakReference to Fragment:
private ArrayList<WeakReference<Fragment>> mFragments = new ArrayList<>();
...
public void addFragment(Fragment frag) {
WeakReference<Fragment> f = new WeakReference<Fragment>(frag);
mFragments.add(f);
}
public Data getData() {
return mData;
}
public void updateFragmentsData() {
for (Iterator<WeakReference<Fragment>> iterator = mFragments.iterator(); iterator.hasNext();) {
WeakReference<Fragment> wf = iterator.next();
Fragment f = wf.get();
if (f != null) {
f.setAdditionData(mData);
} else {
iterator.remove();
}
}
}
Now when fragments attaches, it adds itself to list of fragments in activity and checks if data is already ready and if ready it will use that data. On the other hand, when data is ready asynchronously in activity, it can call updateFragmentsData() to update all fragments data.
I wonder if this approach is correct or it can be incorrect in some situations? Any idea? Is there any better approach for notifying fragments from main activity?
Btw, is it possible to use Handler/Message for communicating between fragments too or not? As another approach?
Best Regards
I can think of three ways.
Use a listener. Write an interface in the activity to use it as a listener. The fragment implements the interface and registers and unregister as a listener at appropriate time.(say at onCreateView and onDestroyView).
This one is my favorite. I hope DataBinding is gaining popularity and it can be used to solve this. Say you define a particular model for the fragment layout. Now you use ObservableFields in the model. Pass this model to your databinding variable. Now change this object from either the activity or the fragment itself, changes will be reflected in the view.
The newly introduced ViewModels. I will be using them from my next project.
Related
In my application I have one Fragment which is responsible for displaying a list of news items. It takes a String parameter which determines which url to pull data from.
I set the Fragment with this code:
private void setFragment(String pageToLoad, NewsFeedFragment newsFeedFragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(newsFeedFragment == null) {
transaction.replace(R.id.container, NewsFeedFragment.newInstance(pageToLoad), pageToLoad);
}
else {
transaction.replace(R.id.container, newsFeedFragment, pageToLoad);
}
mPageToLoad = pageToLoad;
}
In my parent Activity I keep track of which 'page' is currently being viewed:
protected void onSaveInstanceState(#NonNull Bundle outState) {
if(mPageToLoad != null) {
outState.putString("pageToLoad", mPageToLoad);
}
super.onSaveInstanceState(outState);
}
In my parent Activity onCreate method I check whether an instance of NewsFeedFragment has been created and added to the FragmentManager as follows:
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
if (savedInstanceState.containsKey("pageToLoad")) {
String pageToLoad = savedInstanceState.getString("pageToLoad");
if(pageToLoad != null) {
NewsFeedFragment newsFeedFragment = (NewsFeedFragment) getSupportFragmentManager().findFragmentByTag(pageToLoad);
if(newsFeedFragment != null) {
setFragment(pageToLoad, newsFeedFragment);
}
else {
setFragment(pageToLoad, null);
}
}
}
}
}
This works well 99% of the time, the application resumes correctly and displays the last instance of NewsFeedFragment added. However, I have an issue which seems to occur randomly where the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
In NewsFeedFragment the RecyclerView Adapter is a class variable:
public NewsPageAdapter mNewsPageAdapter;
The onActivityCreated method of NewsFeedFragment is as follows:
public void onActivityCreated(Bundle savedInstanceState) {
if(mNewsPageAdapter == null) {
Log.i(TAG, "mNewsPageAdapter is null"); // This is logged when issue occurs
}
if(savedInstanceState == null || mNewsPageAdapter == null) {
new LoadFirstPageTask().execute(); // Fetches news items from web service, creates mNewsPageAdapter, and then calls setupRecyclerView() method
}
else {
setupRecyclerView(savedInstanceState);
}
}
Finally, this is the NewsFeedFragment setupRecyclerView method:
private void setupRecyclerView(Bundle savedInstanceState) {
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mNewsPageAdapter);
}
From what I've described could anyone offer any insight as to why the NewsPageAdapter may sometimes be null when the Fragment is retrieved from The FragmentManager?
Thanks
Ok, so you do new LoadFirstPageTask().execute(); and I guess that will eventually call setupRecyclerView?
I think you're over complicating a solution here.
You have all these conditions, business logic, and decisions made inside a volatile component (a Fragment) whose lifecycle is quite complex and not always what you'd expect; you also couple the maintenance of asynchronous data to this structure, and this is having unexpected side-effects that are hard to pin point and track down.
Creating the Adapter is "cheap" compared to fetching, processing, and producing the data for said adapter.
You don't seem to mention ViewModels anywhere, are you using a viewModel? Or any other sort of pattern like a Presenter, Interactors, useCases?
AsyncTasks are also Deprecated and while I don't advocate to run to the "refactor" hill every time a class is deprecated, I think you could get a better and more stable, testable, and readable solution if you abstract that AsyncTask into a coroutine (all managed by your ViewModel for example).
To put in other terms, your Fragment and Activity shouldn't have to deal with the logic regarding "do I need to load this data or not"; this is someone else's responsibility.
About your code.
Ok, now that I've ranted about how you're doing things, let's dig deeper into your existing Java code.
the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
Whenever we see "sometimes" in a crash, the 1st suspect should be timing/threading. Synchronous code can fail, but it's often orders of magnitude more predictable than Asynchronous code.
If it's "sometimes" null, then the task that is in charge of changing this behavior is not always ready by the time it's needed; or the condition needed for this task to run, is not always what you expect by the time it's checked, and so on, and so forth.
Start by re-architecting your idea into a separate component.
Have the Fragment create its adapter (can be empty of data) as soon as possible, regardless of whether there's data or not.
Have the fragment ask another component for the data. And when the data is available, send it to the Fragment who will in turn set it in the adapter. If the data is already there by the time you ask for it (because you "cached" it), you won't have to wait.
I'd also store the "last viewed page" in the same component, so you don't need to save the state and pass it alongside to a fragment. Rather the fragment asks for "the current data" and the component already knows what it is.
All in all, it's a bit difficult to put all your pieces together because we, the readers, don't have all the code, nor your requirements that lead you to this solution.
I have a fragment X which indeed has a RecyclerView, X has a search view, I use the search view to search something and filter the RecyclerView into few rows. After the filtering, if user clicks on some row, it goes to another fragment say Y. From there if the user clicks back it comes back to X. My task is that X should persist the search results after this coming back. What is the best approach to achieve this?
You can use a the singleton pattern to store the data!
E.g.
// DataManager.java
public class DataManager {
private static DataManager thisInstance;
// Declare instance variables
List<String> searchResultItems;
public static DataManager getSharedInstance() {
if (thisInstance == null) {
thisInstance = new DataManager();
}
return thisInstance;
}
private DataManager() {
searchResultItems = new ArrayList<>();
}
public List<String> getSearchResultItems() {
return searchResultItems;
}
public void setSearchResultItems(List<String> searchResultItems) {
this.searchResultItems = searchResultItems;
}
}
Now you can store and retrive data from everywhere:
// Setter
DataManager.getSharedInstance().setSearchResultItems(items);
// Getter
List<String> items= DataManager.getSharedInstance().getSearchResultItems();
Propertly override onSaveInstanceState in Fragment so that it will store search input - filter. Also override onCreate in such way it will apply saved filter on your RecyclerView.
Before navigating to another fragment, obtain Fragment.SavedState via FragmentManager and save it temporary in Activity which hosts your fragments. Note, this state can be lost if you do not properly save Activity state due of configuration changes (rotate) = you have to override also onSaveInstanceStatein Activity. Or simply save Fragment.SavedState in global scope (some static field, or in Application).
When navigating back to previous fragment, re-create fragment from Fragment.SavedState i. e. invoke Fragment#setInitialSavedState(Fragment.SavedState).
For more details see my research on similar topic.
I have an Android app that uses a pretty common design pattern:
The main activity is essentially presenting a list of objects - on small devices it does so by hosting a single fragment that displays a recyclerview of this list. On larger devices it hosts two fragments, one which has the same recyclerview of objects, and another which will host the detail for individual objects when one is selected in the list.
On smaller devices, when a selection from the list is made, an activity is launched that hosts a fragment that utilizes a ViewPager to allow "swiping" through the list of objects, and edit each one in place.
In both cases, the user is allowed to edit only from the detail fragment.
I currently have my realm instance initialized in the application class, then the default instance retrieved in an activity base class I use to hold some housekeeping methods:
public abstract class SingleFragmentActivity extends AppCompatActivity {
private Realm realm;
protected abstract Fragment createFragment();
#LayoutRes
protected int getLayoutResId() {
return R.layout.activity_fragment;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
// Initialize ProfileLab
ProfileLab.get(realm);
setContentView(getLayoutResId());
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = createFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
if ( realm != null) {
realm.close();
}
}
}
Note that I am storing this instance of realm in a static class "ProfileLab":
// Initialize ProfileLab
ProfileLab.get(realm);
Then in the various fragments that update data, I am doing stuff like:
mProfile = ProfileLab.get().getProfile(profileId);
*
* do CRUD activities here for example:
*
private void deleteProfile() {
ProfileLab.get().deleteProfile(mProfile);
mCallbacks.onProfileUpdated(mProfile);
}
Then in ProfileLab, it looks like:
public boolean deleteProfile(Profile c) {
boolean retVal = true;
try {
mRealm.beginTransaction();
c.deleteFromRealm();
} catch (Exception e) {
retVal = false;
} finally {
if ( mRealm != null ) {
if (retVal) {
mRealm.commitTransaction();
} else {
mRealm.cancelTransaction();
}
}
}
return (retVal);
}
My question - is this a problem to essentially hold that Realm instance open like that throughout the use of the app? I noticed this paragraph in the docs:
If you get a Realm instance from a thread that does not have a Looper
attached, then objects from such instance will not be updated unless
the waitForChange() method is called. It is important to note that
having to hold on to an old version of your data is expensive in terms
of memory and disk space and the cost increases with the number of
versions between the one being retained and the latest. This is why it
is important to close the Realm instance as soon as you are done with
it in the thread.
The thing is, I am not 'done with it' because this is on the UI thread, which is obviously running throughout the lifetime of my app.
I can't really open/close the realm instance just for the atomic updates, because I need to use the result of the initial query to show the list of objects from which to choose to edit - when I tried that initially (I had realm object open/close within each method in ProfileLab itself) I got an error in my recycler adapters that the realm had been closed...
The example code showing use of the recycler view shows realm being retrieved/used/closed at the individual activity level, if I do that between say the two simple activities (hosting the RecyclerView and hosting the ViewPager), will the data updates be reflected in each other?
Opening and closing the realm within try/catch block is recommended. for an example:
try {
Realm realm = Realm.getDefaultInstance();
//Use the realm instance
}catch(Exception e){
//handle exceptions
}finally {
realm.close();
}
It's a basic example when going to use. If you can close within AsyncTask, it'll be better.
The official documentation refers that if you use minSdkVersion >= 19 and Java >= 7, you won't close it manually.
try (Realm realm = Realm.getDefaultInstance()) {
// No need to close the Realm instance manually
}
Realm will automatically keep Realms on Looper threads up to date. That particular line in the documentation mostly refers to background threads. So your code is fine, even if onDestroy might not be called.
You can also read these relevant sections in the docs:
https://realm.io/docs/java/latest/#closing-realms
https://realm.io/docs/java/latest/#realm-instance-lifecycle
I have an activity with a fragment. Inside that fragment there is a viewpager and inside that there is a list. Now once the user clicks on an item in the list the fragment should be replaced with another fragment and I need to pass some data like the list position and some other values linked to that. I can implement this by using interfaces, but as we are using rxjava so want to do it using rx... Don't want to implement the event bus or rxbus pattern right now. So how do I implement it using rxjava?
One way to do it:
/* inside whatever you mean by the list */
PublishSubject<Void> mClickSubject = PublishSubject.create(); //or use another type instead of Void if you need
/*...*/
item.setOnClickListener(v -> mClickSubject.onNext(null));
/*...*/
public Observable<Void> itemClicked() {
return mClickSubject;
}
/* pass your subject/observable all the way to the activity */
/* inside the activity */
private void setupSubscription() {
mCurrentFragment.listItemClicked()
.subscibe(/* switch fragment */);
}
Or another way is to have a singleton / static class holding a member PublishSubject and push items through it. Doing it like this you won't need all the getters to pass the observable from the list to the activity.
I have a HomeActivity which extends Activity that contains Actionbar items. The HomeActivity has 1 fragment (StatusFragment which extends Fragment). In the Fragment there is a ListView which uses a custom ArrayAdapter and a method call to supply the data.
private ParseUser[] GetUsers(){
final ParseQuery<ParseUser> query = ParseUser.getQuery();
ParseUser[] usersArray;
try {
List<ParseUser> users = query.find();
usersArray = users.toArray(new ParseUser[users.size()]);
} catch (ParseException e) {
usersArray = null;
e.printStackTrace();
}
return usersArray;
}
I'm having trouble getting the ListView to update from the OnOptionsItemSelected callback.
case R.id.home_ab_refresh:
StatusFragment pFrag = (StatusFragment) getFragmentManager().findFragmentByTag("mFragment");
pFrag.users = pFrag.GetUsers();
pFrag.mAdapter.notifyDataSetChanged();
return true;
1) Is this an appropriate way to access the Fragment from the Actionbar items (HomeActivity)?
2) Is there a better way to design this code?
Thanks much!
Re 1) I probably wouldn't do a findFragmentByTag() every time, and instead just stick the fragment into a member variable of the activity during the activity's onCreate().
The main issue with the code is something else:
pFrag.users = pFrag.GetUsers();
pFrag.mAdapter.notifyDataSetChanged();
Here you violate the object-oriented design principle of loose coupling. The HomeActivity is too intimately bound to the implementation details of the StatusFragment. What you should do instead is move that code into the fragment and expose it as a single, public method that is named for the intent (goal, purpose) of the action, not its implementation.
// In HomeActivity
pFrag.reloadData();
// In the fragment
public void reloadData() {
this.users = pFrag.GetUsers();
this.mAdapter.notifyDataSetChanged();
}
This way, it's easier to reuse the status fragment elsewhere. More importantly, it's easier to evolve that fragment, you can now completely change the internals without having to change the host activity. This is cleaner from a design perspective.
Re 2) Aside from the issue I already mentioned, you should consider returning an empty array rather than null when an exception occurs. It's generally a better idea to return empty array/collection from finder methods, because people tend to immediately use the result for an iterator or an addAll() or something like that, without null-checking it first.
First of all, you dont make a nullpointer check, since you cant be certain that the FragmentManager will actually return a validFragment.
you can however just catch the onOptionsMenuSelected event in the fragment itself, which will result in a much more capsulated code.
Besides that, when do you refresh the ListView? Wouldnt it make sense to update the listview automatically once the new data has arrived?