天天看點

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

為什麼想做校園日記?

前段時間支付寶的校園日記功能火爆異常,但是卻昙花一現,可是在社會上還是引起了一陣自媒體浪潮,其實這就是人的本性的釋放,人的本性就有喜歡嘚瑟,愛表現自己的成分。在我了解中大部分能火起來的App都有能抓住人的一部分本性需求,是以我就想開發一個校園日記的App,讓它成為最時尚的大學生社交活動App,專為廣大在校童鞋們打造的校園日記分享軟體。可以上傳自己的自拍照片、美食圖檔、心情感想等日記,實作随時随地分享自己,展現自己的需求。

APP功能分解:

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

核心功能就是個人日記的展示,其實這個最終的樣子做出來應該和朋友圈非常類似

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

為什麼選用MVP+RxAndroid+DroiBaaS

技術架構選型:

對程式進行架構設計的原因,歸根到底是為了提高生産力。通過設計使程式子產品化,能夠更簡單的讀懂code以及友善維護和測試。整體的App架構選用MVP來搭建,結合最近比較火熱的RxAndroid實作觀察者事務模式就能夠做到子產品内部的高聚合和子產品之間的低耦合,子產品内被的高聚合。

由于開發的應用需要搭建雲伺服器和資料庫,是以也選用了最近比較流行的一站式後端雲服務DroiBaaS來實作所有雲背景功能。後面會講到具體用到哪些功能和怎麼來使用這些功能。

為什麼選用MVP模式?

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

以上是MVP的工作原理圖。其中Presenter操作View和Mode都是通過接口來實作直接的調用。

傳統的MVC模式很難把View和Controller分開,總是直接在View的事件響應函數裡完成了Controller的代碼,而MVP就完全解決了這個問題。MVP的工作流程如下:Presenter負責邏輯的處理,Model提供資料,View負責顯示。

作為一種新的模式,在MVP中View并不直接使用Model,它們之間的通信是通過Presenter來進行的,所有的互動都發生在Presenter内部。這樣的話最大可能降低了View和Model之間的耦合性,維護和測試起來都是異常的簡單友善。

為什麼選用RxAndroid?

最主要是兩個字簡潔,RxAndroid是RxJava的擴充,它的異步調用随着程式邏輯變得越來越複雜,它的鍊式調用依然能夠保持簡潔。

RxAndroid的回調方法主要有三個,onNext(),onError(),onCompleted()。

• onNext() 對于Subscribler我們可以了解為接收資料。

• onCompleted() 觀測的事件的隊列任務都完成了,當不再有onNext()發送資料時,onCompleted事件被觸發。

• onError() 當事件異常時響應此方法,一旦此方法被觸發,隊列自動終止,不再發送任何資料。

為什麼選用DroiBaaS?

在這之前我的雲背景都來自于阿裡雲+後端工程師,但是我隻是個Android工程師,是以我需要一個更加簡單友善的雲背景生産工具。我選擇雲背景,希望能滿足我以下幾個要求:

  1. 伺服器環境我不會搭建,是以更别提維護了,比如CentOS+Nginx+PHP+MySQL,我也隻是聽說而已
  2. 我更不會寫Server端的Code,因為我隻會安卓App開發,而且這應用隻是我個人開發,也找不到其他人來幫我寫server code
  3. 最好能有現成的可視化管理背景,這樣以後管理和營運起來也友善
  4. 花錢盡量少,最好免費,畢竟是個人興趣和嘗試,不希望試錯成本太高
  5. 能一站式盡量一站式,雖然我也可以用友盟的統計+極光的推送+酷傳的代釋出+百度的廣告+啥啥啥,不過畢竟麻煩麼不是。。

綜合這些需求,我發現最近新鮮出爐的專為APP開發者提供一站式整合雲後端的服務——BaaS比較适合我來使用。無需租用伺服器和開發伺服器端程式,隻需內建BaaS平台提供相應SDK就能夠實作各種雲背景的功能,比如雲資料庫,使用者系統搭建,推送通道,使用者回報收集,版本管理和資料統計的功能,這些功能對于App的開發以及之後的營運都是必須的。目前國内的幾家BaaS雲服務提供商,比如leanCloud、DroiBaaS、Bmob、Maxleap,目前都處于創業階段,因為本身BaaS還處于一個概念期,到普及還需要一段時間,但是用過之後真心覺得相當好用。我相信選用BaaS來搭建App的雲背景這将是之後個人開發者以及中小企業開發者的趨勢。

DroiBaaS相比其他幾家的優勢在于:

  1. 提供沙箱和生産兩種模式,沙箱完成調試再釋出生産環境上線,避免了調試和測試對正式版本的資料污染
  2. DroiObject使用相當的友善,注解的程式設計方式相比其他幾家還是有不錯的便利性
  3. 有管道和廣告背景,App開發出來之後能夠提供一定的推廣和變現的幫助
  4. 文檔比較全面而且詳細,SDK內建方式簡單,打電話咨詢過,客服态度不錯,很耐心也很專業
  5. 免費額度相比較其他幾家還是比較有優勢的,雖然我也不知道會用到多少,但是多一點總歸是好的

系統架構設計:

代碼架構如下

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

使用MVP模式來開發的好處就是代碼架構非常的清晰明了,個人還是比較注重代碼的邏輯性以及可讀性

用到的架構及生産工具

日記的展示界面用了SuperRecyclerView——使RecyclerView更加容易使用的Android類庫

圖檔加載與緩存用了Glide

DroiBaaS的網絡請求都是基于OKHttp的,是以OKHttp和OKio是必須用到的網絡架構

Json的生成和解析用的FastJson

響應式程式設計用的是RxAndroid

高度整合封裝的雲服務BaaS作為第二代雲計算的産物,為App的雲背景開發提供了非常便利的生産工具,提高了開發效率、縮短了上線時間、降低了開發成本,這必将是一個潮流和趨勢,我還是比較看好的。

所有的雲端功能,如推送、自更新、使用者回報、統計、雲資料、使用者管理功能全部是用DroiBaaS的SDK來實作

日記展示UML架構設計:

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

MainActvity繼承CircleContract.View接口,

CirclePresenter繼承CircleContract.Presenter接口

MainActvity 生成一個CirclePresenter對象同時把自身傳入CirclePresenter

MainActvity調用CircleContract.Presenter的各種資料擷取接口,CirclePresenter從雲端擷取到資料後調用CircleContract.View的界面更新接口通知MainActivity來重新整理View

整個MVP架構相當的清晰明了,使用MVP最大的好處就在此處,代碼簡潔,同時簡化了Activity的邏輯,利于以後的調試和單元測試,新功能加起來也非常的友善

資料庫設計如UML圖所示

主要是四個主要互動資料類,這個四個類同時也是後DroiBaaS雲背景資料庫儲存的資料類

CircleItem:日記内容Data

FavortItem:使用者點贊Data

CommetItem:使用者評論Data

User:使用者Data

詳細代碼設計

日記MVP+RxAndroid代碼如下:

整個工程代碼比較多,我在這裡隻貼了日記展示關鍵邏輯的代碼,整個工程的源碼請參考文檔最後的GitHub的連結

CircleContract.java

Model和View的中間接口類

public interface CircleContract {
    interface View extends BaseView{
        void update2DeleteCircle(String circleId);
        void update2AddFavorite(int circlePosition, FavortItem addItem);
        void update2DeleteFavort(int circlePosition, String favortId);
        void update2AddComment(int circlePosition, CommentItem addItem);
        void update2DeleteComment(int circlePosition, String commentId);
        void updateEditTextBodyVisible(int visibility, CommentConfig commentConfig);
        void update2loadData(int loadType, List<CircleItem> datas);
    }

    interface Presenter {
        void loadData(int loadType);
        void deleteCircle(final String circleId);
        void addFavort(final int circlePosition,final String circleId);
        void deleteFavort(final int circlePosition, final String favortId);
        void addComment(String content,final CommentConfig config);
        void deleteComment(final int circlePosition, final String commentId);
        void showEditTextBody(CommentConfig commentConfig);

    }
}
           

CirclePresenter.java

此類使用RxAndroid從雲端擷取資料再發回給View進行異步展示,在這個類中可以看出使用RxAndroid處理異步邏輯非常得心用手,推薦大家使用。

public class CirclePresenter implements CircleContract.Presenter{
    private CircleContract.View view;
    private  static int index = ;
    public CirclePresenter(CircleContract.View view){
        this.view = view;
    }

    @Override
    public void loadData(final int loadType){
        if(loadType == ){
            index = ;
        }
        getCircleData()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<List<CircleItem>>() {
                    @Override
                    public void onCompleted() {
                        view.hideLoading();
                    }

                    @Override
                    public void onError(Throwable e) {
                        view.showToast("網絡錯誤!");
                    }

                    @Override
                    public void onNext(List<CircleItem> data) {
                        view.update2loadData(loadType, data);
                    }
                });
    }


    /**
     * 
    * @Title: deleteCircle 
    * @Description: 删除動态 
    * @param  circleId     
    * @return void    傳回類型 
    * @throws
     */
    @Override
    public void deleteCircle(final String circleId){
        deleteCircleData(circleId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<Boolean>() {
                @Override
                public void onCompleted() {
                    view.hideLoading();
                }

                @Override
                public void onError(Throwable e) {
                    view.showToast("網絡錯誤!");
                }

                @Override
                public void onNext(Boolean result) {
                    if (result) {
                        view.update2DeleteCircle(circleId);
                    }else{
                        view.showToast("删除資料失敗,請重試!");
                    }
                }
            });
}
    /**
     * 
    * @Title: addFavort 
    * @Description: 點贊
    * @param  circlePosition     
    * @return void    傳回類型 
    * @throws
     */
    @Override
    public void addFavort(final int circlePosition,String circleId){
        createFavort(circleId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<FavortItem>() {
                @Override
                public void onCompleted() {
                    view.hideLoading();
                }

                @Override
                public void onError(Throwable e) {
                    view.showToast("網絡錯誤!");
                }

                @Override
                public void onNext(FavortItem data) {
                    if (data != null) {
                        view.update2AddFavorite(circlePosition, data);
                    }else{
                        view.showToast("點贊失敗!");
                    }
                }
            });
    }
    /**
     * 
    * @Title: deleteFavort 
    * @Description: 取消點贊 
    * @param @param circlePosition
    * @param @param favortId     
    * @return void    傳回類型 
    * @throws
     */
    @Override
    public void deleteFavort(final int circlePosition, final String favortId){
        deleteFavort(favortId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<Boolean>() {
                @Override
                public void onCompleted() {
                    view.hideLoading();
                }

                @Override
                public void onError(Throwable e) {
                    view.showToast("網絡錯誤!");
                }

                @Override
                public void onNext(Boolean result) {
                    if (result) {
                        view.update2DeleteFavort(circlePosition, favortId);
                    }else{
                        view.showToast("删除資料失敗,請重試!");
                    }
                }
            });
    }

    /**
     * 
    * @Title: addComment 
    * @Description: 增加評論
    * @param  content
    * @param  config  CommentConfig
    * @return void    傳回類型 
    * @throws
     */
    @Override
    public void addComment(String content,final CommentConfig config){
        if(config == null){
            return;
        }
        createComment(content,config)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<CommentItem>() {
                @Override
                public void onCompleted() {
                    view.hideLoading();
                }

                @Override
                public void onError(Throwable e) {
                    view.showToast("網絡錯誤!");
                }

                @Override
                public void onNext(CommentItem data) {
                    if (data != null) {
                        view.update2AddComment(config.circlePosition, data);
                    }else{
                        view.showToast("評論送出失敗!");
                    }
                }
            });
    }

    /**
     * 
    * @Title: deleteComment 
    * @Description: 删除評論 
    * @param @param circlePosition
    * @param @param commentId     
    * @return void    傳回類型 
    * @throws
     */
    @Override
    public void deleteComment(final int circlePosition,final String commentId){
        deleteComment(commentId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<Boolean>() {
                @Override
                public void onCompleted() {
                    view.hideLoading();
                }

                @Override
                public void onError(Throwable e) {
                    view.showToast("網絡錯誤!");
                }

                @Override
                public void onNext(Boolean result) {
                    if (result) {
                        view.update2DeleteComment(circlePosition, commentId);
                    }else{
                        view.showToast("删除資料失敗,請重試!");
                    }
                }
            });
    }

    /**
     *
     * @param commentConfig
     */
    @Override
    public void showEditTextBody(CommentConfig commentConfig){
        if(view != null){
            view.updateEditTextBodyVisible(View.VISIBLE, commentConfig);
        }
    }


    /**
     * 清除對外部對象的引用,反正記憶體洩露。
     */
    public void recycle(){
        this.view = null;
    }
}    
           

MainActivity.java

日記展現類,通過CirclePresenter擷取的資料後再調用View的接口來展示和更新資料。

public class MainActivity extends BaseActivity implements CircleContract.View{

    protected static final String TAG = MainActivity.class.getSimpleName();
    private CircleAdapter circleAdapter;
    private LinearLayout edittextbody;
    private EditText editText;
    private ImageView sendIv;

    private CirclePresenter presenter;
    private CommentConfig commentConfig;
    private SuperRecyclerView recyclerView;
    private RelativeLayout bodyLayout;
    private LinearLayoutManager layoutManager;

    private final static int TYPE_PULLREFRESH = ;
    private final static int TYPE_UPLOADREFRESH = ;
    private UpLoadDialog uploadDialog;
    private SwipeRefreshLayout.OnRefreshListener refreshListener;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        presenter = new CirclePresenter(this);
        initView();

        //實作自動下拉重新整理功能
        recyclerView.getSwipeToRefresh().post(new Runnable(){
            @Override
            public void run() {
                recyclerView.setRefreshing(true);//執行下拉重新整理的動畫
                refreshListener.onRefresh();//執行資料加載操作
            }
        });
        DroiUpdate.update(this);
    }

    @Override
    protected void onDestroy() {
        if(presenter !=null){
            presenter.recycle();
        }
        super.onDestroy();
    }

    @SuppressLint({ "ClickableViewAccessibility", "InlinedApi" })
    private void initView() {

        initTitle();
        initUploadDialog();

        recyclerView = (SuperRecyclerView) findViewById(R.id.recyclerView);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.addItemDecoration(new DivItemDecoration(, true));
        recyclerView.getMoreProgressView().getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;

        recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (edittextbody.getVisibility() == View.VISIBLE) {
                    updateEditTextBodyVisible(View.GONE, null);
                    return true;
                }
                return false;
            }
        });

        refreshListener = new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                presenter.loadData(TYPE_PULLREFRESH);
            }
        };
        recyclerView.setRefreshListener(refreshListener);

        recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if(newState == RecyclerView.SCROLL_STATE_IDLE){
                    Glide.with(MainActivity.this).resumeRequests();
                }else{
                    Glide.with(MainActivity.this).pauseRequests();
                }

            }
        });

        circleAdapter = new CircleAdapter(this);
        circleAdapter.setCirclePresenter(presenter);
        recyclerView.setAdapter(circleAdapter);

        edittextbody = (LinearLayout) findViewById(R.id.editTextBodyLl);
         editText = (EditText) findViewById(R.id.circleEt);
        sendIv = (ImageView) findViewById(R.id.sendIv);
        sendIv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (presenter != null) {
                    //釋出評論
                    String content =  editText.getText().toString().trim();
                    if(TextUtils.isEmpty(content)){
                        Toast.makeText(MainActivity.this, "評論内容不能為空...", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    presenter.addComment(content, commentConfig);
                }
                updateEditTextBodyVisible(View.GONE, null);
            }
        });

        setViewTreeObserver();
    }

    private void initUploadDialog() {
        uploadDialog = new UpLoadDialog(this);
    }

    private void initTitle() {
        addTitle("校園日記");
        setrightButton("發日記",new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, PublishActivity.class));
            }
        });
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == ) {
           if(edittextbody != null && edittextbody.getVisibility() == View.VISIBLE){
               //edittextbody.setVisibility(View.GONE);
               updateEditTextBodyVisible(View.GONE, null);
               return true;
           }
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public void update2DeleteCircle(String circleId) {
        List<CircleItem> circleItems = circleAdapter.getDatas();
        for(int i=; i<circleItems.size(); i++){
            if(circleId.equals(circleItems.get(i).getObjectId())){
                circleItems.remove(i);
                circleAdapter.notifyDataSetChanged();
                //circleAdapter.notifyItemRemoved(i+1);
                return;
            }
        }
    }

    @Override
    public void update2AddFavorite(int circlePosition, FavortItem addItem) {
        if(addItem != null){
            CircleItem item = (CircleItem) circleAdapter.getDatas().get(circlePosition);
            if(item.getFavorters() == null){
                List<FavortItem> favorts = new ArrayList<>();
                item.setFavorters(favorts);
            }
            item.getFavorters().add(addItem);
            circleAdapter.notifyDataSetChanged();
            //circleAdapter.notifyItemChanged(circlePosition+1);
        }
    }

    @Override
    public void update2DeleteFavort(int circlePosition, String favortId) {
        CircleItem item = (CircleItem) circleAdapter.getDatas().get(circlePosition);
        List<FavortItem> items = item.getFavorters();
        for(int i=; i<items.size(); i++){
            if(favortId.equals(items.get(i).getObjectId())){
                items.remove(i);
                circleAdapter.notifyDataSetChanged();
                //circleAdapter.notifyItemChanged(circlePosition+1);
                return;
            }
        }
    }

    @Override
    public void update2AddComment(int circlePosition, CommentItem addItem) {
        if(addItem != null){
            CircleItem item = (CircleItem) circleAdapter.getDatas().get(circlePosition);
            if(item.getComments() == null){
                List<CommentItem> comments = new ArrayList<>();
                item.setComments(comments);
            }
            item.getComments().add(addItem);
            circleAdapter.notifyDataSetChanged();
            //circleAdapter.notifyItemChanged(circlePosition+1);
        }
        //清空評論文本
         editText.setText("");
    }

    @Override
    public void update2DeleteComment(int circlePosition, String commentId) {
        CircleItem item = (CircleItem) circleAdapter.getDatas().get(circlePosition);
        List<CommentItem> items = item.getComments();
        for(int i=; i<items.size(); i++){
            if(commentId.equals(items.get(i).getObjectId())){
                items.remove(i);
                circleAdapter.notifyDataSetChanged();
                //circleAdapter.notifyItemChanged(circlePosition+1);
                return;
            }
        }
    }

    @Override
    public void updateEditTextBodyVisible(int visibility, CommentConfig commentConfig) {
        this.commentConfig = commentConfig;
        edittextbody.setVisibility(visibility);

        measureCircleItemHighAndCommentItemOffset(commentConfig);

        if(View.VISIBLE==visibility){
             editText.requestFocus();
            //彈出鍵盤
            CommonUtils.showSoftInput( editText.getContext(),  editText);

        }else if(View.GONE==visibility){
            //隐藏鍵盤
            CommonUtils.hideSoftInput( editText.getContext(),  editText);
        }
    }

    @Override
    public void update2loadData(int loadType, List<CircleItem> datas) {
        if(datas == null){
            recyclerView.removeMoreListener();
            recyclerView.hideMoreProgress();
            return;
        }
        if (loadType == TYPE_PULLREFRESH){
            recyclerView.setRefreshing(false);
            circleAdapter.setDatas(datas);
        }else if(loadType == TYPE_UPLOADREFRESH){
            circleAdapter.getDatas().addAll(datas);
        }
        circleAdapter.notifyDataSetChanged();

        if(datas != null && circleAdapter.getDatas().size()< + CircleAdapter.HEADVIEW_SIZE){
            recyclerView.setupMoreListener(new OnMoreListener() {
                @Override
                public void onMoreAsked(int overallItemsCount, int itemsBeforeMore, int maxLastVisiblePosition) {
                    presenter.loadData(TYPE_UPLOADREFRESH);
                }
            }, );
        }else{
            recyclerView.removeMoreListener();
            recyclerView.hideMoreProgress();
        }
    }

    @Override
    public void showLoading(String msg) {
    }

    @Override
    public void hideLoading() {
    }

    @Override
    public void showToast(String error) {
        Toast.makeText(getActivity(),error,Toast.LENGTH_SHORT).show();
    }
}
           

雲端邏輯實作

前面已經提到了整個App沒有搭建自己的伺服器也沒有編寫自己的Server端程式,整個雲端邏輯都是通過DroiBaaS來提供,具體用到了如下功能

  • 使用Core SDK的來搭建App的使用者系統,管理雲資料(日記、點贊、評論、照片等)
  • 使用版本更新SDK來完成應用的自更新,包括手動更新,給以後的APP更新提供通道
  • 使用使用者回報SDK來收集使用者的建議和意見,持續改進自己的産品
  • 使用統計SDK來擷取統計使用者的新增、日活、以及其他自定義事件
  • 使用消息推送SDK來完成應用的推送功能,以後能夠做一些營銷或者是事務通知

但是如何來使用,下面我來按步驟一一介紹。

其實官網也有快速入門文檔,根據快速入門文檔來操作,也能很快上手。

連結:http://www.droibaas.com/html/doc_24138.html

  1. 注冊DroiBaaS帳号

    在網址欄輸入www.droibaas.com或者在百度輸入DroiBaaS進行搜尋,打開官網後,點選右上角的“注冊”按鈕,在跳轉頁面填入你的手機、設定密碼,收到驗證碼填入後就能激活你的DroiBaaS賬戶,然後就可以用DroiBaaS的各種SDK來輕松開發應用了。

  2. 網站背景建立應用

    使用注冊好的賬号登入進入DroiBaaS控制台後,點選控制台界面左上角“建立應用”,在彈出框輸入你應用的名稱,然後點選确認,你就擁有了一個等待開發的應用。

  3. 擷取應用密鑰

    選擇你要開發的應用,進入該應用的應用管理界面

    在跳轉頁面,進入應用設定/安全密鑰,點選複制,即可得到AppID

  4. 下載下傳和安裝DroiBaaS SDK

    在官網上點選上方的下載下傳按鈕就能夠下載下傳到DroiBaaS的SDK,比較好的是還能夠支援打包下載下傳,就不需要我一個一個的去下載下傳了,還是挺友善

    安裝SDK流程比較簡單根據快速入門以及其他SDK的引導的安裝步驟來操作就OK,DroiBaaS使用的是GitHub的遠端倉庫,這樣做的好處有兩個

    • 省去了拷貝aar包到lib目錄的步驟,自動從網上下載下傳
    • 每次編譯釋出的時候都能夠用到SDK的最新版本

      但也存在缺點,因為GitHub的網站被牆了,在國内通路還是比較慢的,是以在下載下傳aar包的時候有時候速度會比較慢。

  5. 使用DroiBaaS功能

一).搭建App使用者系統

DroiBaaS的Core SDK提供了DroiUser類能夠用來建立使用者系統,在這裡我建立了一個類User繼承于DroiUser,在這個類中添加一些自己App需要的屬性,比如:nickName、headUrl、headIcon等

public class User extends DroiUser {
   @DroiExpose
   private String headUrl;
   @DroiExpose
   private DroiFile headIcon;
   @DroiExpose
   private String nickName;

   public User(){
   }
   public String getNickName() {
      return nickName;
   }
   public void setNickName(String nickName) {
      this.nickName = nickName;
   }
   public String getHeadUrl() {
      return headUrl;
   }
   public void setHeadUrl(String headUrl) {
      this.headUrl = headUrl;
   }
   public DroiFile getHeadIcon() {
      return headIcon;
   }
   public void setHeadIcon(DroiFile headIcon) {
      this.headIcon = headIcon;
   }
}
           

注冊使用者

User user = new User();
user.setUserId(username);
user.setPassword(password);
DroiPermission permission = new DroiPermission();
permission.setPublicReadPermission(true);
user.setPermission(permission);
DroiError droiError = user.signUp();
           

登入

DroiError droiError = new DroiError();
User user = DroiUser.login(username, password, User.class, droiError);
修改密碼
DroiUser myUser = DroiUser.getCurrentUser();
if (myUser != null && myUser.isAuthorized() && !myUser.isAnonymous()) {
    DroiError droiError = myUser.changePassword(oldPassword, newPassword);
    subscriber.onNext(droiError);
}
           

上傳頭像

DroiFile headIcon = new DroiFile(new File(path));
User user = DroiUser.getCurrentUser(User.class);
DroiPermission permission = new DroiPermission();
permission.setPublicReadPermission(true);
permission.setPublicWritePermission(false);
headIcon.setPermission(permission);
user.setHeadIcon(headIcon);
user.saveInBackground(new DroiCallback<Boolean>() {
    @Override
    public void result(Boolean aBoolean, DroiError droiError) {
        if (aBoolean) {
            Toast.makeText(mContext, "上傳成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(mContext, "上傳失敗", Toast.LENGTH_SHORT).show();
        }
    }
});
           

二).使用雲資料來管理日記内容

建立一個日記data類CircleItem繼承于DroiObject,使用save函數就能夠在雲端的資料庫儲存日記資料了。相當的簡單和友善,傳統的使用方式往往還要服務端編寫一個接口,用戶端和服務端定好相應的協定,使用http協定并攜帶相應的資料來通路接口,才會完成相應的操作。使用DroiBaaS的雲資料功能,大大簡化了流程,下面我們來看一看具體的使用

public class CircleItem extends DroiObject{

   public final static String TYPE_URL = "1";
   public final static String TYPE_IMG = "2";
   @DroiExpose
   private String content;
   @DroiExpose
   private String createTime;
   @DroiExpose
   private String type;//1:連結  2:圖檔
   @DroiExpose
   private String linkImg;
   @DroiExpose
   private String linkTitle;
   @DroiExpose
   private List<DroiReferenceObject> photos;
   @DroiReference
   private User user;
}
           

釋出日記:

CircleItem data = new CircleItem();
data.setContent(content);
data.setCreateTime(Core.getTimestamp().toString());
data.setUser(User.getCurrentUser(User.class));
data.setType("2");
data.setPhotos(createPhotos(items));
DroiPermission permission = new DroiPermission();
permission.setPublicReadPermission(true);
permission.setPublicWritePermission(false);
data.setPermission(permission);
DroiError droiError = data.save();
           

查詢擷取日記、評論、點贊資料

DroiQuery query = DroiQuery.Builder.newBuilder().limit().orderBy("createTime",false).offset(index * ).query(CircleItem.class).build();
DroiError droiError = new DroiError();
List<CircleItem> circleData = query.runQuery(droiError);
if(circleData != null && circleData.size() != ){
   for(CircleItem item:circleData) {
      //query comment data
      DroiCondition cond = DroiCondition.cond("circleId", DroiCondition.Type.EQ, item.getObjectId());
      DroiQuery cquery = DroiQuery.Builder.newBuilder().where(cond).query(CommentItem.class).build();
      List<CommentItem> cdata = cquery.runQuery(null);
      if (cdata != null && cdata.size() != ) {
         item.setComments(cdata);
      }
      //query favor data
      DroiQuery fquery = DroiQuery.Builder.newBuilder().where(cond).query(FavortItem.class).build();
      List<FavortItem> fdata = fquery.runQuery(null);
      if (fdata != null && fdata.size() != ) {
         item.setFavorters(fdata);
      }
      //get photo file uri
      List<PhotoInfo> photos = item.getPhotos();
      for (PhotoInfo photo:photos) {
         photo.setIconUrl(photo.getIcon().getUri().toString());
      }
      item.setPhotos(photos);
      //get user head url
      User user = item.getUser();
      if(user.getHeadIcon() != null){
         user.setHeadUrl(user.getHeadIcon().getUri().toString());
      }
      item.setUser(user);
   }
           

所有Save的資料都會在雲端以資料庫的形式儲存,友善下次查詢,如下圖

App中用到的資料類(CircleItem,FavortItem,CommetItem,User,Photo)在雲端都會生成相應的表格,我原來需要通過N個步驟才能實作的雲端資料存儲,現在隻需要調用DroiObject.Save就能一鍵儲存至雲端并生産相應表格。

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

三).DroiBaaS其他功能——自更新、使用者回報、統計、推送功能

手動更新和使用者回報功能可以通過在“我的”頁面點選按鈕來調用,推送功能可以在初始化時添加,統計功能按照自己的統計需求進行打點上傳資料,使用這些SDK都需要再Application的onCreate中進行初始化

public class MyApplication extends Application {

   private static Context mContext;
   @Override
   public void onCreate() {
      super.onCreate();
      mContext = getApplicationContext();
      Core.initialize(this);
      DroiFeedback.initialize(this);
      DroiUpdate.initialize(this);
      DroiAnalytics.initialize(this);
      DroiPush.initialize(this);

   }
}
           

1.版本更新:當我們的産品在重大Bug修複、功能增加、增加變現入口的時候,需要對我們的App進行更新,更新的成功率至關重要,一個好的自更新SDK能省不少事。

1) 版本更新SDK在此工程中,總共在兩處添加接口調用。 一次是在應用進入時,在入口Activity的onCreate中,主要實作在應用進入的時候自動檢查是否有更新,有更新的話會幫你下載下傳并安裝(同時支援靜默更新和強制更新),添加了如下代碼:

DroiUpdate.update(this);

還有一次是在我的頁面中,通過手動點選的方式調用來檢查雲端是否有版本需要更新:

DroiUpdate.manualUpdate(mContext)

2) 在DroiBaaS背景配置自更新,配置界面如下

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

2.使用者回報:我們需要通過意見回報來知道使用者對應用的評價以及回報,幫助我們持續改進App,通過點選按鈕進入回報的界面:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        // 其他case
        case R.id.mine_frag_update:
            //手動更新
            DroiFeedback.callFeedback(mContext);
            break;
    }
}
           

所有的使用者回報DroiBaaS的控制台都能夠看得到,你還可以選擇對某些回報進行回複,App使用者也能看到相應的回複,如圖

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

3.消息推送:通過消息推送增加應用的日活,友善活動的推廣等。隻需在Application中添加一行代碼即可實作:

DroiPush.initialize(this);
           

在DroiBaaS背景可以發送推送通知

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

4. 統計功能:大資料時代,大家對于資料也越來越看重,怎麼樣收集自己App的使用者資料,以利于分析使用者分行為,為之後産品改進以及營運提供重要的政策指導。

那麼我在App嘗試在哪些地方打點記錄使用者行為,具體如下:

1) 每個頁面的跳轉,主要是記錄頁面的通路記錄以及每個頁面的停留時間,DroiBaaS的統計SDK本身提供了記錄頁面通路的方式,我隻需要在BaseActivity裡面加上相應代碼即可。

@Override
protected void onResume() {
    super.onResume();
    DroiAnalytics.onResume(BaseActivity.this);
}

@Override
protected void onPause() {
    super.onPause();
    DroiAnalytics.onPause(BaseActivity.this);
}
           

2) 使用者點贊和評論按鈕記錄,主要是為了記錄使用者的活躍時間段以及互動的意願。按鈕的點選記錄通過DroiBaaS統計SDK的自定義事件來實作

@Override
public void onItemClick(ActionItem actionitem, int position) {
    switch (position) {
        case ://點贊、取消點贊
                DroiAnalytics.onEvent(context,"Favort");
            break;
        case ://釋出評論
                DroiAnalytics.onEvent(context,"Comment");
            break;
        default:
            break;
    }
}
           

在DroiBaaS背景能夠看到所有使用者的詳細使用資料了

使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記使用MVP+RxAndroid+DroiBaaS打造雲背景App—校園日記

開發總結

整個開發過程大概是一周時間,之後大概又花了一周時間做了一些UI和邏輯的優化,相比在企業中完成一個App的開發上線動辄兩三個月的開發周期來說,已經是很快很快了,這主要得益于幾點

  1. 前期的系統架構設計盡量全面完善,這樣給後期的Coding的工作省下不少時間
  2. 使用了不少的開源架構,省了不少的事情,而且比自己寫代碼也要穩定高效
  3. 最重要的是背景功能開發采用了DroiBaaS,相比傳統的開發方式,這個省下來的時間最多

目前我的日記功能還沒開發完成,下一步還計劃加入支付功能,可以進行打賞;再添加聊天功能,使用者之間可以進行一些互動;再添加分享視訊以及一鍵分享到其他社交平台功能。其中支付和IM功能我也咨詢過DroiBaaS,他們後續也都會支援,很贊!雲背景服務的高度封裝化,給App的開發帶來了巨大的便利,新形态的第二代一站式後端雲服務也必将是未來3到5年炙手可熱的開發工具。

文檔最後放上福利:源碼工程GitHub連結