作者:李旺成###
時(shí)間:2016年4月12日###
這里使用了一個(gè)解析當(dāng)前天氣 JSON 字符串得到原始 Model 后蠕搜,將該 Model 的數(shù)據(jù)展示到一個(gè)簡(jiǎn)單的頁面上來進(jìn)行演示祥得。
先看下 Demo 的效果圖:
我理解的 VO
VO杠袱,ViewObject 或 ViewModel。關(guān)于它的解釋在 Android MVP 詳解(下)中烂斋,我做過簡(jiǎn)要的闡述手负。這里无牵,再說說我是怎么理解 VO 的飞蛹。
VO须肆,就是一切給 View 提供數(shù)據(jù)的對(duì)象。這個(gè)定義就很廣泛了桩皿,所以我對(duì) VO 做了如下的分類(下面會(huì)細(xì)說)。
VO 的實(shí)現(xiàn)方式
既然幢炸,所有給 View 提供數(shù)據(jù)的對(duì)象都可以稱之為 VO泄隔,那么 VO 的來源或者說形式就很多了。我在這里根據(jù) VO 的實(shí)現(xiàn)方式進(jìn)行了分類宛徊,僅僅是一家之言佛嬉,有疏漏之處逻澳,見諒。
1. 單獨(dú)的 VO 類
在 Android MVP 詳解(下)中建議專門建一個(gè)包 vo暖呕,用來存放該模塊下的所有 VO 類斜做。對(duì)于這一類,那就屬于單獨(dú)的 VO 類湾揽,或者更準(zhǔn)確的說明是“獨(dú)立的 VO 類”瓤逼。
要使用這種類型的 VO,有一個(gè)問題库物,它是獨(dú)立的類霸旗,那么就需要另外的對(duì)象給它提供數(shù)據(jù)。在這里我認(rèn)為提供(傳遞)數(shù)據(jù)的方式戚揭,大致有如下兩種:
A. 使用轉(zhuǎn)換器
專門使用一個(gè)轉(zhuǎn)換器類诱告,來做原始 Model 到 VO 的轉(zhuǎn)換。如示例項(xiàng)目中的 VOConverterUtil.java 類民晒。(在這類里偷了個(gè)懶精居,直接調(diào)用了“構(gòu)造方法中轉(zhuǎn)換”的方式進(jìn)行了轉(zhuǎn)換)
還是看下代碼吧:
public class VOConverterUtil {
public static WeatherVO getWeatherVOFromWeatherBean(WeatherBean weatherBean) {
// 這里偷個(gè)懶
WeatherVO weatherVO = new WeatherVO(weatherBean);
return weatherVO;
}
}
B. 構(gòu)造方法中轉(zhuǎn)換
這個(gè)很好理解,就是在構(gòu)造方法中進(jìn)行數(shù)據(jù)轉(zhuǎn)換潜必。代碼很簡(jiǎn)單靴姿,直接看代碼:
public WeatherVO(WeatherBean weatherBean) {
if (weatherBean == null) return;
isSuccess = "ok".equals(weatherBean.getStatus());
int condCode = Integer.parseInt(weatherBean.getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
condCodeColorStr = "#000066";
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
condCodeColorStr = "#009900";
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
condCodeColorStr = "#993300";
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+weatherBean.getNow().getCond().getTxt()+"</font>");
relativeHumidity = "相對(duì)濕度:" + weatherBean.getNow().getHum();
int tmpInt = Integer.parseInt(weatherBean.getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
airPressure = "氣壓:" + weatherBean.getNow().getPres();
precipitation = "降水量:" + weatherBean.getNow().getPcpn();
visibility = "能見度:" + weatherBean.getNow().getVis() + " KM";
windDirectionAngle = "風(fēng)向角度:" + weatherBean.getNow().getWind().getDeg();
windDirection = "風(fēng)向:" + weatherBean.getNow().getWind().getDir();
windPower = "風(fēng)力:" + weatherBean.getNow().getWind().getSc();
windSpeed = "風(fēng)速" + weatherBean.getNow().getWind().getSpd();
}
2. 實(shí)現(xiàn)接口成 VO
抽出單獨(dú)的類,那么就多了一個(gè)類刮便, Modle 如果很多的話空猜,那不可避免 VO 的數(shù)量也會(huì)增加。有些人可能覺得沒必要恨旱,這增加了項(xiàng)目復(fù)雜度(哈哈辈毯,任何的設(shè)計(jì)都有可能造成復(fù)雜度上升)。那么搜贤,這樣谆沃,我們抽取一個(gè)接口,然后讓原始 Model 去實(shí)現(xiàn)這個(gè)接口 —— 以后就可以“面向接口編程”了仪芒。
思路很簡(jiǎn)單唁影,那么直接上代碼吧:
抽取接口 IWeatherVO.java:
public interface IWeatherVO {
boolean isSuccess(); // "status": "ok", //接口狀態(tài)
int getWeatherInfoIcon(); // "code": "100", //天氣狀況代碼 假設(shè) <0 下雪, < 60 雨掂名,大于 >60 < 90 陰据沈, > 90 晴
Spanned getWeatherInfoText(); // "txt": "晴" //天氣狀況描述 天氣的文本描述
String getRelativeHumidity(); // "hum": "20%", //相對(duì)濕度(%)
int getTemperatureIcon(); // "tmp": "32", //溫度 溫度圖標(biāo)
String getAirPressure(); // "pres": "1001", //氣壓
String getPrecipitation(); // 降水量
String getVisibility(); // "vis": "10", //能見度(km)
String getWindDirectionAngle(); // "deg": "10", //風(fēng)向(360度)
String getWindDirection(); // "dir": "北風(fēng)", //風(fēng)向
String getWindPower(); // "sc": "3級(jí)", //風(fēng)力
String getWindSpeed(); // "spd": "15" //風(fēng)速(kmph)
}
實(shí)現(xiàn)接口
public class WeatherBean implements IWeatherVO {
// 原始 Modle 中的字段都省略了,具體看源碼吧
...
//==========實(shí)現(xiàn) VO 接口==========
@Override
public boolean isSuccess() {
return "ok".equals(status);
}
@Override
public int getWeatherInfoIcon() {
int weatherInfoIcon;
int condCode = Integer.parseInt(getNow().getCond().getCode());
if (condCode < 0) {
weatherInfoIcon = R.mipmap.ic_snow;
} else if (condCode < 60) {
weatherInfoIcon = R.mipmap.ic_rain;
} else if (condCode < 90) {
weatherInfoIcon = R.mipmap.ic_cloudy;
} else {
weatherInfoIcon = R.mipmap.ic_sunshine;
}
return weatherInfoIcon;
}
@Override
public Spanned getWeatherInfoText() {
Spanned weatherInfoText;
int condCode = Integer.parseInt(getNow().getCond().getCode());
String condCodeColorStr = "";
if (condCode < 0) {
condCodeColorStr = "#000066";
} else if (condCode < 60) {
condCodeColorStr = "#009900";
} else if (condCode < 90) {
condCodeColorStr = "#993300";
} else {
condCodeColorStr = "#cccc00";
}
weatherInfoText = Html.fromHtml("<font color='"+condCodeColorStr+"'>"+getNow().getCond().getTxt()+"</font>");
return weatherInfoText;
}
@Override
public String getRelativeHumidity() {
return "相對(duì)濕度:" + getNow().getHum();
}
@Override
public int getTemperatureIcon() {
int temperatureIcon;
int tmpInt = Integer.parseInt(getNow().getTmp());
if (tmpInt < 15) {
temperatureIcon = R.mipmap.ic_lowtemperature;
} else if (tmpInt < 33) {
temperatureIcon = R.mipmap.ic_thermophilic;
} else {
temperatureIcon = R.mipmap.ic_hightemperature;
}
return temperatureIcon;
}
@Override
public String getAirPressure() {
return "氣壓:" + getNow().getPres();
}
@Override
public String getPrecipitation() {
return "降水量:" + getNow().getPcpn();
}
@Override
public String getVisibility() {
return "能見度:" + getNow().getVis() + " KM";
}
@Override
public String getWindDirectionAngle() {
return "風(fēng)向角度:" + getNow().getWind().getDeg();
}
@Override
public String getWindDirection() {
return "風(fēng)向:" + getNow().getWind().getDir();
}
@Override
public String getWindPower() {
return "風(fēng)力:" + getNow().getWind().getSc();
}
@Override
public String getWindSpeed() {
return "風(fēng)速" + getNow().getWind().getSpd();
}
}
3. 添加方法成 VO
這個(gè)就更簡(jiǎn)單了饺蔑,那就是連接口都不抽取了锌介,直接提供上述接口中的方法。這里就不贅述了,思路是和上面提取接口一致孔祸,所提供的方法隆敢,目的就是方便在 View 中直接使用。(這個(gè)在 Android MVP 詳解(下)中討論過崔慧,略)
4. 沒有 VO
沒有 VO拂蝎,那就是根本不使用 VO。如果你的項(xiàng)目是 MVP 的惶室,那么就在 Presenter 中做數(shù)據(jù)轉(zhuǎn)換的工作温自,然后提供給 View 展示。
這對(duì)于很簡(jiǎn)單的 Model 和 簡(jiǎn)單的 View 是沒有問題的拇涤,如果捣作,Model 很復(fù)雜(字段很多,而且不能直接使用)鹅士,那么 Presenter 的任務(wù)就會(huì)很重券躁。
這里就不做演示了,很多人應(yīng)該都在這么用掉盅,或者曾經(jīng)是這么用的也拜。
使用 VO 的好處
上面說了一堆 VO 的實(shí)現(xiàn)方式,但是就是沒提使用 VO 到底有何益處趾痘;或者說 VO 存在的意義慢哈。下面就我個(gè)人的理解,談?wù)勎艺J(rèn)為 VO 的好處永票。
統(tǒng)一命名習(xí)慣
很多時(shí)候數(shù)據(jù)來源是網(wǎng)絡(luò)(服務(wù)器端)卵贱,那么這就可能有一個(gè)問題。服務(wù)器端的命名習(xí)慣可能與客戶端有很大區(qū)別侣集,還有不同服務(wù)器端開發(fā)的命名習(xí)慣也可能不同(如:使用 PHP 開發(fā)的服務(wù)器程序和使用 Java 開發(fā)的服務(wù)器程序命名很可能就是不同的)键俱。
簡(jiǎn)而言之,那就是服務(wù)器反給我們的字段和我們項(xiàng)目中的命名習(xí)慣不同世分,很多人說编振,這沒辦法啊臭埋!總不能讓服務(wù)器改吧踪央!
是的,客戶端和服務(wù)器端的命名很難統(tǒng)一瓢阴,有人會(huì)說畅蹂,不統(tǒng)一就不統(tǒng)一,又不影響使用荣恐。確實(shí)魁莉,不影響使用,但是,我們追求完美不是(先從最基本的命名規(guī)范做起旗唁,哈哈)。
所以痹束,從這個(gè)角度來考慮检疫,我建議原始的 Model 那就按照接口文檔來(當(dāng)然,如果使用 Gson 的話祷嘶,關(guān)于命名不統(tǒng)一還是可以解決的屎媳,有興趣的可以自行 Google)。我們自己針對(duì) View 定義一套 VO论巍,這個(gè)可以完全按照我們自己的命名規(guī)范來烛谊,至少這里是統(tǒng)一的。
解耦 View 和 Model
解耦嘉汰,這個(gè)就不用多說了吧丹禀!我都不直接使用你了,這還不是解耦鞋怀,View 依賴的是 VO双泪,而不再依賴原始的 Modle。關(guān)于解耦所帶來的優(yōu)點(diǎn)密似,這里就不詳述了焙矛,一搜一堆...
鋪平數(shù)據(jù)結(jié)構(gòu)
“鋪平數(shù)據(jù)結(jié)構(gòu)” —— 可以理解為將原來有多級(jí)(層級(jí)較深)的對(duì)象,轉(zhuǎn)換為層級(jí)較淺的對(duì)象残腌。
我曾在項(xiàng)目中遇到這樣一個(gè)問題:有很多相似的頁面村斟,但是服務(wù)器端給的字段都是不同的,這就需要建立多個(gè) Model 來解析服務(wù)器給的數(shù)據(jù)抛猫◇№铮考慮到頁面基本一樣,那就不需要提供多個(gè)頁面了邑滨,直接用一個(gè)頁面日缨,往里面填充不同的數(shù)據(jù)就可以了。那么掖看,問題來了匣距,這會(huì)導(dǎo)致要寫很多重復(fù)的填充 View 的代碼,因?yàn)?Model 是不同的哎壳。
對(duì)于上述的問題毅待,我的解決方案是,將頁面中要使用的數(shù)據(jù)抽取為獨(dú)立的 VO归榕,該頁面只需要從 VO 中獲取數(shù)據(jù)即可尸红。再就是,關(guān)于如何建立 Modle 去解析服務(wù)器數(shù)據(jù)的問題。這里外里,我只建了一個(gè) Modle怎爵,將所有使用這個(gè)頁面的接口中的返回字段都封裝到一個(gè) Modle 中。這得益于 Model 中多了字段盅蝗,并不會(huì)影響 JSON 字符串到對(duì)象的轉(zhuǎn)換(至少 Gson 是這樣的)鳖链。
上面說的這個(gè)例子,也可以認(rèn)為是“鋪平了數(shù)據(jù)結(jié)構(gòu)”墩莫。
在這個(gè)示例 Demo 中芙委,可以很好的演示 —— “鋪平數(shù)據(jù)結(jié)構(gòu)” 。
先看下原始的 JSON 字符串:
{
"status": "ok",
"now": {
"cond": {
"code": "100",
"txt": "晴"
},
"fl": "30",
"hum": "20%",
"pcpn": "0.0",
"pres": "1001",
"tmp": "32",
"vis": "10",
"wind": {
"deg": "10", //風(fēng)向(360度)
"dir": "北風(fēng)", //風(fēng)向
"sc": "3級(jí)", //風(fēng)力
"spd": "15" //風(fēng)速(kmph)
}
}
}
看一下狂秦,上面的 JSON 字符串灌侣,如果需要獲取風(fēng)速,那么需要先訪問 now裂问,在訪問 wind侧啼,然后才能獲取到 spd 字段。在代碼中就如下:
weatherBean.getNow().getWind().getSpd();
而在我們的 VO 中愕秫,可以直接取到:
// 數(shù)據(jù)已經(jīng)轉(zhuǎn)換過了慨菱,這里直接可以取到
public String getWindSpeed() {
return windSpeed;
}
減少可能的問題
其實(shí),View 和 Model 的耦合就是一個(gè)很大的問題戴甩,哈哈符喝,這個(gè)確實(shí)能解決。
還有一些問題可以得到避免甜孤,例如协饲,減少 View 中對(duì) Model 的取值的各種判斷(當(dāng)然 MVP 就能解決),避免 Model 中的數(shù)據(jù)異常導(dǎo)致 View 崩潰缴川。
這里就不多說這個(gè)問題了茉稠,等你遇到的時(shí)候,自然就知道能夠避免哪些問題了。(偷個(gè)懶,這個(gè)以后有機(jī)會(huì)再豐富吧)
VO 使用演示
直接看圖吧氛驮,就不上 GIF 了。
小結(jié)
沒有可以解決一切問題的妙藥膀篮,no magic。
關(guān)于上述 VO 的各種形式岂膳,需要根據(jù)具體的場(chǎng)景(項(xiàng)目)來區(qū)分誓竿,當(dāng)然這也在很大程度上取決于個(gè)人的習(xí)慣以及項(xiàng)目的大小。
如果是比較大的項(xiàng)目谈截,那么建議直接抽出一個(gè) VO 包來筷屡,為每個(gè) View 都提供單獨(dú)的 VO 對(duì)象涧偷,這樣也可以保證項(xiàng)目的統(tǒng)一性,不會(huì)破壞層之間的依賴毙死。
如果是小項(xiàng)目燎潮,那么可以混著用,覺得哪種方式使用起來最方便扼倘,那就使用哪種吧跟啤!
還是那句話,沒有一定之規(guī)唉锌,要依據(jù)使用場(chǎng)景來確定。
項(xiàng)目地址:
GitHub