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?
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.
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.
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 a main activity which has 2 fragments. The main activity has a SearchView in the action bar. Both the fragments have a list of large number of strings, List<String>.
The flow is:
User enters Fragment I --> Selects a string (lets say Selection1) --> Based on Selection1 a list of strings is populated in the second fragment --> Here the user selects a second String ---> Processing based on these two strings.
Now since both the fragments contain a large number of strings, the user enters a query in the SearchView, which filters the list and reduces it to a smaller list displayed in the SearchableActivity.
Now the problem is how does the SearchableActivity get access to these two List<String> to filter them based on the query and display a reduced list to the user.
Currently what I have done is overridden onSearchRequested and pass the data as
#Override
public boolean onSearchRequested()
{
Bundle appData = new Bundle();
appData.putString(FRAGMENT_ID, "Fragment_A");
appData.putStringArrayList(SEARCH_LIST, searchList);
startSearch(null, false, appData, false);
return true;
}
Is there a better way or standard way by which this problem can be handled i.e. an implementation that allows data to be based from my MainActivity to SearchableActivity?
Edit: Adding code. Showing how data is set in the Fragment. onDataReceived is called from the HttpManager which receives the data.
#Override
public void onDataReceived(String type,final Object object)
{
switch(type)
{
case PopItConstants.UPDATE_LIST:
getActivity().runOnUiThread(new Runnable() {
#Override
public void run()
{
updateCinemaList((List<String>) object);
}
});
break;
}
}
public void updateDataList(List<String> data)
{
this.dataList = data;
spinner.setVisibility(View.GONE);
mAdapter.updateList(dataList);
}
I just answered a similar question a few minutes ago, at how can I send a List into another activity in Android Studio
I encourage you to rethink your pattern of simply passing data around among Activities and Fragments. Consider creating one or more data models (non-Android classes) for your application, and making these models available to the Android classes (Activities, Fragments, etc.) that need them.
Remove all of the data storage and manipulation code from your Activities and Fragments, and put it into the model(s).
Okay... So this is how I did it.
Basically, the data received in the two fragments was not simply List<String> but they were models viz. Cinema and Region which contained details other than names including location, rating etc.
So, firstly, I made an interface ISearchable
public Interface ISearchable
{
// This contains the Search Text. An ISearchable item is included
// in search results if query is contained in the String returned by this method
public String getSearchText();
//This is meant to return the String that must be displayed if this item is in search results
public String getDisplayText();
//This is meant to handle onClick of this searchableItem
public void handleOnClick();
}
Both the Cinema and Region models implemented ISearchable.
After this, I used a singleton class DataManager in which I maintained a List<ISearchable> currentSearchList.
public class DataManager
{
.....<singleton implementation>....
List<ISearchable> currentSearchList;
public void setSearchList(List<ISearchable> searchList)
{
this.currentSearchList = searchList;
}
public List<ISearchable> getSearchList()
{
return this.currentSearchList;
}
}
So whenever a fragment (either Fragment_A or Fragment_B) is loaded, it updates this currentSearchList, so that when the SearchableActivity performs search all it has to do is DataManager.getInstance().getSearchList() and then use this list for filtering out a list of matching items.
This is how I handled the problem of having Lists in Activity other than the SearchableActivity using which search needs to be performed.
I understand this might not be the best solution, so, I look forward to suggestions and criticisms, and using that to be arrive at a better solution.
I'm currently using a custom list adapter and modifying the rows of the listview at runtime (changing text, buttons etc).
I want to find a way to save/restore the changes made to the listview at runtime when I start another fragment and then come back to the fragment containing the listview.
Currently the listview is being reloaded every time and all the runtime changes are lost.
The following shows how I am setting the list adapter.
public void setAdapterToListView(ArrayList<item> list) {
ItemList = list;
ItemAdapter adapter = new MenuItemAdapter(list, getActivity(), this);
ItemListView.setAdapter(adapter);
}
I am then using a custom list adapter to make the changes (mentioned above) at runtime. Here's a snippet of how I am changing things at runtime:
if (holder.qty.getText().toString().equals("Qty")) {
relativeLayout.setBackgroundColor(row.getResources().getColor(R.color.navDrawerL ightBlue));
holder.qty.setText("1");
holder.qty.startAnimation(anim);
holder.removeItem.setBackgroundResource(R.drawable.remove_item_red);
holder.removeItem.setEnabled(true);
...
Is there a recommended way to approach this issue?
Adapter
You will want your custom adapter to implement some sort of getter for it's internal data. For instance
public ArrayList<YourDataType> getList() {
return new ArrayList<YourDataType>(mAdapterData);
}
Activity
Then in your activity/fragment, you'll need to save and restore that data.
private static final String STATE_LIST = "State Adapter Data"
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(STATE_LIST, getAdapter().getList());
}
While there is an onRestoreInstanceState() method you could override, I typically restore during onCreate(). Usually more convenient to when other things are getting instantiated. Either or is viable.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//If restoring from state, load the list from the bundle
if (savedInstanceState != null) {
ArrayList<YourDataType> list = savedInstanceState.getParcelableArrayList(STATE_LIST);
ItemAdapter adapter = new MenuItemAdapter(list, getActivity(), this);
} else {
//Else we are creating our Activity from scratch, pull list from where ever you initially get it from
ArrayList<YourDataType> list = getInitData();
ItemAdapter adapter = new MenuItemAdapter(list, getActivity(), this);
}
}
YourDataType
You didn't mention what YourDataType was but I'm assuming it's a custom class. In order to work with the bundled savedstate, it must implement Parcelable. Android's dev link on Parcelable and a StackOverFlow post explaining how to write your own custom class with Parcelable.
Update
Depending on what you are doing with the fragment will dictate if the onSavedInstanceState() method will be called. From the way your question is asked, I'm assuming you are pushing one fragment onto the backstack to load another on top. Then hitting the back button will reloaded that fragment from the backstack...and pass along the bundled state to reload from.
Of course that is just one of many scenarios. It's perfectly possible for the fragment in question to only perform onPause() followed by onStop()...then when redisplaying the fragment, only seeing it do onStart() followed by onResume(). In this case, there would not be a saved state because the fragment was never fully destroyed, so there is nothing to restore. It should be in the same state. If it's not and instead you are seeing it reload the initial data...then you are probably reloading the initial data in or after onStart(). You'll want to move all that initializing data to onCreate() instead.
Another possible case is that you completely destroy the fragment without ever putting it on the backstack. Re-initializing it would not have a saved state to pull from. If you wanted to "restore" this fragment back to the state beforehand, then you can not rely upon onSavedInstanceState(). You will need to persist that information manually somewhere in memory, to disk, or a DB yourself. Then pull from it accordingly.
Life cycles with fragments are unfortunately really complex and depend greatly on usage and even between the support library vs native fragments.
After doing some reading/research I ended up solving this by saving the adapter data to Saved Preferences by using Gson (https://code.google.com/p/google-gson/).
I used Gson to first convert my array of objects into Json and then stored the Json strings in the Saved Preferences.
Subsequently I was able to retrieve these as required and read the Json into Java objects again, store these in an array list and pass this to my listview adapter.
Here's a brief overview of the process for anyone who is looking to do something similar.
Saving to Shared Preferences:
SharedPreferences settings;
Editor editor;
settings = c.getSharedPreferences(PREFS_NAME,Context.MODE_PRIVATE);
editor = settings.edit();
Gson gson = new Gson();
String ItemsJson = gson.toJson(list);
editor.putString(categoryId, ItemsJson);
editor.apply();
Reading from Shared Preferences:
if (settings.contains(categoryId) {
String jsonItems = settings.getString(categoryId, null);
Gson gson = new Gson();
Item[] favoriteItems = gson.fromJson(jsonItems, Item[].class);
list = Arrays.asList(favoriteItems);
ItemsFromSharedPrefs = new ArrayList<>(list);
AllCategories.addAll(ItemsFromSharedPrefs);
} etc...