博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android--从零开始开发一款文章阅读APP
阅读量:5763 次
发布时间:2019-06-18

本文共 21442 字,大约阅读时间需要 71 分钟。

代码地址如下:

http://www.demodashi.com/demo/11212.html

前言

本案例已经开源!如果你想免费下载,可以访问我的,所有案例均在上面,只求给个star。当然愿意支付小小金额请我喝茶也行(大学穷狗-.-)

一、准备工作

  • 使用Android Studio开发
  • 微信和QQ第三方sdk,需要自行申请(这个简单)
  • 本案例使用提供的api,使用MVp+Material Design作为主体架构进行开发
  • 体验完整功能,点击

二、程序实现

目录结构

目录结构如下,我按照功能分包:

目录结果

实现思路

整体架构--MVP+Material

  • 首先你得了解MVP架构在android中的使用,如果你还不了解,可以阅读我的
  • 如果你不熟悉Material可以读

重点代码分析

如果讲述整个App,估计一篇文章说不清楚。那我干脆取其中一条线来分析。

下面主要分析文章列表--文章详情--文章分享

主页文章列表

这里只选择Android文章模块进行介绍:

主页列表

GankContract

public interface GankContract {    interface View extends BaseView
{ //错误 void showError(); //正在加载 void showLoading(); //停止加载 void Stoploading(); //显示数据列表 void showResult(ArrayList
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;            @Override            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {                super.onScrollStateChanged(recyclerView, newState);                LinearLayoutManager manager= (LinearLayoutManager) recyclerView.getLayoutManager();                //没有滚动时候                if (newState==RecyclerView.SCROLL_STATE_IDLE){                    //获的最后一个可见的item                    int lastVisibilityItem=manager.findLastCompletelyVisibleItemPosition();                    int totalItemCount=manager.getItemCount();                    //判断是否滚动到底部并且是向下滑动                    if (lastVisibilityItem==(totalItemCount-1)&&isScrollState){                        presenter.loadMore(1);                    }                }            }//通知Presenter加载数据和设置item点击事件@Override    public void showResult(ArrayList
list) { if (adapter==null){ Log.i(TAG, "showResult: "+list.size()); adapter=new GankNewsAdapter(list,getContext()); adapter.setItemOnClickListener(new OnRecyclerViewOnClickListener() { @Override public void onItemClick(View v, int position) { presenter.StartReading(position); } @Override public void onItemLongClick(View v, int position) { } }); recyclerView.setAdapter(adapter); }else { adapter.notifyDataSetChanged(); } }

GankPresenter

同样只分析重点代码:

//根据当前页数加载列表数据 @Override    public 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() {                @Override                public 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
ganklist=App.DbLiteOrm.query(new QueryBuilder
(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(); } @Override public 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.
query(query)); view.showResult(list); }else { view.showNotNetError(); } } }//判断数据库是否已经存在 public boolean queryIfIdExists(String _id){ ArrayList
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的信息,进入详情阅读@Override public 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); }//随便看看 随机选取@Override public void LookAround() { if (list.isEmpty()){ view.showError(); return; } StartReading(new Random().nextInt(list.size())); }

GankNewsAdapter

因为文章分两种:有图和无图。所有要进行分类加载

//判断是否有图和是否是底部加载item @Override    public 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@Override    public 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加载图片。无图则不加载    @Override    public 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

@Override    protected 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
{ // 显示正在加载 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显示,重点注意好设置属性和正确销毁:

@Override    public 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的对象@Override    public void onDestroyView() {        super.onDestroyView();        webview.removeAllViews();        webview.destroy();        webview=null;    }

DetailPresenter

//复制链接地址    @Override    public 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();    }//添加到收藏或者移除收藏    @Override    public 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);        }    }//查询是否已经收藏    @Override    public 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    @Override    public void shareArticleToQQ(MyQQListener listener) {        //title == desc        if (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);        }    }//分享到微信    @Override    public void shareArticleToWx() {        //title == desc        ShareSingleton.getInstance().shareWebToWx(url,"",title,true);    }//分享到朋友圈    @Override    public void shareArticleToWxCommunity() {        //title == desc        ShareSingleton.getInstance().shareWebToWx(url,"",title,false);    }//分享到微信收藏    @Override    public void shareArticleToWxCollect() {        //title == desc        ShareSingleton.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对象,填写url        WXWebpageObject webpag=new WXWebpageObject();        webpag.webpageUrl=webUrl;        //用WXWebpageObject对象初始化一个WXMediaMessage对象  填写标题和描述        WXMediaMessage msg=new WXMediaMessage(webpag);        msg.title=webTitle;        msg.description=webDesc;        //构造一个Req        SendMessageToWX.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对象,填写url        WXWebpageObject webpag=new WXWebpageObject();        webpag.webpageUrl=webUrl;        //用WXWebpageObject对象初始化一个WXMediaMessage对象  填写标题和描述        WXMediaMessage msg=new WXMediaMessage(webpag);        msg.title=webTitle;        msg.description=webDesc;        //构造一个Req        SendMessageToWX.Req req=new SendMessageToWX.Req();        req.transaction=buildTransaction("webpage");//transaction 字段用于唯一标识一个请求        req.message= msg;        req.scene=SendMessageToWX.Req.WXSceneFavorite;        api.sendReq(req);    }

这篇文章就分析这么多,如果你想了解跟多,欢迎下载源码。主要部分源码都有注释

三、部分运行效果

1.PNG

%E6%88%AA%E5%9B%BE2.png
3.PNG
%E6%88%AA%E5%9B%BE4.png
5.PNG

四、其他补充

如果你有问题可以提交到Github的issue上,也可以给我发邮件。我的邮件是yeshuwei.swy@gmail.com

Android--从零开始开发一款文章阅读APP

代码地址如下:

http://www.demodashi.com/demo/11212.html

注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权

你可能感兴趣的文章
hoj1249 Optimal Array Multiplication Sequence
查看>>
[转载] 晓说——第30期:海上霸主航母(下)
查看>>
PBRT笔记(5)——相机模型
查看>>
HDOJ_ACM_悼念512汶川大地震遇难同胞——珍惜现在,感恩生活
查看>>
Adobe Flash Builder 4.6 打开时提示Failed to create the Java Virtual Machine
查看>>
代码重构(五):继承关系重构规则
查看>>
网络协议——UDP协议
查看>>
php中的引用
查看>>
Json.net 忽略实体某些属性的序列化
查看>>
数据库访问方式
查看>>
Leetcode 4. Median of Two Sorted Arrays
查看>>
99乘法表
查看>>
Linux下USB驱动框架分析【转】
查看>>
为什么linux下多线程程序如此消耗虚拟内存【转】
查看>>
Linux用户空间与内核空间(理解高端内存)【转】
查看>>
使用gcc的-finstrument-functions选项进行函数跟踪【转】
查看>>
设备树解析【转】
查看>>
iOS系统知识架构(转)
查看>>
Daily Scrum - 11/26
查看>>
Android项目结构 以及体系结构
查看>>