I have an activity whose only purpose is to display a list view. There is a custom adapter to serve up the views for each element in the array.
I have break points all over and when I debug, it stops in "count" a number of times - the first few times the return value is zero, then it changes to 3 (the correct value in this case). Then we stop in "getView" - all the right stuff happens, and after we're through with all the break points, then presto magico all three records display on the screen. Yea!
So then I try to run the app outside of the debugger. I get the log message that it's visited "count", and the log message displays the return value so I know it's correct - but "getView" never gets called!!
I'm not sure which bits of code are relevant to this question & don't want to pollute the question with the entire project; please let me know if there's a specific section that would be helpful. I've researched all the "getView not called" questions but those consistently are for a case where getView never gets called, which clearly mine can be…sometimes :(
EDIT: Adapter code
public class DivisionAdapter extends BaseAdapter {
private static final String TAG = "DIV_ADAPT";
private ArrayList<Division> divisionList;
private Context context;
public DivisionAdapter(Context c, ArrayList<Division> divList) {
divisionList = divList;
context = c;
}
#Override
public int getCount() {
Integer count = 0;
if (divisionList != null) count = divisionList.size();
Log.v(TAG,count.toString());
return count;
}
#Override
public Object getItem(int position) {
Object o = null;
if (divisionList != null)
o = divisionList.get(position);
return o;
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.v(TAG,"getView");
if (divisionList == null)
return null;
LinearLayout divisionView = null;
Division thisDiv = divisionList.get(position);
if (convertView == null) {
divisionView = new LinearLayout(context);
LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
li.inflate(R.layout.division_item, divisionView, true);
} else {
divisionView = (LinearLayout) convertView;
}
Log.v(TAG,thisDiv.name());
TextView v = (TextView) divisionView.findViewById(R.id.divisionName);
v.setText(thisDiv.name());
v = (TextView) divisionView.findViewById(R.id.divisionRegion);
v.setText(thisDiv.region());
return divisionView;
}
public void setList(ArrayList<Division> newList) {
divisionList = null;
divisionList = newList;
}
}
And just in case it's useful, some snippets from the activity class:
#Override
public void onResume() {
super.onResume();
refreshList();
}
private void refreshList() {
// use the class static query method to get the list of divisions
Division.query(Division.class,
new StackMobQuery().fieldIsEqualTo("status", "ACTIVE"),
new StackMobQueryCallback<Division>() {
#Override
public void failure(StackMobException arg0) {
// TODO Auto-generated method stub
Log.v(TAG, "query fail");
}
#Override
public void success(List<Division> arg0) {
Log.v(TAG, "query success");
divAdapt.setList((ArrayList<Division>) arg0);
divAdapt.notifyDataSetChanged();
}
});
}
EDIT 2/11:
I found this question: Markers not showing on map after stackmob query which reveals the hitherto unknown fact that stack mob queries run on a background thread. I'm starting to research the relationship between threads and adapters and thought I'd share this clue in case it helps anyone else figure out what's going on here faster than I can. TIA.
idk why this EVER worked in the debugger - that turned out to be a red herring.
As discovered, the StackMobModel static query method does run in a background thread, from which calling NotifyDataSetChanged() is completely ineffectual.
I ended up replacing the success method in the StackMobQueryCallback as follows:
#Override
public void success(final List<Division> arg0) {
Log.v(TAG, "query success");
runOnUiThread(new Runnable() {
public void run() {
updateList((ArrayList<Division>) arg0);
}
});
}
and then added this new method
private void updateList(ArrayList<Division> newList) {
divAdapt.setList(newList);
divAdapt.notifyDataSetChanged();
}
now, when the query returns, the adapter update is directed to run on the proper thread, and hooray, everything looks stitched together just fine and dandy.
whew!
Related
I am working on an Application in Android where I shut down all of my servers. Therefore, I use an ArrayAdapter and a Listview.
In a background process, I iterate over the IP - Addresses and shutdown all of my servers.
Now, I want when iterating over my servers to color each row in the ListView in Green ( means still working on it to shut it down ) or Red as soon as the server is shut down.
I am able to color each row in a different color when extending the ArrayAdapter and then in the getView method coloring them all differently.
But how can I do that when iterating over each row during the background process?
My adapter is being set during the call of my Activity class.
Do I have to put the setAdapter method in my backgroundprocess, too, or something like that?
Here is my code:
protected void onCreate(Bundle savedInstanceState) {
initComponents();
}
private void initComponents() {
model = new SharedPreferenceModel(getBaseContext());
mydb = new DatabaseHelper(this);
array_list = mydb.getAllCotacts();
hostsOnline = new ArrayList<String>();
btnShutdown = findViewById(R.id.btnShutdown);
lv = (ListView) findViewById(R.id.listView);
CustomArrayAdapter custom = new CustomArrayAdapter(this, android.R.layout.simple_list_item_1, array_list);
lv.setAdapter(custom);
}
private void addListeners(final ShutdownServers shutdownServers) {
btnShutdown.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
new AsyncTask<Integer, String, Void>() {
#Override
protected Void doInBackground(Integer... params) {
try {
for(int i = 0; i<array_list.size(); i++){
posInArray++;
String host = array_list.get(i);
if(host.equals("192.168.1.1"))
publishProgress("Shutdown " + host);
else
executeRemoteCommand(getBaseContext(), host);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
protected void onProgressUpdate(String... values) {
hostsOnline.add(values[0]);
custom.setNotifyOnChange(true);
custom.notifyDataSetChanged();
}
}.execute(1);
}
});
}
Thanks for your help!
You can use setNotifyOnChange(boolean) method and corresponding add(), remove etc. methods to control list state (adding, removing, changing items). Keep in mind, that changing state of backing array field won't trigger UI changes automatically without that. If you want to control changes manually, you can use notifyDataSetChanged() method of ArrayAdapter.
It's all because ArrayAdapter tries to instantiate views only once and reuse them for different array elements when scrolling down. View's state should be only modified in getView() which normally would be called only once per array element, when it's about to be rendered on screen first time. However, you can force 'redraw' using notifyDataSetChanged() at any time to keep UI state consistent with backing array field.
lv.setBackgroundResource(R.drawable.your file)// from drawable
lv.setBackgroundResource(Color.BLACK)// from color by default
Now I was able to solve the colouring problem. Here is my solution:
public View getView(int position, #Nullable View convertView, #NonNull ViewGroup parent) {
// Get the current item from ListView
View view = super.getView(position,convertView,parent);
if(notifyCalling==1 && position == getPos()){
Log.d("getView - if - position", String.valueOf(position));
view.setBackgroundColor(Color.GREEN);
}else if(notifyCalling ==1 && position < getPos()){
Log.d("getView - elseif - position", String.valueOf(position));
view.setBackgroundColor(Color.RED);
}else if (position % 2 == 1) {
view.setBackgroundColor(Color.LTGRAY);
} else {
view.setBackgroundColor(Color.WHITE);
}
return view;
}
private void addListeners(final ShutdownServers shutdownServers) {
btnShutdown.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
btnShutdown.setClickable(false);
new AsyncTask<Integer, String, Void>() {
#Override
protected Void doInBackground(Integer... params) {
try {
for(int i = 0; i<array_list.size(); i++){
String host = array_list.get(i);
publishProgress(host);
executeRemoteCommand(getBaseContext(), host);
setIndex(i+1);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
protected void onProgressUpdate(String... values) {
custom.setNotifyOnChange(true);
custom.notifyDataSetChanged(getIndex());
}
}.execute(1);
}
});
}
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;
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 am trying to update my listview to make an "endless scroll". What happens is that the first 40 results load fine, when i get to the bottom of the scroll, next 40 results replace the first 40...
What I want is for second set of 40 results to add to the first 40 so I have an endless list and ability to scroll back to the beginning of the list.
I am posting my code below. Thank you!
public class SearchResults extends Activity implements BannerAdListener, OnScrollListener{
private LinearLayout bottomNav;
private ListView ringtoneList;
private int start = 0, num = 40, curPage = 1;
private Handler handler = new Handler();
private ProgressDialog progressDialog = null;
private ArrayList<Ringtone> ringtones;
private MoPubView moPubView;
private String searchString;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras == null) {
// no search string defined
finish();
} else {
searchString = extras.getString("search_string");
}
setContentView(R.layout.search_results);
ringtoneList = (ListView)findViewById(R.id.ringtone_list);
ringtoneList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> av, View view, int position, long id) {
Intent i = new Intent(SearchResults.this, RingtoneView.class);
i.putExtra("ringtone", ringtones.get(position));
startActivity(i);
}
});
performSearch();
moPubView = (MoPubView) findViewById(R.id.adview);
moPubView.setAdUnitId(Utils.MoPubBannerId);
moPubView.loadAd();
moPubView.setBannerAdListener(this);
ringtoneList.setOnScrollListener(this);
}
public void onScroll(AbsListView view, int firstVisible, final int visibleCount, int totalCount) {
Log.i("List", "firstVisible="+firstVisible+" visibleCount="+visibleCount+" totalCount="+totalCount);
boolean loadMore = firstVisible + visibleCount >= totalCount;
if(loadMore) {
Log.i("List", "Loading More Results");
curPage++;
start = num * (curPage-1);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
}
});
}
}
}
.start();
}
}
public void onScrollStateChanged(AbsListView v, int s) { }
private void performSearch() {
progressDialog = ProgressDialog.show(SearchResults.this, getResources().getString(R.string.loading_message), getResources().getString(R.string.loading_search), true);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
updateList();
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
ringtoneList.setVisibility(View.INVISIBLE);
bottomNav.setVisibility(View.INVISIBLE);
}
});
}
progressDialog.dismiss();
}
}.start();
}
private void updateList() {
handler.post(new Runnable() {
public void run() {
//Log.d("search", "ringtones.size() " + ringtones.size());
ringtoneList.setVisibility(View.VISIBLE);
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
}
Please help! Thank you!
While I can't be 100% sure, I think your problem has to do with the fact that you're setting a new adapter that only has the section of ringtones that in loads. It probably has to do with this snippet:
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
Instead of putting an entirely new array of ringtones, you should add on to the one you already have. Your ringtones variable is already an instance variable, so I'm sure if you changed this line:
ringtones = Utils.search(start, num, searchString);
to the following:
ringtones.addAll(Utils.search(start, num, searchString));
It might fix your problem.
Your code is a little bit messy, but your updateList method is creating a NEW RingtoneRowAdapter. You should ADD items to the list and call
mRingtoneRowAdapter.notifyDatasetChanged();
That will tell the adapter to get new views (if needed) along with a lot of internal stuff happening at Adapter's level. So it's kinda:
private void updateList() {
handler.post(new Runnable() {
public void run() {
if ( mAdapter == null ) {
mAdapter = new RingtoneRowAdapter(SearchResults.this, ringtones);
ringtoneList.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
ringtoneList.setVisibility(View.VISIBLE);
}
});
}
And of course, as already suggested, don't init your ringtones array all the time. Just add data to it.
You don't need (most of the times) to hide the list, you can add in your layout, any View (including a full Layout!) with an id of:
android:id="#android:id/empty"
And if your ListView has an id of android:id="#id/android:list", and you're in a ListFragment or ListActivity, then when the list is empty, the "empty" layout will be shown (which can be a dummy view if you don't want to see it).
Just suggestions :)
UPDATE: For your null, I see that the logic is a little bit weird and since we don't know the requirements for your app (i.e. what to do if there are no results, what to do if the results are invalid, etc.) I'll assume you just want to make sure it works and deal with that later.
So with that in mind, I see two problems.
I assume your Utils.search method can return null, because you're checking for it. To me that feels strange, I'd rather return an empty array indicating that the search produced no results; that little remark aside, you are not checking (or we don't know because we haven't seen the source for your search method), if the searchString is null or not.
You haven't provided a stack trace, so we can't tell where the null is happening (either inside search or ?)
A quick solution would be to check for null before searching… I would personally do this INSIDE the search function.
Something like:
public ArrayList<Ringtone> search(final int start, final int num, final String searchString) {
if ( searchString == null ) {
//DECIDE WHAT YOU WANT TO DO, EITHER:
return null;
// OR YOU CAN RETURN AN EMPTY ARRAY
return new ArrayList<Ringtone>();
}
// You should check for these (change according to your rules)
if (start < 0 ) {
start = 0; // protect yourself from bad data.
}
if ( num < 0 ) {
num = 0;
}
/// THE REST OF YOUR search FUNCTION
return <your array>
}
Now another thing is that you may want the search to return incremental results (so you can ADD them to your array (instead of returning a new array every time). For that, as already suggested, use the addAll trick, but then, DON'T return null, return an new empty array, so there's no harm done if there's nothing else to add.
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?