Android--从零开始开发一款文章阅读APP
代码地址如下:
http://www.demodashi.com/demo/11212.html
前言
本案例已经开源!如果你想免费下载,可以访问我的Github,所有案例均在上面,只求给个star。当然愿意支付小小金额请我喝茶也行(大学穷狗-.-)
一、准备工作
- 使用Android Studio开发
- 微信和QQ第三方sdk,需要自行申请(这个简单)
- 本案例使用干活集中营提供的api,使用MVp+Material Design作为主体架构进行开发
- 体验完整功能,点击下载APK
二、程序实现
目录结构
目录结构如下,我按照功能分包:
实现思路
整体架构–MVP+Material
- 首先你得了解MVP架构在android中的使用,如果你还不了解,可以阅读我的这篇文章
- 如果你不熟悉Material可以读官方文档
重点代码分析
如果讲述整个App,估计一篇文章说不清楚。那我干脆取其中一条线来分析。
下面主要分析文章列表–文章详情–文章分享
主页文章列表
这里只选择Android文章模块进行介绍:
GankContract
public interface GankContract {interface View extends BaseView<Presenter>{//错误void showError();//正在加载void showLoading();//停止加载void Stoploading();//显示数据列表void showResult(ArrayList<GankNews.Question> list);//网络错误void showNotNetError();}interface Presenter extends BasePresenter{// 请求数据void loadPosts(int PagerNum, boolean cleaing);//刷新数据void reflush();//加载更多void loadMore(int PagerNum);//显示详情void StartReading(int positon);//随便看看void LookAround();}
}
GankFragment
Fragment的内容主要是文章列表,我们只分享重点:
//下拉刷新实现
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {boolean isScrollState=false;@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);LinearLayoutManager manager= (LinearLayoutManager) recyclerView.getLayoutManager();//没有滚动时候if (newState==RecyclerView.SCROLL_STATE_IDLE){//获的最后一个可见的itemint lastVisibilityItem=manager.findLastCompletelyVisibleItemPosition();int totalItemCount=manager.getItemCount();//判断是否滚动到底部并且是向下滑动if (lastVisibilityItem==(totalItemCount-1)&&isScrollState){presenter.loadMore(1);}}}//通知Presenter加载数据和设置item点击事件
@Overridepublic void showResult(ArrayList<GankNews.Question> list) {if (adapter==null){Log.i(TAG, "showResult: "+list.size());adapter=new GankNewsAdapter(list,getContext());adapter.setItemOnClickListener(new OnRecyclerViewOnClickListener() {@Overridepublic void onItemClick(View v, int position) {presenter.StartReading(position);}@Overridepublic void onItemLongClick(View v, int position) {}});recyclerView.setAdapter(adapter);}else {adapter.notifyDataSetChanged();}}
GankPresenter
同样只分析重点代码:
//根据当前页数加载列表数据@Overridepublic void loadPosts(int PagerNum, final boolean cleaing) {CurrentPagerNum=PagerNum;if (cleaing) {view.showLoading();}if (Network.networkConnected(context)) {model.load(Api.Gank_Android + PagerNum, new OnStringListener() {@Overridepublic void onSuccess(String result) {try {
// Log.i(TAG, "gankpresenter.model.load.result"+result);GankNews news = gson.fromJson(result, GankNews.class);//contenvalues只能存储基本类型的数据,像string,int之类的,不能存储对象这种东西,而HashTable却可以存储对象。
// ContentValues values = new ContentValues();if (cleaing) {list.clear();}for (GankNews.Question item : news.getResults()) {/*** 1.数据库查重:首先检测数据库中是否已经储存过该条数据* 2:因为每次重启后都是在网络上重新下载数据 如果是数据库已经存在的数据则不会重新加载,也导致了这些数据当前id值为空* ,所有要绑定队友的id值.*/if (!queryIfIdExists(item.get_id())){DbLiteOrm.insert(item, ConflictAlgorithm.Replace);}else {ArrayList<GankNews.Question> ganklist=App.DbLiteOrm.query(new QueryBuilder<GankNews.Question>(GankNews.Question.class).where(GankNews.Question.COL_ID+"=?",new String[]{item.get_id()}));GankNews.Question gankitem=ganklist.get(0);item.setId(gankitem.getId());}list.add(item);}view.showResult(list);}catch (JsonSyntaxException e){view.showError();}view.Stoploading();}@Overridepublic void onError(VolleyError error) {view.Stoploading();view.showError();}});} else {//更新列表缓存 因为详情页都是用webView呈现 所以缓存content为空if (cleaing){QueryBuilder query=new QueryBuilder(GankNews.Question.class);query.appendOrderDescBy("id");query.limit(0,10*CurrentPagerNum);list.addAll(DbLiteOrm.<GankNews.Question>query(query));view.showResult(list);}else {view.showNotNetError();}}}//判断数据库是否已经存在public boolean queryIfIdExists(String _id){ArrayList<GankNews.Question> questionArrayList=App.DbLiteOrm.query(new QueryBuilder(GankNews.Question.class).where(GankNews.Question.COL_ID+"=?",new String[]{_id}));if (questionArrayList.size()==0){return false;}return true;}//传递当前点击item的信息,进入详情阅读
@Overridepublic void StartReading(int positon) {//每个item就是一组数据GankNews.Question item=list.get(positon);Intent intent = new Intent(context, DetailActivity.class);intent.putExtra("type", BeanTeype.TYPE_Gank);intent.putExtra("id",list.get(positon).getId());int id=list.get(positon).getId();Log.i(TAG, "StartReading: "+id);intent.putExtra("_id", list.get(positon).get_id());intent.putExtra("url",list.get(positon).getUrl());intent.putExtra("title", list.get(positon).getDesc());if (item.getImages()==null){intent.putExtra("imgUrl", "");}else {intent.putExtra("imgUrl", list.get(positon).getImages().get(0));}/*** Content的startActivity方法,需要开启一个新的task。如果使用 Activity的startActivity方法,* 不会有任何限制,因为Activity继承自Context,重载了startActivity方法。*/intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);context.startActivity(intent);}//随便看看 随机选取
@Overridepublic void LookAround() {if (list.isEmpty()){view.showError();return;}StartReading(new Random().nextInt(list.size()));}
GankNewsAdapter
因为文章分两种:有图和无图。所有要进行分类加载
//判断是否有图和是否是底部加载item@Overridepublic int getItemViewType(int position) {if (position==getItemCount()-1){return TYPE_FOOTER;}if (list.get(position).getImages()==null){return TYPE_NO_IMG;}return TYPE_NORMTAL;}//根据type加载不同ViewHolder
@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {switch (viewType){case TYPE_NORMTAL:return new NormalViewHolder(inflater.inflate(R.layout.home_list_item_layout,parent,false),listener);case TYPE_FOOTER:return new FooterViewHolder(inflater.inflate(R.layout.list_footer,parent,false));case TYPE_NO_IMG:return new NoImageViewHolder(inflater.inflate(R.layout.home_list_item_without_image,parent,false),listener);}return null;}//使用Glide加载图片。无图则不加载@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {if (!(holder instanceof FooterViewHolder)){GankNews.Question item=list.get(position);if (item!=null){if (holder instanceof NormalViewHolder){Glide.with(context).load(item.getImages().get(0)).asBitmap().placeholder(R.mipmap.loading).diskCacheStrategy(DiskCacheStrategy.SOURCE).error(R.mipmap.loading).centerCrop().into(((NormalViewHolder) holder).imageView);((NormalViewHolder) holder).textView.setText(item.getDesc());}else if (holder instanceof NoImageViewHolder){((NoImageViewHolder) holder).textViewNoImg.setText(item.getDesc());}}}}
详情页
DetailActivity
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.frame);if (savedInstanceState!=null){detailFragment= (DetailFragment) getSupportFragmentManager().getFragment(savedInstanceState,"detailFragment");}else {detailFragment=DetailFragment.newInstance();getSupportFragmentManager().beginTransaction().replace(R.id.container,detailFragment).commit();}//获取列表传过来的具体item数据Intent intent=getIntent();DetailPresenter presenter=new DetailPresenter(detailFragment,DetailActivity.this);presenter.setType((BeanTeype) intent.getSerializableExtra("type"));presenter.setId(intent.getIntExtra("id",1));presenter.set_id(intent.getStringExtra("_id"));presenter.setTitle(intent.getStringExtra("title"));presenter.setUrl(intent.getStringExtra("url"));presenter.setImgUrl(intent.getStringExtra("imgUrl"));}
DetailContract
public class DetailContract {interface Presenter extends BasePresenter{/*** 流浪器中打开* 复制文本* 复制连接* 添加收藏或取消收藏* 查询是否收藏* 请求数据* 分享到QQ* 分享到微信* 分享到朋友圈* 分享到微信收藏*/void openInBrower();void copyText();void copyLink();void addToOrDeleteFromBookMarks();boolean queryIsBooksMarks();void requestData();void shareArticleToQQ(final MyQQListener listener);void shareArticleToWx();void shareArticleToWxCommunity();void shareArticleToWxCollect();}interface View extends BaseView<Presenter> {// 显示正在加载void showLoading();// 停止加载void stopLoading();// 显示加载错误void showLoadingError();// 显示分享时错误void showSharingError();// 正确获取数据后显示内容
// void showResult(String result);
// // 对于body字段的消息,直接接在url的内容void showResultWithoutBody(String url);// 设置顶部大图void showCover(String url);// 设置标题void setTitle(String title);// 设置是否显示图片void setImageMode(boolean showImage);// 用户选择在浏览器中打开时,如果没有安装浏览器,显示没有找到浏览器错误void showBrowserNotFoundError();// 显示已复制文字内容void showTextCopied();// 显示文字复制失败void showCopyTextError();// 显示已添加至收藏夹void showAddedToBookmarks();// 显示已从收藏夹中移除void showDeletedFromBookmarks();void showNotNetError();void shareSuccess();void shareError();void shareCancel();}
}
DetailFragment
详情页主题是使用WebView显示,重点注意好设置属性和正确销毁:
@Overridepublic void initView(View view) {......//webview设置属性webview.getSettings().setJavaScriptEnabled(true);//缩放,设置为不能缩放可以防止页面上出现放大和缩小的图标webview.getSettings().setBuiltInZoomControls(false);//缓存webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);//开启DOM storage API功能webview.getSettings().setDomStorageEnabled(true);//开启application Cache功能webview.getSettings().setAppCacheEnabled(false);.....}//早onDestroy中销毁WebView的对象
@Overridepublic void onDestroyView() {super.onDestroyView();webview.removeAllViews();webview.destroy();webview=null;}
DetailPresenter
//复制链接地址@Overridepublic void copyLink() {ClipboardManager manager= (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);ClipData data=null;switch (type){case TYPE_Gank:data=ClipData.newPlainText("text",url);}manager.setPrimaryClip(data);view.showTextCopied();}//添加到收藏或者移除收藏@Overridepublic void addToOrDeleteFromBookMarks() {switch (type){case TYPE_Gank:GankNews.Question gank= App.DbLiteOrm.queryById(id,GankNews.Question.class);if (queryIsBooksMarks()){view.showDeletedFromBookmarks();gank.mark=false;}else {view.showAddedToBookmarks();gank.mark=true;}App.DbLiteOrm.update(gank);break;case TYPE_Front:FrontNews.Question front=App.DbLiteOrm.queryById(id,FrontNews.Question.class);if (queryIsBooksMarks()){view.showDeletedFromBookmarks();front.mark=false;}else {view.showAddedToBookmarks();front.mark=true;}App.DbLiteOrm.update(front);break;case TYPE_IOS:IosNews.Question ios=App.DbLiteOrm.queryById(id,IosNews.Question.class);if (queryIsBooksMarks()){view.showDeletedFromBookmarks();ios.mark=false;}else {view.showAddedToBookmarks();ios.mark=true;}App.DbLiteOrm.update(ios);}}//查询是否已经收藏@Overridepublic boolean queryIsBooksMarks() {if (_id ==null || type==null){view.showLoadingError();return false;}//true为已经收藏 false未收藏switch (type){case TYPE_Gank:GankNews.Question gank= App.DbLiteOrm.queryById(id,GankNews.Question.class);OrmLog.i(TAG,gank);boolean isMark=gank.mark;if (isMark){return true;}else {return false;}case TYPE_Front:FrontNews.Question front=App.DbLiteOrm.queryById(id,FrontNews.Question.class);if (front.mark){return true;}else {return false;}case TYPE_IOS:Log.i(TAG, "queryIsBooksMarks: "+id);IosNews.Question ios=App.DbLiteOrm.queryById(id,IosNews.Question.class);OrmLog.i(TAG,ios);if (ios.mark){return true;}else {return false;}}return false;}//分享到QQ@Overridepublic void shareArticleToQQ(MyQQListener listener) {//title == descif (TextUtils.isEmpty(imgUrl)){ShareSingleton.getInstance().shareToQQ((Activity) context,url,"推荐给你一篇文章",title, R.string.app_name, QQShare.SHARE_TO_QQ_FLAG_QZONE_ITEM_HIDE,listener);}else {ShareSingleton.getInstance().shareToQQ((Activity) context,url,"推荐给你一篇文章",title,imgUrl,R.string.app_name, QQShare.SHARE_TO_QQ_FLAG_QZONE_ITEM_HIDE,listener);}}//分享到微信@Overridepublic void shareArticleToWx() {//title == descShareSingleton.getInstance().shareWebToWx(url,"",title,true);}//分享到朋友圈@Overridepublic void shareArticleToWxCommunity() {//title == descShareSingleton.getInstance().shareWebToWx(url,"",title,false);}//分享到微信收藏@Overridepublic void shareArticleToWxCollect() {//title == descShareSingleton.getInstance().shareWebToWxCollect(url,"干货",title);}
ShareSingleton
关于微信和QQ分享的具体方法还得参考官方文章,我这里提出我自己写好的分享单例类
public class ShareSingleton {private Tencent mTencent;public static IWXAPI api;private static final int THUMB_SIZE = 150;//单例模式private ShareSingleton() {}public static final ShareSingleton getInstance(){return Singleton.INSTANCE;}private static class Singleton{private static final ShareSingleton INSTANCE=new ShareSingleton();}/*** 图文分享 图片来源网络* !! 分享操作要在主线程中完成* @param activity* @param targetUrl 这条分享消息被好友点击后的跳转URL。* @param shareTitle 分享的标题, 最长30个字符。* @param shareSummary 分享的消息摘要,最长40个字。* @param netImgUrl 可填 分享图片的URL或者本地路径* @param appName 手Q客户端顶部,替换“返回”按钮文字,如果为空,用返回代替* @param shareToQQExtInt 额外选项 是否自动打开分享到QZone的对话框* @param listener 分享回调接口*/public void shareToQQ(Activity activity,String targetUrl,String shareTitle,String shareSummary,@Nullable String netImgUrl,@StringRes int appName,int shareToQQExtInt,MyQQListener listener){if (mTencent==null){mTencent=Tencent.createInstance(Constants.QQ_APP_ID,activity.getApplicationContext());}final Bundle params = new Bundle();params.putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_DEFAULT);params.putString(QQShare.SHARE_TO_QQ_TARGET_URL,targetUrl);params.putString(QQShare.SHARE_TO_QQ_TITLE, shareTitle);params.putString(QQShare.SHARE_TO_QQ_SUMMARY, shareSummary );params.putString(QQShare.SHARE_TO_QQ_IMAGE_URL, netImgUrl);params.putString(QQShare.SHARE_TO_QQ_APP_NAME,activity.getString(appName));params.putInt(QQShare.SHARE_TO_QQ_EXT_INT, shareToQQExtInt);mTencent.shareToQQ(activity, params, listener);}/*** 文章分享 无图* !! 分享操作要在主线程中完成* @param activity* @param targetUrl 这条分享消息被好友点击后的跳转URL。* @param shareTitle 分享的标题, 最长30个字符。* @param shareSummary 分享的消息摘要,最长40个字。* @param appName 手Q客户端顶部,替换“返回”按钮文字,如果为空,用返回代替* @param shareToQQExtInt 额外选项 是否自动打开分享到QZone的对话框* @param listener 分享回调接口*/public void shareToQQ(Activity activity,String targetUrl,String shareTitle,String shareSummary,@StringRes int appName,int shareToQQExtInt,MyQQListener listener){if (mTencent==null){mTencent=Tencent.createInstance(Constants.QQ_APP_ID,activity.getApplicationContext());}final Bundle params = new Bundle();params.putInt(QQShare.SHARE_TO_QQ_KEY_TYPE, QQShare.SHARE_TO_QQ_TYPE_DEFAULT);params.putString(QQShare.SHARE_TO_QQ_TARGET_URL,targetUrl);params.putString(QQShare.SHARE_TO_QQ_TITLE, shareTitle);params.putString(QQShare.SHARE_TO_QQ_SUMMARY, shareSummary );params.putString(QQShare.SHARE_TO_QQ_APP_NAME,activity.getString(appName));params.putInt(QQShare.SHARE_TO_QQ_EXT_INT, shareToQQExtInt);mTencent.shareToQQ(activity, params, listener);}/*** 分享文章到微信/朋友圈* @param webUrl* @param webTitle* @param webDesc* @param isShareFriend*/public void shareWebToWx(@NonNull String webUrl,String webTitle,String webDesc,boolean isShareFriend){
// 注册操作也可以写死在Application中// 通过WXAPIFactory工厂,获取IWXAPI的实例api=WXAPIFactory.createWXAPI(App.getContext(),Constants.WX_APP_ID,true);// 将该app注册到微信api.registerApp(Constants.WX_APP_ID);//初始化一个WXWebpageObject对象,填写urlWXWebpageObject webpag=new WXWebpageObject();webpag.webpageUrl=webUrl;//用WXWebpageObject对象初始化一个WXMediaMessage对象 填写标题和描述WXMediaMessage msg=new WXMediaMessage(webpag);msg.title=webTitle;msg.description=webDesc;//构造一个ReqSendMessageToWX.Req req=new SendMessageToWX.Req();req.transaction=buildTransaction("webpage");//transaction 字段用于唯一标识一个请求req.message= msg;req.scene=isShareFriend ? SendMessageToWX.Req.WXSceneSession : SendMessageToWX.Req.WXSceneTimeline;api.sendReq(req);}/*** 分享文章到微信收藏* @param webUrl* @param webTitle* @param webDesc*/public void shareWebToWxCollect(@NonNull String webUrl, String webTitle, String webDesc){
// 注册操作也可以写死在Application中// 通过WXAPIFactory工厂,获取IWXAPI的实例api=WXAPIFactory.createWXAPI(App.getContext(),Constants.WX_APP_ID,true);// 将该app注册到微信api.registerApp(Constants.WX_APP_ID);//初始化一个WXWebpageObject对象,填写urlWXWebpageObject webpag=new WXWebpageObject();webpag.webpageUrl=webUrl;//用WXWebpageObject对象初始化一个WXMediaMessage对象 填写标题和描述WXMediaMessage msg=new WXMediaMessage(webpag);msg.title=webTitle;msg.description=webDesc;//构造一个ReqSendMessageToWX.Req req=new SendMessageToWX.Req();req.transaction=buildTransaction("webpage");//transaction 字段用于唯一标识一个请求req.message= msg;req.scene=SendMessageToWX.Req.WXSceneFavorite;api.sendReq(req);}
这篇文章就分析这么多,如果你想了解跟多,欢迎下载源码。主要部分源码都有注释
三、部分运行效果
四、其他补充
如果你有问题可以提交到Github的issue上,也可以给我发邮件。我的邮件是yeshuwei.swy@gmail.com
Android–从零开始开发一款文章阅读APP
代码地址如下:
http://www.demodashi.com/demo/11212.html注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权
Android--从零开始开发一款文章阅读APP相关推荐
- 如何从零开始开发一款嵌入式产品(20年的嵌入式经验分享学习)
如何从零开始开发一款嵌入式产品(20年的嵌入式经验分享学习)_转 来源:www.armjishu.com 作者:jesse 转载请注明出处 首先,如果你有幸看到这篇文章,千万不要试图在2个小时内阅读完 ...
- 开发一款即时通讯App,从这几步开始
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯云视频发表于云+社区专栏 关注公众号"腾讯云视频",一键获取 技术干货 | 优惠活动 | 视频方案 " ...
- AndroidFire,一款新闻阅读 App
AndroidFire 项目地址:AndroidFire 简介:AndroidFire,一款新闻阅读 App,基于 Material Design + MVP + RxJava + Retrofit ...
- 餐饮水单打印软件_开发一款餐饮手机app系统软件什么价格?有哪些方面需要考虑?...
开发一款餐饮手机app系统软件什么价格?有哪些方面需要考虑? 近年来,餐饮类的APP如雨后春笋般快速增长,无论是上档次的酒店,还是各大餐厅,都有各自的专属APP.餐饮APP的开发能让大型酒店/餐厅获得 ...
- 爆款文章阅读量快速提升的一个关键点!
还记得新媒体行业发展之初,第一批入场的选手进入赛道,大家都是不断的批量生产内容,获得大量阅读:然后通过各种方式野蛮生长,获得更多阅读量,一步步成为头部账号. 那时候阅读量是个好东西,快速涨粉是个好现象 ...
- 爆款文章阅读量快速提升的一个关键点
还记得新媒体行业发展之初,第一批入场的选手进入赛道,大家都是不断的批量生产内容,获得大量阅读:然后通过各种方式野蛮生长,获得更多阅读量,一步步成为头部账号. 那时候阅读量是个好东西,快速涨粉是个好现象 ...
- 一个传统物流企业开发一款专属物流app需要多少成本?
物流APP开发专家指出:近年来,我国电子商务经济处于快速发展阶段,在一定程度上促进了物流业的发展.同时,没有物流系统的支持,电子商务经济也能取得今天的成就!根据小编,获得的数据,淘宝手机交易量占近70 ...
- 《iOS移动开发从入门到精通》图书连载一:如果你也想开发一款自己的APP,可以看一下这篇文
前言:互联网+时代给自己多一个选择的机会,尝试开发一款属于自己的APP,绝对是件激动人心的事情!<iOS移动开发从入门到精通>已经上市并和大家见面.从今天起,我会将把图书的部分内容以连载的 ...
- Android商城开发系列(二)——App启动欢迎页面制作
商城APP一般都会在应用启动时有一个欢迎界面,下面我们来实现一个最简单的欢迎页开发:就是打开商城App,先出现欢迎界面,停留几秒钟,自动进入应用程序的主界面. 首先先定义WelcomeActivity ...
最新文章
- 河南科技大学c语言章节作业答案,河南科技大学C语言试题
- HDU3037(卢卡斯定理)
- es6新特性中...的用法
- Python3 局部变量与全局变量作用域
- 1、webpack入门例子。
- 逻辑学 —— 复杂问题谬误
- [BJOI2017]开车
- 漫画小说听书三合一分销平台源码
- web项目中如何启动爬虫程序?Django+Requests+Ajax制作可视化翻译界面详解
- 计算机cpu位数是啥,怎么看电脑cpu的位数
- SAP SD销售订单保存提示 定价错误: 必要条件 MWSI 丢失解决方法
- [4G/5G/6G专题基础-159]: CQI值的滤波方法
- 100句充满智慧的人生格言
- 论文复现-1:Perturbation CheckLists for Evaluating NLG Evaluation Metrics
- 解释源代码文件、目标代码文件和可执行文件之间的区别
- Linux学习 高级网络配置
- 阿里巴巴牵头发起对雅虎的250亿美元并购
- 男人最喜欢女人说的五句话
- 基于深度残差学习的图像识别 Deep Residual Learning for Image Recognition
- 与女儿谈商业模式(1):盖茨的商业模式