TouTiao开源项目 分析笔记6
1.NewsChannelBean简单类笔记
1.1.Comparable接口的实现和使用
因为NewsChannelBean实现了Comparable<NewsChannelBean>
public class NewsChannelBean implements Comparable<NewsChannelBean>
一开始不明白为什么要实现这个接口。
==>其实就是强行对实现它的每个类的对象进行整体排序。
之后会报错,因为没有重写比较函数。
再重写一个这个函数进行大小比较
@Override public int compareTo(@NonNull NewsChannelBean o) { return this.position - o.getPosition(); }
这个bean类有一个position。所以间接利用position进行了大小的排序。写得非常妙。
1.2.重写一个equals函数判断两个对象是否相等
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NewsChannelBean bean = (NewsChannelBean) o; if (isEnable != bean.isEnable) return false; if (position != bean.position) return false; if (channelId != null ?
!channelId.equals(bean.channelId) : bean.channelId != null) return false; return channelName != null ?
channelName.equals(bean.channelName) : bean.channelName == null; }
整个比较过程就是按照顺序一个一个比较,直到全部都相同为止。
1.3.重写equals的时候,必须重写hashCode函数。
下面这段话摘自Effective Java一书:
在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终
如一地返回同一个整数。
如果两个对象根据equals方法比较是相等的,那么调用两个对象的hashCode方法必须返回相同的整数结果。
如果两个对象根据equals方法比较是不等的,则hashCode方法不一定得返回不同的整数。
对于第二条和第三条很好理解,但是第一条,很多时候就会忽略。在《Java编程思想》一书中的P495页也有同第一条类似的
一段话:
“设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。
如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,
那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,
因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码”。
这里重写返回哈希的函数为:
@Override public int hashCode() { int result = channelId != null ? channelId.hashCode() : 0; result = 31 * result + (channelName != null ? channelName.hashCode() : 0); result = 31 * result + isEnable; result = 31 * result + position; return result; }
1.4.所以整个bean类就搞定了。
package com.meiji.toutiao.bean.news; import android.support.annotation.NonNull; /** * Created by Meiji on 2017/3/10. */ public class NewsChannelBean implements Comparable<NewsChannelBean> { private String channelId; private String channelName; private int isEnable; private int position; public String getChannelId() { return channelId; } public void setChannelId(String channelId) { this.channelId = channelId; } public String getChannelName() { return channelName; } public void setChannelName(String channelName) { this.channelName = channelName; } public int getIsEnable() { return isEnable; } public void setIsEnable(int isEnable) { this.isEnable = isEnable; } public int getPosition() { return position; } public void setPosition(int position) { this.position = position; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NewsChannelBean bean = (NewsChannelBean) o; if (isEnable != bean.isEnable) return false; if (position != bean.position) return false; if (channelId != null ? !channelId.equals(bean.channelId) : bean.channelId != null) return false; return channelName != null ? channelName.equals(bean.channelName) : bean.channelName == null; } @Override public int hashCode() { int result = channelId != null ? channelId.hashCode() : 0; result = 31 * result + (channelName != null ? channelName.hashCode() : 0); result = 31 * result + isEnable; result = 31 * result + position; return result; } @Override public int compareTo(@NonNull NewsChannelBean o) { return this.position - o.getPosition(); } }
2.构造NewsChannelDao处理底层数据增删查改
2.1.获取可以写数据库的实例。
private SQLiteDatabase db; public NewsChannelDao() { this.db = DatabaseHelper.getDatabase(); }
这个DatabaseHelper是一个自定义的数据库帮助类,用来产生可以写数据库的实例。而且要求加上同步锁。
//这里是DatabaseHelper类
public static synchronized SQLiteDatabase getDatabase() { if (db == null) { db = getInstance().getWritableDatabase(); } return db; }
2.2.添加初始数据。
public void addInitData() { String categoryId[] = InitApp.AppContext.getResources().getStringArray
(R.array.mobile_news_id); String categoryName[] = InitApp.AppContext.getResources().getStringArray
(R.array.mobile_news_name); for (int i = 0; i < 8; i++) { add(categoryId[i], categoryName[i], Constant.NEWS_CHANNEL_ENABLE, i); } for (int i = 8; i < categoryId.length; i++) { add(categoryId[i], categoryName[i], Constant.NEWS_CHANNEL_DISABLE, i); } }
这个R.array.mobile_news_id是自定义的新闻类别id
这个R.array.mobile_news_name是自定义的新闻名称
2.3.添加add方法。
public boolean add(String channelId, String channelName, int isEnable, int position) { ContentValues values = new ContentValues(); values.put(NewsChannelTable.ID, channelId); values.put(NewsChannelTable.NAME, channelName); values.put(NewsChannelTable.IS_ENABLE, isEnable); values.put(NewsChannelTable.POSITION, position); long result = db.insert(NewsChannelTable.TABLENAME, null, values); return result != -1; }
这里用ContentValues整理插入数据库的数据。
2.4.查询方法。
public List<NewsChannelBean> query(int isEnable) { Cursor cursor = db.query(NewsChannelTable.TABLENAME, null,
NewsChannelTable.IS_ENABLE + "=?", new String[]{isEnable + ""}, null, null, null); List<NewsChannelBean> list = new ArrayList<>(); while (cursor.moveToNext()) { NewsChannelBean bean = new NewsChannelBean(); bean.setChannelId(cursor.getString(NewsChannelTable.ID_ID)); bean.setChannelName(cursor.getString(NewsChannelTable.ID_NAME)); bean.setIsEnable(cursor.getInt(NewsChannelTable.ID_ISENABLE)); bean.setPosition(cursor.getInt(NewsChannelTable.ID_POSITION)); list.add(bean); } cursor.close(); return list; }
2.5.查询所有
public List<NewsChannelBean> queryAll() { Cursor cursor = db.query(NewsChannelTable.TABLENAME, null, null, null, null, null, null); List<NewsChannelBean> list = new ArrayList<>(); while (cursor.moveToNext()) { NewsChannelBean bean = new NewsChannelBean(); bean.setChannelId(cursor.getString(NewsChannelTable.ID_ID)); bean.setChannelName(cursor.getString(NewsChannelTable.ID_NAME)); bean.setIsEnable(cursor.getInt(NewsChannelTable.ID_ISENABLE)); bean.setPosition(cursor.getInt(NewsChannelTable.ID_POSITION)); list.add(bean); } cursor.close(); return list; }
2.6.移除所有
public boolean removeAll() { int result = db.delete(NewsChannelTable.TABLENAME, null, null); return result != -1; }
2.7.NewsChannelDao源代码
package com.jasonjan.headnews.database.dao; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.jasonjan.headnews.R; import com.jasonjan.headnews.bean.news.NewsChannelBean; import com.jasonjan.headnews.database.helper.DatabaseHelper; import com.jasonjan.headnews.database.table.NewsChannelTable; import com.jasonjan.headnews.global.Constant; import com.jasonjan.headnews.global.InitApp; import java.util.ArrayList; import java.util.List; /** * Created by JasonJan on 2017/12/4. */ public class NewsChannelDao { private SQLiteDatabase db; public NewsChannelDao(){ this.db= DatabaseHelper.getDatabase(); } public void addInitData(){ String categoryId[]= InitApp.AppContext.getResources().getStringArray(R.array.mobile_news_id); String categoryName[]=InitApp.AppContext.getResources().getStringArray(R.array.mobile_news_name); for(int i=0;i<8;i++){ add(categoryId[i], categoryName[i], Constant.NEWS_CHANNEL_ENABLE, i); } for (int i = 8; i < categoryId.length; i++) { add(categoryId[i], categoryName[i], Constant.NEWS_CHANNEL_DISABLE, i); } } public boolean add(String channelId,String channelName,int isEnable,int position){ ContentValues values=new ContentValues(); values.put(NewsChannelTable.ID,channelId); values.put(NewsChannelTable.NAME,channelName); values.put(NewsChannelTable.IS_ENABLE,isEnable); values.put(NewsChannelTable.POSITION,position); long result=db.insert(NewsChannelTable.TABLENAME,null,values); return result!=-1; } public List<NewsChannelBean> query(int isEnable) { Cursor cursor = db.query(NewsChannelTable.TABLENAME, null, NewsChannelTable.IS_ENABLE + "=?", new String[]{isEnable + ""}, null, null, null); List<NewsChannelBean> list = new ArrayList<>(); while (cursor.moveToNext()) { NewsChannelBean bean = new NewsChannelBean(); bean.setChannelId(cursor.getString(NewsChannelTable.ID_ID)); bean.setChannelName(cursor.getString(NewsChannelTable.ID_NAME)); bean.setIsEnable(cursor.getInt(NewsChannelTable.ID_ISENABLE)); bean.setPosition(cursor.getInt(NewsChannelTable.ID_POSITION)); list.add(bean); } cursor.close(); return list; } public List<NewsChannelBean> queryAll() { Cursor cursor = db.query(NewsChannelTable.TABLENAME, null, null, null, null, null, null); List<NewsChannelBean> list = new ArrayList<>(); while (cursor.moveToNext()) { NewsChannelBean bean = new NewsChannelBean(); bean.setChannelId(cursor.getString(NewsChannelTable.ID_ID)); bean.setChannelName(cursor.getString(NewsChannelTable.ID_NAME)); bean.setIsEnable(cursor.getInt(NewsChannelTable.ID_ISENABLE)); bean.setPosition(cursor.getInt(NewsChannelTable.ID_POSITION)); list.add(bean); } cursor.close(); return list; } public void updateAll(List<NewsChannelBean> list) { } public boolean removeAll() { int result = db.delete(NewsChannelTable.TABLENAME, null, null); return result != -1; } }
3.BaseListFragment抽象类
3.1.最底层的接口IBaseView<T>
这里是这样定义这个接口的。
public interface IBaseView<T> { /** * 显示加载动画 */ void onShowLoading(); /** * 隐藏加载 */ void onHideLoading(); /** * 显示网络错误 */ void onShowNetError(); /** * 设置 presenter */ void setPresenter(T presenter); /** * 绑定生命周期 */ <T> LifecycleTransformer<T> bindToLife(); }
定义了5个方法,在实现类中必须要实现的方法(除了抽象类不必全部实现)
- onShowLoading==>显示加载动画
- onHideLoading==>隐藏加载动画
- onShowNetError==>显示网络错误
- setPresenter==>设置presenter
- bindToLife==>绑定生命周期
<T> LifecycleTransformer<T> bindToLife()这个方法:
前面的<T> 只是说明这是一个泛型方法。
返回值是LifecycleTransformer<T> 是RxJava中定义的一个类型。
3.2.同理看一下IBaseListView<T>
public interface IBaseListView<T> extends IBaseView<T> { /** * 显示加载动画 */ void onShowLoading(); /** * 隐藏加载 */ void onHideLoading(); /** * 显示网络错误 */ void onShowNetError(); /** * 设置 presenter */ void setPresenter(T presenter); /** * 绑定生命周期 */ <T> LifecycleTransformer<T> bindToLife(); /** * 设置适配器 */ void onSetAdapter(List<?> list); /** * 加载完毕 */ void onShowNoMore(); }
明白了:接口是可以继承的。
这里多了两个方法:onSetAdapter(List<?> list)==>顾名思义,这里设置的一定是一个List类型的适配器。
onShowNoMore()==>这里多了一个加载完毕的方法。
3.3.IBasePresenter接口类
这个接口相对于比较简单。
public interface IBasePresenter { /** * 刷新数据 */ void doRefresh(); /** * 显示网络错误 */ void doShowNetError(); }
- doRefresh==>用来刷新数据。
- doShowNetError==>显示网络错误。
3.4.抽象类BaseFragment<T extends IBasePresenter>
T就是就是继承了IBasePresenter接口的一个泛型。
但是这个BaseFragment中并没有实现接口中的方法,因为它本身也是一个抽象类。方法交给子类去实现吧。
所以这个抽象类中定义了一些方法。
这里面又重写3个抽象方法。
/** * 绑定布局文件 * * @return 布局文件ID */ protected abstract int attachLayoutId(); /** * 初始化视图控件 */ protected abstract void initView(View view); /** * 初始化数据 */ protected abstract void initData() throws NullPointerException;
attachLayoutId==>可以获取布局文件ID
initView==>初始化视图控件
initData==>初始化数据
然后定义了一个沟通者presenter,采用泛型定义。
protected T presenter;
当然只实现了一个IBaseView接口方法
/** * 绑定生命周期 */ @Override public <T> LifecycleTransformer<T> bindToLife() { return bindUntilEvent(FragmentEvent.DESTROY); }
所以这就是为什么不把IBaseView中的接口方法全部加到BaseFragment中,而是分了一个抽象类+一个接口。
在BaseFragment抽象类中要进行初始化Toolbar
/** * 初始化 Toolbar */ protected void initToolBar(Toolbar toolbar, boolean homeAsUpEnabled, String title) { ((BaseActivity) getActivity()).initToolBar(toolbar, homeAsUpEnabled, title); }
传入的参数有:标题栏布局,是否有左箭头,title。
首先执行onCreate==>设置presenter
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setPresenter(presenter); }
然后执行onCreateView==>initView(view)==>initData()
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { View view = inflater.inflate(attachLayoutId(), container, false); initView(view); initData(); return view; }
得到fragment视图布局id,初始化视图为抽象函数,在子类中实现。初始化数据为抽象函数,在子类中实现。
3.5.懒加载碎片LazyLoadFragment<T extends IBasePresenter>
继承关系:
public abstract class LazyLoadFragment<T extends IBasePresenter> extends BaseFragment<T>
继承了上面刚分析的抽象类,这个也是一个抽象类,不必实现所有的抽象函数。
先分析三个boolean
protected boolean isViewInitiated; protected boolean isVisibleToUser; protected boolean isDataInitiated;
- isViewInitiated==>视图是否初始化。
- isVisibleToUser==>是否对用户可见。
- isDataInitiated==>数据是否初始化完成。
然后执行onCreate
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); }
然后执行onActivityCreated
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isViewInitiated = true; prepareFetchData(); }
确定视图已经初始化了。修改isViewInitiated为true。
然后执行准备取数据。
public boolean prepareFetchData() { return prepareFetchData(false); }
然后定义一个带有一个参数(是否强制刷新的变量)的取数据的函数。
public boolean prepareFetchData(boolean forceUpdate) { if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) { fetchData(); isDataInitiated = true; return true; } return false; }
如果对用户可见、视图初始化完毕、(数据未初始化完毕或强制刷新)==>执行取数据的抽象函数。
执行定义的抽象函数==>fetchData()
public abstract void fetchData();
最后将数据是否初始化完毕的变量赋值为true。
3.6.抽象函数BaseListFragment<T extends IBasePresenter>
对于类名称加一个T,或者加一个<T extends ?>的理解
看一下继承方式。
public abstract class BaseListFragment<T extends IBasePresenter>
extends LazyLoadFragment<T>
implements IBaseListView<T>, SwipeRefreshLayout.OnRefreshListener
继承了懒加载碎片==>主要抽象函数fetchData()取数据
实现了IBaseListView<T>==>主要有7个抽象方法。
实现了SwipeRefreshLayout.OnRefreshListener==>主要方法onRefresh()刷新数据
#调用了开源框架==>Multitype框架==>处理复杂的视图列表
github地址:https://github.com/drakeet/MultiType
预览一下成员变量。
public static final String TAG = "BaseListFragment"; protected RecyclerView recyclerView; protected SwipeRefreshLayout swipeRefreshLayout; protected MultiTypeAdapter adapter; protected Items oldItems = new Items(); protected boolean canLoadMore = false; protected Observable<Integer> observable;
这里的MultiTypeAdapter+Items 都是第三方库框架,方便处理复杂的视图。
这里的Observable<Integer>是RxJava框架。
实现了BaseFragment中的抽象函数==>attachLayoutId()绑定布局文件。
@Override protected int attachLayoutId() { return R.layout.fragment_list; }
这个布局文件是自己早就写好了的。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/windowBackground" android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:fadeScrollbars="true" android:scrollbarFadeDuration="1" android:scrollbars="vertical" app:layoutManager="LinearLayoutManager"> </android.support.v7.widget.RecyclerView> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout>
预览效果如下:
初始化视图initView(View view)
@Override protected void initView(View view) { recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setHasFixedSize(true); swipeRefreshLayout = view.findViewById(R.id.refresh_layout); swipeRefreshLayout.setColorSchemeColors(SettingUtil.getInstance().getColor()); swipeRefreshLayout.setOnRefreshListener(this); }
recyclerView是列表。
swipeRefreshLayout是包裹列表的一个可以刷新的布局。
实现IBaseView<T>接口中的显示加载动画+隐藏加载动画函数。
@Override public void onShowLoading() { swipeRefreshLayout.post(new Runnable() { @Override public void run() { swipeRefreshLayout.setRefreshing(true); } }); } @Override public void onHideLoading() { swipeRefreshLayout.post(new Runnable() { @Override public void run() { swipeRefreshLayout.setRefreshing(false); } }); }
实现懒加载碎片中的抽象函数==>fetchData()来取数据。
@Override public void fetchData() { observable = RxBus.getInstance().register(BaseListFragment.TAG); observable.subscribe(new Consumer<Integer>() { @Override public void accept(@NonNull Integer integer) throws Exception { adapter.notifyDataSetChanged(); } }); }
这里进行订阅。拿到数据,然后刷新。
实现IBaseView<T>中的接口函数onShowNetError。
@Override public void onShowNetError() { Toast.makeText(getActivity(), R.string.network_error, Toast.LENGTH_SHORT).show(); getActivity().runOnUiThread(new Runnable() { @Override public void run() { adapter.setItems(new Items()); adapter.notifyDataSetChanged(); canLoadMore = false; } }); }
这里提示用户,网络不给力。
生命周期函数onResume。
@Override public void onResume() { super.onResume(); // 设置下拉刷新的按钮的颜色 swipeRefreshLayout.setColorSchemeColors(SettingUtil.getInstance().getColor()); }
实现IBaseListView额外的两个函数中的一个onShowNoMore加载完毕接口函数。
@Override public void onShowNoMore() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (oldItems.size() > 0) { Items newItems = new Items(oldItems); newItems.remove(newItems.size() - 1); newItems.add(new LoadingEndBean()); adapter.setItems(newItems); adapter.notifyDataSetChanged(); } else if (oldItems.size() == 0) { oldItems.add(new LoadingEndBean()); adapter.setItems(oldItems); adapter.notifyDataSetChanged(); } canLoadMore = false; } }); }
实现刷新框的onRefresh
@Override public void onRefresh() { int firstVisibleItemPosition = ((LinearLayoutManager)
recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); if (firstVisibleItemPosition == 0) { presenter.doRefresh(); return; } recyclerView.scrollToPosition(5); recyclerView.smoothScrollToPosition(0); }
先判断第一次可见的位置在最前面,也就是一开始没有数据的时候。
如果是第一次点进去的话,必然要执行刷新。
然后recyclerView回到顶部。
最后执行一个onDestroy来反注册。
@Override public void onDestroy() { RxBus.getInstance().unregister(BaseListFragment.TAG, observable); super.onDestroy(); }
RxFragment有两个类型:
import com.trello.rxlifecycle2.components.RxFragment;
import com.trello.rxlifecycle2.components.support.RxFragment;
RxFragment==>类型是app.Fragment
support.RxFragment==>类型是v4.app.Fragment 是兼容的。