zoukankan      html  css  js  c++  java
  • 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;
                @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<GankNews.Question> 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<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();
                    }
    
                    @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.<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的信息,进入详情阅读
    @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<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显示,重点注意好设置属性和正确销毁:

     @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);
        }
    

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

    三、部分运行效果





    四、其他补充

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

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

    代码地址如下:
    http://www.demodashi.com/demo/11212.html

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

  • 相关阅读:
    sql 自定义函数-16进制转10进制
    编写一个单独的Web Service for Delphi
    Web Service
    无需WEB服务器的WEBServices
    Svn总是提示输入账号密码
    阿里云服务器SQLSERVER 2019 远程服务器环境搭建
    svn客户端使用
    数据库设计规则(重新整理)
    数据库表字段命名规范
    怎样去掉DELPHI 10.3.3 启动后的 security alert 提示窗体
  • 原文地址:https://www.cnblogs.com/demodashi/p/8508857.html
Copyright © 2011-2022 走看看