I have the following class extended Fragment:
public static class DummySectionFragment extends Fragment {
public static final String ARG_SECTION_NUMBER = "section_number";
private GridView events;
public DummySectionFragment() {}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle args = getArguments();
switch (args.getInt(ARG_SECTION_NUMBER)) {
case 1 : return initializeCalendarScreen(inflater);
case 2 : return inflater.inflate(R.layout.todo_lists_layout, null);
}
return null;
}
public void updateGridView() {
int daysInCurrentMonth=(MainActivity.month==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month];
int daysInPrevMonth=(MainActivity.month-1==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month-1];
int daysInNextMonth=(MainActivity.month+1==1) ? daysInFebruary : COUNT_OF_DAYS_IN_MONTH[MainActivity.month+1];
Calendar calendar=Calendar.getInstance();
calendar.set(MainActivity.year, MainActivity.month, 1);
int startDayOfWeek=calendar.get(Calendar.DAY_OF_WEEK);
List<Integer> dates=new ArrayList<Integer>();
for (int i=0; i<startDayOfWeek-1; i++) {
dates.add(daysInPrevMonth-i);
}
for (int i=0; i<daysInCurrentMonth; i++) {
dates.add(i+1);
}
ArrayAdapter<Integer> adapter=new ArrayAdapter<Integer>(getActivity(), R.layout.grid_view_cell_layout, R.id.textViewGridCell, dates);
events.setAdapter(adapter);
}
private View initializeCalendarScreen(LayoutInflater inflater) {
View layout=inflater.inflate(R.layout.calendar_layout, null);
events=(GridView)layout.findViewById(R.id.gridViewCalendar);
updateGridView();
return layout;
}
}
It works good (I use it for FragmentPagerAdapter). And I use method updateGridView() from Activity for updating value in GridView. But if I do it I will got NullPointerException, because getActivity() for ArrayAdapter returns null. Please, tell me, how can I update Fragment UI from Activity? Thank you
ONE POSSIBLE SOLUTION IS:
Create static global variable context
create context = inflater.getContext().
Use context to create ArrayAdapter when updating UI.
Change:
...=new ArrayAdapter<Integer>(context,...)
Use an Observer in the Fragment instead of calling updateGridView() outside your Fragment. Have an Observer in your Fragment observe an Observable in your main Activity. When the data needs to be updated, just call the notifyObservers() method of the Observable. The Observer class should be a child class of your Fragment and should look something like this:
class UpdateGridObserver implements Observer {
#Override
public void update(Observable observable, Object data) {
// Do stuff like:
// runOnUiThread is REQUIRED because updating the adapter
// and ListView from a background thread is not allowed
getActivity().runOnUiThread(new Runnable(){
updateGridView();
});
}
}
This allows for the data to be updated without having to worry about scope and all that jazz. As long as the Observable object is accessible to the Observer object you should be good to go.
You can extend the Observable class (as with any class) as well to have a custom Observable object if you need more capability than just notification. It's outside the scope of what I think you are looking for, but I've linked some wikis and howtos below.
Observer-Observable Pattern: http://en.wikipedia.org/wiki/Observer_pattern
More: http://www.javaworld.com/jw-10-1996/jw-10-howto.html
Related
In my ViewModel class I have a method that fetches data from the internet and sets them on LiveData via setValue() (I've used the approach from these docs):
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
if (mDataList == null) {
mDataList = new MutableLiveData<>();
}
Handler handler = new Handler(Looper.getMainLooper());
// getData() connects to the internet and fetches the online data
mInternetConnection.getData(new InternetConnection.ConnectionCallback<List<?>>() {
#Override
public void onComplete(Result<List<?>> result) {
if (result instanceof Result.Success) {
mDataList.setValue(((Result.Success<List<?>>) result).data);
} else {
Log.e(LOG_TAG, "ViewModel could not obtain data list");
}
}
}, handler);
return mDataList;
}
But mDataList.getValue() is null. The setValue() method is called on the main thread.
onComplete gets called for sure (checked).
When I check whether the value of mDataList is null in the onComplete method right after setValue(), the logs show that it is not null.
Why is it null and how should you get the value of the modified mDataList?
I've spent hours looking for a solution on the web, but couldn't find anything that could help. The related questions on this site don't help to solve the problem either.
EDIT:
From the docs:
In this example, the callback passed into the Repository's makeLoginRequest call is executed on the main thread. That means you can directly modify the UI from the callback or use LiveData.setValue() to communicate with the UI.
So I've followed that, but this part: "or use LiveData.setValue() to communicate with the UI" doesn't work.
The observer is set on it in the Fragment, but LiveData still don't get updated - list in onChanged is null, the screen remains blank, no errors.
Fragment code:
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
View rootView = binding.getRoot();
binding.listRecyclv.setLayoutManager(new LinearLayoutManager(rootView.getContext()));
mAdapter = createAdapter(rootView.getContext(), pageViewModel.getList().getValue());
binding.listRecyclv.setAdapter(mAdapter);
pageViewModel.getList().observe(getViewLifecycleOwner(), new Observer<List<?>>() {
#Override
public void onChanged(#Nullable List<?> list) {
mAdapter.setList((ArrayList<MyObj>) list);
binding.listRecyclv.setAdapter(mAdapter);
}
}
}
});
Getter in ViewModel:
public LiveData<List<?>> getList() {
return getDataList();
}
Code in Adapter:
public class AppAdapter extends RecyclerView.Adapter<AppAdapter.AppViewHolder> {
...
public void setList(ArrayList<MyObj> list) {
mAppList = list;
notifyDataSetChanged();
}
Thanks in advance.
mDataList is null because the call you're making to get data is asynchronous. This means that the function will return before your api call (and its callbacks) does.
Read more here https://www.bmc.com/blogs/asynchronous-programming/
To get the list you need to observe data changes.
So something like this:
From the activity:
Kotlin:
viewModel.getDataList().observe(this) { dataList ->
//TODO: Do something with the list
}
Java:
final Observer<String> listObserver = new Observer<List<>>() {
#Override
public void onChanged(#Nullable final List<> newName) {
//TODO: Use the list
}
};
viewModel.getDataList().observe(this, listObserver);
The logic inside the observer will be called every time the list changes.
I solved it this way:
Changed this line in Fragment's onCreate method
from:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pageViewModel = new ViewModelProvider(this).get(PageViewModel.class);
to
pageViewModel = new ViewModelProvider(requireActivity()).get(PageViewModel.class);
Thanks to this answer
Changed the data type of getDataList() method in the PageViewModel class from LiveData to MutableLiveData (without it the above step doesn't help):
from:
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
to:
private MutableLiveData<List<?> getDataList() {
and now it works! The observer gets triggered now and the data are updated.
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
So I have an Activity. The Activity hosts a ViewPager with tabs, each tab holding a Fragment in it. The Fragments themselves have a RecyclerView each. I need to communicate changes from the RecyclerView's adapter to the activity.
Currently, I am using the listener pattern and communicating using interface between each of the components. i.e I have an interface between the RecyclerView's adapter and the Fragment holding it. Then an interface from the Fragment to the ViewPager's FragmentStatePagerAdapter which is creating all the Fragments. And 1 more interface between the ViewPager's adapter and the Activity hosting the ViewPager. I feel that there are too many interfaces for all the components because of how they are structured.
Currently I am not facing issues as such but I think the listener pattern is acting like an anti-pattern due to all the nested components. Instead of creating independent components I think the hierarchy will make it difficult for making code changes in future.
Am I doing it correctly or is there a better way to do it? Is this a case where I should use an Event Bus or Observer Pattern (If yes can you point me to some examples where someone overcame a similar problems using it)?
NOTE : If it matters, I need it to maintain a global object in the activity, something like a shopping cart where I can add or remove items and these items are present in RecyclerView's adapter from where I can add it to the cart and also increment or decrement the count for a particular item. The ViewPager and Tabs help segregate these items in various categories.
Edit 1 : Some code trying out #LucaNicoletti's approach -
I have skipped one level that is the level with the ViewPager's FragmentStatePagerAdapter. I guess that should not matter and stripped of some other code to keep it small.
MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener, FoodAdapter.OnFoodItemCountChangeListener {
#Override
public void onFoodItemDecreased(FoodItemModel foodItemModel, int count) {
Log.d("Test", "Dec");
}
#Override
public void onFoodItemIncreased(FoodItemModel foodItemModel, int count) {
Log.d("Test", "Inc");
}
// Other methods here
}
Fragment hosting the Adapter:
public class FoodCategoryListFragment extends Fragment implements FoodAdapter.OnFoodItemCountChangeListener {
// Other boring variables like recyclerview and layout managers
FoodAdapter foodAdapter;
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Other boring intializations for recyclerview and stuff
// I set the click listener here directly on the adapter instance
// I don't have this adapter instance in my activity
foodAdapter.setOnFoodItemClickListener(this);
rvFoodList.setAdapter(foodAdapter);
}
}
The adapter class at the lowest level:
public class FoodAdapter extends RecyclerView.Adapter<FoodAdapter.FoodViewHolder> {
private OnFoodItemCountChangeListener onFoodItemCountChangeListener;
private List<FoodItemModel> foodItems;
// The interface
public interface OnFoodItemCountChangeListener {
void onFoodItemIncreased(FoodItemModel foodItemModel, int count);
void onFoodItemDecreased(FoodItemModel foodItemModel, int count);
}
// This is called from the fragment since I don't have the adapter instance
// in my activty
public void setOnFoodItemClickListener(OnFoodItemCountChangeListener onFoodItemCountChangeListener) {
this.onFoodItemCountChangeListener = onFoodItemCountChangeListener;
}
// Other boring adapter stuff here
#Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.bMinus:
onFoodItemCountChangeListener.onFoodItemDecreased(foodItems.get(getAdapterPosition()),
Integer.parseInt(etCounter.getText().toString()));
}
break;
case R.id.bPlus:
onFoodItemCountChangeListener.onFoodItemIncreased(foodItems.get(getAdapterPosition()),
Integer.parseInt(etCounter.getText().toString()));
}
break;
}
}
}
my comments were:
what you should/could do it's to have a global data repo which holds the shopping cart and listeners associated with changes to it. Like a singleton, like ShoppingCart.getInstance().addListener(this); and ShoppingCart.getInstance().addItem(new Item(id));
and
Yes. That's what I'm suggesting. Do not forget that this Singleton can never ever holds Context or Activity because u don't want to leak memory, so always call removeListener. On my opinion it would reduce dependency as all your view controllers only interact with the data model
and I'll add some code to exemplify as a proper answer.
Below is a very crude, typed by heart code, but it should give an idea. All the UI elements are only tied to the data, and not to each other.
Similar stuff could be implemented with libraries that provide observable pattern out of the box for data-only objects.
public class ShoppingCart {
private ShoppingCart single;
private static void init(){
.. init single if not null
}
private List<Item> items = new ArrayList<>();
public int numberOfItems;
public long totalPrice;
private static void addItem(Item item){
init()
single.items.add(item);
single.numberOfItems++;
single.totalPrice+=item.price;
dispatchChange();
}
private static void removeItem(Item item){
init();
single.numberOfItems--;
single.totalPrice-=item.price;
dispatchChange();
single.items.remove(item);
}
private void dispatchChange(){
// TODO: write real loop here
for(single.listeners) listener.onCartChanged(single.cart);
}
public interface Listener {
void onCartChanged(ShoppingCart cart);
}
private List<Listener> listeners = new ArrayList<>();
// TODO: addListener and removeListener code
public static class Item {
String id;
String name;
long price;
}
}
To communicate between components (Activity, Fragment) you have to use an event bus.
In android, you could choose between:
RxJava
Otto
Green Robot EventBus
A blog to explain this.
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;
}
}
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