上路传送眼:
Android练手小项目(KTReader)基于mvp架构(一)
下路传送眼:
Android练手小项目(KTReader)基于mvp架构(三)
GIthub地址: https://github.com/yiuhet/KTReader
上篇文章中我们完成了基类和启动界面。
而这次我们要做的的就是能显示知乎日报内容的fragment。
这次我们使用到了开源框架Rxjava2+Okhttp3+retrofit2实现网络请求,Glide加载图片。
先附上效果图:
准备工作
- 首先,添加依赖如下
compile 'com.github.bumptech.glide:glide:3.8.0'
compile 'com.squareup.okhttp3:okhttp:3.8.0'//貌似不用添加,retrofit2封装了okhttp
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
compile 'com.google.code.gson:gson:2.8.0' //貌似不用添加,converter-gson中已经封装了gson库
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
-
创建一个自定义的MyApplication,用来实现从任意位置获取程序的context
app.MyApplication.class:
public class MyApplication extends Application {
private static Context sContext ;
private static String sCacheDir;
public static Context getContext() {
return sContext;
}
public static String getAppCacheDir() {
return sCacheDir;
}
@Override
public void onCreate() {
super.onCreate();
sContext = getApplicationContext();
if (getExternalCacheDir() != null && ExistSDCard()){
sCacheDir = getExternalCacheDir().toString();
} else {
sCacheDir = getCacheDir().toString();
}
}
private boolean ExistSDCard() {
return android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED);
}
}
创建好后别忘了配置AndroidManifest.xml:
在application内添加
android:name=".app.MyApplication"
- 创建工具类和常量类
utils.NetWorkUtil.class: (判断是否联网的工具类)
public class NetWorkUtil {
private NetWorkUtil(){
};
public static boolean isNetWorkAvailable(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
public static boolean isWifiConnected(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
}
}
utils.CommonUtils.class:(目前只有弹toast的功能)
public class CommonUtils {
private static Toast mToast;
public static void ShowTips(Context context, String tips) {
if (mToast == null) {
mToast = Toast.makeText(context,tips,Toast.LENGTH_SHORT);
} else {
mToast.setText(tips);
}
mToast.show();
}
}
app.Constant.class :(常量类,目前只有知乎的基本url)
public class Constant {
public static final String ZHIHU_BASE_URL = "http://news-at.zhihu.com/api/4/news/";
}
下面用到了retrofit2 + okhttp3 + rxjava3 的知识 附上参考资料
你真的会用Retrofit2吗?Retrofit2完全教程
Android网络编程(六)OkHttp3用法全解析
深入解析OkHttp3
Retrofit2+okhttp3拦截器处理在线和离线缓存
手把手教你使用 RxJava 2.0(一)
- 创建个RetrofitManager,处理网络请求
utils.RetrofitManager.class:
public class RetrofitManager {
private static RetrofitManager retrofitManager;
private RetrofitManager() {
}
// 无论有无网络都读取缓存。(有时间限制) 把拦截器设置到addNetworkOnterceptor
private static Interceptor netInterceptor1 = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
int maxAge = 60; //60s为缓存的有效时间,60s内获取的是缓存数据,超过60S我们就去网络重新请求数据
return response
.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public,max-age=" + maxAge)
.build();
}
};
//有网络读取网络的数据,没有网络读取缓存。
private static class netInterceptor2 implements Interceptor{
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//没有网络时强制使用缓存数据
if (!NetWorkUtil.isNetWorkAvailable(MyApplication.getContext())) {
request = request.newBuilder()
//强制使用缓存数据
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response originalResponse = chain.proceed(request);
if (true) {
return originalResponse .newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public,max-age=" + 0) //0为不进行缓存
.build();
} else {
int maxAge = 4 * 24 * 60 * 60; //缓存保存时间
return originalResponse .newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, only-if-cached, max-age=" + maxAge)
.build();
}
}
};
//缓存位置
private static File cacheFile = new File(MyApplication.getAppCacheDir(), "caheData_zhihu");
//设置缓存大小
private static int DEFAULT_DIR_CACHE = 10 * 1024 * 1024;
private static Cache cache = new Cache(cacheFile, DEFAULT_DIR_CACHE);
private static OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new netInterceptor2())
.addNetworkInterceptor(new netInterceptor2())
.cache(cache)
.build();
public static RetrofitManager getInstence() {
if (retrofitManager == null) {
synchronized (RetrofitManager.class) {
if (retrofitManager == null) {
retrofitManager = new RetrofitManager();
}
}
}
return retrofitManager;
}
private Retrofit retrofit;
public Retrofit getRetrofit(String url) {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(url) //必须以‘/’结尾
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava2作为CallAdapter
.client(client)//如果没有添加,那么retrofit2会自动给我们添加了一个。
.addConverterFactory(GsonConverterFactory.create())//Retrofit2可以帮我们自动解析返回数据,
.build();
}
return retrofit;
}
}
api.ZhihuApi:
public interface ZhihuApi {
@GET("latest")
Observable<ZhihuLatest> getZhihuLatest();
@GET("before/{date}")
Observable<ZhihuLatest> getBefore(@Path("date") String date);
}
Model层 :
模型实体类ZhihuLatest直接使用GsonFormat工具快速生成(model.entity.ZhihuLatest)
知乎日报Model接口
model.ZhihuLatestModel:
public interface ZhihuLatestModel {
void loadZhihuLatest(OnZhihuLatestListener listener);
void loadMore(OnZhihuLatestListener listener);
}
-
获取知乎日报数据的Model实现
model.impq.ZhihuLatestModelImp1.class:
public class ZhihuLatestModelImp1 implements ZhihuLatestModel {
/*获取知乎日报数据的Model实现*/
private ZhihuApi mZhihuApiService; //请求服务
private List<ZhihuLatest.StoriesEntity> mZhihuLatestList; //储存entity的list。
private String date; //网络请求的url参数,首次加载数据获取,调用getmore方法时,date-1.
public ZhihuLatestModelImp1 () {
mZhihuLatestList = new ArrayList<>();
mZhihuApiService = RetrofitManager
.getInstence()
.getRetrofit("http://news-at.zhihu.com/api/4/news/")
.create(ZhihuApi.class); //创建请求服务
}
public List<ZhihuLatest.StoriesEntity> getmZhihuLatestList(){
return mZhihuLatestList;
}
@Override
public void loadZhihuLatest(final OnZhihuLatestListener listener) {
mZhihuLatestList.clear();
//数据层的操作,网络请求数据
if (mZhihuApiService != null) {
mZhihuApiService.getZhihuLatest()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ZhihuLatest>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ZhihuLatest zhihuLatest) {
date = zhihuLatest.date;
for (int i =0;i < zhihuLatest.stories.size(); i++) {
mZhihuLatestList.add(zhihuLatest.stories.get(i));
}
listener.onLoadZhihuLatestSuccess(); //加载成功时 回调接口方法。
}
@Override
public void onError(@NonNull Throwable e) {
listener.onLoadDataError(e.toString());//加载失败时 回调接口方法。
}
@Override
public void onComplete() {
}
});
}
}
@Override
public void loadMore(final OnZhihuLatestListener listener) {
// date = String.valueOf(Integer.parseInt(date) - 1); 2333,之前犯傻直接减1就当求前一天了
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Calendar calendar = new GregorianCalendar();;//获取日历实例
try {
calendar.setTime(sdf.parse(date));
calendar.add(Calendar.HOUR_OF_DAY, -1); //设置为前一天
date = sdf.format(calendar.getTime());//获得前一天
} catch (ParseException e) {
e.printStackTrace();
}
//数据层的操作,网络请求数据
if (mZhihuApiService != null) {
mZhihuApiService.getBefore(date)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ZhihuLatest>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ZhihuLatest zhihuLatest) {
for (int i =0;i < zhihuLatest.stories.size(); i++) {
mZhihuLatestList.add(zhihuLatest.stories.get(i));
}
listener.onLoadMoreSuccess();//加载成功时 回调接口方法。
}
@Override
public void onError(@NonNull Throwable e) {
listener.onLoadDataError(e.toString());//加载失败时 回调接口方法。
}
@Override
public void onComplete() {
}
});
}
}
}
View层
首先我们先确定需要实现的功能
- 从知乎日报上拉取数据 ( 知乎日报 API 分析)
- 当屏幕拉到底部时加载更多数据
- 创建回调接口
view.ZhihuView:
public interface ZhihuView {
void onStartGetData();
void onGetZhihuLatestSuccess();
void onGetMoreSuccess();
void onGetDataFailed(String error);
}
- 在创建ZhiHuFragment之前,我们要先创建一个组件和adapter
widget.ZhihuItem:
public class ZhihuItem extends RelativeLayout {
private Context mContext;
@BindView(R.id.zhihu_iv)
ImageView mZhihuIv;
@BindView(R.id.zhihu_title)
TextView mZhihuTitle;
public ZhihuItem(Context context) {
this(context, null);
}
public ZhihuItem(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.view_zhihu_item, this);
ButterKnife.bind(this, this);
}
public void bindView(ZhihuLatest.StoriesEntity zhihuLatest) {
mZhihuTitle.setText(zhihuLatest.title);
String url = zhihuLatest.images.get(0).toString();
//Glide 获取图片
Glide.with(mContext)
.load(url)
.placeholder(R.drawable.loading) //占位图片
.error(R.drawable.error) //错误图片
.into(mZhihuIv);
}
}
组件的布局文件:
view_zhihu_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/zhihu_iv"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="5dp"
android:layout_alignParentStart="true"/>
<TextView
android:layout_centerVertical="true"
android:id="@+id/zhihu_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/zhihu_iv"
android:layout_marginLeft="20dp"
android:textColor="@android:color/black"
android:textSize="18dp" />
</RelativeLayout>
创建的ZhihuAdapter自己写了点击监听器接口,会在fragment里添加监听事件。
adapter.ZhihuAdapter:
public class ZhihuAdapter extends RecyclerView.Adapter<ZhihuAdapter.ZhihuViewHolder> {
private Context mContext;
List<ZhihuLatest.StoriesEntity> mZhihuLatestList;
private OnItemClickListener mItemClickListener;
public ZhihuAdapter(Context context, List<ZhihuLatest.StoriesEntity> zhihuLatestList) {
mContext =context;
mZhihuLatestList = zhihuLatestList;
}
@Override
public ZhihuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ZhihuItem zhihuItem = new ZhihuItem(mContext);
return new ZhihuViewHolder(zhihuItem);
}
@Override
public void onBindViewHolder(ZhihuViewHolder holder, int position) {
final ZhihuLatest.StoriesEntity zhihuLatest = mZhihuLatestList.get(position);
holder.zhihuItem.bindView(zhihuLatest);
holder.zhihuItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onItemClick(zhihuLatest.id);
}
}
});
}
@Override
public int getItemCount() {
return mZhihuLatestList.size();
}
public class ZhihuViewHolder extends RecyclerView.ViewHolder {
public ZhihuItem zhihuItem;
public ZhihuViewHolder(ZhihuItem itemView) {
super(itemView);
zhihuItem = itemView;
}
}
public interface OnItemClickListener {
void onItemClick(int id);
}
public void setOnItemClickListener(OnItemClickListener listener) {
mItemClickListener = listener;
}
}
-
创建ZhiHuFragment
ui.fragment.ZhiHuFragment:
public class ZhiHuFragment extends BaseFragment<ZhihuView, ZhihuPresenterImp1> implements ZhihuView {
@BindView(R.id.recycle_zhihu)
RecyclerView mRecycleZhihu;
Unbinder unbinder;
@BindView(R.id.prograss)
ProgressBar mPrograss;
private ZhihuAdapter mZhihuAdapter;
@Override
public void onStartGetZhihuLatest() {
mPrograss.setVisibility(View.VISIBLE);
}
@Override
public void onGetZhihuLatestSuccess() {
mPrograss.setVisibility(View.GONE);
mZhihuAdapter.notifyDataSetChanged();
}
@Override
public void onGetZhihuLatestFailed(String error) {
mPrograss.setVisibility(View.GONE);
toast(error);
}
@Override
public void onStartGetMore() {
mPrograss.setVisibility(View.VISIBLE);
}
@Override
public void onGetMoreSuccess() {
mPrograss.setVisibility(View.GONE);
mZhihuAdapter.notifyDataSetChanged();
}
@Override
public void onGetMoreFailed(String error) {
mPrograss.setVisibility(View.GONE);
toast(error);
}
@Override
protected int getLayoutRes() {
return R.layout.fragment_zhihu;
}
@Override
protected ZhihuPresenterImp1 createPresenter() {
return new ZhihuPresenterImp1(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// TODO: inflate a fragment view
View rootView = super.onCreateView(inflater, container, savedInstanceState);
unbinder = ButterKnife.bind(this, rootView);
init();
mPresenter.getLatest();
return rootView;
}
private void init() {
mRecycleZhihu.setLayoutManager(new LinearLayoutManager(getContext()));
mRecycleZhihu.setHasFixedSize(true);
mRecycleZhihu.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));
mRecycleZhihu.setItemAnimator(new DefaultItemAnimator());
mRecycleZhihu.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isSlideToBottom(recyclerView)) {
mPresenter.getMore();
}
}
});
mZhihuAdapter = new ZhihuAdapter(getContext(), mPresenter.getmZhihuLatestList());
mZhihuAdapter.setOnItemClickListener(mOnItemClickListener);
mRecycleZhihu.setAdapter(mZhihuAdapter);
}
public static boolean isSlideToBottom(RecyclerView recyclerView) {
if (recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset()
>= recyclerView.computeVerticalScrollRange())
return true;
return false;
}
private ZhihuAdapter.OnItemClickListener mOnItemClickListener = new ZhihuAdapter.OnItemClickListener() {
@Override
public void onItemClick(int id) {
toast(Constant.ZHIHU_BASE_URL + String.valueOf(id));
}
};
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}
Presenter层
在ZhihuPresenterImp1类里实现数据和视图的绑定
-
先写一个回调接口:
(在Presenter层实现,给Model层回调,更改View层的状态,确保Model层不直接操作View层)
presenter.OnZhihuLatestListener:
public interface OnZhihuLatestListener {
/**
* 成功时回调
*/
void onLoadZhihuLatestSuccess();
void onLoadMoreSuccess();
/**
* 失败时回调
*/
void onLoadDataError(String error);
}
-
再写一个presenter接口:
presenter.ZhihuPresenter :
public interface ZhihuPresenter {
void getLatest();
void getMore();
}
-
最后写Prestener实现类:
presenter.imp1.ZhihuPresenterImp1.class:
public class ZhihuPresenterImp1 extends BasePresenter<ZhihuView> implements ZhihuPresenter,OnZhihuLatestListener{
/*Presenter作为中间层,持有View和Model的引用*/
private ZhihuView mZhihuView;
private ZhihuLatestModelImp1 zhihuLatestModelImp1;
public ZhihuPresenterImp1(ZhihuView zhihuView) {
mZhihuView = zhihuView;
zhihuLatestModelImp1 = new ZhihuLatestModelImp1();
}
public List<ZhihuLatest.StoriesEntity> getmZhihuLatestList() {
return zhihuLatestModelImp1.getmZhihuLatestList();
}
@Override
public void getLatest() {
mZhihuView.onStartGetData();
zhihuLatestModelImp1.loadZhihuLatest(this);
}
@Override
public void getMore() {
mZhihuView.onStartGetData();
zhihuLatestModelImp1.loadMore(this);
}
@Override
public void onLoadZhihuLatestSuccess() {
mZhihuView.onGetZhihuLatestSuccess();
}
@Override
public void onLoadMoreSuccess() {
mZhihuView.onGetMoreSuccess();
}
@Override
public void onLoadDataError(String error) {
mZhihuView.onGetDataFailed(error);
}
}
最后,创建一个带有侧滑菜单的MainActivity
- 暂且只在其内部添加一个ZhiHuFragment。
- 侧滑菜单具体功能之后会实现。
- 双击返回键退出
ui.activity.MainActivity.class:
public class MainActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener {
@BindView(R.id.toolbar)
Toolbar mToolbar;
@BindView(R.id.fragment_main)
FrameLayout fragmentMain;
@BindView(R.id.nav_view)
NavigationView mNavView;
@BindView(R.id.drawer_layout)
DrawerLayout mDrawerLayout;
private long exitTime = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
initView();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_main, new ZhiHuFragment()).commit();
}
private void initView() {
setSupportActionBar(mToolbar);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, mDrawerLayout, mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
mDrawerLayout.addDrawerListener(toggle);
toggle.syncState();
mNavView.setNavigationItemSelectedListener(this);
}
@Override
protected int getLayoutRes() {
return R.layout.activity_main;
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
if ((System.currentTimeMillis() - exitTime) > 2000) {
CommonUtils.ShowTips(MainActivity.this, "再点一次,退出");
exitTime = System.currentTimeMillis();
} else {
super.onBackPressed();
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.nav_camera) {
// Handle the camera action
}
mDrawerLayout.closeDrawer(GravityCompat.START);
return true;
}
修改布局文件
activity_main.xml:
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment_main">
</FrameLayout>
</LinearLayout>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</android.support.v4.widget.DrawerLayout>
app_bar_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
tools:context="com.example.yiuhet.ktreader.ui.activity.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>