使用MVP+RxAndroid+DroiBaaS打造雲後臺App—校園日記
爲什麼想做校園日記?
前段時間支付寶的校園日記功能火爆異常,但是卻曇花一現,可是在社會上還是引起了一陣自媒體浪潮,其實這就是人的本性的釋放,人的本性就有喜歡嘚瑟,愛表現自己的成分。在我理解中大部分能火起來的App都有能抓住人的一部分本性需求,所以我就想開發一個校園日記的App,讓它成爲最時尚的大學生社交活動App,專爲廣大在校童鞋們打造的校園日記分享軟件。可以上傳自己的自拍照片、美食圖片、心情感想等日記,實現隨時隨地分享自己,展現自己的需求。
APP功能分解:
核心功能就是個人日記的展示,其實這個最終的樣子做出來應該和朋友圈非常類似
爲什麼選用MVP+RxAndroid+DroiBaaS
技術架構選型:
對程序進行架構設計的原因,歸根到底是爲了提高生產力。通過設計使程序模塊化,能夠更簡單的讀懂code以及方便維護和測試。整體的App架構選用MVP來搭建,結合最近比較火熱的RxAndroid實現觀察者事務模式就能夠做到模塊內部的高聚合和模塊之間的低耦合,模塊內被的高聚合。
由於開發的應用需要搭建雲服務器和數據庫,所以也選用了最近比較流行的一站式後端雲服務DroiBaaS來實現所有云後臺功能。後面會講到具體用到哪些功能和怎麼來使用這些功能。
爲什麼選用MVP模式?
以上是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工程師,所以我需要一個更加簡單方便的雲後臺生產工具。我選擇雲後臺,希望能滿足我以下幾個要求:
- 服務器環境我不會搭建,所以更別提維護了,比如CentOS+Nginx+PHP+MySQL,我也只是聽說而已
- 我更不會寫Server端的Code,因爲我只會安卓App開發,而且這應用只是我個人開發,也找不到其他人來幫我寫server code
- 最好能有現成的可視化管理後臺,這樣以後管理和運營起來也方便
- 花錢儘量少,最好免費,畢竟是個人興趣和嘗試,不希望試錯成本太高
- 能一站式儘量一站式,雖然我也可以用友盟的統計+極光的推送+酷傳的代發佈+百度的廣告+啥啥啥,不過畢竟麻煩麼不是。。
綜合這些需求,我發現最近新鮮出爐的專爲APP開發者提供一站式整合雲後端的服務——BaaS比較適合我來使用。無需租用服務器和開發服務器端程序,只需集成BaaS平臺提供相應SDK就能夠實現各種雲後臺的功能,比如雲數據庫,用戶系統搭建,推送通道,用戶反饋收集,版本管理和數據統計的功能,這些功能對於App的開發以及之後的運營都是必須的。目前國內的幾家BaaS雲服務提供商,比如leanCloud、DroiBaaS、Bmob、Maxleap,目前都處於創業階段,因爲本身BaaS還處於一個概念期,到普及還需要一段時間,但是用過之後真心覺得相當好用。我相信選用BaaS來搭建App的雲後臺這將是之後個人開發者以及中小企業開發者的趨勢。
DroiBaaS相比其他幾家的優勢在於:
- 提供沙箱和生產兩種模式,沙箱完成調試再發布生產環境上線,避免了調試和測試對正式版本的數據污染
- DroiObject使用相當的方便,註解的編程方式相比其他幾家還是有不錯的便利性
- 有渠道和廣告背景,App開發出來之後能夠提供一定的推廣和變現的幫助
- 文檔比較全面而且詳細,SDK集成方式簡單,打電話諮詢過,客服態度不錯,很耐心也很專業
- 免費額度相比較其他幾家還是比較有優勢的,雖然我也不知道會用到多少,但是多一點總歸是好的
系統框架設計:
代碼架構如下
使用MVP模式來開發的好處就是代碼架構非常的清晰明瞭,個人還是比較注重代碼的邏輯性以及可讀性
用到的框架及生產工具
日記的展示界面用了SuperRecyclerView——使RecyclerView更加容易使用的Android類庫
圖片加載與緩存用了Glide
DroiBaaS的網絡請求都是基於OKHttp的,所以OKHttp和OKio是必須用到的網絡框架
Json的生成和解析用的FastJson
響應式編程用的是RxAndroid
高度整合封裝的雲服務BaaS作爲第二代雲計算的產物,爲App的雲後臺開發提供了非常便利的生產工具,提高了開發效率、縮短了上線時間、降低了開發成本,這必將是一個潮流和趨勢,我還是比較看好的。
所有的雲端功能,如推送、自更新、用戶反饋、統計、雲數據、用戶管理功能全部是用DroiBaaS的SDK來實現
日記展示UML架構設計:
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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
CirclePresenter.java
此類使用RxAndroid從雲端獲取數據再發回給View進行異步展示,在這個類中可以看出使用RxAndroid處理異步邏輯非常得心用手,推薦大家使用。
public class CirclePresenter implements CircleContract.Presenter{
private CircleContract.View view;
private static int index = 0;
public CirclePresenter(CircleContract.View view){
this.view = view;
}
@Override
public void loadData(final int loadType){
if(loadType == 1){
index = 0;
}
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
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 = 1;
private final static int TYPE_UPLOADREFRESH = 2;
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(2, 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() == 0) {
if(edittextbody != null && edittextbody.getVisibility() == View.VISIBLE){
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=0; i<circleItems.size(); i++){
if(circleId.equals(circleItems.get(i).getObjectId())){
circleItems.remove(i);
circleAdapter.notifyDataSetChanged();
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();
}
}
@Override
public void update2DeleteFavort(int circlePosition, String favortId) {
CircleItem item = (CircleItem) circleAdapter.getDatas().get(circlePosition);
List<FavortItem> items = item.getFavorters();
for(int i=0; i<items.size(); i++){
if(favortId.equals(items.get(i).getObjectId())){
items.remove(i);
circleAdapter.notifyDataSetChanged();
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();
}
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=0; i<items.size(); i++){
if(commentId.equals(items.get(i).getObjectId())){
items.remove(i);
circleAdapter.notifyDataSetChanged();
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()<45 + CircleAdapter.HEADVIEW_SIZE){
recyclerView.setupMoreListener(new OnMoreListener() {
@Override
public void onMoreAsked(int overallItemsCount, int itemsBeforeMore, int maxLastVisiblePosition) {
presenter.loadData(TYPE_UPLOADREFRESH);
}
}, 1);
}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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
雲端邏輯實現
前面已經提到了整個App沒有搭建自己的服務器也沒有編寫自己的Server端程序,整個雲端邏輯都是通過DroiBaaS來提供,具體用到了如下功能
- 使用Core SDK的來搭建App的用戶系統,管理雲數據(日記、點贊、評論、照片等)
- 使用版本更新SDK來完成應用的自更新,包括手動更新,給以後的APP更新提供通道
- 使用用戶反饋SDK來收集用戶的建議和意見,持續改進自己的產品
- 使用統計SDK來獲取統計用戶的新增、日活、以及其他自定義事件
- 使用消息推送SDK來完成應用的推送功能,以後能夠做一些營銷或者是事務通知
但是如何來使用,下面我來按步驟一一介紹。
其實官網也有快速入門文檔,根據快速入門文檔來操作,也能很快上手。
鏈接:http://www.droibaas.com/html/doc_24138.html
- 註冊DroiBaaS帳號
在網址欄輸入www.droibaas.com或者在百度輸入DroiBaaS進行搜索,打開官網後,點擊右上角的“註冊”按鈕,在跳轉頁面填入你的手機、設置密碼,收到驗證碼填入後就能激活你的DroiBaaS賬戶,然後就可以用DroiBaaS的各種SDK來輕鬆開發應用了。 - 網站後臺創建應用
使用註冊好的賬號登錄進入DroiBaaS控制檯後,點擊控制檯界面左上角“創建應用”,在彈出框輸入你應用的名稱,然後點擊確認,你就擁有了一個等待開發的應用。 - 獲取應用密鑰
選擇你要開發的應用,進入該應用的應用管理界面
在跳轉頁面,進入應用設置/安全密鑰,點擊複製,即可得到AppID -
下載和安裝DroiBaaS SDK
在官網上點擊上方的下載按鈕就能夠下載到DroiBaaS的SDK,比較好的是還能夠支持打包下載,就不需要我一個一個的去下載了,還是挺方便
安裝SDK流程比較簡單根據快速入門以及其他SDK的引導的安裝步驟來操作就OK,DroiBaaS使用的是GitHub的遠程倉庫,這樣做的好處有兩個
- 省去了拷貝aar包到lib目錄的步驟,自動從網上下載
- 每次編譯發佈的時候都能夠用到SDK的最新版本
但也存在缺點,因爲GitHub的網站被牆了,在國內訪問還是比較慢的,所以在下載aar包的時候有時候速度會比較慢。
-
使用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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
註冊用戶
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();
}
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
二).使用雲數據來管理日記內容
創建一個日記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;
@DroiExpose
private String linkImg;
@DroiExpose
private String linkTitle;
@DroiExpose
private List<DroiReferenceObject> photos;
@DroiReference
private User user;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
發佈日記:
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(3).orderBy("createTime",false).offset(index * 3).query(CircleItem.class).build()
DroiError droiError = new DroiError()
List<CircleItem> circleData = query.runQuery(droiError)
if(circleData != null && circleData.size() != 0){
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() != 0) {
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() != 0) {
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)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
所有Save的數據都會在雲端以數據庫的形式保存,方便下次查詢,如下圖
App中用到的數據類(CircleItem,FavortItem,CommetItem,User,Photo)在雲端都會生成相應的表格,我原來需要通過N個步驟才能實現的雲端數據存儲,現在只需要調用DroiObject.Save就能一鍵保存至雲端並生產相應表格。
三).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後臺配置自更新,配置界面如下
2.用戶反饋:我們需要通過意見反饋來知道用戶對應用的評價以及反饋,幫助我們持續改進App,通過點擊按鈕進入反饋的界面:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.mine_frag_update:
DroiFeedback.callFeedback(mContext);
break;
}
}
所有的用戶反饋DroiBaaS的控制檯都能夠看得到,你還可以選擇對某些反饋進行回覆,App用戶也能看到相應的回覆,如圖
3.消息推送:通過消息推送增加應用的日活,方便活動的推廣等。只需在Application中添加一行代碼即可實現:
DroiPush.initialize(this);
在DroiBaaS後臺可以發送推送通知
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 0:
DroiAnalytics.onEvent(context,"Favort");
break;
case 1:
DroiAnalytics.onEvent(context,"Comment");
break;
default:
break;
}
}
在DroiBaaS後臺能夠看到所有用戶的詳細使用數據了
開發總結
整個開發過程大概是一週時間,之後大概又花了一週時間做了一些UI和邏輯的優化,相比在企業中完成一個App的開發上線動輒兩三個月的開發週期來說,已經是很快很快了,這主要得益於幾點
- 前期的系統框架設計儘量全面完善,這樣給後期的Coding的工作省下不少時間
- 使用了不少的開源框架,省了不少的事情,而且比自己寫代碼也要穩定高效
- 最重要的是後臺功能開發採用了DroiBaaS,相比傳統的開發方式,這個省下來的時間最多
目前我的日記功能還沒開發完成,下一步還計劃加入支付功能,可以進行打賞;再添加聊天功能,用戶之間可以進行一些交互;再添加分享視頻以及一鍵分享到其他社交平臺功能。其中支付和IM功能我也諮詢過DroiBaaS,他們後續也都會支持,很贊!雲後臺服務的高度封裝化,給App的開發帶來了巨大的便利,新形態的第二代一站式後端雲服務也必將是未來3到5年炙手可熱的開發工具。
文檔最後放上福利:源碼工程GitHub鏈接