Android--從零開始開發一款文章閱讀APP

代碼地址以下:
http://www.demodashi.com/demo/11212.htmlhtml

前言

本案例已經開源!若是你想免費下載,能夠訪問個人Github,全部案例均在上面,只求給個star。固然願意支付小小金額請我喝茶也行(大學窮狗-.-)android

1、準備工做

  • 使用Android Studio開發
  • 微信和QQ第三方sdk,須要自行申請(這個簡單)
  • 本案例使用幹活集中營提供的api,使用MVp+Material Design做爲主體架構進行開發
  • 體驗完整功能,點擊下載APK

2、程序實現

目錄結構

目錄結構以下,我按照功能分包:
目錄結果ios

實現思路

總體架構--MVP+Material

  • 首先你得了解MVP架構在android中的使用,若是你還不瞭解,能夠閱讀個人這篇文章
  • 若是你不熟悉Material能夠讀官方文檔

重點代碼分析

若是講述整個App,估計一篇文章說不清楚。那我乾脆取其中一條線來分析。

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

主頁文章列表

這裏只選擇Android文章模塊進行介紹:
主頁列表github

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的內容主要是文章列表,咱們只分享重點:
web

//下拉刷新實現
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

由於文章分兩種:有圖和無圖。全部要進行分類加載
api

//判斷是否有圖和是不是底部加載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);
    }

這篇文章就分析這麼多,若是你想了解跟多,歡迎下載源碼。主要部分源碼都有註釋

3、部分運行效果





4、其餘補充

若是你有問題能夠提交到Github的issue上,也能夠給我發郵件。個人郵件是yeshuwei.swy@gmail.com

Android--從零開始開發一款文章閱讀APP

代碼地址以下:
http://www.demodashi.com/demo/11212.html

注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索