廢話不多說(shuō),下面開始:
說(shuō)起封裝,有的人覺(jué)得很簡(jiǎn)單,有的人會(huì)覺(jué)得無(wú)從下手,無(wú)論是哪種情況,想要寫出比好的封裝,最簡(jiǎn)單的方法就是被坑了。為什么這么說(shuō)呢,因?yàn)橹挥挟?dāng)你自己被自己寫的代碼坑到時(shí),你才知道這里該封裝,該如何封裝,坑的越痛,記憶越深刻,別問(wèn)我怎么知道的,我不想說(shuō)。當(dāng)然,既然我在寫這篇文章,目的就是希望萌新們少踩點(diǎn)坑,有些東西如果能夠一開始就做好,豈不美哉。
??上面說(shuō)的都是比較虛的東西,下面我們來(lái)點(diǎn)干貨,到底在真正的項(xiàng)目中,我們?cè)谀切┑胤叫枰庋b?應(yīng)該怎么去封裝?
1.對(duì)第三方庫(kù)的封裝
使用了蠻多第三方庫(kù),也加了不少群,發(fā)現(xiàn)很多萌新(甚至是工作了近一年的準(zhǔn)初級(jí)程序猿)在使用別人的庫(kù)時(shí),都喜歡直接照著文檔就在自己的項(xiàng)目中到處使用,完全沒(méi)有意識(shí)到這么寫的隱患。如果一個(gè)庫(kù)需有很多配置參數(shù),相信大部分人還是知道,應(yīng)該自己寫個(gè)工具類來(lái)進(jìn)行統(tǒng)一配置,但是當(dāng)一個(gè)庫(kù)的用法比較簡(jiǎn)單時(shí),許多人就喜歡直接到處寫了,這樣其實(shí)會(huì)有很多問(wèn)題的。
??首先,耦合過(guò)強(qiáng)不易替換。不對(duì)第三方進(jìn)行封裝,最直接的影響就是不能夠快速替換,以我們最常見的圖片加載庫(kù)來(lái)說(shuō),早幾年比較流行的圖片加載框架是一個(gè)叫做ImageLoader的庫(kù)。試想一下,一個(gè)app中,使用圖片加載的地方,少則十幾處,多則幾十處,如果你每個(gè)地方都引用了ImageLoader這個(gè)類,那么當(dāng)你某一天需要換掉這個(gè)庫(kù)的時(shí)候會(huì)發(fā)生什么?沒(méi)錯(cuò),你需要到處修改,雖然studio有重構(gòu)快捷鍵和全局查找功能,可以方便的找到所有引用的地方,但是明明可以靠代碼解決的問(wèn)題,又何必給自己挖坑呢。
??其次,不易修改。有些庫(kù),可能本身配置就比較少,剛好它的默認(rèn)配置就能滿足你當(dāng)前的需求,這時(shí),很多人也就直接用了。那么,你們有沒(méi)有想過(guò),后面你發(fā)現(xiàn)需要修改某一個(gè)屬性配置時(shí)要怎么辦呢?沒(méi)錯(cuò)和上面類似的,你又要去到處該,簡(jiǎn)直就是地獄了。
??那么我們?cè)撊绾螌?duì)第三方進(jìn)行封裝呢?這里只說(shuō)幾點(diǎn)我認(rèn)為的基本要求,1.第三方庫(kù)的所有類只能出現(xiàn)在自己的封裝類中,項(xiàng)目里其他任何地方不應(yīng)該對(duì)第三方庫(kù)產(chǎn)生引用,只有這樣才能做到,你想替換這個(gè)庫(kù)時(shí),只進(jìn)行最少的代碼修改。2.如果項(xiàng)目本身對(duì)一個(gè)庫(kù)的配置沒(méi)有太多變化的時(shí)候,盡量不要把配置過(guò)多的暴露到封裝類之外,這樣做可以保證封裝類調(diào)用的簡(jiǎn)潔性。
??下面以一個(gè)下拉刷新庫(kù)為例進(jìn)行簡(jiǎn)單封裝
??刷新庫(kù)SmartRefreshLayout--https://github.com/scwang90/SmartRefreshLayout
??許多萌新,基本上拿到一個(gè)庫(kù)就開始照著別人的文檔往項(xiàng)目加了,比如這樣用
public class RefreshActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//下面示例中的值等于默認(rèn)值
SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
refreshLayout.setDragRate(0.5f);//顯示下拉高度/手指真實(shí)下拉高度
refreshLayout.setRefreshHeader(new ClassicsHeader(this));//設(shè)置Header
refreshLayout.setRefreshFooter(new ClassicsFooter(this));//設(shè)置Footer
refreshLayout.autoRefresh();//自動(dòng)刷新
}
}
調(diào)試了一下,沒(méi)什么問(wèn)題,然后就開始在項(xiàng)目各處使用。突然有一天,產(chǎn)品經(jīng)理發(fā)話了,這個(gè)刷新樣式不好看,重新設(shè)計(jì),然后你就一臉懵逼了,你這個(gè)設(shè)置寫的到處都是,只能去一個(gè)一個(gè)改了。當(dāng)然這個(gè)問(wèn)題其實(shí)是個(gè)很基礎(chǔ)的問(wèn)題,基本上有個(gè)把月經(jīng)驗(yàn)的同學(xué)都知道需要統(tǒng)一配置,所以也許他們會(huì)這么寫:
public class RefreshActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//下面示例中的值等于默認(rèn)值
SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
RefreshLayoutUtil.refreshLayoutConfig(refreshLayout ,this);
refreshLayout.autoRefresh();//自動(dòng)刷新
}
}
=========================分割線==================================
public class RefreshLayoutUtil {
public static void refreshLayoutConfig (SmartRefreshLayout refreshLayout, Context context) {
refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
refreshLayout.setDragRate(0.5f);//顯示下拉高度/手指真實(shí)下拉高度
refreshLayout.setRefreshHeader(new ClassicsHeader(context));//設(shè)置Header
refreshLayout.setRefreshFooter(new ClassicsFooter(context));//設(shè)置Footer
}
}
這也就是我上面說(shuō)的對(duì)第三庫(kù)進(jìn)行統(tǒng)一封裝的一部分,我們這里采用的是對(duì)通用配置進(jìn)行統(tǒng)一封裝,好了現(xiàn)在產(chǎn)品經(jīng)理告訴我,你想改幾次樣式?來(lái)來(lái),隨便改,皺下眉頭算我輸。
??擁有了上面的小技巧,大家終于能輕松一點(diǎn)。然而,你剛躺下,被子都沒(méi)捂熱,突然產(chǎn)品經(jīng)理一個(gè)電話,XX你的app刷新就崩潰了。然后你就開始debug看了,最后發(fā)現(xiàn)不是你的問(wèn)題,是這個(gè)庫(kù)本身就有bug,然后你打算上github上提給作者,一看,我去,作者竟然已經(jīng)宣布停止維護(hù)了。那怎么辦?有些同學(xué)會(huì)選擇把庫(kù)下載下來(lái),自己試著修改,不過(guò)礙于水平有限,大部分人在嘗試了一下修改然后無(wú)果后都會(huì)選擇換成另一個(gè)庫(kù)。
??好嘛,我們現(xiàn)在又來(lái)?yè)Q庫(kù)試試,去掉庫(kù)依賴,我去,竟然有幾十個(gè)類和xml報(bào)錯(cuò),這次是真的絕望了,看來(lái)只能刪代碼跑路了。。。
??開個(gè)玩笑,我們肯定不可能跑路的,只能硬著頭皮改。關(guān)鍵是改之前你要考慮,下次再出這個(gè)問(wèn)題怎么辦呢?當(dāng)然還是通過(guò)封裝來(lái)解決了:
public class RefreshLayout extends SmartRefreshLayout {
public RefreshLayout (Context context) {
super(context);
}
public RefreshLayout (Context context, AttributeSet attrs) {
super(context, attrs);
}
public RefreshLayout (Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnRefreshListener (final RefreshListener refreshListener) {
setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
refreshListener.onRefresh((RefreshLayout) refreshlayout);
}
});
setOnLoadmoreListener(new OnLoadmoreListener() {
@Override
public void onLoadmore (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
refreshListener.onLoading((RefreshLayout) refreshlayout);
}
});
}
public void startRefresh () {
super.autoRefresh();
}
public void startLoadMore () {
super.autoLoadmore();
}
public interface RefreshListener {
void onRefresh (RefreshLayout refreshLayout);
void onLoading (RefreshLayout refreshLayout);
}
public void finishRefreshing () {
if (!isRefreshing()) {
return;
}
super.finishRefresh();
}
public SmartRefreshLayout finishLoadmore () {
if (!isLoading()) {
return null;
}
return super.finishLoadmore();
}
.......
省略部分配置代碼
.......
}
可能有的同學(xué)一看這代碼就瘋了,說(shuō),這不是閑的蛋疼嘛,為什么要自己繼承一次,而且還把父類的方法重新封裝了一次?
??其實(shí)一開始我已經(jīng)說(shuō)得很清楚了,對(duì)于第三庫(kù)的封裝就是要避免外界直接引用第三庫(kù)的任何類或方法,試想一下,如果我們一開始就是這么寫的,當(dāng)你去掉了這個(gè)刷新庫(kù)的依賴時(shí)哪里還會(huì)報(bào)錯(cuò)?沒(méi)錯(cuò),如果你遵照著上面的原則進(jìn)行封裝,你會(huì)發(fā)現(xiàn)僅僅只有這個(gè)類會(huì)報(bào)錯(cuò),其他任何類和xml布局都不會(huì)報(bào)錯(cuò),不報(bào)錯(cuò)也就說(shuō)明其他地方不用做任何修改,你只需要接入你的新庫(kù),然后修改自己這個(gè)封裝類,也許你的新庫(kù)中刷新方法不叫autoRefresh,就叫refresh,不過(guò)沒(méi)什么關(guān)系,你只需要把super.autoRefresh改成super.refresh就行了。其他的類其實(shí)依然是調(diào)用你的封裝方法startRefresh,所以立即就能生效了。
上面說(shuō)的是對(duì)第三方控件的封裝,下面來(lái)說(shuō)說(shuō)對(duì)第三方工具類類型庫(kù)的封裝,其實(shí)原理都一樣,只要具備了上面的封裝思想,再來(lái)封裝工具類也就舉一反三了。下面以一個(gè)圖片選擇庫(kù)為例:
??https://github.com/LuckSiege/PictureSelector
public static void chooseImage(Activity activity,int maxCount){
PictureSelector.create(activity)
.openGallery(PictureMimeType.ofImage())//全部.PictureMimeType.ofAll()、圖片.ofImage()、視頻.ofVideo()
// .theme()//主題樣式(不設(shè)置為默認(rèn)樣式) 也可參考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum(maxCount)// 最大圖片選擇數(shù)量 int
.minSelectNum(0)// 最小選擇數(shù)量 int
.imageSpanCount(4)// 每行顯示個(gè)數(shù) int
.selectionMode(PictureConfig.MULTIPLE)// 多選 or 單選 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.previewImage(false)// 是否可預(yù)覽圖片 true or false
.isCamera(false)// 是否顯示拍照按鈕 true or false
.isZoomAnim(true)// 圖片列表點(diǎn)擊 縮放效果 默認(rèn)true
.sizeMultiplier(0.5f)// glide 加載圖片大小 0~1之間 如設(shè)置 .glideOverride()無(wú)效
.setOutputCameraPath("/CustomPath")// 自定義拍照保存路徑,可不填
.enableCrop(false)// 是否裁剪 true or false
.compress(true)// 是否壓縮 true or false
.compressMode(PictureConfig.LUBAN_COMPRESS_MODE)//系統(tǒng)自帶 or 魯班壓縮 PictureConfig.SYSTEM_COMPRESS_MODE or LUBAN_COMPRESS_MODE
// .glideOverride()// int glide 加載寬高,越小圖片列表越流暢,但會(huì)影響列表圖片瀏覽的清晰度
.withAspectRatio(1,1)// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定義
.hideBottomControls(true)// 是否顯示uCrop工具欄,默認(rèn)不顯示 true or false
.isGif(true)// 是否顯示gif圖片 true or false
.freeStyleCropEnabled(true)// 裁剪框是否可拖拽 true or false
.circleDimmedLayer(false)// 是否圓形裁剪 true or false
.showCropFrame(true)// 是否顯示裁剪矩形邊框 圓形裁剪時(shí)建議設(shè)為false true or false
.showCropGrid(false)// 是否顯示裁剪矩形網(wǎng)格 圓形裁剪時(shí)建議設(shè)為false true or false
.openClickSound(false)// 是否開啟點(diǎn)擊聲音 true or false
.cropCompressQuality(80)// 裁剪壓縮質(zhì)量 默認(rèn)90 int
.compressMaxKB(200)//壓縮最大值kb compressGrade()為L(zhǎng)uban.CUSTOM_GEAR有效 int
// .compressWH() // 壓縮寬高比 compressGrade()為L(zhǎng)uban.CUSTOM_GEAR有效 int
// .cropWH()// 裁剪寬高比,設(shè)置如果大于圖片本身寬高則無(wú)效 int
.rotateEnabled(false) // 裁剪是否可旋轉(zhuǎn)圖片 true or false
.scaleEnabled(true)// 裁剪是否可放大縮小圖片 true or false
.forResult(PictureConfig.CHOOSE_REQUEST);//結(jié)果回調(diào)onActivityResult code
}
好了,完了。這也叫封裝?沒(méi)錯(cuò),這也叫封裝了。只是相對(duì)來(lái)說(shuō),我們這種封裝算是比較死的,因?yàn)楦鞣N屬性都沒(méi)有給出修改的方法,但是你要相信就算是你僅僅這樣簡(jiǎn)單封裝了一下,也絕對(duì)好過(guò)你到處去引用第三方庫(kù)的類很多。原因嘛就不再重復(fù)了,上面已經(jīng)說(shuō)得很清楚了,這個(gè)封裝雖然簡(jiǎn)陋,但是你想讓他變靈活的話大可以自己修改下。對(duì)于像這種屬性比較多的庫(kù)建議使用builder模式進(jìn)行封裝,這樣調(diào)用和修改都比較簡(jiǎn)單,具體封裝代碼這里就不再寫了,有興趣的同學(xué)可以自己嘗試下。
注意:并不是所有庫(kù)都一定要進(jìn)行封裝,有些庫(kù)可能會(huì)由于某些原因沒(méi)有辦法進(jìn)行很好的封裝,比如Butter knife,rxjava。當(dāng)然我這里說(shuō)的不能很好的封裝并不是指都完全不封裝,而是指可能無(wú)法完全按照我上面說(shuō)的幾個(gè)原則進(jìn)行封裝,比如rxjava,他涉及很多類和方法,你要完全達(dá)到使用時(shí)不直接調(diào)用它的類基本上要封裝很多代碼,這時(shí)我們可以退而求其次,只封裝一些通用配置屬性。而像Butter knife這個(gè)庫(kù)則基本上處于完全無(wú)法封裝的狀態(tài)。
??說(shuō)句題外話,在有其他解決方案的時(shí)候強(qiáng)烈不建議使用類似于Butter knife這種庫(kù)。Butter knife除了能偷懶少些幾個(gè)findView以外,基本上毫無(wú)卵用,但是這種庫(kù)最大的問(wèn)題就在于耦合很高,你的項(xiàng)目一旦加入了這種東西,想再去掉就很麻煩了,這也是我為什么一直不用Butter knife的原因,如果你真的很討厭寫findView,我建議你使用官方的dataBinding或者使用kotlin開發(fā),他們都能解決findView的問(wèn)題,而且效果遠(yuǎn)比Butter knife好多了。當(dāng)然了,這里純屬個(gè)人見解,如果你覺(jué)得Butter knife好用的話,那就繼續(xù)用吧
??如何判斷一個(gè)第三方庫(kù)該不該封裝?能不能封裝?很簡(jiǎn)單,如果一個(gè)庫(kù)需要的封裝代碼過(guò)多時(shí),就可以考慮只簡(jiǎn)單封裝一下,讓外界引用幾個(gè)第三方庫(kù)的類也沒(méi)關(guān)系。當(dāng)然了,你要會(huì)風(fēng)險(xiǎn)評(píng)估,如果這個(gè)庫(kù)有大概率會(huì)被替換掉的話,就要一開始就封裝好。目前來(lái)說(shuō),像rxjava這種,完全封裝的話,意義就不大,首先它涉及的東西太多,要封裝肯定需要自己寫很多東西,其次這種庫(kù)基本沒(méi)什么可替代的,所以對(duì)于你來(lái)說(shuō)一般只會(huì)考慮用不用,如果你本來(lái)用了,現(xiàn)在要去掉它的話,不管你封不封裝,基本上都要大面積修改代碼,那花大力氣封裝也就是浪費(fèi)時(shí)間了。
2.項(xiàng)目中的邏輯封裝
這里來(lái)說(shuō)說(shuō)對(duì)項(xiàng)目中部分邏輯的封裝,其實(shí)有了上面對(duì)第三方庫(kù)的封裝,那么對(duì)自己項(xiàng)目中什么東西該封裝,大家心里應(yīng)該都有點(diǎn)數(shù)了,還是直接舉個(gè)栗子,請(qǐng)看如下代碼:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
nameTV.setFocusable(false);
nameTV.setClickable(false);
sexTV.setFocusable(false);
sexTV.setClickable(false);
ageTV.setFocusable(false);
ageTV.setClickable(false);
??這是一個(gè)很常見的需求,某些界面可能既是編輯頁(yè)面又是詳情界面,這時(shí)可能就需要我們動(dòng)態(tài)改變輸入框的可點(diǎn)擊狀態(tài),上面這么寫看上去似乎除了比較丑陋以外并沒(méi)有太大問(wèn)題,但是現(xiàn)在需求又雙叒變了,進(jìn)入編輯時(shí)我們要清空每個(gè)輸入框文本并更改提示文字,好嘛,你可能不得不這么改:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
nameTV.setFocusable(false);
nameTV.setClickable(false);
nameTV.setText("");
nameTV.setHint("請(qǐng)輸入");
sexTV.setFocusable(false);
sexTV.setClickable(false);
sexTV.setText("");
sexTV.setHint("請(qǐng)輸入");
ageTV.setFocusable(false);
ageTV.setClickable(false);
ageTV.setText("");
ageTV.setHint("請(qǐng)輸入");
??有木有感覺(jué)很蛋疼?沒(méi)有?那我告訴你,我這個(gè)界面其實(shí)有15個(gè)輸入框,好嘛,你去改改看。那么我們換一種方法來(lái)寫試試:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
editTextConfig(nameTV,sexTV,ageTV);
=========================分割線=======================================
private void editTextConfig (EditText... editTexts) {
if (editTexts == null || editTexts.length == 0) {
return;
}
for (EditText editText : editTexts) {
editText.setFocusable(false);
editText.setClickable(false);
editText.setText("");
editText.setHint("請(qǐng)輸入");
}
}
??這下你想加任何配置都只需要修改editTextConfig方法即可,你15個(gè)控件也罷,只需要在參數(shù)列表傳入就行??梢钥闯觯@里我們僅僅是封裝一個(gè)簡(jiǎn)單的內(nèi)部方法,就大大提高了代碼的可維護(hù)性,相信大家看了這個(gè)例子已經(jīng)明白封裝有多么重要了。
??好了,該講的也差不多講完了,項(xiàng)目中的封裝,這個(gè)真的要靠自己去實(shí)踐,我這里也只能分享一個(gè)簡(jiǎn)單的例子,希望大家能夠舉一反三。最后就說(shuō)說(shuō)我個(gè)人覺(jué)得項(xiàng)目中那些東西是必須封裝的:
- 復(fù)雜的邏輯代碼。把相同的復(fù)雜邏輯代碼到處寫,改的是后絕對(duì)虐哭你。
- 出現(xiàn)頻率非常高的代碼。例如對(duì)項(xiàng)目中的所有金額進(jìn)行小數(shù)兩位四舍五入,這個(gè)邏輯本身很簡(jiǎn)單,代碼也只有一行的樣子,當(dāng)時(shí)如果你到處寫的話,突然產(chǎn)品經(jīng)理抽風(fēng)要精確到3位,你怎么辦?所以說(shuō),出現(xiàn)頻率很高的代碼,哪怕只有一行,也應(yīng)該封裝一次。
- 特殊的業(yè)務(wù)邏輯代碼。某些項(xiàng)目中特有的業(yè)務(wù)邏輯,可能本身出現(xiàn)次數(shù)不是太多4到5次吧,邏輯也不復(fù)雜,20行左右吧,你是不是又想偷懶不封裝?是不是覺(jué)得反正才那么幾處,大不了就是復(fù)制粘貼嘛。ok,你改完上線,然后又雙叒被產(chǎn)品經(jīng)理罵了,原來(lái)這塊邏輯本來(lái)出現(xiàn)在5個(gè)地方,結(jié)果你只改了4個(gè),漏掉了一個(gè)地方?jīng)]改。
??以上的就是基本上來(lái)講必須要封裝的,不過(guò)最后一條的定義其實(shí)比較模糊,這個(gè)只能自己根據(jù)經(jīng)驗(yàn)去判斷?,F(xiàn)在你應(yīng)該清楚了,程序員封裝不是僅僅為了偷懶,少些代碼。不封裝的話,多復(fù)制幾遍都只是小問(wèn)題,冰山一角而已,真正后期維護(hù)才是整個(gè)冰山。
??好了,本文到這里就結(jié)束了,以上觀點(diǎn)純屬個(gè)人看法,如有不同見解歡迎指教。后面我還會(huì)繼續(xù)分享我的踩坑日記,歡迎大家持續(xù)關(guān)注
??我的開源項(xiàng)目:MVP框架庫(kù)