上路传送眼:
Android练手小项目(KTReader)基于mvp架构(五)
GIthub地址: https://github.com/yiuhet/KTReader
上篇文章中我们完成了图片模块。
而这次我们要做的的就是历史记录和收藏功能。
惯例上图
这次我们完成的功能有:
- 建立数据库保存历史纪录和收藏记录
- 清空历史纪录
- 足迹板块记录了知乎日报的历史记录,点击可进入详情页
- 收藏板块记录了收藏的文章和图片,点击可进入详情页
所用到的知识点有:
- SQLiteOpenHelper
- 数据库的增删查
- ExpandableListView
可完善和加强的内容或功能有:
- 收藏页图片的排列美化
1. 数据库的建立
我们在数据库中建立三个表储存数据:
- History
- Collect
- Unsplash
三个表里为了简单我们储存同样的数据类型:
model.entity.HistoryCollect
public class HistoryCollect {
private String title;
private String url;
private String time;
public HistoryCollect(String title, String url, String time) {
this.title = title;
this.url = url;
this.time = time;
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
public String getTime() {
return time;
}
}
我们写个MyDataBaseHelper继承自SQLiteOpenHelper,使用这个类来建立数据库和表。
utils.MyDataBaseHelper
public class MyDataBaseHelper extends SQLiteOpenHelper {
public static final String HISTORY = "History";
public static final String COLLECT = "Collect";
public static final String UNSPLASH = "Unsplash";
public static final String CREATE_TABLE = "create table %s ("
+ "id integer primary key autoincrement, "
+ "title text, "
+ "url text, "
+ "time text)";
private Context mContext;
public MyDataBaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(String.format(CREATE_TABLE,HISTORY));
db.execSQL(String.format(CREATE_TABLE,COLLECT));
db.execSQL(String.format(CREATE_TABLE,UNSPLASH));
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
2. M层(操作数据库)
由于上面三个表中的数据为同一类型,所以我们的M层可以通用在历史记录和收藏记录上。我们先写一个工具类来统一操作数据库,这个工具类我们使其为单例,类里有以下方法:
- insertData(String table, String title, String url)
- isExist(String table, String url)
- deleteDataCollect(String table, String id)
- getData(String table)
- clearHistory()
方法的功能如方法名所示,根据传进来的不同表名来对不同表进行增删查改的操作。
utils.DBUtils
public class DBUtils {
private static DBUtils sDBUtis;
private SQLiteDatabase mSQLiteDatabase;
private DBUtils(Context context) {
mSQLiteDatabase = new MyDataBaseHelper(context, "KTReader.db", null, 1).getWritableDatabase();
}
public static DBUtils getInstence(Context context) {
if (sDBUtis == null) {
synchronized (DBUtils.class) {
if (sDBUtis == null) {
sDBUtis = new DBUtils(context);
}
}
}
return sDBUtis;
}
public void insertData(String table, String title, String url) {
//每个表储存数量上限为20条,超过就删除最旧的记录
Cursor cursor = mSQLiteDatabase.query(table, null, null, null, null, null, "id asc");
if (cursor.getCount() > 20 && cursor.moveToNext()) {
mSQLiteDatabase.delete(table, "id=?", new String[]{String.valueOf(cursor.getInt(cursor.getColumnIndex("id")))});
}
cursor.close();
//插入数据
ContentValues contentValues = new ContentValues();
contentValues.put("title", title);
contentValues.put("url", url);
Calendar c = Calendar.getInstance();
String time = String.format("%d月%d日",c.get(Calendar.MONTH)+1,c.get(Calendar.DATE));
contentValues.put("time", time);
mSQLiteDatabase.insertWithOnConflict(table, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);
}
public boolean isExist(String table, String url) {
boolean isRead = false;
Cursor cursor = mSQLiteDatabase.query(table, null, null, null, null, null, null);
while (cursor.moveToNext()) {
if (cursor.getString(cursor.getColumnIndex("url")).equals(url)) {
isRead = true;
break;
}
}
cursor.close();
return isRead;
}
public void deleteDataCollect(String table, String id) {
mSQLiteDatabase.delete(table,"url=?", new String[] {id});
}
public List<HistoryCollect> getData(String table) {
List<HistoryCollect> historyCollectList = new ArrayList<>();
Cursor cursor = mSQLiteDatabase.query(table, null, null, null, null, null, "id desc");
while (cursor.moveToNext()) {
String title;
String url;
String time;
title = cursor.getString(cursor.getColumnIndex("title"));
url = cursor.getString(cursor.getColumnIndex("url"));
time = cursor.getString(cursor.getColumnIndex("time"));
HistoryCollect historyCollect = new HistoryCollect(title, url, time);
historyCollectList.add(historyCollect);
}
cursor.close();
return historyCollectList;
}
public void clearHistory() {
mSQLiteDatabase.delete(MyDataBaseHelper.HISTORY, null, null);
}
}
按照mvp的结构,开始写m层的接口,接口简单的实现读取三个表和清空历史的方法:
model.HistoryCollectModel
public interface HistoryCollectModel {
void loadHistory(OnHistoryCollectListener listener);
void loadCollect(OnHistoryCollectListener listener);
void loadUnsplash(OnHistoryCollectListener listener);
void clearHistory();
}
最后就是model的实现类,实现类里分别创建三个数组保存不同表的数据:
model.imp1.HistoryCollectModelImp1
public class HistoryCollectModelImp1 implements HistoryCollectModel {
private List<HistoryCollect> mHistoryList;
private List<HistoryCollect> mCollectList;
private List<HistoryCollect> mUnsplashList;
@Override
public void loadHistory(OnHistoryCollectListener listener) {
mHistoryList = DBUtils.getInstence(MyApplication.getContext()).getData(MyDataBaseHelper.HISTORY);
if (mHistoryList.size() > 0) {
listener.onHistorySuccess(mHistoryList);
} else {
listener.onError("未找到数据");
}
}
@Override
public void loadCollect(OnHistoryCollectListener listener) {
mCollectList = DBUtils.getInstence(MyApplication.getContext()).getData(MyDataBaseHelper.COLLECT);
if (mCollectList.size() > 0) {
listener.onCollectSuccess(mCollectList);
} else {
listener.onError("未找到数据");
}
}
@Override
public void loadUnsplash(OnHistoryCollectListener listener) {
mUnsplashList = DBUtils.getInstence(MyApplication.getContext()).getData(MyDataBaseHelper.UNSPLASH);
if (mUnsplashList.size() > 0) {
listener.onUnsplashSuccess(mUnsplashList);
} else {
listener.onError("未找到数据");
}
}
@Override
public void clearHistory() {
DBUtils.getInstence(MyApplication.getContext()).clearHistory();
}
}
其中OnHistoryCollectListener这一回调接口在p层实现:
presenter.listener.OnHistoryCollectListener
public interface OnHistoryCollectListener {
void onHistorySuccess(List<HistoryCollect> historyList);
void onCollectSuccess(List<HistoryCollect> collectList);
void onUnsplashSuccess(List<HistoryCollect> unsplashList);
void onError(String error);
}
3. V层
为了通用,我们直接在接口中实现所有的方法:
view.HistoryCollectView
public interface HistoryCollectView {
void onStartGetData();
void onGetHistorySuccess(List<HistoryCollect> history);
void onGetCollectSuccess(List<HistoryCollect> collect);
void onGetUnsplashSuccess(List<HistoryCollect> unsplash);
void onGetDataFailed(String error);
}
足迹功能
历史记录界面可以只简单的使用RecyclerView,M层通过P层把数据传进activity后,更新adapter,实现历史记录的展示,同时在toolbar上做个清空历史的按钮,点击就清空History表。
HistoryActivity代码里并没有什么新的东西,之前好多activity都有相似的代码.这里也就不赘述了。
ui.activity.HistoryActivity
public class HistoryActivity extends MVPBaseActivity<HistoryCollectView, HistoryCollectPresenterImp1> implements HistoryCollectView {
@BindView(R.id.toolbar)
Toolbar mToolbar;
@BindView(R.id.recycle_history)
RecyclerView mRecycleHistory;
@BindView(R.id.prograss)
ProgressBar mPrograss;
List<HistoryCollect> mHistoryCollects = new ArrayList<>();
HistoryAdapter mHistoryAdapter;
@Override
protected HistoryCollectPresenterImp1 createPresenter() {
return new HistoryCollectPresenterImp1(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
initView();
}
private void initView() {
initToolbar();
mPresenter.getData(MyDataBaseHelper.HISTORY);
mRecycleHistory.setLayoutManager(new LinearLayoutManager(this));
mRecycleHistory.setHasFixedSize(true);
mRecycleHistory.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
mRecycleHistory.setItemAnimator(new DefaultItemAnimator());
mHistoryAdapter = new HistoryAdapter(this, mHistoryCollects);
mHistoryAdapter.setOnItemClickListener(mOnItemClickListener);
mRecycleHistory.setAdapter(mHistoryAdapter);
}
HistoryAdapter.OnItemClickListener mOnItemClickListener = new HistoryAdapter.OnItemClickListener() {
@Override
public void onItemClick(String id, String title) {
Intent intent = new Intent(getContext(), ZhihuDetailActivity.class);
intent.putExtra("ZHIHUID",id);
intent.putExtra("ZHIHUTITLE",title);
startActivity(intent);
}
};
private void initToolbar() {
mToolbar.setTitle("足迹");
setSupportActionBar(mToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
@Override
protected int getLayoutRes() {
return R.layout.activity_history;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_history, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_clear) {
showClearDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showClearDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("是否确定清空历史纪录")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPresenter.clearHistory();
toast("记录已清空");
mHistoryCollects.clear();
mHistoryAdapter.notifyDataSetChanged();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.show();
}
@Override
public void onStartGetData() {
if (mPrograss != null) {
mPrograss.setVisibility(View.VISIBLE);
}
}
@Override
public void onGetHistorySuccess(List<HistoryCollect> historyCollects) {
if (mPrograss != null) {
mPrograss.setVisibility(View.GONE);
}
mHistoryCollects = historyCollects;
if (mHistoryAdapter != null) {
mHistoryAdapter.notifyDataSetChanged();
}
}
@Override
public void onGetCollectSuccess(List<HistoryCollect> historyCollects) {
}
@Override
public void onGetUnsplashSuccess(List<HistoryCollect> historyCollects) {
}
@Override
public void onGetDataFailed(String error) {
if (mPrograss != null) {
mPrograss.setVisibility(View.GONE);
}
toast(error);
}
}
其中adapter的代码:
adapter.HistoryAdapter
public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder>{
private OnItemClickListener mItemClickListener;
private List<HistoryCollect> mHistoryCollects = new ArrayList<>();
private Context mContext;
public HistoryAdapter(Context context, List<HistoryCollect> historyCollects) {
mHistoryCollects = historyCollects;
mContext = context;
}
@Override
public HistoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
HistoryItem historyItem = new HistoryItem(mContext);
return new HistoryViewHolder(historyItem);
}
@Override
public void onBindViewHolder(final HistoryViewHolder holder, int position) {
final HistoryCollect historyCollect = mHistoryCollects.get(position);
holder.historyItem.bindView(historyCollect);
holder.historyItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onItemClick(historyCollect.getUrl(),historyCollect.getTitle());
}
}
});
}
public class HistoryViewHolder extends RecyclerView.ViewHolder {
public HistoryItem historyItem;
public HistoryViewHolder(HistoryItem itemView) {
super(itemView);
historyItem = itemView;
}
}
@Override
public int getItemCount() {
return mHistoryCollects.size();
}
public interface OnItemClickListener {
void onItemClick(String id, String title);
}
public void setOnItemClickListener(OnItemClickListener listener) {
mItemClickListener = listener;
}
}
收藏功能
收藏界面我们使用了折叠列表,列表内有知乎日报和图片精选两项,其中知乎日报的视图和历史界面一样,图片精选也只是简单的imageview+textview显示图片和保存日期。这里主要讲讲ExpandableListView的适配器写法。
CollectAdapter是为ExpandableListView写的适配器,继承自BaseExpandableListAdapter,里面需要重写的方法有:
- int getGroupCount()
**获得父项的数量 ** - int getChildrenCount(int groupPosition)
获得某个父项的子项数目 - Object getGroup(int groupPosition)
获得某个父项 - Object getChild(int groupPosition, int childPosition)
获得某个父项的某个子项 - long getGroupId(int groupPosition)
**获得某个父项的id ** - long getChildId(int groupPosition, int childPosition)
**获得某个父项的某个子项的id ** - boolean hasStableIds()
**表名同一ID是否总是引用同一对象,不用管 ** - View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)
获得父项显示的view - View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)
获得子项显示的view - boolean isChildSelectable(int groupPosition, int childPosition)
**子项是否可选中,点击事件要返回ture **
知道了每个方法的作用之后,就很好写了,我们要在CollectAdapter中定义两个数据列表,父项和子项的,为了解析view 我们还要个Context对象,所以构建函数就写出来了。
public CollectAdapter(ArrayList<String> gData,ArrayList<ArrayList<HistoryCollect>> iData, Context mContext) {
this.gData = gData;
this.iData = iData;
this.mContext = mContext;
}
后面的方法详情见代码:
adapter.CollectAdapter
public class CollectAdapter extends BaseExpandableListAdapter{
private ArrayList<String> gData;
private ArrayList<ArrayList<HistoryCollect>> iData;
private Context mContext;
public CollectAdapter(ArrayList<String> gData,ArrayList<ArrayList<HistoryCollect>> iData, Context mContext) {
this.gData = gData;
this.iData = iData;
this.mContext = mContext;
}
@Override
public int getGroupCount() {
return gData.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return iData.get(groupPosition).size();
}
@Override
public Object getGroup(int groupPosition) {
return gData.get(groupPosition);
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return iData.get(groupPosition).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
ViewHolderGroup groupHolder;
if(convertView == null){
convertView = LayoutInflater.from(mContext).inflate(
R.layout.item_exlist_group, parent, false);
groupHolder = new ViewHolderGroup();
groupHolder.tv_group_name = (TextView) convertView.findViewById(R.id.tv_group_name);
convertView.setTag(groupHolder);
}else{
groupHolder = (ViewHolderGroup) convertView.getTag();
}
groupHolder.tv_group_name.setText(gData.get(groupPosition).toString());
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (groupPosition == 0) {
ViewHolderItem itemHolder;
convertView = LayoutInflater.from(mContext).inflate(
R.layout.item_exlist_item, parent, false);
itemHolder = new ViewHolderItem();
itemHolder.tv_title = (TextView) convertView.findViewById(R.id.collect_title);
itemHolder.tv_time = (TextView) convertView.findViewById(R.id.collect_time);
convertView.setTag(itemHolder);
itemHolder.tv_title.setText(iData.get(groupPosition).get(childPosition).getTitle());
itemHolder.tv_time.setText(iData.get(groupPosition).get(childPosition).getTime());
} else {
ViewHolderUnsplash itemHolder;
convertView = LayoutInflater.from(mContext).inflate(
R.layout.item_exlist_imgitem, parent, false);
itemHolder = new ViewHolderUnsplash();
itemHolder.iv_image = (ImageView) convertView.findViewById(R.id.iv_unsplash);
itemHolder.tv_time = (TextView) convertView.findViewById(R.id.tv_collect_time);
convertView.setTag(itemHolder);
//这里应该是从缓存里读取 而不是在线显示
Glide.with(mContext)
.load(iData.get(groupPosition).get(childPosition).getUrl())
.into(itemHolder.iv_image);
itemHolder.tv_time.setText(iData.get(groupPosition).get(childPosition).getTime());
}
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
private static class ViewHolderGroup{
private TextView tv_group_name;
}
private static class ViewHolderItem{
private TextView tv_title;
private TextView tv_time;
}
private static class ViewHolderUnsplash{
private ImageView iv_image;
private TextView tv_time;
}
}
adapter完成后,开始写activity:
CollectActivity中我们对子项的点击事件设置为打开相应的activity,
当p层从m层获取到数据并返回给v层时,我们把数据更新到mCollectAdapter,然后刷新视图,呈现给用户。
ui.activity.CollectActivity
public class CollectActivity extends MVPBaseActivity<HistoryCollectView, HistoryCollectPresenterImp1> implements HistoryCollectView {
@BindView(R.id.toolbar)
Toolbar mToolbar;
@BindView(R.id.prograss)
ProgressBar mPrograss;
@BindView(R.id.content_listview)
ExpandableListView mContentListview;
private ArrayList<String> gData = null;
private ArrayList<ArrayList<HistoryCollect>> iData = null;
private ArrayList<HistoryCollect> lData = null;
CollectAdapter mCollectAdapter;
@Override
protected HistoryCollectPresenterImp1 createPresenter() {
return new HistoryCollectPresenterImp1(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
initData();
initView();
}
private void initData() {
gData = new ArrayList<String>();
iData = new ArrayList<ArrayList<HistoryCollect>>();
gData.add("知乎日报");
gData.add("图片精选");
//知乎日报
lData = new ArrayList<HistoryCollect>();
iData.add(lData);
//图片精选
lData = new ArrayList<HistoryCollect>();
iData.add(lData);
}
private void initView() {
initToolbar();
mCollectAdapter = new CollectAdapter(gData,iData,this);
mContentListview.setAdapter(mCollectAdapter);
mContentListview.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
if (groupPosition ==0) {
Intent intent = new Intent(CollectActivity.this, ZhihuDetailActivity.class);
intent.putExtra("ZHIHUID",iData.get(groupPosition).get(childPosition).getUrl());
intent.putExtra("ZHIHUTITLE",iData.get(groupPosition).get(childPosition).getTitle());
startActivity(intent);
} else {
Intent intent = new Intent(CollectActivity.this, UnsplashPhotoActivity.class);
intent.putExtra("PHOTOID", iData.get(groupPosition).get(childPosition).getTitle());
CircularAnimUtil.startActivity(CollectActivity.this, intent, v,
R.color.colorPrimary);
}
return true;
}
});
mPresenter.getData(MyDataBaseHelper.COLLECT);
mPresenter.getData(MyDataBaseHelper.UNSPLASH);
}
private void initToolbar() {
mToolbar.setTitle("收藏");
setSupportActionBar(mToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
@Override
protected int getLayoutRes() {
return R.layout.activity_collect;
}
@Override
public void onStartGetData() {
if (mPrograss != null) {
mPrograss.setVisibility(View.VISIBLE);
}
}
@Override
public void onGetHistorySuccess(List<HistoryCollect> historyCollects) {
}
@Override
public void onGetCollectSuccess(List<HistoryCollect> historyCollects) {
if (mPrograss != null) {
mPrograss.setVisibility(View.GONE);
}
iData.get(0).clear();
iData.get(0).addAll(historyCollects);
mCollectAdapter.notifyDataSetChanged();
}
@Override
public void onGetUnsplashSuccess(List<HistoryCollect> historyCollects) {
Log.d("dfegeav",historyCollects.get(0).getUrl());
if (mPrograss != null) {
mPrograss.setVisibility(View.GONE);
}
iData.get(1).clear();
iData.get(1).addAll(historyCollects);
mCollectAdapter.notifyDataSetChanged();
}
@Override
public void onGetDataFailed(String error) {
if (mPrograss != null) {
mPrograss.setVisibility(View.GONE);
}
}
}
4. P层
p层全是老套路,没啥写的,直接上代码:
presenter.HistoryCollectPresenter
public interface HistoryCollectPresenter {
void getData(String table);
void clearHistory();
}
presenter.imp1.HistoryCollectPresenterImp1
public class HistoryCollectPresenterImp1 extends BasePresenter<HistoryCollectView> implements HistoryCollectPresenter,OnHistoryCollectListener {
private HistoryCollectView mHistoryCollectView;
private HistoryCollectModelImp1 mHistoryCollectModelImp1;
public HistoryCollectPresenterImp1(HistoryCollectView historyCollectView) {
mHistoryCollectView = historyCollectView;
mHistoryCollectModelImp1 = new HistoryCollectModelImp1();
}
@Override
public void getData(String table) {
mHistoryCollectView.onStartGetData();
switch (table) {
case MyDataBaseHelper.HISTORY:
mHistoryCollectModelImp1.loadHistory(this);
break;
case MyDataBaseHelper.COLLECT:
mHistoryCollectModelImp1.loadCollect(this);
break;
case MyDataBaseHelper.UNSPLASH:
mHistoryCollectModelImp1.loadUnsplash(this);
}
}
@Override
public void clearHistory() {
mHistoryCollectModelImp1.clearHistory();
}
@Override
public void onHistorySuccess(List<HistoryCollect> historyList) {
mHistoryCollectView.onGetHistorySuccess(historyList);
}
@Override
public void onCollectSuccess(List<HistoryCollect> collectList) {
mHistoryCollectView.onGetCollectSuccess(collectList);
}
@Override
public void onUnsplashSuccess(List<HistoryCollect> unsplashList) {
mHistoryCollectView.onGetUnsplashSuccess(unsplashList);
}
@Override
public void onError(String error) {
mHistoryCollectView.onGetDataFailed(error);
}
}