天天看點

在網絡請求中使用實體

聽說過 fastJSON 嗎?聽說過 GSON 嗎?我面試過很多 Android 開發人員,他們的項目大多不用 fastJSON 或者 GSON 這種實體化程式設計的思路。他們在擷取 MobileAPI 網絡請求傳回的 JSON 資料時,使用 JSONObject 或者 JSONArray 來承載資料,然後把傳回的資料當作一個字典,根據鍵取出相應的值。 

如果僅僅是在轉換 MobileAPI 傳回的 JSON 資料時手動取值也就算了,隻要能把取到的值填充到一個實體中就成。但是我見過最糟糕的程式是,把 JSON 資料直接轉成 JSONObject或者 JSONArray,然後就一直使用這樣的對象了,甚至将 JSONObject 從一個 Acivity 傳遞到另一個 Activity,要知道 JSONObject 和 JSONArray 都是不支援序列化的,是以隻好将這種對象封裝到一個全局變量中,在跳轉前設定,在跳轉後取出,寫一個這樣的糟糕示例:

先給出 MobileAPI 傳回的 JSON 字元串:

{ "weatherinfo":{"city":" 北京 ",

"cityid":"101010100","temp":"24",

"WD":" 南風 ",

"WS":"2 級 ",

"SD":"74%",

"WSE":"2",

"time":"17:45","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB","njd":" 暫無實況 ",

} } "qy":"1005"

使用 JSONObject 的編碼如下,代碼中的 result 變量就是上面的 JSON 字元串,更詳細的Demo 請參見 WeatherByJsonObjectActivity: 

try {JSONObject jsonResponse = new JSONObject(result);JSONObject weatherinfo = jsonResponse

.getJSONObject("weatherinfo");String city = weatherinfo.getString("city");int cityId = weatherinfo.getInt("cityid");

tvCity.setText(city);

tvCityId.setText(String.valueOf(cityId));} catch (JSONException e) {

e.printStackTrace();}

這樣的寫法有以下兩個問題:
      

1)根據 key 值取 value,我們可以認為這是一個字典。同樣的功能實作,字典比實體更晦澀難懂,容易産生 bug。

2)每次都要手動從 JSONObject 或者 JSONArray中取值,很煩瑣。

接下來我們分别使用 fastJSON 和 GSON,介紹一下實體程式設計的方式,相應的,請在項目中添加對fastJSON 和 GSON 這兩個 jar 的引用,如圖 1-6 所示。

我們使用 fastJSON 對上述代碼進行改造,要事先準備兩個實體 WeatherEntity 和 WeatherInfo,用于JSON 字元串到實體之間的映射:

圖 1-6 在 Android 項目中添加 fastJSON和 GSON 的 jar 包

WeatherEntity weatherEntity = JSON.parseObject(content, WeatherEntity.class);WeatherInfo weatherInfo = weatherEntity.getWeatherInfo();

if (weatherInfo != null) {

tvCity.setText(weatherInfo.getCity());

tvCityId.setText(weatherInfo.getCityid());}

使用 GSON 的方式也差不多:

Gson gson = new Gson();

WeatherEntity weatherEntity = gson.fromJson(content, WeatherEntity.class);WeatherInfo weatherInfo = weatherEntity.getWeatherInfo();

if (weatherInfo != null) {

tvCity.setText(weatherInfo.getCity());

tvCityId.setText(weatherInfo.getCityid());}

這裡說一件非常狗血的事情,就是在我們使用 fastJSON 後,App 四處起火,主要表現為:1)加了符号 Annotation 的實體屬性,一使用就崩潰。2)當有泛型屬性時,一使用就崩潰。 

在調試的時候沒事,可是每次打簽名混淆包,就會出現上述問題。我們幾個開發人員曾

經查到晚上十點半,最後才發現是混淆檔案缺了以下兩行代碼導緻的:-keepattributes Signature // 避免混淆泛型

-keepattributes *Annotation* // 不混淆注解 

實體生成器

當使用實體程式設計的時候,我有個切身感受,就是每次根據 JSON 字元串去編寫一個實體的時候非常麻煩。不僅僅是 Android,當我們進行 iOS 和WindowsPhone 程式設計時,也需要把 JSON 轉換為相應的實體。

建立實體是一件很煩瑣的事情,我們需要一個工具,幫助我們自動生成不同開發平台下的實體。于是便有了 EntityGenerater 這個工具。就像馬雲說的那樣,工具都是懶人發明的。當初我在推進實體化程式設計的時候,我的 iOS 團隊早已習慣了字典式取資料的方式,對建立實體這種新機制不是很感興趣,除非我發明一個能夠自動生成實體的工具,提高開發效率。于是我就到開源社群找到了一個類似的工具 JSON C# Class Generator ,1 但是它隻能生成WindowsPhone 的實體,于是我就稍微改造了一下這個工具,讓它同時也可以生成 Android 和 iOS 實體,如圖 1-7 所示。

圖 1-7 實體生成器的左邊界面

在左邊的文本框輸出 JSON 字元串後,點選 Load 按鈕,就會在右邊的清單中預覽到實體間的層次關系,以及 JSON 字元串中的字段與 JSON 實體中的屬性之間的對應關系,如圖 1-8 所示。

同時,這個清單還是可以編輯的。我們可以靈活修改要生成的 JSON 實體的屬性名稱。點選 Generate 按鈕,就會在 C:\JSON 目錄下生成 JSON 實體了。

再後來,考慮到 iOS 團隊每次使用實體生成器都要切換到 Windows 系統,這是一件何其麻煩的事情啊。于是我就又開發了實體生成器的 Web 版本,這樣就能滿足所有團隊的需要了。 

在頁面跳轉中使用實體

在一個頁面中,資料的來源有兩種:

1)調用 MobileAPI 擷取 JSON 資料。

2)從上一個頁面傳遞過來。

我們上一小節介紹了如何将從 MobileAPI 請求到的 JSON 資料轉換為實體,接下來,我

們看一下 Activity 之間的資料應該如何傳遞。一種偷懶的辦法是,設定一個全局變量,在來源頁設定全局變量,在目标頁接收全局

變量。

以下是來源頁 MainActivity 的代碼:

Intent intent = new Intent(MainActivity.this, LoginActivity.class);intent.putExtra(AppConstants.Email, "[email protected]");

CinemaBean cinema = new CinemaBean();cinema.setCinemaId("1");cinema.setCinemaName(" 星美 ");

// 使用全局變量的方式傳遞參數GlobalVariables.Cinema = cinema;

startActivity(intent); 

以下是目标頁 LoginActivity 的代碼:

CinemaBean cinema = GlobalVariables.Cinema;if (cinema != null) {

cinemaName = cinema.getCinemaName();} else {

cinemaName = "";}

這裡的 GlobalVariables 類是一個全局變量,定義如下:

public class GlobalVariables {

public static CinemaBean Cinema;}

我是不建議使用全局變量的。App 一旦被切換到背景,當手機記憶體不足的時候,就會回收這些全局變量,進而當 App 再次切換回前台時,再繼續使用全局變量,就會因為它們為空而崩潰。

如果必須使用全局變量,就一定要把它們序列化到本地。這樣即使全局變量為空,也能從本地檔案中恢複。在 3.5 節,我會專門講解如何解決全局變量導緻 App 崩潰的問題。

本節我們着重研究如何不使用全局變量,而是使用 Intent 在頁面間來傳遞資料實體的機制。

首先,在來源頁 MainActivity 要這樣寫:

Intent intent = new Intent(MainActivity.this, LoginNewActivity.class);

intent.putExtra(AppConstants.Email, "[email protected]");

CinemaBean cinema = new CinemaBean();cinema.setCinemaId("1");cinema.setCinemaName(" 星美 ");

// 使用 intent 上挂可序列化實體的方式傳遞參數intent.putExtra(AppConstants.Cinema, cinema);

startActivity(intent);

其次,目标頁 LoginActivity 要這樣寫:

CinemaBean cinema = (CinemaBean)getIntent().getSerializableExtra(AppConstants.Cinema);

if (cinema != null) {

cinemaName = cinema.getCinemaName();

} else {

cinemaName = "";}

這裡的 CinemaBean 要實作 Serializable 接口,以支援序列化:public class CinemaBean implements Serializable { 

private static final long serialVersionUID = 1L;

private String cinemaId;private String cinemaName;

public CinemaBean() {}

public String getCinemaId() {} return cinemaId;

public void setCinemaId(String cinemaId) {} this.cinemaId = cinemaId;

public String getCinemaName() {} return cinemaName;

public void setCinemaName(String cinemaName) {

this.cinemaName = cinemaName;}