Some background information:
I am using a Activity>ParentFragment(Holds ViewPager)>Child fragments.
Child Fragments are added dynamically with add, remove buttons.
I am using MVP architecture
Actual Problem:
In child fragment, we have listview that populates using an asynctaskloader via a presenter.
Child Fragment:
//Initialize Views
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
root = inflater.inflate(R.layout.fragment_search_view_child, container, false);
.......
mSearchViewPresenter= new SearchViewPresenter(
getActivity(),
new GoogleSuggestLoader(getContext()),
getActivity().getLoaderManager(),
this, id
);
SearchList list=new SearchList();
//requestList from presenter
searchListAdapter =new SearchViewListAdapter(getActivity(), list, this);
listView.setAdapter(searchListAdapter);
......
return root;
}
#Override
public void onResume(){
super.onResume();
mSearchViewPresenter.start();
searchBar.addTextChangedListener(textWatcher);
}
In the presenter class we have:
public SearchViewPresenter(#NonNull Context context, #NonNull GoogleSuggestLoader googleloader,#NonNull LoaderManager loaderManager,
#NonNull SearchViewContract.View tasksView, #NonNull String id) {
// mLoader = checkNotNull(loader, "loader cannot be null!");
mLoaderManager = checkNotNull(loaderManager, "loader manager cannot be null");
// mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mSearchView = checkNotNull(tasksView, "tasksView cannot be null!");
mSearchView.setPresenter(this);
searchList=new SearchList();
this.googleLoader=googleloader;
this.context=context;
this.id=loaderID;
// this.id=Integer.parseInt(id);
}
#Override
public void start() {
Log.d("start>initloader","log");
mLoaderManager.restartLoader(1, null, this);
}
//TODO implement these when you are ready to use loader to cache local browsing history
#Override
public android.content.Loader<List<String>> onCreateLoader(int i, Bundle bundle) {
int loaderid=googleLoader.getId();
Log.d("Loader: ", "created");
googleLoader=new GoogleSuggestLoader(context);
googleLoader.setUrl("");
googleLoader.setUrl(mSearchView.provideTextQuery());
return googleLoader;
}
#Override
public void onLoadFinished(android.content.Loader<List<String>> loader, List<String> data) {
Log.d("Loader: ", "loadFinished");
searchList.clear();
for (int i = 0; i < data.size(); ++i) {
searchList.addListItem(data.get(i), null, LIST_TYPE_SEARCH, android.R.drawable.btn_plus);
Log.d("data Entry: ",i+ " is: "+searchList.getText(i));
}
mSearchView.updateSearchList(searchList);
}
#Override
public void onLoaderReset(android.content.Loader<List<String>> loader) {
}
Also we have this code in the presenter that is triggered by a edittext box on the fragment view being edited.
#Override
public void notifyTextEntry() {
//DETERMINE HOW TO GIVE LIST HERE
// Dummy List
Log.d("notifyTextEntry","log");
if(googleLoader==null)googleLoader=new GoogleSuggestLoader(context);
googleLoader.setUrl(mSearchView.provideTextQuery());
// mLoaderManager.getLoader(id).abandon();
mLoaderManager.getLoader(1).forceLoad();
mLoaderManager.getLoader(1).onContentChanged();
Log.d("length ", searchList.length().toString());
// googleLoader.onContentChanged();
}
Lastly we have the loader here:
public class GoogleSuggestLoader extends AsyncTaskLoader<List<String>>{
/** Query URL */
private String mUrl;
private static final String BASE_URL="https://suggestqueries.google.com/complete/search?client=firefox&oe=utf-8&q=";
private List<String> suggestions =new ArrayList<>();
public GoogleSuggestLoader(Context context) {
super(context);
this.mUrl=BASE_URL;
}
public void setUrl(String mUrl){
this.mUrl=BASE_URL+mUrl;
};
#Override
protected void onStartLoading() {forceLoad(); }
#Override
public List<String> loadInBackground() {
if (mUrl == null) {
return null;
}
try {
suggestions = new ArrayList<>();
Log.d("notifyinsideLoader","log");
String result=GoogleSuggestParser.parseTemp(mUrl);
if(result!=null) {
JSONArray json = new JSONArray(result);
if (json != null) {
JSONArray inner=new JSONArray((json.getString(1)));
if(inner!=null){
for (int i = 0; i < inner.length(); ++i) {
//only show 3 results
if(i==3)break;
Log.d("notifyinsideLoader",inner.getString(i));
suggestions.add(inner.getString(i));
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
return suggestions;
}
}
So the problem:
The code loads the data fine to the listview on the fragment. When orientation changes loader is not calling onLoadFinished. I have tested the loader and it is processing the data fine.
I have already tried forceload and onContentChanged in the presenter to no avail.
If you need anymore info or if I should just use something else like RxJava let me know. But I would really like to get this working.
Before you ask I have seen similar problems like: AsyncTaskLoader: onLoadFinished not called after orientation change however I am using the same id so this problem should not exist.
The answer was on this page AsyncTaskLoader doesn't call onLoadFinished
but details were not given as to how to move to this.
So let me explain here for anyone else with this problem in future.
Support library is meant for fragments. So the class that is in charge of callbacks has to be importing AND implementing the correct methods from the support library. Same as if you are using MVP your presenter must extend from support loadermanager.
i.e: import android.support.v4.app.LoaderManager; Then implement correct callbacks.
Like
#Override
public android.support.v4.content.Loader<List<String>> onCreateLoader(int i, Bundle bundle) {
...
return new loader
}
and
#Override
public void onLoadFinished(android.support.v4.content.Loader<List<String>> loader, List<String> data) {
//do something here to your UI with data
}
Secondly: The loader itself must be extending from support asynctaskloader.
i.e: import android.support.v4.content.AsyncTaskLoader;
Related
I made simple Client server to android.
I have problem when I send an object from server to the client.
The object is received ok and when I check the log, it shows me the the object was sent successfully.
The problem occurs when I'm trying to get this object and put it in my ListView adapter.
The adapter works, I checked it with a random ArrayList I created.
My issue is when I'm trying to to put the values of AsyncTask in my adapter.
public class RestaurantListFragment extends Fragment {
private ArrayList<Resturant> res = new ArrayList<>();
private ListAdapter adapter;
private Firebase restRef = new Firebase("https://restmeup.firebaseio.com/restaurants");
private Client mClient;
// private connectTask t = (connectTask)new connectTask().execute();
public RestaurantListFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new connectTask().execute();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// new connectTask(getView()).execute();
final View rootView = inflater.inflate(R.layout.fragment_two, container, false);
ListView restaurantList = (ListView) rootView.findViewById(R.id.list);
adapter = new ListAdapter(getContext(), res, getActivity());
restaurantList.setAdapter(adapter);
// connectTask t = (connectTask)new connectTask().execute();
if (mClient != null) {
mClient.sendMessage("bar");
}
SqlQueriesConverter sql = new SqlQueriesConverter();
sql.getResurantsListQuery("bar");
// sql.getUserFavoritesResturants(accessToken.getUserId());
mClient.sendMessage(sql.getQuery());
// t.setArray(res);
mClient.sendMessage("/quit");
mClient.stopClient();
final EditText searchText = (EditText)rootView.findViewById(R.id.searchListView);
searchText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
System.out.println("Before---------");
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String text = searchText.getText().toString().toLowerCase(Locale.getDefault());
adapter.filter(text);
adapter.notifyDataSetChanged();
System.out.println("array: " + res.toString());
}
#Override
public void afterTextChanged(Editable s) {
System.out.println("After---------");
}
});
// Inflate the layout for this fragment
return rootView;
}
public class connectTask extends AsyncTask<ArrayList<?>,ArrayList<?>,Client> {
// private Client mClient;
private ArrayList<?> arrayList = new ArrayList<>();
#Override
protected Client doInBackground(ArrayList<?>... message) {
//we create a Client object and
mClient = new Client(new Client.OnMessageReceived() {
#Override
//here the messageReceived method is implemented
public void messageReceived(ArrayList<?> message) {
//this method calls the onProgressUpdate
// publishProgress(message);
onProgressUpdate(message);
}
});
mClient.run();
return null;
}
// #Override
protected void onProgressUpdate(ArrayList<?>... values) {
super.onProgressUpdate(values);
ArrayList<?> arr2;
if (values[0].get(0) instanceof Resturant){
Log.d("step 1", "1");
if (((ArrayList<?>)values[0]).get(0)instanceof Resturant) {
// arr2 = (ArrayList<Resturant>) values[0];
res = (ArrayList<Resturant>) values[0];
adapter.notifyDataSetChanged();
Log.d("array",res.toString());
}
}
if (values[0].get(0)instanceof Review){
arr2 = (ArrayList<Review>) values[0];
}
if (values[0].get(0)instanceof UserFavorites){
arr2 = (ArrayList<Review>) values[0];
Log.d("step 2", "2");
}
}
}
}
There are two things you need to change to use the AsyncTask as you intend. The first change you need is to return the ArrayList you get from your mClient in the doInBackground method. This is a bit troublesome because it looks like the Client is already running asynchronously since you pass a callback to get the result (this is the Client.OnMessageReceived anonymous class you have there). The second change would be to implement onPostExecute on your AsyncTask since that is where the results from doInBackground are sent. You'd add the result sent from doInBackground to your adapter in there.
In any case, since it looks like Client is already performing the work asynchronously, you shouldn't need to use an AsyncTask at all. Just implement the logic to add the results to your adapter in the Client.OnMessageReceived callback.
Just get the code you already have in onProgressUpdate and throw it inside messageReceived. Something like this:
mClient = new Client(new Client.OnMessageReceived() {
#Override
//here the messageReceived method is implemented
public void messageReceived(ArrayList<?> values) {
if (values[0].get(0) instanceof Resturant){
Log.d("step 1", "1");
if (((ArrayList<?>)values[0]).get(0)instanceof Resturant) {
res = (ArrayList<Resturant>) values[0];
adapter.notifyDataSetChanged();
Log.d("array",res.toString());
}
}
}
});
I'm making an Reddit app for my android exam and I have a question about inheritence.
I have a Fragment who has a RecyclerView. That recyclerview contains a list of redditposts. My app consists of multiple subreddits (funny, gaming, news, etc..). Every subreddit has his own Fragment. I have some methods that every Fragment has to have. (a showProgressBar, hideProgressBar, populateResult, etc...) I think it would be simple if i just make an Fragment class where all the subreddit Fragments can inheritance from. I could put all the methods in that fragment class because the methods are the same for every subreddit fragment. But my lecturer said that is a bad use of inheritance. So does anybody have a best practice around this problem?
This is the fragment i'm talking about:
package com.example.thomas.redditapp;
public class FunnyFragment extends Fragment {
private OnListFragmentInteractionListener mListener;
#Bind(R.id.funny_recyclerview)
RecyclerView mRecyclerView;
#Bind(R.id.progressBarFetch)
ProgressBar progress;
private RedditHelper helper;
private RedditPostRecyclerViewAdapter mAdapter;
List<RedditPost> redditPosts;
public FunnyFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
helper = null;
helper = new RedditHelper(SubRedditEnum.funny, this);
redditPosts = new ArrayList<>();
startLoad();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_funny_list, container, false);
ButterKnife.bind(this, view);
showProgressBar();
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mAdapter = new RedditPostRecyclerViewAdapter(redditPosts, mListener, mRecyclerView);
mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {
#Override
public void onLoadMore() {
redditPosts.add(null);
helper.loadListFromUrl();
}
});
mRecyclerView.setAdapter(mAdapter);
return view;
}
protected void startLoad() {
if (helper != null) {
helper.loadListFromDb();
}
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
if (isTaskRunning()) {
showProgressBar();
} else {
hideProgressBar();
}
super.onActivityCreated(savedInstanceState);
}
public void hideProgressBar() {
progress.setVisibility(View.GONE);
}
public void showProgressBar() {
progress.setVisibility(View.VISIBLE);
progress.setIndeterminate(true);
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnListFragmentInteractionListener) {
mListener = (OnListFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnListFragmentInteractionListener");
}
}
#Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public void populateResult(List<RedditPost> result) {
if(!redditPosts.isEmpty()){
redditPosts.remove(redditPosts.size() - 1);
}
redditPosts.addAll(result);
mAdapter.setLoaded();
mAdapter.notifyDataSetChanged();
}
protected boolean isTaskRunning() {
if (helper == null) {
return false;
} else if (helper.getStatus() == 0) {
return false;
} else {
return true;
}
}
}
I call the hideProgressBar(), showProgressBar() and populateResult() in my helper class.
There's a long standing mantra in programming that states: "Favor composition over inheritance"
You can read about the details of this statement and a lot of discussion here.
In this case, inheritance is unnecessary because you can simply build 1 Fragment and, on initialization pass it the subreddit, thus avoiding any constraining links between a super and subclass that may not even have any sort of polymorphic relationship.
Hi I'm implementing a custom asynctaskloader to load data in the background. The problem is when the user goes back to the activity after he minimizes the app(or pick photo from gallary), all the asynctaskloaders associated with the activity and asynctaskloaders associated with the fragments in the activity start all over again.
How can I prevent the loader to restart the loading process when the activity restarts?
My baseAsynctaskLoader class:
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
public abstract class BaseAsyncTaskLoader extends AsyncTaskLoader{
private static final String TAG = "BaseAsyncTaskLoader";
protected Context context;
protected Object mData;
public BaseAsyncTaskLoader( Context context ) {
super(context);
Log.d(TAG, "BaseLoader");
this.context = context.getApplicationContext();
}
#Override
public abstract Object loadInBackground();
#Override
public void deliverResult(Object data){
Log.d( TAG, "deliverResult" );
if (isReset()) {
return;
}
mData = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading(){
Log.d( TAG, "onStartLoading" );
if (mData != null) {
deliverResult(mData);
}
if (takeContentChanged() || mData == null) {
forceLoad();
}
}
#Override
protected void onStopLoading(){
Log.d( TAG, "onStopLoading" );
cancelLoad();
}
#Override
protected void onReset(){
Log.d( TAG, "onReset" );
super.onReset();
onStopLoading();
if(mData !=null){
mData=null;
}
}
#Override
public void onCanceled(Object data){
Log.d( TAG, "onCanceled" );
super.onCanceled(data);
}
}
Loader class sample:
public class AddDetailService extends BaseAsyncTaskLoader{
Activity activity;
Bundle bundle;
String outputstringrespone="";
public AddCarService(Activity activity, Bundle bundle){
super(activity);
this.activity = activity;
this.bundle = bundle;
}
#Override
public Object loadInBackground(){
try{
int userId = bundle.getInt(USER_ID);
int modelId = bundle.getInt(MODEL_ID);
outputstringrespone = addDetails(userId, modelId);
}catch(Exception e){
Log.d(TAG, e.toString());
}
return null;
}
#Override
public void deliverResult(Object data){
super.deliverResult(data);
Log.d(TAG,"output--"+outputstringrespone);
if(outputstringrespone.equalsIgnoreCase("Success")){
Toast.makeText(activity, "Details Added Successfully", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(activity, "Details not added",Toast.LENGTH_SHORT).show();
}
}
}
LoaderCallback in activity:
getLoaderManager().initLoader(LoaderId.ADD_DETAIL, bundle, addDetailsCallbacks);
LoaderManager.LoaderCallbacks addDetailsCallbacks = new LoaderManager.LoaderCallbacks(){
#Override
public Loader onCreateLoader(int id, Bundle args){
return new AddDetailService(getActivity(), args);
}
#Override
public void onLoadFinished(Loader loader, Object data){
getLoaderManager().destroyLoader(LoaderId.ADD_DETAIL);
}
#Override
public void onLoaderReset(Loader loader){
}
};
This is the desired behavior of LoaderManager framework - it takes care of reloading the data with the initiated Loaders in case the enclosing Activity or Fragment get re-created.
In fact, some implementations of Loaders do not reload the data, but simply provide access to the latest data that has been cached internally.
Bottom line: you observe correct behavior of LoaderManager framework.
It is not clear what it is you're trying to accomplish with your Loader, but it looks like you chose an incorrect tool. If your goal is to perform the action just once, then you should use AsyncTask instead of Loaders.
Can any one please explain how to make endless adapter concept for view pager
I am currently using view pager to see my datas. On every 10th swipe of the view pager I need to hit the server and take dynamic response and need to update the viewpager. Obviously we need to use the endless adapter concept. But I was confused with the exact concept. Anyone please do the needful...
Thanks in advance...
I’ve implemented an endless ViewPager. I think it suits you needs. The request is simulated with a time delay in the AsyncTask thread.
//ViewPagerActivity
public class ViewPagerActivity extends FragmentActivity {
private ViewPager vp_endless;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_endless_view_pager);
vp_endless = (ViewPager) findViewById(R.id.vp_endless);
vp_endless.setAdapter(new FragmentViewPagerAdapter(getSupportFragmentManager()));
}
}
//FragmentViewPagerAdapter
public class FragmentViewPagerAdapter extends FragmentStatePagerAdapter {
private List<CustomObject> _customObjects;
private volatile boolean isRequesting;
private static final int ITEMS_PER_REQUEST = 10;
public FragmentViewPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
_customObjects = HandlerCustomObject.INSTANCE._customObjects;
}
#Override
public Fragment getItem(int position) {
CustomFragment fragment = new CustomFragment();
fragment.setPositionInViewPager(position);
if (position == _customObjects.size() && !isRequesting)
new AsyncRequestItems().execute("www.test.com");
return fragment;
}
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
public class AsyncRequestItems extends AsyncTask<String, Void, Void> {
#Override
protected Void doInBackground(String... urls) {
isRequesting = true;
//Fake request lag
try {Thread.sleep(2500);}
catch (InterruptedException e) {e.printStackTrace();}
for (int i = 0; i < ITEMS_PER_REQUEST; i++) {
_customObjects.add(new CustomObject());
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
isRequesting = false;
}
}
}
//CustomFragment
public class CustomFragment extends Fragment {
private CustomObject _customObject;
private TextView tv_position;
private ProgressBar pb_loading;
private View root;
private int _positionInViewPager;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
root = inflater.inflate(R.layout.frament_endless_view_pager, container, false);
pb_loading = (ProgressBar) root.findViewById(R.id.pb_loading);
tv_position = (TextView) root.findViewById(R.id.tv_position);
_customObject = retrieveDataSafety();
if(_customObject != null) bindData();
else createCountDownToListenerForUpdates();
return root;
}
public void createCountDownToListenerForUpdates() {
new CountDownTimer(10000, 250) {
public void onTick(long millisUntilFinished) {
_customObject = retrieveDataSafety();
if(_customObject != null) {
bindData();
cancel();
}
}
public void onFinish() {}
}.start();
}
private CustomObject retrieveDataSafety() {
List<CustomObject> customObjects = HandlerCustomObject.INSTANCE._customObjects;
if(customObjects.size() > _positionInViewPager)
return customObjects.get(_positionInViewPager);
else
return null;
}
private void bindData() {
pb_loading.setVisibility(View.GONE);
String feedback = "Position: " + _positionInViewPager;
feedback += System.getProperty("line.separator");
feedback += "Created At: " + _customObject._createdAt;
tv_position.setText(feedback);
}
public void setPositionInViewPager(int positionAtViewPager) {
_positionInViewPager = positionAtViewPager;
}
}
//CustomObject
public class CustomObject {
public String _createdAt;
public CustomObject() {
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
_createdAt = dateFormat.format(new Date());
}
}
//HandlerCustomObject
public enum HandlerCustomObject {
INSTANCE;
public List<CustomObject> _customObjects = new ArrayList<CustomObject>();
}
Well, let's start from the beginning.
If you would like to have 'endless' number of pages you need to use some trick. E.g. you can't store endless number of pages in memory. Probably Android will destroy PageView everytime, when it isn't visible. To avoid destroying and recreating those views all the time you can consider recycling mechanism, which are used e.g. ListView. Here you can check and analyse idea how to implement recycling mechanism for pager adapter.
Moreover to make your UI fluid, try to make request and download new data before user gets to X0th page (10, 20, 30, 40...). You can start downloading data e.g when user is at X5th (5, 15, 25...) page. Store data from requests to model (it could be e.g. sqlite db), and user proper data based on page number.
It's just a brief of solution, but it's interesting problem to solve as well;)
Edit
I've started looking for inspiration and just found standalone view recycler implemented by Jake Wharton and called Salvage. Maybe it will be good start to create solution for your problem.
I'm trying to use an AsyncTaskLoader to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device before the load for the first selected item has completed, then the onLoadFinished() call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.
Here is the code I'm using. Activity:
public final class DemoActivity extends Activity
implements NumberListFragment.RowTappedListener,
LoaderManager.LoaderCallbacks<String> {
private static final AtomicInteger activityCounter = new AtomicInteger(0);
private int myActivityId;
private ResultFragment resultFragment;
private Integer selectedNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myActivityId = activityCounter.incrementAndGet();
Log.d("DemoActivity", "onCreate for " + myActivityId);
setContentView(R.layout.demo);
resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d("DemoActivity", "onDestroy for " + myActivityId);
}
#Override
public void onRowTapped(Integer number) {
selectedNumber = number;
resultFragment.setResultText("Fetching details for item " + number + "...");
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new ResultLoader(this, selectedNumber);
}
#Override
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
resultFragment.setResultText(data);
}
#Override
public void onLoaderReset(Loader<String> loader) {
}
static final class ResultLoader extends AsyncTaskLoader<String> {
private static final Random random = new Random();
private final Integer number;
private String result;
ResultLoader(Context context, Integer number) {
super(context);
this.number = number;
}
#Override
public String loadInBackground() {
// Simulate expensive Web call
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
}
#Override
public void deliverResult(String data) {
if (isReset()) {
// An async query came in while the loader is stopped
return;
}
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
// Only do a load if we have a source to load from
if (number != null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
result = null;
}
}
}
List fragment:
public final class NumberListFragment extends ListFragment {
interface RowTappedListener {
void onRowTapped(Integer number);
}
private RowTappedListener rowTappedListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
rowTappedListener = (RowTappedListener) activity;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
R.layout.simple_list_item_1,
Arrays.asList(1, 2, 3, 4, 5, 6));
setListAdapter(adapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
rowTappedListener.onRowTapped(adapter.getItem(position));
}
}
Result fragment:
public final class ResultFragment extends Fragment {
private TextView resultLabel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.result_fragment, container, false);
resultLabel = (TextView) root.findViewById(R.id.result_label);
if (savedInstanceState != null) {
resultLabel.setText(savedInstanceState.getString("labelText", ""));
}
return root;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("labelText", resultLabel.getText().toString());
}
void setResultText(String resultText) {
resultLabel.setText(resultText);
}
}
I've been able to get this working using plain AsyncTasks but I'm trying to learn more about Loaders since they handle the configuration changes automatically.
EDIT: I think I may have tracked down the issue by looking at the source for LoaderManager. When initLoader is called after the configuration change, the LoaderInfo object has its mCallbacks field updated with the new activity as the implementation of LoaderCallbacks, as I would expect.
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
However, when there is a pending loader, the main LoaderInfo object also has an mPendingLoader field with a reference to a LoaderCallbacks as well, and this object is never updated with the new activity in the mCallbacks field. I would expect to see the code look like this instead:
// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
It appears to be because of this that the pending loader calls onLoadFinished on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.
The new question is: Have I found a bug, or is this the expected behavior?
In most cases you should just ignore such reports if Activity is already destroyed.
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
if (isDestroyed()) {
Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
return;
}
resultFragment.setResultText(data);
}
Also you should insert checking isDestroyed() in any inner classes. Runnable - is the most used case.
For example:
// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
public void run() {
// some heavy operations
...
// notification to UI thread
handler.post(new Runnable() {
// this runnable can link to 'dead' activity or any outer instance
if (isDestroyed()) {
return;
}
// we are alive
onSomeHeavyOperationFinished();
});
}
});
But in such cases the best way is to avoid passing strong reference on Activity to another thread (AsynkTask, Loader, Executor, etc).
The most reliable solution is here:
// BackgroundExecutor.java
public class BackgroundExecutor {
private static final Executor instance = Executors.newSingleThreadExecutor();
public static void execute(Runnable command) {
instance.execute(command);
}
}
// MyActivity.java
public class MyActivity extends Activity {
// Some callback method from any button you want
public void onSomeButtonClicked() {
// Show toast or progress bar if needed
// Start your heavy operation
BackgroundExecutor.execute(new SomeHeavyOperation(this));
}
public void onSomeHeavyOperationFinished() {
if (isDestroyed()) {
return;
}
// Hide progress bar, update UI
}
}
// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
private final WeakReference<MyActivity> ref;
public SomeHeavyOperation(MyActivity owner) {
// Unlike inner class we do not store strong reference to Activity here
this.ref = new WeakReference<MyActivity>(owner);
}
public void run() {
// Perform your heavy operation
// ...
// Done!
// It's time to notify Activity
final MyActivity owner = ref.get();
// Already died reference
if (owner == null) return;
// Perform notification in UI thread
owner.runOnUiThread(new Runnable() {
public void run() {
owner.onSomeHeavyOperationFinished();
}
});
}
}
Maybe not best solution but ...
This code restart loader every time, which is bad but only work around that works - if you want to used loader.
Loader l = getLoaderManager().getLoader(MY_LOADER);
if (l != null) {
getLoaderManager().restartLoader(MY_LOADER, null, this);
} else {
getLoaderManager().initLoader(MY_LOADER, null, this);
}
BTW. I am using Cursorloader ...
A possible solution is to start the AsyncTask in a custom singleton object and access the onFinished() result from the singleton within your Activity. Every time you rotate your screen, go onPause() or onResume(), the latest result will be used/accessed. If you still don't have a result in your singleton object, you know it is still busy or that you can relaunch the task.
Another approach is to work with a service bus like Otto, or to work with a Service.
Ok I'm trying to understand this excuse me if I misunderstood anything, but you are losing references to something when the device rotates.
Taking a stab...
would adding
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest for that activity fix your error? or prevent onLoadFinished() from saying the activity stopped?