Android loaders, the way to go? - android

I am used to building lists in android using adapters. If I need some long-to-get data, I use an asynctask, or a simple runnable, to update the data structure on which the adapter rely, and call notifyDataChanged on the adapter.
Although it is not straightforward, I finally find this is a simple model and it allows a good separation of logic presentation (in the asynctask, update a data structure) and the view (an adapter acting as a view factory, mostly).
Nevertheless, I read recently about loaders introduced in HoneyComb and included in the backward compatibility support-library, I tried them and find the introduce a lot of complexity. They are difficult to handle and add some kind of magic to this whole process through loader managers, add a lot of code and don't decrease the number of classes or collaborating items but I may be wrong and would like to hear some good points on loaders.
What are they advantages of loaders in terms of lines of code, clarity and effort ?
What are they advantages of loaders in terms of role separation during data loading, or more broadly, in terms of design ?
Are they the way to go, should I replace all my list data loading to implement them through loaders ?
Ok, this is a developers' forum, so here is an example. Please, make it better with loaders :
package com.sof.test.loader;
import java.util.ArrayList;
import java.util.List;
import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.TextView;
/** The activity. */
public class LoaderTestActivity extends ListActivity {
private DataSourceOrDomainModel dataSourceOrDomainModel = new DataSourceOrDomainModel();
private List<Person> listPerson;
private PersonListAdapter personListAdapter;
private TextView emptyView;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listPerson = new ArrayList<Person>();
personListAdapter = new PersonListAdapter( listPerson );
setListAdapter( personListAdapter );
setUpEmptyView();
new PersonLoaderThread().execute();
}
public void setUpEmptyView() {
emptyView = new TextView( this );
emptyView.setLayoutParams( new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ) );
emptyView.setVisibility(View.GONE);
((ViewGroup)getListView().getParent()).addView(emptyView);
getListView().setEmptyView(emptyView);
}
/** Simulate a long task to get data. */
private class PersonLoaderThread extends AsyncTask<Void, Integer, List<Person>> {
#Override
protected List<Person> doInBackground(Void... params) {
return dataSourceOrDomainModel.getListPerson( new ProgressHandler());
}
#Override
protected void onProgressUpdate(Integer... values) {
emptyView.setText( "Loading data :" + String.valueOf( values[ 0 ] ) +" %" );
}
#Override
protected void onPostExecute(List<Person> result) {
listPerson.clear();
listPerson.addAll( result );
personListAdapter.notifyDataSetChanged();
}
private class ProgressHandler implements ProgressListener {
#Override
public void personLoaded(int count, int total) {
publishProgress( 100*count / total );
}
}
}
/** List item view factory : the adapter. */
private class PersonListAdapter extends ArrayAdapter<Person> {
public PersonListAdapter( List<Person> listPerson ) {
super(LoaderTestActivity.this, 0, listPerson );
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if( convertView == null ) {
convertView = new PersonView( getContext() );
}
PersonView personView = (PersonView) convertView;
personView.setPerson( (Person) getItem(position) );
return personView;
}
}
}
A small callback interface for progress
package com.sof.test.loader;
/** Callback handler during data load progress. */
public interface ProgressListener {
public void personLoaded(int count, int total );
}
A list item widget
package com.sof.test.loader;
import com.sof.test.loader.R;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;
/** List Item View, display a person */
public class PersonView extends LinearLayout {
private TextView personNameView;
private TextView personFirstNameView;
public PersonView(Context context) {
super(context);
LayoutInflater inflater= (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate( R.layout.person_view,this );
personNameView = (TextView) findViewById( R.id.person_name );
personFirstNameView = (TextView) findViewById( R.id.person_firstname );
}
public void setPerson( Person person ) {
personNameView.setText( person.getName() );
personFirstNameView.setText( person.getFirstName() );
}
}
It's xml : res/person_view.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/person_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/person_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true" />
<TextView
android:id="#+id/person_firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/person_name" />
</RelativeLayout>
The data source or model, providing data (slowly)
package com.sof.test.loader;
import java.util.ArrayList;
import java.util.List;
/** A source of data, can be a database, a WEB service or a model. */
public class DataSourceOrDomainModel {
private static final int PERSON_COUNT = 100;
public List<Person> getListPerson( ProgressListener listener ) {
List<Person> listPerson = new ArrayList<Person>();
for( int i=0; i < PERSON_COUNT ; i ++ ) {
listPerson.add( new Person( "person", "" + i ) );
//kids, never do that at home !
pause();
if( listener != null ) {
listener.personLoaded(i,PERSON_COUNT);
}//if
}
return listPerson;
}//met
private void pause() {
try {
Thread.sleep( 100 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
The POJO representing a person :
package com.sof.test.loader;
/** A simple POJO to be displayed in a list, can be manipualted as a domain object. */
public class Person {
private String name;
private String firstName;
public Person(String name, String firstName) {
this.name = name;
this.firstName = firstName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}//class

In case someone is looking for the loader version of my previous example : here it is :
package com.sof.test.loader;
import java.util.ArrayList;
import android.app.LoaderManager;
import java.util.List;
import android.app.ListActivity;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.TextView;
/** The activity. */
public class LoaderTestActivity2 extends ListActivity implements
LoaderManager.LoaderCallbacks<List<Person>> {
private DataSourceOrDomainModel dataSourceOrDomainModel = new DataSourceOrDomainModel();
private List<Person> listPerson;
private PersonListAdapter personListAdapter;
private TextView emptyView;
private Loader<List<Person>> personLoader;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listPerson = new ArrayList<Person>();
personListAdapter = new PersonListAdapter(listPerson);
setListAdapter(personListAdapter);
personLoader = new PersonLoader(this, dataSourceOrDomainModel, new ProgressHandler() );
setUpEmptyView();
getLoaderManager().initLoader(0, null, this);
personLoader.forceLoad();
}
public void setUpEmptyView() {
emptyView = new TextView(this);
emptyView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
emptyView.setVisibility(View.GONE);
((ViewGroup) getListView().getParent()).addView(emptyView);
getListView().setEmptyView(emptyView);
}
public void publishProgress(int progress) {
emptyView.setText("Loading data :" + String.valueOf(progress) + " %");
}
#Override
public Loader<List<Person>> onCreateLoader(int arg0, Bundle arg1) {
return personLoader;
}
#Override
public void onLoadFinished(Loader<List<Person>> personLoader, List<Person> result) {
listPerson.clear();
listPerson.addAll(result);
personListAdapter.notifyDataSetChanged();
}
#Override
public void onLoaderReset(Loader<List<Person>> arg0) {
listPerson.clear();
personListAdapter.notifyDataSetChanged();
}
/** List item view factory : the adapter. */
private class PersonListAdapter extends ArrayAdapter<Person> {
public PersonListAdapter(List<Person> listPerson) {
super(LoaderTestActivity2.this, 0, listPerson);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = new PersonView(getContext());
}
PersonView personView = (PersonView) convertView;
personView.setPerson((Person) getItem(position));
return personView;
}
}
private class ProgressHandler implements ProgressListener {
#Override
public void personLoaded(final int count, final int total) {
runOnUiThread( new Runnable() {
#Override
public void run() {
publishProgress(100 * count / total);
}
});
}
}
}
class PersonLoader extends AsyncTaskLoader<List<Person>> {
private DataSourceOrDomainModel dataSourceOrDomainModel;
private ProgressListener progressHandler;
public PersonLoader(Context context, DataSourceOrDomainModel dataSourceOrDomainModel, ProgressListener progressHandler ) {
super(context);
this.dataSourceOrDomainModel = dataSourceOrDomainModel;
this.progressHandler = progressHandler;
}
#Override
public List<Person> loadInBackground() {
return dataSourceOrDomainModel.getListPerson( progressHandler );
}
}
It would be more difficult to add support (support librairy) to this example as there is no equivalent of ListAcitivity in the support librairy. I would have either to create a ListFragment or create an FragmentActivity and give it a layout including a list.

One problem your code has which loaders aim to fix is what happens if your activity is restarted (say due to device rotation or config change) while your async task is still in progress? in your case your restarted activity will start a 2nd instance of the task and throw away the results from the first one. When the first one completes you can end up with crashes due to the fact your async task has a reference is what is now a finished activity.
And yes using loaders often makes for more/more complex code, particularly if you can't use one of the provided loaders.

Related

Call another activity in Android using onPostExecute method list item click event in AsyncTask

I am working on a project which populating SQLite database table for list view and using simple array adapter.
I'm using Asyntask for that purpose and I have problem when:
I want to call another activity and
pass some values which I get from the setOnItemClickListener
I need to archive this two things in onPostExecute setOnItemClickListener method. This is my code for that.
package com.me.doctor.doctor_me;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
public class BackgroundTask extends AsyncTask<String,Doctor,String> {
Context ctx;
DoctorAdapter doctorAdapter;
Activity activity;
ListView listView;
Doctor doctor;
DisplayDoctor displayDoctor;
BackgroundTask(Context ctx){
this.ctx = ctx;
activity = (Activity) ctx;
doctor = new Doctor();
displayDoctor = new DisplayDoctor();
}
#Override
protected String doInBackground(String... strings) {
String method = strings[0];
DatabaseOperation databaseOperation = new DatabaseOperation(ctx);
if(method.equals("get_info")){
listView = activity.findViewById(R.id.display_list_view);
SQLiteDatabase db = databaseOperation.getReadableDatabase();
Cursor cursor = databaseOperation.getInformation(db);
doctorAdapter = new DoctorAdapter(ctx,R.layout.display_doctor_row);
String name, category, hospital;
int id;
while(cursor.moveToNext()){
id = cursor.getInt(cursor.getColumnIndex("d_id"));
name = cursor.getString(cursor.getColumnIndex("d_name"));
category = cursor.getString(cursor.getColumnIndex("d_category"));
hospital = cursor.getString(cursor.getColumnIndex("d_hospital"));
Doctor doctor = new Doctor(id,name,category,hospital);
publishProgress(doctor);
}
return "get_info";
}
return null;
}
#Override
protected void onProgressUpdate(Doctor... values) {
// add each of doctor class object add method inside the adapter class
doctorAdapter.add(values[0]);
}
#Override
protected void onPostExecute(String s) {
if(s.equals("get_info")){
listView.setAdapter(doctorAdapter);
listView.setOnItemClickListener(new android.widget.AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
final int position, long id) {
Doctor doctor = (Doctor)parent.getItemAtPosition(position);
String ID = Integer.toString(doctor.getId());
Toast.makeText(ctx,ID,Toast.LENGTH_LONG).show();
// I need fire another activity and pass some values which i getting here
}
});
}else{
Toast.makeText(ctx,s,Toast.LENGTH_LONG).show();
}
}
}
And this is the class which call to the AsyncTask Class
package com.me.doctor.doctor_me;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class DisplayDoctor extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.display_doctor_layout);
BackgroundTask backgroundTask = new BackgroundTask(this);
backgroundTask.execute("get_info");
}
}
I had investigate closed question on Stack Overflow, but I did not found a solution.
Short answer:
you already have context in Background task
Context ctx;
use this this to call next activiry
ctx.startActivity(nextActivityIntent)
u can add values to the intent like this
Intent nextActivityIntent = new Intent(ctx,NextActivity.class);
nextActivityIntent.putExtra("data", "some data");
with async task i guess you are trying to query data base on another thread
You can use loaders for the same
Loaders run on separate thread
here is a simple example of cursor loader
example taken from github
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks {
public static final int LOADER_ID = 0;
private ArrayAdapter<String> forecastAdapter;
private ForecastAdapter mForecastAdapter;
public ForecastFragment() { }
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
getLoaderManager().initLoader(LOADER_ID, null, this);
super.onActivityCreated(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
return rootView;
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
//some database query
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
//some action
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}

Refreshing ListView each time I search a new term

I am using google book API to search a book with the title.It shows me the result for the first time. The next I search it clears the ListView but not showing the new search result.
There is a peculiar error. When I search a book it shows me 3 to 4 books whereas I have set MaxResult to 10. When I manually call the API in the browser I get 10 results.
Here is the code I am using.
public class MainActivity extends AppCompatActivity implements LoaderCallbacks<List<Book>>{
private static final int BOOK_ID = 1;
public static final String LOG_TAG = MainActivity.class.getName();
private static String book_url;
private BookAdapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button bt=(Button)findViewById(R.id.search);
bt.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
EditText searchTerm=(EditText)findViewById(R.id.search_book);
String search=searchTerm.getText().toString();
book_url="https://www.googleapis.com/books/v1/volumes?q="+search+"&maxResults=10";
LoaderManager loaderManager = getLoaderManager();
loaderManager.initLoader(BOOK_ID, null, MainActivity.this);
mAdapter=new BookAdapter(MainActivity.this,new ArrayList<Book>());
ListView bookListView=(ListView)findViewById(R.id.list);
mAdapter.notifyDataSetChanged();
bookListView.setAdapter(mAdapter);
}
});
}
public Loader<List<Book>> onCreateLoader(int i,Bundle bundle){
return new BookLoader(this,book_url);
}
public void onLoadFinished(Loader<List<Book>> loader,List<Book> books){
mAdapter.clear();
if(books!=null&&!books.isEmpty()){
mAdapter.addAll(books);
}
}
public void onLoaderReset(Loader<List<Book>> loader){
mAdapter.clear();
}
}
I tried notifyDataSetChanged(). But it doesn't work.
Here is the code for custom adapter
package com.example.android.booksearch;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
/**
* Created by AKSPAN12 on 19-07-2017.
*/
public class BookAdapter extends ArrayAdapter<Book> {
public BookAdapter(Context context, List<Book> books) {
super(context, 0,books);
}
public View getView(int position, View convertView, ViewGroup parent){
View listItemView=convertView;
if(listItemView==null){
listItemView= LayoutInflater.from(getContext()).inflate(R.layout.book_list,parent,false);
}
Book currentBook=getItem(position);
String author=currentBook.getAuthor();
TextView authorView=(TextView)listItemView.findViewById(R.id.book_author);
authorView.setText(author);
String bookName=currentBook.getBook();
TextView bookView=(TextView)listItemView.findViewById(R.id.book_name);
bookView.setText(bookName);
return listItemView;
}
}
It shows me the result for the first time. The next I search it clears the ListView but not showing the new search result.
You probably need to restart your loader.

Handling countdown timers in recyclerview - android

I'm currently working on an app that in its core functionallity handles multiple count down timers in a recycler view.
Now, the way I've implemented this is to simply initiate a count down timer for every ViewHolder that is created. My problem is sometimes whenever a timer is finished, all the other timers are acting weird.
For instance, I want that everytime a timer is up, the ViewHolder of that timer would change the background of the CardView UI component that the above class holds reference to.
What actually happens is that if one timer is up, the other view holders are accessed and being changed.
Here is my adapter code where all of this is happen. I tried to document as much as I could:
package bikurim.silverfix.com.bikurim.adapters;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.CountDownTimer;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import bikurim.silverfix.com.bikurim.Constants;
import bikurim.silverfix.com.bikurim.models.Family;
import bikurim.silverfix.com.bikurim.R;
import bikurim.silverfix.com.bikurim.utils.CountDownManager;
import bikurim.silverfix.com.bikurim.utils.TimerEventListener;
public class FamiliesAdapter extends RecyclerView.Adapter<FamiliesAdapter.FamilyViewHolder> implements Filterable, TimerEventListener {
// last position holds the last position of the element that was added, for animation purposes
private static int lastPosition = -1;
private Context context;
private ArrayList<Family> families;
private ArrayList<Family> dataSet;
private FamilyFilter filter;
private CountDownManager countDownManager;
public FamiliesAdapter(Context context, ArrayList<Family> families) {
this.context = context;
this.dataSet = families;
this.families = dataSet;
countDownManager = new CountDownManager(Constants.Values.TIME_INTERVAL, this);
countDownManager.start();
}
#Override
public FamilyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Inflating the view from a given layout resource file
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.family_list_item, parent, false);
final FamilyViewHolder pvh = new FamilyViewHolder(v);
return pvh;
}
#Override
public void onBindViewHolder(final FamilyViewHolder holder, int position) {
// Binds the Family object to the holder view
Family family = families.get(position);
holder.bindData(family);
countDownManager.addTimer(holder);
// Sets animation on the given view, in case it wasn't displayed before
setSlideAnimation(holder.cardView, position, false);
}
#Override
public int getItemCount() {
return families != null ? families.size() : 0;
}
/* Gets the current the filter object */
#Override
public Filter getFilter() {
if (filter == null)
filter = new FamilyFilter();
return filter;
}
/* The following 2 methods are an implementations of the ViewHolderListener.
* Inside every view holder lies an instance of ViewHolderListener, for communication between the two*/
public void startAnimationOnItem(FamilyViewHolder holder, boolean isEndAnimation) {
setSlideAnimation(holder.cardView, holder.getAdapterPosition(), isEndAnimation);
}
/* Changes the UI of a holder to a time's up view with a flickering ImageButton and a TextView*/
#Override
public void onFinish(FamilyViewHolder holder) {
// Switches between the clock icon to the alarm icon
holder.clock.setVisibility(View.GONE);
holder.remove.setVisibility(View.VISIBLE);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.RIGHT_OF, holder.remove.getId());
params.addRule(RelativeLayout.CENTER_VERTICAL);
holder.timeLeft.setLayoutParams(params);
holder.timeLeft.setText(context.getString(R.string.time_up_message));
setFadeAnimation(holder.remove);
holder.cardView.setBackgroundResource(R.color.cardview_light_background);
startAnimationOnItem(holder, true);
holder.family.timeLeft = 0;
}
#Override
public void onLessThanMinute(FamilyViewHolder holder) {
holder.cardView.setBackgroundResource(R.color.time_up_bg);
holder.isBackgroundChanged = true;
}
/* Starts a slide in animation for a given Card View */
private void setSlideAnimation(View viewToAnimate, int position, boolean isEndAnimation) {
Animation animation = AnimationUtils.loadAnimation(context, android.R.anim.slide_in_left);
if (!isEndAnimation) {
if (position > lastPosition) {
viewToAnimate.startAnimation(animation);
lastPosition = position;
return;
}
}
viewToAnimate.startAnimation(animation);
}
private void setFadeAnimation(View viewToAnimate) {
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(viewToAnimate, "alpha", 1f, .1f);
fadeOut.setDuration(750);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(viewToAnimate, "alpha", .1f, 1f);
fadeIn.setDuration(750);
final AnimatorSet mAnimationSet = new AnimatorSet();
mAnimationSet.play(fadeIn).after(fadeOut);
mAnimationSet.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimationSet.start();
}
});
mAnimationSet.start();
}
public void removeData(int pos, FamilyViewHolder holder) {
// Sets the last position to the given deleted position for animation purposes
lastPosition = pos;
// Removes the family object from the data set
families.remove(pos);
notifyItemRemoved(pos);
notifyItemRangeChanged(pos, getItemCount());
// Cancels the the timer and removes it from the entry set
countDownManager.cancelTimer(holder);
}
/* Cancels the timers and clears the entry set */
public void cancelTimers() {
countDownManager.reset();
countDownManager.stop();
}
/* Clears the adapter's data and resets the last position to -1 */
public void clearData() {
cancelTimers();
filter = null;
lastPosition = -1;
}
/* The official view holder of the adapter. Holds references to the relevant views inside the layout, and */
public static class FamilyViewHolder extends RecyclerView.ViewHolder {
public boolean isBackgroundChanged;
public CardView cardView;
public TextView personLname;
public TextView timeLeft;
public TextView visitors;
public ImageView clock;
public ImageButton remove;
public Family family;
private ColorStateList colorStateList;
public FamilyViewHolder(View itemView) {
super(itemView);
// isBackgroundChanged represents whether the holder is under 60 seconds or not
isBackgroundChanged = false;
// Getting the references for the UI components
cardView = (CardView) itemView.findViewById(R.id.cv);
personLname = (TextView) itemView.findViewById(R.id.person_Lname);
visitors = (TextView) itemView.findViewById(R.id.visitors);
timeLeft = (TextView) itemView.findViewById(R.id.person_timeLeft);
clock = (ImageView) itemView.findViewById(R.id.clock);
remove = (ImageButton) itemView.findViewById(R.id.time_up);
// Sets a reference to the old colors of the text view
colorStateList = timeLeft.getTextColors();
}
public void bindData(Family item) {
family = item;
personLname.setText(family.lastName);
visitors.setText("מבקרים: " + family.visitorsNum);
}
public ColorStateList getOriginalColors() {
return colorStateList;
}
}
/* Filter class that queries the constraint on the data set every whenInMillis the user types a key */
private class FamilyFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint.length() == 0 || constraint == null) {
results.values = dataSet;
results.count = dataSet.size();
} else {
ArrayList<Family> queryResults = new ArrayList<Family>();
for (Family f : dataSet) {
if (constraint.charAt(0) == f.lastName.toUpperCase().indexOf(0)) {
queryResults.add(f);
} else if (f.lastName.toUpperCase().contains(constraint.toString().toUpperCase())) {
queryResults.add(f);
}
}
results.values = queryResults;
results.count = queryResults.size();
}
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
// Now we have to inform the adapter about the new list filtered
synchronized (families) {
families = (ArrayList<Family>) results.values;
}
notifyDataSetChanged();
}
}
}
Why are the view holders get scrambled when a timer is finished?
UPDATE: I tried to implement this a little bit different. Instead of using multiple count downs, I use only one which holds an array of holders which need to be updated. But the problem remains the same. Here is my CountDownManager class:
package bikurim.silverfix.com.bikurim.utils;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import java.util.ArrayList;
import bikurim.silverfix.com.bikurim.Constants;
import bikurim.silverfix.com.bikurim.adapters.FamiliesAdapter;
/**
* Created by David on 07/07/2016.
* #author David
* Inspired by MiguelLavigne
*
* A custom CountDownTimer class, which takes care of all the running timers
*/
public class CountDownManager {
private final long interval;
private long base;
// Holds references for all the visible text views
private ArrayList<FamiliesAdapter.FamilyViewHolder> holders;
private TimerEventListener listener;
public CountDownManager(long interval, TimerEventListener listener) {
this.listener = listener;
this.interval = interval;
holders = new ArrayList<>();
}
public void start() {
base = System.currentTimeMillis();
handler.sendMessage(handler.obtainMessage(MSG));
}
public void stop() {
handler.removeMessages(MSG);
}
public void reset() {
synchronized (this) {
base = System.currentTimeMillis();
}
}
public void addTimer(FamiliesAdapter.FamilyViewHolder holder) {
synchronized (holders) {
if(!holders.contains(holder))
holders.add(holder);
}
}
public void cancelTimer(FamiliesAdapter.FamilyViewHolder holder) {
synchronized (holders) {
holders.remove(holder);
}
}
public void onTick(long elapsedTime) {
long timeLeft, lengthOfVisit;
FamiliesAdapter.FamilyViewHolder holderToDelete = null;
for (FamiliesAdapter.FamilyViewHolder holder : holders) {
lengthOfVisit = holder.family.whenInMillis - base;
timeLeft = lengthOfVisit - elapsedTime;
if(timeLeft > 0) {
if(timeLeft <= Constants.Values.ALERT_TIME && holder.isBackgroundChanged != false) {
listener.onLessThanMinute(holder);
}
holder.family.timeLeft = timeLeft;
holder.timeLeft.setText(Utils.updateFormatTime(timeLeft));
} else {
listener.onFinish(holder);
holderToDelete = holder;
}
}
// Checks if there is a holder who's ran out of time. If so, removes it from the list
if(holderToDelete != null)
holders.remove(holderToDelete);
}
private static final int MSG = 1;
private Handler handler = new Handler() {
#Override
public void handleMessage(Message msg) {
synchronized (CountDownManager.this) {
long elapsedTime = System.currentTimeMillis() - base;
onTick(elapsedTime);
sendMessageDelayed(obtainMessage(MSG), interval);
}
}
};
}
I'm doing something wrong but I can't seem to pin-point the problem.
It's not possible to cancel a countdown timer from within a countdown timer. This is apparently because the next event is already scheduled when you call cancel and when that event is triggered, it will trigger the next event. Your issues are probably because the timer is not being cancelled.
I've had to do this before and my approach was to use one countdown timer for all the views. You don't want to call notifyDataSetChanged as this would refresh everything. Instead, create a custom count up timer that holds an array of WeakReferences to all the visible Views that need the time ticked. You can update all these Views in one go at each tick. You can put the end time or start time in the tag for the View, so in your count up imer get the end time out of the tag and do your calculations.
You can start and stop the timer in the lifecycle methods. Hope this helps.
For your issue I suggest you rather use Handle & Runnable. Using them you get event of end time and perform required action. You can check my answer here https://stackoverflow.com/a/53543180/6711554.

Android listview image button

First of all I'd like to thank all of you for helping me on numerous occasions before. SO have alwasy been my go-to place when faced with problems with programming.
This time however, I've been unable to find a solution to my problem...
I'm currently developing an android app that uses two activities (one main activity and a search activity).
Both of which hold a listview - the main activity has an already populated and fully functional listview and, the search activity has a populated one which seems to lack functionallity.
The listview is created in a custom adapter and contains an image button (toggling with a selector) and four textviews.
My problem is that when I toggle the image button in my search activity and then press back to get back to the main activity and finally go back to the search results, the image button is "unchecked".
Since the image button is a CRUCIAL point in both the design and the following functionallity, I'd really appriciate any kind of help.
package com.example.geostocks;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.json.JSONArray;
import org.json.JSONException;
import com.example.geostocks.R;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import android.widget.SearchView;
/* MainActivity.
* Made by: Joakim Bajoul Kakaei (881129-0298)
* Description: Creates and sets up the MainActivity programmatically.
* It is mainly associated with activity_main.xml, but uses the
* listview_layout.xml in connection to it.
*
* The idea was to keep the class as "clean" as possible and use
* multiple classes to help making coding and efficiency more easier.
*/
public class MainActivity extends Activity {
public Menu m; // Variable to store a menu-item as a public variable.
// This helps with data allocation; instead of declaring the
// searchview and searchmanager in the creation of the
// application,
// it will wait to do it once the user decides to start the
// search.
// an array to store all jsonobjects in.
private JSONArray companies;
// an arraylist to store multiple companyobjects created later.
private List<companiesBuilder> allCompanies = new ArrayList<companiesBuilder>();
/*
* (non-Javadoc)
*
* #see android.app.Activity#onCreate(android.os.Bundle)
*/
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
/*
* executes the AsyncTask (top10). When the task is executed, it
* then gets the JSONArray which is bouncing around.
*/
companies = new top10().execute("DO IT!").get();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (ExecutionException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
/*
* The following snippet mainly creates the adapterobject and associates
* it with it's elements, context and layout.
*/
final ListView companyList = (ListView) findViewById(R.id.listView);
final companiesAdapter compAdp = new companiesAdapter(this,
R.layout.listview_layout);
System.out.println("after"); // debugging
/*
* a loop to create companyBuilder-objects from the JSONArray and then
* add those objects to an ArrayList (allCompanies).
*/
for (int i = 0; companies.length() > i; i++) {
System.out.println("companies-looper"); // debugging
System.out.println(companies.length()); // debugging
try {
System.out.println(companies.getJSONObject(i)); // debugging
allCompanies.add(new companiesBuilder(companies
.getJSONObject(i)));
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/*
* this loop goes through every company that has been built and adds it
* to the custom listview adapter.
*/
for (companiesBuilder built : allCompanies) {
compAdp.add(built);
}
companyList.setAdapter(compAdp);
}
/*
* (non-Javadoc)
*
* #see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
*/
#Override
public boolean onCreateOptionsMenu(Menu menu) {
m = menu; // adds a reference to the variable m (menu).
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main, menu);
return (super.onCreateOptionsMenu(menu)); // returns the super for
// efficiency.
}
/*
* (non-Javadoc)
*
* #see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
*/
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.search:
openSearch(); // when searchbutton is clicked, it will start the
// opensearch method.
return true;
case R.id.action_settings:
// openSettings();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/*
* Written by: Joakim Bajoul Kakaei Description: Invoked when user presses
* search icon. Opens up the searchview in the menu.
*/
public void openSearch() {
/*
* snippet taken from
* http://developer.android.com/training/search/setup.html with some
* changes to it.
*/
SearchManager sm = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView sv = (SearchView) m.findItem(R.id.search).getActionView();
sv.setSearchableInfo(sm.getSearchableInfo(getComponentName()));
}
/*
* top10 (AsyncTask) Name: Joakim Bajoul Kakaei (881129-0298) Description:
* This class handles the connection between the JSONparser and the
* mainActivity using a different thread. It's mainly used to help with
* memory allocation as well as making sure the main-thread isn't too
* overloaded with too many assignments.
*/
private class top10 extends AsyncTask<String, String, JSONArray> {
#Override
protected JSONArray doInBackground(String... params) {
JSONparser jparser = new JSONparser();
companies = jparser.topCompanies();
System.out.println("background"); // debugging
return companies;
}
#Override
protected void onPostExecute(JSONArray jarr) {
System.out.println("onpost");
}
}
}
package com.example.geostocks;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.json.JSONArray;
import org.json.JSONException;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ListView;
public class SearchActivity extends Activity {
JSONArray companies;
String query;
private List<companiesBuilder> allCompanies = new ArrayList<companiesBuilder>();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
handleIntent(getIntent());
try {
/*
* executes the AsyncTask (top10). When the task is executed, it
* then gets the JSONArray which is bouncing around.
*/
companies = new searchList().execute("DO IT!").get();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (ExecutionException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
/*
* The following snippet mainly creates the adapterobject and associates
* it with it's elements, context and layout.
*/
final ListView companyList = (ListView) findViewById(R.id.listView_search);
final companiesAdapter compAdp = new companiesAdapter(this,
R.layout.listview_layout);
System.out.println("after"); // debugging
/*
* a loop to create companyBuilder-objects from the JSONArray and then
* add those objects to an ArrayList (allCompanies).
*/
for (int i = 0; companies.length() > i; i++) {
System.out.println("companies-looper"); // debugging
System.out.println(companies.length()); // debugging
try {
System.out.println(companies.getJSONObject(i)); // debugging
allCompanies.add(new companiesBuilder(companies
.getJSONObject(i)));
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/*
* this loop goes through every company that has been built and adds it
* to the custom listview adapter.
*/
for (companiesBuilder built : allCompanies) {
compAdp.add(built);
}
companyList.setAdapter(compAdp);
}
#Override
public void onBackPressed() {
super.onBackPressed();
}
#Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
query = intent.getStringExtra(SearchManager.QUERY);
System.out.println(query);
// use the query to search your data somehow
}
}
private class searchList extends AsyncTask<String, String, JSONArray> {
#Override
protected JSONArray doInBackground(String... params) {
JSONparser jparser = new JSONparser();
companies = jparser.search(query);
System.out.println("background"); // debugging
return companies;
}
#Override
protected void onPostExecute(JSONArray jarr) {
System.out.println("onpost");
}
}
}
Custom listviewAdapter:
package com.example.geostocks;
import java.util.ArrayList;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.TextView;
/* companiesAdapter.
* Made by: Joakim Bajoul Kakaei (881129-0298)
* Description: Since the listview we have decided to use has multiple objects on each row,
* the listview's adapter needs to be customized to meet those requirements.
* This class may be revised later on (to add or remove objects from the rowItemObjects).
*/
public class companiesAdapter extends ArrayAdapter<companiesBuilder> implements
OnClickListener {
private final int companiesBuilderResource;
private ArrayList<companiesBuilder> companies;
public companiesAdapter(final Context context,
final int companiesBuilderResource) {
super(context, 0);
this.companiesBuilderResource = companiesBuilderResource;
companies = new ArrayList<companiesBuilder>();
}
public void add(companiesBuilder row) {
companies.add(row);
notifyDataSetChanged();
}
#Override
public int getCount() {
return companies.size();
}
#Override
public companiesBuilder getItem(int i) {
return companies.get(i);
}
#Override
public long getItemId(int i) {
return i;
}
#Override
public View getView(final int position, final View convertView,
final ViewGroup parent) {
// We need to get the best view (re-used if possible) and then
// retrieve its corresponding rowItem, to optimize lookup efficiency.
final View view = getourView(convertView);
final rowItem rowItem = getrowItem(view);
// final companiesBuilder company = getItem(position);
final companiesBuilder row = companies.get(position);
// Setting up both the titleobject's with their corresponding variables
// (from the row-object).
rowItem.titleView_left.setText(row.getName());
rowItem.titleView_right.setText(row.getPrice());
// Setting up the subtitle's items will be a little bit tougher, since
// it requires
// some manipulating of the xml-data.
rowItem.subTitleView_left.setText(row.getSymbol());
/*
* If-clauses to change the right subtitle's color palette to make it
* easier for the user to distinguish increase and decrease.
*/
if (Double.parseDouble(row.getChange()) < 0) {
rowItem.subTitleView_right
.setTextColor(Color.parseColor("#E51400"));
rowItem.subTitleView_right.setText(row.getPercent() + "%" + " "
+ "( " + row.getChange() + ")");
} else if (Double.parseDouble(row.getChange()) > 0) {
rowItem.subTitleView_right
.setTextColor(Color.parseColor("#339933"));
rowItem.subTitleView_right.setText(row.getPercent() + "%" + " "
+ "( +" + row.getChange() + ")");
} else {
rowItem.subTitleView_right.setText(row.getPercent() + "%" + " "
+ "(" + row.getChange() + ")");
}
// Setting image view is simple enough...
rowItem.imageButton.setSelected(row.getSelected());
view.setOnClickListener(new OnClickListener() {
/*
* Add an onClickListener that is associated with view (view being
* every row).
*/
#Override
public void onClick(View v) {
new AlertDialog.Builder(getContext()).setTitle(row.getName())
.show();
}
});
rowItem.imageButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View button) {
if (button.isSelected()) {
button.setSelected(false);
} else {
button.setSelected(true);
}
row.setSelect(view.isSelected());
}
});
return view;
}
private View getourView(final View convertView) {
// The ourView will be recycling / reusing the convertview if
// possible.
// Guess why? Exactly. CUZ WE LOOOOOOOOOOVE HAVING EFFICIENT CODE! <3
View ourView = null;
if (null == convertView) {
final Context context = getContext();
final LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ourView = inflater.inflate(companiesBuilderResource, null);
} else {
ourView = convertView;
}
return ourView;
}
private rowItem getrowItem(final View ourView) {
// Recycling and reusing tags and objects is the name of the game!
final Object tag = ourView.getTag();
rowItem rowItem = null;
/*
* Sets up the listview's rowitems with their corresponding listview
* items (textviews and images).
*/
if (null == tag || !(tag instanceof rowItem)) {
rowItem = new rowItem();
rowItem.titleView_right = (TextView) ourView
.findViewById(R.id.title_right);
rowItem.titleView_left = (TextView) ourView
.findViewById(R.id.title_left);
rowItem.subTitleView_left = (TextView) ourView
.findViewById(R.id.subtitle_left);
rowItem.subTitleView_right = (TextView) ourView
.findViewById(R.id.subtitle_right);
rowItem.imageButton = (ImageButton) ourView
.findViewById(R.id.add_button);
ourView.setTag(rowItem);
} else {
rowItem = (rowItem) tag;
}
return rowItem;
}
/*
* since views are recycled, these references will always be there to be
* reused. Again, it's all about optimization!
*/
private static class rowItem {
public TextView titleView_left;
public TextView titleView_right;
public TextView subTitleView_left;
public TextView subTitleView_right;
public ImageButton imageButton;
}
#Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
}
}
Selector for imagebutton
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#drawable/ic_added_button" android:state_selected="true"/>
<item android:drawable="#drawable/ic_add_button" android:state_selected="false"/>
</selector>
Create model for companies (ex. Company class).
In Company class add property isSelected.
Fill array of companies from your JSONArray. Pass this array to ListView adapter.
In rowItem.imageButton.setOnClickListener update rowItem state (selected or not) in source array (array of companies, which was created from JSONArray).
Note: if you will save list row state, you should save this state in source array of your list adapter.
In MainActivity:
CompanyListAdapter adapter = new CompanyListAdapter(activity);
ArrayList<Company> companies = getCompaniesFromJsonArray();
for(Company company : companies){
adapter.add(company);
}
listView.setAdapter(adapter);
Company.java
package com.donvigo.TestCustomActionBar;
public class Company {
private String name;
private boolean isSelected;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
}
CompanyListAdapter.java
package com.donvigo.TestCustomActionBar;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
public class CompanyListAdapter extends BaseAdapter {
private Activity activity;
private ArrayList<Company> companies;
public CompanyListAdapter(Activity activity){
this.activity = activity;
companies = new ArrayList<Company>();
}
public void add(Company company){
companies.add(company);
notifyDataSetChanged();
}
#Override
public int getCount() {
return companies.size();
}
#Override
public Object getItem(int i) {
return companies.get(i);
}
#Override
public long getItemId(int i) {
return i;
}
#Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
ViewHolder holder;
if(convertView == null){
convertView = activity.getLayoutInflater().inflate(R.layout.list_row_layout, null);
holder = new ViewHolder();
holder.textViewName = (ImageView) convertView.findViewById(R.id.textViewName);
holder.rowImage = (ImageView) convertView.findViewById(R.id.rowImage);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
final Company row = companies.get(position);
// refresh state of your row image
holder.rowImage.setSelected(row.isSelected());
holder.rowImage.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (view.isSelected()) {
view.setSelected(false);
} else {
view.setSelected(true);
}
row.setSelected(view.isSelected());
}
});
return convertView;
}
private class ViewHolder{
public TextView textViewName;
public ImageView rowImage;
}
}
This is main idea. Also, if you will save row states in ListView permanently (not just 1 time when you download and show data from internet) - you have to save you companies data in database, or in another data storage (ex.: in text file) and next time when you open activity - restore previously saved companies list.
Seen your code, no where you are saving checked state of button. Point is when you navigate from MainActivity to SearchActivity your listview with custom adapter is populated and displayed, there you set some button, now once you navigate back to main activity from there you SearchAvtivity is destroyed and is completely redrawn when you navigate back. Hence you see every then fresh again.
If you want to retain checked state of button, you should save their latest state in preference and load it from there or you can keep global data variable for holding checked states of buttons so to retain it during entire life cycle of your applications.
Read about Activity life cycle and stack, when they are destroyed and created, this will throw you more insight over why this is happening.
hope it help.

Having trouble with Custom Support Loader

I am setting up to use the Loader pattern and had issues using the cursor approach, so I have refactored my code because my tables do not use _id as the primary key because of the use of association tables and I setup my code to use the same basic structure as the LoaderCustomSupport.java example from the android developer site. All of the code works without errors and I can see that I have the proper data back and ready for the ListFragment to display but after the onLoadFinished call back completes the setData on the adapter the getView is never called. My getView looks like this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
PhoneNumberListHolder holder;
if (row == null) {
row = mInflater.inflate(R.layout.phonenumber_row, parent, false);
holder=new PhoneNumberListHolder(row);
row.setTag(holder);
} else {
holder = (PhoneNumberListHolder)row.getTag();
}
holder.populateForm(this.phoneNumbers.get(position));
return row;
}
I am trying to use the holder pattern, but I am thinking that maybe it is part of my issue. Any ideas where I might be going wrong?
Here is the loader code (Like I said I followed the Google example for my first run changing what I thought I would need)
The Abstract Loader for my Class
/*
* Custom version of CommonsWare, LLC, AbstractCursorLoader
*
*/
package myApp.service.data;
import java.util.List;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
abstract public class AbstractPhoneNumberLoader extends AsyncTaskLoader<List<PhoneNumber>> {
abstract protected List<PhoneNumber> buildPhoneNumber();
List<PhoneNumber> lastPhoneNumber=null;
public AbstractPhoneNumberLoader(Context context) {
super(context);
}
#Override
public List<PhoneNumber> loadInBackground() {
List<PhoneNumber> data=buildPhoneNumber();
if (data!=null) {
// Make sure we fill the person
data.size();
}
return (data);
}
/**
* This will run on the UI thread, routing the results from the
* background to the consumer of the Person object
* (e.g., a PhoneNumberListAdapter).
*/
#Override
public void deliverResult(List<PhoneNumber> data) {
if (isReset()) {
// An async query attempted a call while the loader is stopped
if (data!=null) {
data.clear();
data=null;
//not sure the best option here since we cannot close the List object
}
return;
}
List<PhoneNumber> oldPhoneNumber=lastPhoneNumber;
lastPhoneNumber=data;
if (isStarted()) {
super.deliverResult(data);
}
if (oldPhoneNumber!=null && oldPhoneNumber!=data && oldPhoneNumber.isEmpty()) {
oldPhoneNumber.clear();
oldPhoneNumber=null;
}
}
/**
* Start an asynchronous load of the requested data.
* When the result is ready the callbacks will be called
* on the UI thread. If a previous load has completed
* and is still valid the result may be passed back to the
* caller immediately.
*
* Note: Must be called from the UI thread
*/
#Override
protected void onStartLoading() {
if (lastPhoneNumber!=null) {
deliverResult(lastPhoneNumber);
}
if (takeContentChanged() || lastPhoneNumber==null) {
forceLoad();
}
}
/**
* Must be called from the UI thread, triggered by
* a call to stopLoading().
*/
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task
cancelLoad();
}
/**
* Must be called from the UI thread, triggered by a
* call to cancel(). Here, we make sure our Person
* is null, if it still exists and is not already empty.
*/
#Override
public void onCanceled(List<PhoneNumber> data) {
if (data!=null && !data.isEmpty()) {
data.clear();
}
}
/**
* Must be called from the UI thread, triggered by a
* call to reset(). Here, we make sure our Person
* is empty, if it still exists and is not already empty.
*/
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (lastPhoneNumber!=null && !lastPhoneNumber.isEmpty()) {
lastPhoneNumber.clear();
}
lastPhoneNumber=null;
}
}
The Data Loader
/*
* Custom version of CommonsWare, LLC, SQLiteCursorLoader
*
*/
package myApp.service.data;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.List;
import myApp.service.data.ActorDbAdapter;
import android.content.Context;
public class PhoneNumberDataLoader extends AbstractPhoneNumberLoader {
ActorDbAdapter db=null;
protected final String actorId;
protected final String _PHONENUMBERID = "PhoneNumberId";
protected SimpleDateFormat dateFormat = new SimpleDateFormat("dd/mm/yyyy");
/**
* Constructor - takes the context to allow the database to be
* opened/created
*
* #param ctx the Context within which to work
*/
public PhoneNumberDataLoader(Context ctx, String actorId) {
super(ctx);
this.actorId = actorId;
getHelper(ctx);
}
// Get a database connection
private void getHelper(Context ctx) {
if (db==null) {
db=new ActorDbAdapter(ctx);
}
db.open();
}
// Loader Methods
/**
* Runs on a worker thread and performs the actual
* database query to retrieve the PhoneNumber List.
*/
#Override
protected List<PhoneNumber> buildPhoneNumber() {
return(db.readPhoneById(actorId));
}
/**
* Writes a semi-user-readable roster of contents to
* supplied output.
*/
#Override
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
super.dump(prefix, fd, writer, args);
writer.print(prefix);
writer.print("actorId=");
writer.println(actorId);
}
public void insert(PhoneNumber data, String actorId) {
new InsertTask(this).execute(db, data, actorId);
}
// Saved for Later, get Reads and Writes working first
// public void update(PhoneNumber data, String actorId, String whereClause, String[] whereArgs) {
// new UpdateTask(this).execute(db, data, actorId, whereClause, whereArgs);
// }
//
// public void delete(String actorId, String phoneNumberId, String whereClause, String[] whereArgs) {
// new DeleteTask(this).execute(db, actorId, phoneNumberId, whereClause, whereArgs);
// }
public void execSQL(String actorId) {
new ExecSQLTask(this).execute(db, actorId);
}
private class InsertTask extends ContentChangingTask<Object, Void, Void> {
InsertTask(PhoneNumberDataLoader loader) {
super(loader);
}
#Override
protected Void doInBackground(Object... params) {
ActorDbAdapter db=(ActorDbAdapter)params[0];
PhoneNumber data=(PhoneNumber)params[1];
int actorId=Integer.parseInt((String)params[2]);
db.createPhoneNumber(data, actorId);
return(null);
}
}
// Saved for Later, get Reads and Writes working first
// private class UpdateTask extends
// ContentChangingTask<Object, Void, Void> {
// UpdateTask(PhoneNumberDataLoader loader) {
// super(loader);
// }
//
// #Override
// protected Void doInBackground(Object... params) {
// ActorDbAdapter db=(ActorDbAdapter)params[0];
// String table=(String)params[1];
// int actorId=Integer.parseInt((String)params[2]);
// String where=(String)params[3];
// String[] whereParams=(String[])params[4];
//
// db.updatePhoneNumber(table, values, where, whereParams);
//
// return(null);
// }
// }
//
// private class DeleteTask extends
// ContentChangingTask<Object, Void, Void> {
// DeleteTask(PhoneNumberDataLoader loader) {
// super(loader);
// }
//
// #Override
// protected Void doInBackground(Object... params) {
// ActorDbAdapter db=(ActorDbAdapter)params[0];
// int actorId=Integer.parseInt((String)params[1]);
// int phoneNumberId=Integer.parseInt((String)params[2]);
// String where=(String)params[3];
// String[] whereParams=(String[])params[3];
//
// db.deletePhoneNumber(table, where, whereParams);
//
// return(null);
// }
// }
private class ExecSQLTask extends
ContentChangingTask<Object, Void, Void> {
ExecSQLTask(PhoneNumberDataLoader loader) {
super(loader);
}
#Override
protected Void doInBackground(Object... params) {
ActorDbAdapter db=(ActorDbAdapter)params[0];
String actorId=(String)params[1];
db.readPhoneById(actorId);
return(null);
}
}
}
Here is my full ListAdapter
package myApp.planner.utilities;
import java.util.ArrayList;
import java.util.List;
import myApp.planner.R;
import myApp.planner.codes.PhoneOrAddressTypeCode;
import myApp.service.data.PhoneNumber;
import myApp.service.data.PhoneNumberListData;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.TextView;
public class PhoneNumberListAdapter extends ArrayAdapter<PhoneNumber> {
private List<PhoneNumber> phoneNumbers;
private final LayoutInflater mInflater;
private Activity activity;
public PhoneNumberListAdapter(Activity a, int textViewResourceId) {
super(a, textViewResourceId);
activity = a;
mInflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void setData(List<PhoneNumber> data) {
if (this.phoneNumbers==null) {
this.phoneNumbers = new ArrayList<PhoneNumber>();
}
this.phoneNumbers.clear();
if (data != null) {
for (PhoneNumber phoneNumber : data) {
this.phoneNumbers.add(phoneNumber);
}
}
}
public static class PhoneNumberListHolder {
private TextView actorid=null;
private TextView phonenumberid=null;
private TextView phonetype=null;
private TextView phonenumber=null;
private CheckBox isprimary=null;
PhoneNumberListHolder(View row) {
actorid=(TextView)row.findViewById(R.id.actorid);
phonenumberid=(TextView)row.findViewById(R.id.phonenumberid);
phonetype=(TextView)row.findViewById(R.id.txtphonetype);
phonenumber=(TextView)row.findViewById(R.id.txtphonenumber);
isprimary=(CheckBox)row.findViewById(R.id.isprimary);
}
//void populateForm(ArrayList<PhoneNumberListData> c, int position) {
void populateForm(PhoneNumber data) {
//PhoneNumberListData data = c.get(position);
// Attempt to add the Actor ID
if (actorid != null){
actorid.setText(data.getActor().get(0).getActorId()==0 ? "0": Integer.toString(data.getActor().get(0).getActorId()));
}
// Attempt to add the Phone Number Item ID
if (phonenumberid != null){
phonenumberid.setText(data.getPhoneNumberId()==0 ? "0": Integer.toString(data.getPhoneNumberId()));
}
// Attempt to add the Phone Number Type
if (phonetype != null){
phonetype.setText(data.getPhoneType()==null ? "": PhoneOrAddressTypeCode.valueOf(data.getPhoneType()).toString());
}
// Attempt to add the Phone Number
if (phonenumber != null){
phonenumber.setText(data.getPhoneNumber1()==null ? "": data.getPhoneNumber1());
}
// Attempt to add the is Primary Flag
if (isprimary != null){
isprimary.setChecked(data.getIsPrimary()==0 ? Boolean.FALSE: Boolean.TRUE);
}
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
PhoneNumberListHolder holder;
if (row == null) {
row = mInflater.inflate(R.layout.phonenumber_row, parent, false);
holder=new PhoneNumberListHolder(row);
row.setTag(holder);
} else {
holder = (PhoneNumberListHolder)row.getTag();
}
holder.populateForm(this.phoneNumbers.get(position));
return row;
}
}
The ListFragment:
package myApp.planner;
import java.util.ArrayList;
import java.util.List;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SearchViewCompat;
import android.support.v4.widget.SearchViewCompat.OnQueryTextListenerCompat;
import android.text.TextUtils;
import android.content.Intent;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.database.Cursor;
import android.view.ContextMenu;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import myApp.planner.R;
import myApp.planner.utilities.PhoneNumberListAdapter;
import myApp.service.data.PhoneNumber;
import myApp.service.data.PhoneNumberDataLoader;
public class ActorPhoneNumberListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<List<PhoneNumber>> {
public final static String ID_EXTRA="myapp.planner.actorid";
protected final static String TAG = "ActorPhoneNumberListFragment";
private static final int ADD_ID=Menu.FIRST + 1;
private static final int DELETE_ID=Menu.FIRST + 3;
private PhoneNumberListAdapter mAdapter=null;
private PhoneNumberDataLoader loader=null;
private String mCurFilter;
private String actorId = "0";
//private SharedPreferences prefs=null;
OnActorPhoneNumberListListener listener=null;
OnQueryTextListenerCompat mOnQueryTextListenerCompat;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
this.actorId = getActivity().getIntent().getExtras().getString(ID_EXTRA).toString();
}
#Override
public void onResume() {
super.onResume();
Bundle args=getArguments();
if (args!=null) {
loadPhoneNumbers(args.getString(ID_EXTRA));
}
// init Empty Test for no Phone numbers Found
// Add the menu options that we need to manage the list
setHasOptionsMenu(true);
// Hookup the dbAdapter and create a blank adapter
initList();
}
#Override
public void onPause() {
super.onPause();
}
#Override
public void onListItemClick(ListView list, View view, int position, long id) {
if (listener!=null) {
//We will actually want the PhoneNumber Id here for the popup edit screen
String mId = actorId;
listener.onActorPhoneNumberListSelected(mId);
}
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.phonenumber_opton, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId()==R.id.addNewPhone) {
//add();
return(true);
} else if (item.getItemId()==R.id.help) {
startActivity(new Intent(getActivity(), HelpPage.class));
return(true);
} else if (item.getItemId()==R.id.phoneRefresh) {
//startActivity(new Intent(getActivity(), ActorPhoneNumberListFragment.class));
//We may just need to refresh the loader
return(true);
} else
return(super.onOptionsItemSelected(item));
}
public void setOnActorPhoneNumberListListener(OnActorPhoneNumberListListener listener) {
this.listener=listener;
}
public void loadPhoneNumbers(String actorId) {
this.actorId=actorId;
}
private void initList() {
mAdapter=new PhoneNumberListAdapter(getActivity(), R.layout.phonenumber_row);
setListAdapter(mAdapter);
// Start out with a progress indicator.
setListShown(false);
getActivity().getSupportLoaderManager().initLoader(0, null, this);
}
public interface OnActorPhoneNumberListListener {
void onActorPhoneNumberListSelected(String actorId);
}
#Override
public Loader<List<PhoneNumber>> onCreateLoader(int loaderId, Bundle args) {
loader= new PhoneNumberDataLoader(getActivity(), actorId);
return(loader);
}
#Override
public void onLoadFinished(Loader<List<PhoneNumber>> loader, List<PhoneNumber> data) {
// Now give the data to the adapter
mAdapter.setData(data);
mAdapter.notifyDataSetChanged();
//setListAdapter(mAdapter);
// Show the list
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
#Override
public void onLoaderReset(Loader<List<PhoneNumber>> arg0) {
// TODO Auto-generated method stub
mAdapter.setData(null);
}
}

Categories

Resources