From 454a84ca7271848f27f87af76949e91de0af7f69 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Tue, 31 May 2016 15:33:33 -0700 Subject: [PATCH] BaseVH and BaseAdapter finished --- .../com/philliphsu/clock2/BaseAdapter.java | 91 +++++++++++++++++-- .../com/philliphsu/clock2/BaseViewHolder.java | 26 ++++-- .../com/philliphsu/clock2/MainActivity.java | 4 +- .../clock2/OnListItemInteractionListener.java | 16 ++++ .../clock2/alarms/AlarmViewHolder.java | 51 +++-------- .../clock2/alarms/AlarmsAdapter.java | 72 +++++---------- .../clock2/alarms/AlarmsFragment.java | 15 ++- 7 files changed, 161 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/OnListItemInteractionListener.java diff --git a/app/src/main/java/com/philliphsu/clock2/BaseAdapter.java b/app/src/main/java/com/philliphsu/clock2/BaseAdapter.java index 575e334..689db1a 100644 --- a/app/src/main/java/com/philliphsu/clock2/BaseAdapter.java +++ b/app/src/main/java/com/philliphsu/clock2/BaseAdapter.java @@ -1,25 +1,100 @@ package com.philliphsu.clock2; +import android.support.v7.util.SortedList; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.util.SortedListAdapterCallback; import android.view.ViewGroup; +import java.util.List; + /** * Created by Phillip Hsu on 5/31/2016. */ -public abstract class BaseAdapter extends RecyclerView.Adapter> { +public abstract class BaseAdapter> extends RecyclerView.Adapter { - @Override - public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return null; + private final OnListItemInteractionListener mListener; + private final SortedList mItems; + + protected BaseAdapter(Class cls, List items, OnListItemInteractionListener listener) { + mItems = new SortedList<>(cls, new SortedListAdapterCallback(this) { + @Override + public int compare(T o1, T o2) { + //return BaseAdapter.this.*compare(o1, o2); // *: See note below + return BaseAdapter.this.compare(o1, o2); + // Only need to specify the type when calling a _generic_ method + // that defines _its own type parameters_ in its signature, but even + // then, it's not actually necessary because the compiler can + // infer the type. + // This is just an _abstract_ method that takes params of type T. + } + + @Override + public boolean areContentsTheSame(T oldItem, T newItem) { + return BaseAdapter.this.areContentsTheSame(oldItem, newItem); + } + + @Override + public boolean areItemsTheSame(T item1, T item2) { + return BaseAdapter.this.areItemsTheSame(item1, item2); + } + }); + mListener = listener; + mItems.addAll(items); + } + + protected abstract VH onCreateViewHolder(ViewGroup parent, OnListItemInteractionListener listener); + + protected abstract int compare(T o1, T o2); + + protected abstract boolean areContentsTheSame(T oldItem, T newItem); + + protected abstract boolean areItemsTheSame(T item1, T item2); + + @Override // not final to allow subclasses to use the viewType if needed + public VH onCreateViewHolder(ViewGroup parent, int viewType) { + return onCreateViewHolder(parent, mListener); } @Override - public void onBindViewHolder(BaseViewHolder holder, int position) { - + public final void onBindViewHolder(VH holder, int position) { + holder.onBind(mItems.get(position)); } @Override - public int getItemCount() { - return 0; + public final int getItemCount() { + return mItems.size(); + } + + public final void replaceData(List items) { + mItems.clear(); + mItems.addAll(items); + } + + protected final T getItem(int position) { + return mItems.get(position); + } + + public final int addItem(T item) { + return mItems.add(item); + } + + public final boolean removeItem(T item) { + return mItems.remove(item); + } + + public final void updateItem(T oldItem, T newItem) { + // SortedList finds the index of an item by using its callback's compare() method. + // We can describe our current item update process is as follows: + // * An item's fields are modified + // * The changes are saved to the repository + // * A item update callback is fired to the RV + // * The RV calls its adapter's updateItem(), passing the instance of the modified item as both arguments + // (because modifying an item keeps the same instance of the item) + // * The SortedList tries to find the index of the param oldItem, but since its fields are changed, + // the search may end up failing because compare() could return the wrong index. + // A workaround is to copy construct the original item instance BEFORE you modify the fields. + // Then, oldItem should point to the copied instance. + // Alternatively, a better approach is to make items immutable. + mItems.updateItemAt(mItems.indexOf(oldItem), newItem); } } diff --git a/app/src/main/java/com/philliphsu/clock2/BaseViewHolder.java b/app/src/main/java/com/philliphsu/clock2/BaseViewHolder.java index ad7e4d7..ae86d92 100644 --- a/app/src/main/java/com/philliphsu/clock2/BaseViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/BaseViewHolder.java @@ -1,5 +1,6 @@ package com.philliphsu.clock2; +import android.content.Context; import android.support.annotation.CallSuper; import android.support.annotation.LayoutRes; import android.support.v7.widget.RecyclerView; @@ -14,27 +15,36 @@ import butterknife.ButterKnife; */ public abstract class BaseViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private final OnClickListener mOnClickListener; + private final Context mContext; + private final OnListItemInteractionListener mListener; private T mItem; - public BaseViewHolder(ViewGroup parent, @LayoutRes int layoutRes, OnClickListener listener) { + public BaseViewHolder(ViewGroup parent, @LayoutRes int layoutRes, OnListItemInteractionListener listener) { super(LayoutInflater.from(parent.getContext()) .inflate(layoutRes, parent, false)); ButterKnife.bind(this, itemView); - mOnClickListener = listener; + mContext = parent.getContext(); + mListener = listener; + itemView.setOnClickListener(this); } + /** + * Call to super must be the first line in the overridden implementation, + * so that the base class can keep a reference to the item parameter. + */ @CallSuper public void onBind(T item) { mItem = item; } - @Override - public final void onClick(View v) { - mOnClickListener.onClick(mItem); + public final Context getContext() { + return mContext; } - public interface OnClickListener { - void onClick(T item); + @Override + public final void onClick(View v) { + if (mListener != null) { + mListener.onListItemInteraction(mItem); + } } } diff --git a/app/src/main/java/com/philliphsu/clock2/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java index b108f0d..395b4df 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -24,7 +24,7 @@ import android.widget.TextView; import com.philliphsu.clock2.alarms.AlarmsFragment; import com.philliphsu.clock2.ringtone.RingtoneActivity; -public class MainActivity extends AppCompatActivity implements AlarmsFragment.OnListFragmentInteractionListener { +public class MainActivity extends AppCompatActivity implements AlarmsFragment.OnAlarmInteractionListener { /** * The {@link android.support.v4.view.PagerAdapter} that will provide @@ -179,7 +179,7 @@ public class MainActivity extends AppCompatActivity implements AlarmsFragment.On } @Override - public void onListFragmentInteraction(Alarm item) { + public void onListItemInteraction(Alarm item) { // TODO react to click } diff --git a/app/src/main/java/com/philliphsu/clock2/OnListItemInteractionListener.java b/app/src/main/java/com/philliphsu/clock2/OnListItemInteractionListener.java new file mode 100644 index 0000000..1cbdc39 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/OnListItemInteractionListener.java @@ -0,0 +1,16 @@ +package com.philliphsu.clock2; + +/** + * Created by Phillip Hsu on 5/31/2016. + * This interface MUST be extended by Fragments that display a RecyclerView as a list. + * The reason for this is Fragments need to do an instanceof check on their host Context + * to see if it implements this interface, and instanceof cannot be used with generic type + * parameters. Why not just define this interface as a member of the Fragment class? + * Because the Fragment's BaseAdapter needs a reference to this interface, and we don't want + * to couple the BaseAdapter + * to the Fragment. By keeping this interface as generic as possible, the BaseAdapter can + * be easily adapted to not just Fragments, but also custom Views, Activities, etc. + */ +public interface OnListItemInteractionListener { + void onListItemInteraction(T item); +} diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java index a001bcc..a837d7d 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java @@ -1,27 +1,25 @@ package com.philliphsu.clock2.alarms; -import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.annotation.CallSuper; -import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SwitchCompat; import android.text.SpannableString; import android.text.Spanned; import android.text.format.DateFormat; import android.text.style.RelativeSizeSpan; -import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import com.philliphsu.clock2.Alarm; +import com.philliphsu.clock2.BaseViewHolder; import com.philliphsu.clock2.DaysOfWeek; +import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; import java.util.Date; import butterknife.Bind; -import butterknife.ButterKnife; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -30,13 +28,9 @@ import static com.philliphsu.clock2.DaysOfWeek.NUM_DAYS; /** * Created by Phillip Hsu on 5/31/2016. */ -public class AlarmViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { +public class AlarmViewHolder extends BaseViewHolder { private static final RelativeSizeSpan AMPM_SIZE_SPAN = new RelativeSizeSpan(0.5f); - private final Context mContext; - private final AlarmsFragment.OnListFragmentInteractionListener mListener; - private Alarm mItem; - @Bind(R.id.time) TextView mTime; @Bind(R.id.on_off_switch) SwitchCompat mSwitch; @Bind(R.id.label) TextView mLabel; @@ -44,27 +38,15 @@ public class AlarmViewHolder extends RecyclerView.ViewHolder implements View.OnC @Bind(R.id.recurring_days) TextView mDays; @Bind(R.id.dismiss) Button mDismissButton; - /*public AlarmViewHolder(ViewGroup parent, BaseViewHolder.OnClickListener listener) { - - }*/ - - public AlarmViewHolder(View view, AlarmsFragment.OnListFragmentInteractionListener listener) { - super(view); - ButterKnife.bind(this, view); - mContext = view.getContext(); - mListener = listener; - view.setOnClickListener(this); + public AlarmViewHolder(ViewGroup parent, OnListItemInteractionListener listener) { + super(parent, R.layout.item_alarm, listener); } - /** - * Call to super must be the first line in the overridden implementation, - * so that the base class can keep a reference to the item parameter. - */ - @CallSuper + @Override public void onBind(Alarm alarm) { - mItem = alarm; - String time = DateFormat.getTimeFormat(mContext).format(new Date(alarm.ringsAt())); - if (DateFormat.is24HourFormat(mContext)) { + super.onBind(alarm); + String time = DateFormat.getTimeFormat(getContext()).format(new Date(alarm.ringsAt())); + if (DateFormat.is24HourFormat(getContext())) { mTime.setText(time); } else { // No way around having to construct this on binding @@ -78,7 +60,7 @@ public class AlarmViewHolder extends RecyclerView.ViewHolder implements View.OnC //TODO:mCountdown.showAsText(alarm.ringsIn()); mCountdown.setVisibility(VISIBLE); //todo:mCountdown.getTickHandler().startTicking(true) - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); // how many hours before alarm is considered upcoming // TODO: shared prefs /*int hoursBeforeUpcoming = Integer.parseInt(prefs.getString( @@ -110,14 +92,14 @@ public class AlarmViewHolder extends RecyclerView.ViewHolder implements View.OnC if (numRecurringDays > 0) { String text; if (numRecurringDays == NUM_DAYS) { - text = mContext.getString(R.string.every_day); + text = getContext().getString(R.string.every_day); } else { StringBuilder sb = new StringBuilder(); for (int i = 0; // ordinal number, i.e. the position in the week, not an actual day! i < NUM_DAYS; i++) { if (alarm.isRecurring(i)) { // Is the i-th day in the week recurring? // This is the actual day at the i-th position in the week. - int weekDay = DaysOfWeek.getInstance(mContext).weekDay(i); + int weekDay = DaysOfWeek.getInstance(getContext()).weekDay(i); sb.append(DaysOfWeek.getLabel(weekDay)).append(", "); } } @@ -131,11 +113,4 @@ public class AlarmViewHolder extends RecyclerView.ViewHolder implements View.OnC mDays.setVisibility(GONE); } } - - @Override - public void onClick(View v) { - if (mListener != null) { - mListener.onListFragmentInteraction(mItem); - } - } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java index e910703..438ef10 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java @@ -1,71 +1,43 @@ package com.philliphsu.clock2.alarms; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.util.SortedListAdapterCallback; -import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; -import com.philliphsu.clock2.R; +import com.philliphsu.clock2.BaseAdapter; +import com.philliphsu.clock2.OnListItemInteractionListener; import java.util.Arrays; import java.util.List; -/** - * {@link RecyclerView.Adapter} that can display a {@link Alarm} and makes a call to the - * specified {@link AlarmsFragment.OnListFragmentInteractionListener}. - */ -public class AlarmsAdapter extends RecyclerView.Adapter { +public class AlarmsAdapter extends BaseAdapter { - private final SortedList mItems; - private final AlarmsFragment.OnListFragmentInteractionListener mListener; - - public AlarmsAdapter(List alarms, AlarmsFragment.OnListFragmentInteractionListener listener) { - mItems = new SortedList<>(Alarm.class, new SortedListAdapterCallback(this) { - @Override - public int compare(Alarm o1, Alarm o2) { - return Long.compare(o1.ringsAt(), o2.ringsAt()); - } - - @Override - public boolean areContentsTheSame(Alarm oldItem, Alarm newItem) { - return oldItem.hour() == newItem.hour() - && oldItem.minutes() == newItem.minutes() - && oldItem.isEnabled() == newItem.isEnabled() - && oldItem.label().equals(newItem.label()) - && oldItem.ringsIn() == newItem.ringsIn() - && Arrays.equals(oldItem.recurringDays(), newItem.recurringDays()) - && oldItem.snoozingUntil() == newItem.snoozingUntil(); - } - - @Override - public boolean areItemsTheSame(Alarm item1, Alarm item2) { - return item1.id() == item2.id(); - } - }); - mItems.addAll(alarms); - mListener = listener; + public AlarmsAdapter(List alarms, OnListItemInteractionListener listener) { + super(Alarm.class, alarms, listener); } @Override - public AlarmViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - // TODO: Move this to the BaseAdapter. - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_alarm, parent, false); - return new AlarmViewHolder(view, mListener); + public AlarmViewHolder onCreateViewHolder(ViewGroup parent, OnListItemInteractionListener listener) { + return new AlarmViewHolder(parent, listener); } @Override - public void onBindViewHolder(final AlarmViewHolder holder, int position) { - // TODO: Move this to the BaseAdapter. - holder.onBind(mItems.get(position)); + public int compare(Alarm o1, Alarm o2) { + return Long.compare(o1.ringsAt(), o2.ringsAt()); } @Override - public int getItemCount() { - // TODO: Move this to the BaseAdapter. - return mItems.size(); + public boolean areContentsTheSame(Alarm oldItem, Alarm newItem) { + return oldItem.hour() == newItem.hour() + && oldItem.minutes() == newItem.minutes() + && oldItem.isEnabled() == newItem.isEnabled() + && oldItem.label().equals(newItem.label()) + && oldItem.ringsIn() == newItem.ringsIn() + && Arrays.equals(oldItem.recurringDays(), newItem.recurringDays()) + && oldItem.snoozingUntil() == newItem.snoozingUntil(); + } + + @Override + public boolean areItemsTheSame(Alarm item1, Alarm item2) { + return item1.id() == item2.id(); } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index b989591..7411fb4 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -11,13 +11,14 @@ import android.view.View; import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; +import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; import com.philliphsu.clock2.alarms.dummy.DummyContent; /** * A fragment representing a list of Items. *

- * Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener} + * Activities containing this fragment MUST implement the {@link OnAlarmInteractionListener} * interface. */ public class AlarmsFragment extends Fragment { @@ -26,7 +27,7 @@ public class AlarmsFragment extends Fragment { private static final String ARG_COLUMN_COUNT = "column-count"; // TODO: Customize parameters private int mColumnCount = 1; - private OnListFragmentInteractionListener mListener; + private OnAlarmInteractionListener mListener; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -76,11 +77,11 @@ public class AlarmsFragment extends Fragment { @Override public void onAttach(Context context) { super.onAttach(context); - if (context instanceof OnListFragmentInteractionListener) { - mListener = (OnListFragmentInteractionListener) context; + if (context instanceof OnAlarmInteractionListener) { + mListener = (OnAlarmInteractionListener) context; } else { throw new RuntimeException(context.toString() - + " must implement OnListFragmentInteractionListener"); + + " must implement OnAlarmInteractionListener"); } } @@ -100,7 +101,5 @@ public class AlarmsFragment extends Fragment { * "http://developer.android.com/training/basics/fragments/communicating.html" * >Communicating with Other Fragments for more information. */ - public interface OnListFragmentInteractionListener { - void onListFragmentInteraction(Alarm item); - } + public interface OnAlarmInteractionListener extends OnListItemInteractionListener {} }