項(xiàng)目地址:https://github.com/razerdp/FriendCircle
一起擼個(gè)朋友圈吧這是本文所處文集爽撒,所有更新都會(huì)在這個(gè)文集里面哦,歡迎關(guān)注
上篇鏈接:http://www.reibang.com/p/deda1849a084
下篇鏈接:http://www.reibang.com/p/8d24f9b7a63a
在我們這個(gè)項(xiàng)目開發(fā)進(jìn)行到一半,因?yàn)槟承﹩栴},我決定停下來重構(gòu)一下窄潭。
是的赃绊,作為一枚開發(fā)译红,我覺得重構(gòu)是一件很正常的事情铛铁。還好,因?yàn)槲覀兊腣iewHolder的抽象化崎溃,重構(gòu)工程不大蜻直,也許我們可以看作是一次局部改造吧。
本次重構(gòu)的內(nèi)容如下:
- 評(píng)論控件的改進(jìn)
- viewholder和activity之間的關(guān)系推翻重做(采用mvp模式)
本篇文章將要講述的是為何要重構(gòu)以及重構(gòu)的過程袁串。
原因:
原因很簡單概而,因?yàn)榇a寫著寫著發(fā)現(xiàn)我們?cè)绞窍胨神詈蠀s越發(fā)覺不知不覺間緊耦合。
還記得上一篇文章的這張圖嗎囱修?
看起來感覺還行赎瑰,的確,在做評(píng)論之前這個(gè)結(jié)構(gòu)確實(shí)還行破镰,但要做評(píng)論的時(shí)候餐曼,這個(gè)結(jié)構(gòu)就不行了。
原因很簡單鲜漩,因?yàn)槲覀兊腸ontroller是單向的源譬。
viewholder單向操作controller,controller單項(xiàng)通知Activity更新adapter孕似。
在此之前瓶佳,我們的操作幾乎都是單向的,比如點(diǎn)贊鳞青。
點(diǎn)贊的操作就是:贊一個(gè)→請(qǐng)求→請(qǐng)求完成→刷新數(shù)據(jù)→展示→完霸饲。
這沒問題,互不涉及臂拓,互不干擾厚脉,各自完成各自的工作。
但到現(xiàn)在胶惰,我們需要評(píng)論了傻工,這時(shí)候問題來了:
- 評(píng)論控件不可能在ViewHolder,肯定是在Activity層
- 評(píng)論等操作的觸發(fā)條件卻是在ViewHolder孵滞,比如點(diǎn)了某個(gè)評(píng)論進(jìn)行回復(fù)等中捆。
那么問題就出在這個(gè)單向聯(lián)系的問題,很明顯我們現(xiàn)在需要的是ViewHolder調(diào)用Activity的某個(gè)方法來控制評(píng)論框坊饶。如果按照現(xiàn)在這種結(jié)構(gòu)做下去泄伪,得到的,只能是這幾個(gè)類的聯(lián)系越來越緊密匿级。
所以蟋滴,我們需要采取一下別的方式了。
于是就有了這次的重構(gòu)痘绎。
重構(gòu):
說到各種模式津函,對(duì)于老大MVC,在越來越重的需求里孤页,發(fā)現(xiàn)Model層的負(fù)擔(dān)太重尔苦,又要處理事件觸發(fā)又要處理數(shù)據(jù)什么的。
于是就有了最近挺火的MVP行施,關(guān)于MVP允坚,網(wǎng)上有很多很多的例子和解析,這里就不多說明了悲龟。
當(dāng)然屋讶,還有MVVM,因?yàn)镈ataBinding而火起來须教,也許以后在java代碼里調(diào)用view皿渗,塞入數(shù)據(jù)這種代碼可能很少。
但目前起碼不會(huì)轻腺,所以為了防止大破壞乐疆,我們采取MVP的模式。
首先贬养,我們需要干掉舊的挤土,拿起手中的砍刀(del鍵),到我們項(xiàng)目的app下把controller這個(gè)包包給砍掉误算。
然后仰美,我們把新歡mvp這個(gè)包包弄進(jìn)來-V-(舊的不去新的不來←_←)
于是我們的結(jié)構(gòu)就變成了這樣(重新整理過):
其他地方改動(dòng)不大迷殿,app包下現(xiàn)在如下:
- app包主要存放各種基類或配置,接口等
- adapter:我們的朋友圈adapter基類就在這里安家哦
- config:各種配置參數(shù)咖杂,比如下拉刷新模式的定義庆寺,是否點(diǎn)贊的狀態(tài)定義什么的。
- https:volley的封裝诉字,嗯懦尝。。壤圃。咱們只是從network換成了https陵霉,其實(shí)我覺得應(yīng)該換成http,畢竟http和https是兩種東東對(duì)吧
- interfaces:接口喲
- mvp:本次的主角
其他包換湯不換藥伍绳,我們主要看mvp踊挠。
按照規(guī)范,咱們弄一個(gè)model/presenter/view三個(gè)包包墨叛,然后m層里面放的bean包和實(shí)現(xiàn)類的包止毕,而根包下放的則是接口。
其余相同
Step 1:View
首先我們定義一個(gè)接口漠趁,叫做DynamicView扁凛,該接口實(shí)現(xiàn)三個(gè)方法:
- 刷新點(diǎn)贊數(shù)據(jù)
- 刷新評(píng)論數(shù)據(jù)
- 展示評(píng)論框
/**
* Created by 大燈泡 on 2016/3/17.
* mvp-視圖層
* 僅用于更新/展示數(shù)據(jù)和部分的用戶交互
*/
public interface DynamicView{
// 點(diǎn)贊刷新
void refreshPraiseData(int currentDynamicPos,
@CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList);
// 評(píng)論刷新
void refreshCommentData(int currentDynamicPos,
@RequestType.CommentRequestType int requestType, @NonNull List<CommentInfo> commentList);
// 評(píng)論框展示
void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo);
}
在上一篇文章里,我說過暴露了MomentsInfo是個(gè)很不好的方案闯传,所以這次我們只需要把當(dāng)前操作的動(dòng)態(tài)(viewholder)所在的位置拋出去就好了谨朝,外面可以通過adapter得到對(duì)應(yīng)數(shù)據(jù)。
我們view實(shí)現(xiàn)的前兩個(gè)個(gè)方法僅僅用于數(shù)據(jù)刷新甥绿,數(shù)據(jù)來源當(dāng)然是我們親愛的model了字币,第三個(gè)方法則是view層本該承擔(dān)的部分交互。
寫完View共缕,我們接下來寫寫model洗出。
Step 2:Model
對(duì)于Model,我們可以直接選擇一個(gè)類图谷,但對(duì)于功能復(fù)雜點(diǎn)的翩活,我們還是應(yīng)該使用接口來松耦合。
那啥便贵,多組合少繼承菠镇,接口化高抽象。這才是一個(gè)優(yōu)秀項(xiàng)目所應(yīng)該擁有的對(duì)吧(當(dāng)然承璃,我這個(gè)項(xiàng)目并非屬于優(yōu)秀項(xiàng)目-V-利耍,但我們可以盡力做到優(yōu)秀)
廢話不多說,首先創(chuàng)建一個(gè)針對(duì)點(diǎn)贊用的接口:
/**
* Created by 大燈泡 on 2016/3/17.
* model - 點(diǎn)贊接口
*/
public interface PraiseModel {
//點(diǎn)贊
void addPraise(int currentDynamicPos, long userid, long dynamicid);
// 取消點(diǎn)贊
void cancelPraise(int currentDynamicPos, long userid, long dynamicid);
}
點(diǎn)贊接口的參數(shù)跟我們之前的controller并沒有很大的區(qū)別。
然后創(chuàng)建我們的實(shí)現(xiàn)類:DynamicModelImpl
/**
* Created by 大燈泡 on 2016/3/17.
* mvp - model
* 復(fù)雜邏輯的操作/請(qǐng)求/耗時(shí)等隘梨,處理后的數(shù)據(jù)提供
*
* 若過于復(fù)雜的操作程癌,可能需要接口來松耦合
*/
public class DynamicModelImpl implements BaseResponseListener, PraiseModel {
private DynamicResultCallBack callBack;
public DynamicModelImpl(DynamicResultCallBack callBack) {
this.callBack = callBack;
}
//=============================================================request
private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;
//=============================================================public methods
@Override
public void addPraise(int currentDynamicPos, long userid, long dynamicid) {
}
@Override
public void cancelPraise(int currentDynamicPos, long userid, long dynamicid) {
}
//=============================================================request methods
@Override
public void onStart(BaseResponse response) {
}
@Override
public void onStop(BaseResponse response) {
}
@Override
public void onFailure(BaseResponse response) {
}
@Override
public void onSuccess(BaseResponse response) {
}
}
我們的實(shí)現(xiàn)類最基本需要實(shí)現(xiàn)一個(gè)接口:BaseResponseListener
因?yàn)檎?qǐng)求都在這里執(zhí)行,所以我們當(dāng)初設(shè)計(jì)volley時(shí)就單獨(dú)抽象出來了出嘹,這樣就可以任意類使用席楚。
同時(shí)實(shí)現(xiàn)我們的model接口,于是就得到了上述代碼的結(jié)構(gòu)了税稼。
最后我們還需要一個(gè)callback,用來回調(diào)通知presenter垮斯,告訴他咱們工作完成了郎仆,請(qǐng)上報(bào)總司令進(jìn)行下一階段任務(wù)的安排。
Step 3:Presenter
Presenter的主要作用是用來溝通M層和V層兜蠕,所以我們的Presenter需要持有V層和M層扰肌,但P層和V層的溝通主要還是通過接口,果然接口拯救世界靶苎睢(畢竟咱們沒有多繼承對(duì)吧)
于是我們的Presenter這么寫:
/**
* Created by 大燈泡 on 2016/3/17.
* mvp - 引導(dǎo)層
* 用于接收view的操作命令分發(fā)到model層實(shí)現(xiàn)
*/
public class DynamicPresenterImpl implements DynamicResultCallBack {
private DynamicModelImpl mModel;
private DynamicView mView;
public DynamicPresenterImpl(DynamicView view) {
mView = view;
mModel = new DynamicModelImpl(this);
}
// 點(diǎn)贊
public void addPraise(int curDynamicPos, long dynamicId) {
mModel.addPraise(curDynamicPos, LocalHostInfo.INSTANCE.getHostId(), dynamicId);
}
// 取消贊
public void cancelPraise(int curDynamicPos, long dynamicId) {
mModel.cancelPraise(curDynamicPos, LocalHostInfo.INSTANCE.getHostId(), dynamicId);
}
@Override
public void onResultCallBack(BaseResponse response) {
}
}
其中DynamicResultCallBack我們說過曙旭,是用來回調(diào)接受M層處理后的數(shù)據(jù)的,它只有一個(gè)方法:onResultCallBack(BaseResponse response) { }晶府,當(dāng)請(qǐng)求成功后回調(diào)桂躏。
至此,我們的MVP結(jié)構(gòu)大致寫好川陆。接下來需要做的剂习,就是去Activity改造
Step 4:Activity(View)改造
回到我們的FriendCircleDemoActivity,首先我們實(shí)現(xiàn)DynamicView這個(gè)接口较沪,并在onCreate里面把presenter給new出來:
/**
* Created by 大燈泡 on 2016/2/25.
* 朋友圈demo窗口
*/
public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicView, View.OnClickListener {
private FriendCircleRequest mCircleRequest;
private DynamicPresenterImpl mPresenter;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPresenter = new DynamicPresenterImpl(this);
initView();
initReq();
//mListView.manualRefresh();
}
...
//=============================================================mvp - view's method
@Override
public void refreshPraiseData(int currentDynamicPos,
@CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList) {
}
@Override
public void refreshCommentData(int currentDynamicPos,
@RequestType.CommentRequestType int requestType,
@NonNull List<CommentInfo> commentList) {
}
@Override
public void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo) {
}
}
事實(shí)上鳞绕,為了更方便的針對(duì)生命期控制,我們的presenter理論上來說還需要寫上對(duì)應(yīng)生命期方法尸曼,但们何,這里咱們就算了,畢竟玩來玩去也就一個(gè)Activity控轿。
我們的view實(shí)現(xiàn)了DynamicView的三個(gè)方法冤竹,這里將會(huì)是在數(shù)據(jù)請(qǐng)求完成后presenter通知我們更新的。
事實(shí)上我們可以現(xiàn)在就補(bǔ)全我們的代碼了解幽,比如點(diǎn)贊:
@Override
public void refreshPraiseData(int currentDynamicPos,
@CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList) {
MomentsInfo info = mAdapter.getItem(currentDynamicPos);
if (info != null) {
info.dynamicInfo.praiseState = praiseState;
if (info.praiseList != null) {
info.praiseList.clear();
info.praiseList.addAll(praiseList);
}
else {
info.praiseList = praiseList;
}
}
mAdapter.notifyDataSetChanged();
}
相比于以前的代碼贴见,這次的代碼簡潔了很多,現(xiàn)在回頭看看歷史代碼躲株,我都不敢看了TAT片部。。。
同樣档悠,我們也可以提前寫好其他實(shí)現(xiàn)的代碼廊鸥。這里就略過了。
Step 5:補(bǔ)全代碼
首先我們補(bǔ)全Model的代碼辖所,事實(shí)上惰说,Model的代碼跟上篇寫的controller差異不大,這里就展示一下點(diǎn)贊的代碼缘回,取消贊也是差不多的吆视。
@Override
public void addPraise(int currentDynamicPos, long userid, long dynamicid) {
if (mDynamicAddPraiseRequest == null) {
mDynamicAddPraiseRequest = new DynamicAddPraiseRequest();
mDynamicAddPraiseRequest.setOnResponseListener(this);
mDynamicAddPraiseRequest.setRequestType(RequestType.ADD_PRAISE);
}
mDynamicAddPraiseRequest.setCurrentDynamicPos(currentDynamicPos);
mDynamicAddPraiseRequest.userid = userid;
mDynamicAddPraiseRequest.dynamicid = dynamicid;
mDynamicAddPraiseRequest.execute();
}
通過request我們需要把當(dāng)前的動(dòng)態(tài)位置傳進(jìn)去,然后在完成后把值設(shè)置到BaseResponse回調(diào)給presenter.
接下來補(bǔ)全Presenter的代碼:
Presenter相對(duì)輕松很多酥宴,因?yàn)閺?fù)雜的都在model完成了啦吧,presenter做的僅僅是分發(fā)一下:
我們主要關(guān)注callback里面:
@Override
public void onResultCallBack(BaseResponse response) {
if (mView != null) {
final int curDynamicPos = (int) response.getData();
switch (response.getRequestType()) {
case RequestType.ADD_PRAISE:
List<UserInfo> praiseList = (List<UserInfo>) response.getDatas();
mView.refreshPraiseData(curDynamicPos, CommonValue.HAS_PRAISE, praiseList);
break;
case RequestType.CANCEL_PRAISE:
List<UserInfo> praiseList2 = (List<UserInfo>) response.getDatas();
mView.refreshPraiseData(curDynamicPos, CommonValue.NOT_PRAISE, praiseList2);
break;
}
}
}
callback里面我們針對(duì)不同的請(qǐng)求類型通知view進(jìn)行不同的數(shù)據(jù)刷新操作,至于view的代碼拙寡,在上面我們已經(jīng)寫過了授滓。
Finally
這次也算是我的第一次使用MVP,看著教程對(duì)擼的肆糕,也不知道寫的對(duì)不對(duì)般堆,但感覺MVP在結(jié)構(gòu)上真的看的很舒服,職責(zé)分明诚啃,維護(hù)很爽淮摔。
噢,忘了绍申,咱么重構(gòu)還有第二點(diǎn)噩咪,評(píng)論控件的重構(gòu)。极阅。胃碾。。
這里就放兩段代碼說明一下吧:
public void setCommentText(CommentInfo info) {
if (info == null) return;
boolean hasContent = false;
//根據(jù)hashCode判斷內(nèi)容是否一致
if (key == 0) {
key = info.hashCode();
}
else {
hasContent = (key == info.hashCode());
}
if (!hasContent) {
key = info.hashCode();
setText("");
setTag(info);
createCommentStringBuilder(info);
}
else {
try {
setText(mSpannableStringBuilderAllVer);
} catch (NullPointerException e) {
e.printStackTrace();
Log.e(TAG, "雖然在下覺得不可能會(huì)有這個(gè)情況筋搏,但還是捕捉下吧仆百,萬一被打臉呢。奔脐。俄周。");
}
}
}
這是之前的代碼
```java
public void setCommentText(CommentInfo info) {
if (info == null) return;
try {
setTag(info);
createCommentStringBuilder(info);
} catch (NullPointerException e) {
e.printStackTrace();
Log.e(TAG, "雖然在下覺得不可能會(huì)有這個(gè)情況,但還是捕捉下吧髓迎,萬一被打臉呢峦朗。。排龄。");
}
}
private void createCommentStringBuilder(@NonNull CommentInfo info) {
if (mSpannableStringBuilderAllVer == null) {
mSpannableStringBuilderAllVer = new SpannableStringBuilderAllVer();
}
else {
mSpannableStringBuilderAllVer.clear();
mSpannableStringBuilderAllVer.clearSpans();
}
...
}
這是現(xiàn)在的代碼波势。
兩者不同的區(qū)別在于mSpannableStringBuilderAllVer這個(gè)對(duì)象。
在之前我的想法是這樣的:
如果更新前后兩個(gè)commentInfo對(duì)象一致,同時(shí)mSpannableStringBuilderAllVer存在尺铣,則直接使用拴曲,無需更新
但因?yàn)槲覀冡槍?duì)CommentWidget進(jìn)行過優(yōu)化,也就是對(duì)commentwidget進(jìn)行removeView入池文章點(diǎn)我
所以導(dǎo)致了info可能是一樣凛忿,但呈現(xiàn)起來卻是本該屬于這個(gè)動(dòng)態(tài)的評(píng)論跑到了別的動(dòng)態(tài)里了澈灼,原因就是這個(gè)池里面的對(duì)象引用的是同一個(gè)。
所以這次我們換成無論如何都需要重新進(jìn)行text的賦值店溢,問題得以解決叁熔。
下一篇,我們完成評(píng)論功能床牧。
ps:剛剛弄了一下數(shù)據(jù)庫者疤,有一條動(dòng)態(tài)是50條評(píng)論哦。叠赦。。革砸。對(duì)于這個(gè)假數(shù)據(jù)的構(gòu)造除秀,每天都在精分現(xiàn)場(chǎng)呢。算利。册踩。。等評(píng)論功能弄好后效拭,就不需要那么痛苦了暂吉。。缎患。雖然每個(gè)評(píng)論都是由“羽翼君”這位用戶進(jìn)行評(píng)論←_←