I was looking over this answer and it seemed to only deal with a single textview.
Basically, I have an Android application with n fragments, each of which has a textview that is populated from a remote call to a database. Each time the fragment is selected, that remote call will fire and the textview should be repopulated.
Currently, I am using a central AsyncTask to accomplish this, however I am starting to wonder if it is the correct way to go about doing so (some textviews take too long to update for small amounts of data, some don't get updated at all, etc.).
Here is the code from my RetrieveData class. Essentially, it figures out which textview is to be updated, and then populates that textview.
public class RetrieveData extends AsyncTask<String, String, String[]> {
private int txtViewID = -1;
private Activity mainActivity;
public RetrieveData(Activity a) { mainActivity = a; }
protected String[] doInBackground(String... urls) {
String[] data;
// call web script to return JSON data
...
// figure out which fragment called which script
if (urls[0] == "get_A.php") {
data = parseJSONdata(); // parse out the JSON
txtViewID = R.id.txtViewA; // find INT-based ID
} else if (urls[0] == "get_B.php") {
data = parseOtherJSONdata(); // different type of call
txtViewID = R.id.txtViewB;
} else ... {
...
}
} catch (Exception e) {
System.out.println("Error: " + e.toString());
}
return data;
}
#Override
protected void onPostExecute(String[] op) {
if (txtViewID != -1) { // call was made
TextView tv = (TextView)mainActivity.findViewById(txtViewID);
tv.setText(op[0]);
}
and here is how I call this from a Fragment:
public class MainFragment extends Fragment {
Activity mainActivity;
public MainFragment(Activity a) { mainActivity = a; }
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View v =inflater.inflate(R.layout.main_tab,container,false);
new RetrieveData(mainActivity).execute("get_A.php","1");
return v;
}
}
To me, its very kludgy and probably belies my newness to Android, so any suggestions for improvement are heartily appreciated.
You can do a couple of things to improve the robustness and performance and fix some issues which will creep in later:
Don't use findViewById() outside of init/setup type methods. It is an expensive call as it has to "search" your hierarchy for the ID you are requesting.
Don't use an overloaded constructor for your Fragment which takes the Activity. The Fragment default constructor should be empty. This allows the system to properly re-create your Fragment when configuration changes (screen rotates.) The Fragment will receive its attached Activity at the correct time when its onAttach() method is called, so there is no need to do this.
You shouldn't need the Activity at all for what you're trying to do. Instead, have your Fragment get the correct TextView from your layout in its onCreateView(). What you do from there is really up to you:
Pass the TextView instance to your RetrieveData class constructor as the one to be updated. This eliminates the hard coded IDs in your RetrieveData class, which gets rid of some explicit coupling and is a better approach. This is still very tightly coupled, though, since it depends on having a specific View so still not a great option IMHO.
Have the RetrieveData class define an inner Callback interface and have the Fragment implement it. The constructor for RetrieveData can then take an instance of the Callback interface (e.g. your Fragment instance) and when its onPostExecute() runs it just calls back the Fragment with the appropriate data. Now it is up to your Fragment implementation to make the right decision on what UI element it is hosting to update with the data. It may be a TextView now, but in the future you could make it something else, etc. Now you have decoupled the class from all explicit UI ties and put the responsibility on the thing hosting the UI elements: the Fragment.
Here's a brief example of the 2nd bullet:
public RetrieveData extends AsyncTask<String, String, String[]> {
// Define the interface used to provide results
public interface Callback {
public void onDataLoaded(String[] result);
}
private Callback mCb;
public RetrieveData(Callback cb) {
mCb = cb;
}
...
#Override
public void onPostExecute(String[] result) {
mCb.onDataLoaded(result);
}
}
public MyFragment extends Fragment implements RetrieveData.Callback {
TextView mResult;
RetrieveData mAsyncRetriever;
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.main_tab,container,false);
// Get the TextView now where we want to show results.
// This avoids calling findViewById() constantly.
mResult = (TextView)root.findViewById(R.id.example_result);
...
}
#Override
public void onResume() {
// Keep a reference to the AsyncTask so we can properly
// cancel it when our lifecycle events dictate so.
mAsyncRetriever = new RetrieveData(this);
mAsyncRetriever.execute("get_A.php");
}
#Override
public void onPause() {
// If we have a pending data load going on, kill it.
if (mAsyncRetriever != null) {
mAsyncRetriever.cancel(true);
mAsyncRetriever = null;
}
}
#Override
public void onDataLoaded(String[] result) {
// Only pulling the first result provided
mResult.setText(result[0]);
// The RetrieveData is done, get rid of our ref
mAsyncRetriever = null;
}
}
Related
I created the instance of View Model in onCreate method of an activity.
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
Then i have a method, AddTicket, which uses viewModel to hit a service and on response from viewModel i dismiss loading animation.
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, response ->{
dismissLoadingAnimation();
}
Now after adding a ticket, user can repress the Add Ticket button, and the addTicket() method will be called again.
but this time observer defined in ViewModel gets called 2 times, resulting in 2 network calls, and 2 dismissLoadingAnimation execution.
And if i keep pressing addTicket button, the number of executing observer defined inside ViewModel keep increases.
This is my View Model code.
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public MutableLiveData AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
return mObservableResponse;
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
The number of executing observer defined inside ViewModel keep increases becasue with every click You're registering new observers. You're not supposed to register observer with onClick() method.
You should do it in onCreate() method of your Activity or in onViewCreated method of your fragment. If You'll do that, there won't be a need to removeObserver when You'll finish work. Lifecycle mechanism will cover it for you.
But if you really want answer for you question, this is how you can do it
yourViewModel.yourList.removeObservers(this)
Passing this means passing your Activity, or there is a second way:
yourViewModel.yourList.removeObserver(observer)
val observer = object : Observer<YourObject> {
override fun onChanged(t: YourObject?) {
//todo
}
}
The purpose of Viewmodel is to expose observables (Livedata)
The purpose of View(Activity/Fragment) is to get these observables and observe them
Whenever there is a change in these observables(Livedata) the change is automatically posted to the active subscribed owners(Activity/Fragment), so you need not remove them in onPause/onStop as it is not mandatory
I can suggest few changes to your code to solve the problem with the above mentioned pointers
ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public LiveData<String> getResponseLiveData(){
return mObservableResponse;
}
public void AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
View
onCreate(){
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
observeForResponse();
}
private void observeForResponse(){
ticketViewModel.getResponseLiveData().observe(this, response ->{
//do what has to be updated in UI
}
}
public void addTicket(View view){
ticketViewModel.AddTicket(id);
}
Hope this is of help :)
You only need to call the observe once, I prefer to do it in onResume and then call removeObserver in onPause:
Adds the given observer to the observers list
You keep adding listeners to the data so you get multiple callbacks.
Edit:
I took an existing code sample of mine for a Fragment and renamed everything (I hope), there's no example here for setting the data into the ViewModel but it should be ticketViewModel.AddTicket(id); in your case.
public class ListFragment extends Fragment {
private MyViewModel viewModel;
private MyRecyclerViewAdapter recyclerViewAdapter;
private Observer<List<DatabaseObject>> dataObserver;
private RecyclerView recyclerView;
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
initRecyclerView(rootView, getContext());
initObservers();
return rootView;
}
private void initRecyclerView(View rootView, Context context) {
recyclerViewAdapter = new MyRecyclerViewAdapter(context);
recyclerView = rootView.findViewById(R.id.recycler_view);
recyclerView.setAdapter(recyclerViewAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new DividerNoLastItemDecoration());
}
private void initObservers() {
dataObserver = new Observer<List<DatabaseObject>>() {
#Override
public void onChanged(#Nullable final List<DatabaseObject> data) {
recyclerViewAdapter.setData(data);
}
};
}
#Override
public void onResume() {
super.onResume();
initViewModel();
}
private void initViewModel() {
FragmentActivity activity = getActivity();
if (activity != null) {
viewModel = ViewModelProviders.of(activity).get(MyViewModel.class);
viewModel.getData().observe(activity, dataObserver);
}
}
#Override
public void onPause() {
super.onPause();
if (viewModel != null) {
viewModel.getData().removeObserver(dataObserver);
viewModel = null;
}
}
}
I had similar problem. You could try to use SingleLiveEvent
Or, in my, more complicated case, i had to use custom observer. It would looks like this:
public class CustomObserver implements Observer<YourType> {
private MyViewModel mViewModel;
public CustomObserver (){}
public void setViewModel(MyViewModel model) {
mViewModel = model;
}
#Override
public void onChanged(#Nullable YourType object) {
mViewModel.AddTicket(id).removeObserver(this); // removing previous
mmViewModel.refreshTickets(); // refreshing Data/UI
// ... do the job here
// in your case it`s: dismissLoadingAnimation();
}
}
And using it like:
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, myCustomObserver);
}
If you are willing to do some changes, i think we can handle it in much cleaner way
LiveData is meant to be used to contain a property value of a view
In ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
private MutableLiveData<Boolean> showLoadingAnimationLiveData = new MutableLiveData<String>();
public LiveData<Boolean> getShowLoadingAnimationLiveData(){
return showLoadingAnimationLiveData;
}
public void addTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
showLoadingAnimationLiveData.setValue(true);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
showLoadingAnimationLiveData.setValue(false);
}
}
}
In 'onCreate' of your Activity/Fragment
ticketViewModel.getShowLoadingAnimationLiveData().observe(this,showLoadingAnimation->{
if(showLoadingAnimation != null && showLoadingAnimation){
startLoadingAnimation();
}else{
dismissLoadingAnimation();
}
})
The main concept is to divide the responsibilities,
Activity/Fragment doesn't need to know which process is going on, they only need to know what are the current properties/state of there child views.
We need to maintain a LiveData in ViewModels for each changing property/state depending on Views. ViewModel needs to handle the view states depending on whats happening.
Only responsibility the Activity/Fragment has about a process is to trigger it and forget and ViewModel needs handle everything(like informing Repositories to do the work and changing View Properties).
In your Case,
'addTicket' is a process about which Activity/Fragment doesn't need to know about there status.
The only responsibility of Activity/Fragment about that process is to trigger it.
ViewModel is one who needs to analyze the state of process(in-progress/success/failed) and give appropriate values to the LiveDatas to inform the respective Views
(First of all: no, it's not a duplicate of mentioned question :P read, then push the button.)
I'm using the BottomNavigationView in one of my apps, loading Fragments with lists, which get their data from a ViewModel/LiveData/Dao. When selecting a Fragment via the BNV, it seems its animation somehow fights for UI-Thread time with the Fragment loading, causing it only to finish completely after the lists are displayed - which confuses me. I was under the impression, that LiveData calls are being handled async by default?
Stuttering gif
Is this a known thing?
ViewModel
public class ScheduleViewModel extends ViewModel {
private final LiveData<List<ScheduleInfo>> arrivals;
private final LiveData<List<ScheduleInfo>> departures;
public ScheduleViewModel() {
arrivals = SigmoDb.schedule().getArrivals();
departures = SigmoDb.schedule().getDepartures();
}
public LiveData<List<ScheduleInfo>> getArrivals() {
return arrivals;
}
public LiveData<List<ScheduleInfo>> getDepartures() {
return departures;
}
}
Fragment
public class ArrivalsFragment extends MainFragment {
private ScheduleDetailsAdapter adapter;
private ScheduleViewModel viewModel;
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable List<ScheduleInfo> infoList) {
adapter.setData(infoList);
}
};
public static ArrivalsFragment newInstance() {
return new ArrivalsFragment();
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adapter = new ScheduleDetailsAdapter(getActivity());
// using the parent fragment as LifeCycleOwner, since both its
// child Fragments use the same ViewModel
Fragment parent = getParentFragment();
if (parent == null) {
parent = this;
}
viewModel = ViewModelProviders.of(parent).get(ScheduleViewModel.class);
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
reObserveViewModel();
}
// remove and re-add observer until Google fixes the multiple observer issue
// TODO: remove when Google fixes the issue
// https://github.com/googlesamples/android-architecture-components/issues/47
private void reObserveViewModel() {
viewModel.getArrivals().removeObservers(this);
viewModel.getArrivals().observe(this, arrivalsObserver);
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_arrivals_departures, container, false);
RecyclerView recyclerView = view.findViewById(R.id.rv_schedule_details);
LinearLayoutManager llm = new LinearLayoutManager(this.getContext());
recyclerView.setLayoutManager(llm);
recyclerView.setAdapter(adapter);
return view;
}
}
For info: I timestamped the ViewModel's constructor start and end (to rule out those calls somehow being on the UI thread - takes 1 millisecond).
Narrowed down the issue
After Robin Davies' answer, I tried the Android Profiler and although I get some GC events now and then, I don't get them all the time with the stuttering being there every single time. However, delaying setting of the adapter data in the observer by 100ms seems to let the BNV animation complete when switching to the ArrivalsFragment:
No stuttering gif
All I did was changing
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable List<ScheduleInfo> infoList) {
adapter.setData(infoList);
}
};
to
private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
#Override
public void onChanged(#Nullable final List<ScheduleInfo> infoList) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
adapter.setData(infoList);
}
}, 100);
}
};
So it seems that this part of your answer
Also if you post list results back to the foreground thread and
populate the adapter while the animation is running, that will force a
layout pass that will interfere with animation.
is the one I was struggling with in my particular case. While I'm a bit disappointed having to revert to using delays to make the animation fluent, I'm happy to have found the culprit and thank you very much for your help :)
Yes this is common enough problem.
Assuming that you've already moved heavy processing onto an background threads...
If you are doing really heavy lifting on the background thread, you can trigger garbage collects, which can block the foreground thread long enough to cause stuttering. Also if you post list results back to the foreground thread and populate the adapter while the animation is running, that will force a layout pass that will interfere with animation.
Try using the CPU usage/profiling tools to see what exactly is holding up the foreground thread.
Solutions to consider would be to postpone population of the fragments until the animation is finished. Or pre-populate the fragment. Or maybe block the background thread while animation is running (perhaps). Or postpone the animation until the fragment is populated and laid out (which gets potentially unpleasant). If the problem isn't caused by a garbage collect, you could delay creation/population of the adapter until the animation is finished.
I am working on an Android app. The code I attach is creating a recyclerview. The very first thing we do is to create an asynctask that would fetch data on an SQLite database and load it into the adapter->recylcerview. While the background task is working, a progressdialog is shown to the user.
public class HomeActivity extends AppCompatActivity
{
private RecyclerView recycler;
private RecyclerViewAdapter adapter;
private SwipeRefreshLayout swipeRefresh;
private progressDialog progressDialog;
// ... some code here
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ... some code here
createRecyclerView();
loadRecyclerView();
// ... some code here
}
private void loadRecyclerView()
{
new LoadingBackgroundTask().execute();
}
private void createRecyclerView()
{
Context context = getApplicationContext();
recycler = (RecyclerView) findViewById(R.id.recycle_view_home);
recycler.setHasFixedSize(true);
RecyclerView.LayoutManager lManager = new LinearLayoutManager(context);
recycler.setLayoutManager(lManager);
adapter = new RecyclerViewAdapter();
recycler.setAdapter(adapter);
recycler.setItemAnimator(new DefaultItemAnimator());
}
private class LoadingBackgroundTask extends AsyncTask<Void, Void, List<items>> {
#Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = ProgressDialog.show(HomeActivity.this, getString(R.string.dialog_load_list),getString(R.string.dialog_please_wait), false, false);
}
#Override
protected List doInBackground(Void... params) {
List<items> lists;
//Data Source Class ( SQLite)
ListDS listDS = new ListDS(getApplicationContext());
list = listDS.getList();
return list;
}
#Override
protected void onPostExecute(List result) {
super.onPostExecute(result);
//it inserts de list on recyclerview performing animation
adapter.animate(result);
progressDialog.dissmiss();
swipeRefresh.setRefreshing(false);
recycler.scrollToPosition(0);
}
}
}
So far, so good. However, as you probably know this code has some well-known issues; for example if I rotate the screen while asynctask is doing its magic, it will crash the app.
I've tried an alternative I've seen Googling, rxandroid.
(Sorry if I typed something wrong, I am doing it by memory)
public class HomeActivity extends AppCompatActivity
{
private Subscriber suscriptor;
private progressDialog progressDialog;
//some code ....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
suscriptor = new Subscriber() {
#Override
public void onCompleted() {
progressDialog.dismiss();
Log.d("SUSCRIPTOR","ON COMPLETE");
}
#Override
public void onError(Throwable e) {
Log.d("SUSCRIPTOR","ON ERROR");
}
#Override
public void onNext(Object o) {
adapter.animate((List<items>)o);
}
};
Observable.create(
new Observable.OnSubscribe<List<items>>() {
#Override
public void call(Subscriber<? super List<items>> sub) {
progressDialog = ProgressDialog.show(HomeActivity.this, getString(R.string.dialog_load_list),getString(R.string.dialog_please_wait), false, false);
List<items> lists;
//Data Source Class ( SQLite)
ListDS listDS = new ListDS(getApplicationContext());
list = listDS.getList();
sub.onNext(list);
sub.onCompleted();
}
#Override
protected void finalize() throws Throwable {
super.finalize();
Log.d("OBSERAVBLE","FINALIZED");
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.cache()
.subscribe(suscriptor);
}
#Override
public void onDestroy()
{
if(suscriptor!=null)
{
if(!suscriptor.isUnsubscribed())
{
suscriptor.unsubscribe();
}
}
super.onDestroy();
}
}
Now the app is not crashing anymore when I rotate the screen. However, the observable keeps working on the background until it finishes but as I unsubscribe to avoid crashing, I don't receive the results properly. Moreover, progressbar disappears even though the observable keeps working as I mentioned.
Looking for a solution, I found there is a pattern called "Ted Mosby" which seems to solve my problem. Although it looks promising, I think is too much coding for something I felt it is not worth it and that rxandroid may have a workaround.
So, my question is how can I get what I want without getting immersed in an architectural coding mess too big for my purpose? Could you give an example if you guys have solved this? Do you think I am wrong and I should implement TedMosby pattern?
Mosby is a Model-View-Presenter (MVP) library. So the pattern you named "ted mosby pattern" is actually MVP.
But you didn't have understood what MVP is all about. It's not about retaining async running taks, even thought this could be achieved with Mosby. MVP is about separation of concerns. View is just displaying UI elements, Presenter is controlling the View, i.e. the presenter tells the view: now display the progress diaolog, now hide the progress dialog, etc. In other words, the presenter controls the state of the view. The Model could be an async task or a RxJava Observable. The Presenter than gets the result back and tell the view to display it. You code is decoupled into 3 layers Model (also called business logic) and Presenter and View. The advantage is that you can change the view (i.e. replace progress dialog with an progressbar widget) without touching any code that loads data (Presenter and business logic). Additionally with MVP your code becomes testable.
So what you should compare is: Should I use AsyncTask or RxJava for loading data. With Mosby you would execute your http request in the presenter. While orientation changes are done the presenter doesn't get destroyed (and hence the background task doesn't get canceled).
However, MVP is not the solution for everything. If you have to ensure that a single http call is executed correctly (i.e. sign up for a community) you should think about using an android service.
What you can do is whatever you do in activity like your AsycTask and RecyclerView, put it inside a fragment and, setRetainInstance(true) in onCreateView() method of fragment and load that fragment in your activity.
setRetainInstance(true) won't let your fragment instance destroy when screen is rotated.
Your Observable should handle himself the fact on unsubscription. There are two mechanism for this:
check subscriber.isUnsubscribed. You can do it between or after "heavy" steps
add unsubscription callback. You can use it to stop long running operations, release resources etc
Take a look at this code:
Observable.create(
new Observable.OnSubscribe<List<items>>() {
#Override
public void call(Subscriber<? super List<items>> sub) {
sub.add(Subscriptions.create(new Action0() {
#Override
public void call() {
cancelLongRunningOperationIfItStillRunning();
}
}));
if (!sub.isUnsubscribed()) {
//start long running operation here
}
}
})
.doOnSubscribe(new Action0() {
#Override
public void call() {
}
})
You shouldn't reference to your activity/context/progress dialog etc inside Observable. Instead use doOnSubscribe if you want to do some side effects.
I'm doing some test after change device configuration (change language, orientation, etc), and i notice that after this, the method "notifyDataSetChanged()" is not working.
The action example:
I'm calling updateList() everytime i do an action like delete, save, etc. The user click a delete button, a DialogFragment is shown, "Are you sure you want to delete?", when i change the orientation, or the language, or any configuration of the device and then click "yes" on the Dialog, the data is removed, but the list doesn't update. I need to quit the activity, then go back to see the alteration.
BookAdapter:
public void updateList(ArrayList<Book> books) {
bookList = books;
notifyDataSetChanged();
}
What can i do to make it works after the configuration change?
Edit:
BookAdapter Constructor:
public BookAdapter(Context c, ArrayList<Book> books) {
context = c;
bookList = books
bookDAO = BookDAO.getInstance(context);
}
BookFragment:
public class BookFragment extends Fragment {
private BookDAO bookDAO;
private BookAdapter bookAdapter;
private ListView listBook;
private View view;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
bookDAO = bookDAO.getInstance(getActivity());
view = inflater.inflate(R.layout.book_tab, container, false);
ArrayList<Book> listBook = null;
try {
llistBook = bookDAO.getAll();
} catch (Exception e) {
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
return view;
}
bookAdapter = new BookAdapter(getActivity(), listBook);
listBook = (ListView)view.findViewById(R.id.listBook);
listBook.setAdapter(bookAdapter);
return view;
}
}
You can try implementing BookAdapter as a Singleton to confirm that you are not calling updateList(..) from a stale reference.
Changes that you will need to make:
// I am assuming that you are using a BaseAdapter because
// BookAdapter's constructor that you provided in the code above
// does not contain a call to super(....)
public class BookAdapter extends BaseAdapter {
private static BookAdapter mAdapter;
private Context context;
private static ArrayList<Book> bookList;
private BookDAO bookDAO;
// To keep at most one instance of BookAdapter
public static BookAdapter getInstance(Context con, ArrayList<Book> books) {
// If an instance exists, return it
if (mAdapter != null) {
bookList = books;
return mAdapter;
}
// Else, craete a new instance
mAdapter = new MyAdapter(con, books);
return mAdapter;
}
// BookAdapter's only constructor is declared as private to restrict access
private BookAdapter(Context con, ArrayList<Book> books) {
context = con;
bookList = books;
bookDAO = BookDAO.getInstance(context);
}
public void updateList(ArrayList<Book> books) {
bookList = books;
notifyDataSetChanged();
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Retrieve object
Book bookItem = bookList.get(position);
....
....
}
}
This is how the Fragment's onCreateView will change:
bookAdapter = BookAdapter.getInstance(getActivity(), listBook);
Code that will be executed when the user presses yes to Are you sure you want to delete?:
// Remove entry from bookDAO
// Remove entry from listBook
// OR update listBook:
try {
listBook = bookDAO.getAll();
} catch (Exception e) {
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
}
// Assertion: "listBook" does not contain the
// item that was just deleted from "bookDAO"
// Update ListView's contents
bookAdapter.updateList(listBook);
The problem occurs because every time your rotate, change language, etc... the activity is recreated and your fragments are also recreated (new instance), so the notififyDataSetChanged is actually notifying the old instances of your fragments.
A solution for that would be. Make your fragments static. Then you create some refresh method for your fragments, and called it when you press yes for your dialog.
In your activity you should have something like this.
private static BookFragment bookFragment;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
if (fragment1 == null) {
bookFragment = new BookFragment();
}
...
}
Create some interface like:
public interface Refreshable {
public void refresh();
}
Then implement this interface all your fragments.
In the method that is called when the dialog is answered positively you should call
...
fragment.refresh();
...
Inside the refresh method of you fragment you can call its adapter method updateList(...)
Might not be the prettier solution, but it works....
Why this happen... Google's Android Dev Team might know.
I am using an ArrayList of Strings in my GridViewAdapter which extends BaseAdapter.
so in case i changed the data concerning the List, i cann notifyDataSetChanged() on the Adapter. I dont really see the point why you cant call it?
So what i would do is ovverride this method, and just call notifyDataSetChanged()
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
adapter.notifyDataSetChanged();
}
Besides that, does your Book data chnage based on your configuration/orientation?
Try using the same list for the whole adapter lifecycle, and change only its content:
public class BookAdapter extends ArrayAdapter<Book> {
private final booklist;
// ..
public BookAdapter(Context c, ArrayList<Book> books) {
super(c, R.layout.yourlayout, books);
context = c;
bookList = books
bookDAO = BookDAO.getInstance(context);
}
public void updateList(ArrayList<Book> books) {
bookList.clear();
boolList.addAll(books);
notifyDataSetChanged();
}
// ..
}
Why are you keeping your View as a data member?
When keeping a view across configuration changes, you hold a reference to the previous instance, before the configuration change was happening. Since it's the view holding your list - it may miss data updates.
Try to remove this field, and the use of it, and see if the list now updates.
I think the problem comes from the fact that any configuration change such as the orientation will restart your current Activity.
Because of this, I guess some parts of your code are still referencing the previous activity that does not exist anymore and the notifyDataSetChanged is not working anymore.
There are 2 things you can quickly try:
Add this line in the manifest file for your activity: android:configChanges="orientation|locale". This change means to the system that you will handle yourself the changes to do during orientation or language changes. Therefore, the app will not recreate the activity by itself anymore (so the activity should work the same).
The other trick can be to add this line at the beginning of the function onCreateView:
setRetainInstance(true);.
This line will retain the fragment state during configuration changes as the documentation explains:
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being re-created.
onAttach(Activity) and onActivityCreated(Bundle) will still be called.
Just be informed that as explained the Fragment lifecycle will change a little.
3) Last option could be to retain the activity state using onSaveInstanceState and onRestoreInstanceState as explained in details in this answer: https://stackoverflow.com/a/151940/2206688
I have got the same problem for 8 days now and i hope that someone can help me, i am working on an application that parse multiple RSS feeds and display them on listview with fragment but the problem is that the UI get blocked for 2 to 4 seconds, and every time the user touches the screen the application crashes.
I tracked the problem and i found out that i had to bring the data asynchronously, which i actually did, but every time i have a fragment, the same problem occurs.
In fact,the fragment blocks when it brings data if it does it from a main thread, while i actually bring data asynchronously, that's why i can't get the reason for the blocking.
This is how i change my code, this is my interface
public interface OnTaskFinishedListener {
void onTaskFinished(ArrayList<Article> articles);
}
i am getting the ArrayList in the AsyncTask like this
public class AndroidSaxFeedParserAsync extends AsyncTask<String, Long,ArrayList<Article>> {
ArrayList<Article> rssItems = new ArrayList<Article>();
public URL feedUrl;
OnTaskFinishedListener onTaskFinishedListener;
public AndroidSaxFeedParserAsync(OnTaskFinishedListener _onTaskFinishedListener)
{
onTaskFinishedListener = _onTaskFinishedListener ;
}
#Override
protected void onPostExecute(ArrayList<Article> result) {
super.onPostExecute(result);
onTaskFinishedListener.onTaskFinished(result);
}
#Override
protected ArrayList<Article> doInBackground(String... params) {
//....
return rssItems;
}
}
and finally i get it here in my fragment like this
public class FeedPlayerIndexFragment extends SherlockFragment implements OnTaskFinishedListener{
String url="http://www.whatever.com/index.php?format=feed&type=rss";
ArrayList<Article> feeds;
ItemAdapterArticle lfa;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
new AndroidSaxFeedParserAsync(new OnTaskFinishedListener() {
#Override
public void onTaskFinished(ArrayList<Article> articles) {
}
}).execute(url);
View view=inflater.inflate(R.layout.main, container, false);
//....
return view;
}
#Override
public void onTaskFinished(ArrayList<Article> articles) {
lfa = new ItemAdapterArticle(getActivity().getApplicationContext(), articles);
}
}
Problem is that you're using AsyncTask.get() call which is actually blocking. There's another solution on this problem which doesn't block the UI thread. You need to define a simple interface:
public interface OnTaskFinishedListener {
void onTaskFinished(List<Article> articles);
}
The calling Fragment should implement this interface. When instantiating the AsyncTask you send your Fragment as an OnTaskFinishedListener instance as a parameter to constructor. The AsyncTask should hold the reference to OnTaskFinishedListener instance, and when the work is done, in onPostExecute(), you call onTaskFinished() on the instance. Now, in the Fragment you can respond to this call and use the articles parameter to populate your UI components. Hope this helps.