Android练手小项目(KTReader)基于mvp架构(二)
上路传送眼:
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 getZhihuLatest(); @GET("before/{date}") Observable 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 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 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() { @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() { @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"?>
创建的ZhihuAdapter自己写了点击监听器接口,会在fragment里添加监听事件。
adapter.ZhihuAdapter:
public class ZhihuAdapter extends RecyclerView.Adapter { private Context mContext; List mZhihuLatestList; private OnItemClickListener mItemClickListener; public ZhihuAdapter(Context context, List 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 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 implements ZhihuPresenter,OnZhihuLatestListener{ /*Presenter作为中间层,持有View和Model的引用*/ private ZhihuView mZhihuView; private ZhihuLatestModelImp1 zhihuLatestModelImp1; public ZhihuPresenterImp1(ZhihuView zhihuView) { mZhihuView = zhihuView; zhihuLatestModelImp1 = new ZhihuLatestModelImp1(); } public List 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:
app_bar_main.xml:
<?xml version="1.0" encoding="utf-8"?>
更多相关文章
- android Content Provider的使用
- SQlite字段类型升级
- Android(安卓)TTS学习——TTS初体验
- Android深入讲解WebView——下
- android parcelable 详细介绍
- OpenCV4Android开发之旅(一)----OpenCV2.4简介及 app通过Java接
- AIDL——Android接口描述语言
- 在android中使用OrmLite数据库框架
- 关于Android使用新浪API的一些说明