在Flutter開發過程中快速生成json解析模板類的工具 | 掘金技術徵文

debuggerx發表於2018-07-17

如果你已經入門了Flutter的開發,並準備實現一個包含網路資料請求並解析資料顯示在介面上的應用,那麼你一定需要了解在Flutter中應該如何完成對JSON資料的解析和處理。

如果還沒有看過官方的相關文件,請移步至JSON和序列化檢視。

可以看到Flutter使用的sdk中提供了dart:convert 包用於JSON的序列化和反序列化,其功能與iOS的NSJSONSerialization類似,都是直接將JSON資料轉成NSDictionary / NSArray(或者JAVA中的Map/List), 然後開發時通過輸入欄位名字串的方式從中取值,非常的不方便。

另一種方式則是編寫模型類,這樣可以在已知JSON結構和欄位的情況下預先設定好類結構,開發時則可以方便地通過'點語法'進行資料欄位取值,獲得IDE程式碼提示的輔助,從而減少手誤提高開發效率。然而編寫大量相似的模板類程式碼無聊又費力,即使使用官方提供的json_serializable package包進行程式碼生成也還是相當麻煩。

如果開發過android、iOS或者java後臺應用,那麼你應該曾使用過各種外掛用於將JSON字串解析生成模板類(例如GsonFormatESJsonFormat ),這些工具極大提高了開發效率。

那麼Flutter開發中有沒有類似的工具可以使用呢?本文就將介紹我參照這些工具專為Flutter編寫的一個小工具(還有一個類似的線上工具:JSON to Dart,感興趣的可以試試~~):

JSONFormat4Flutter

github地址:github.com/debuggerx01…

使用操作演示:

在Flutter開發過程中快速生成json解析模板類的工具 | 掘金技術徵文

使用說明

1.介面操作 (參考錄屏:parse.gif)

  1. 工具執行以後,先將複製好的json字串貼上到左側文字框,然後點選'格式化'按鈕;如果提示出錯請檢查json是否合法
  2. 格式化成功後左側json將會按照縮排格式化顯示,並且右側表格將顯示分析得出的json結構,'Fields'列顯示層級和原始分析資料,'Name'列顯示每個欄位的名稱,'Type'列用於設定欄位的資料型別
    1. 對於普通資料型別(int、 double、 boolean、 String),Types列的型別將會自動給出,請儘量避免在上面滾動滑鼠滾輪導致型別選擇改變
    2. 對於值null的欄位,Types列的型別會自動設定為Object,並以黃色背景作為警告。此時如果直接生成程式碼也是可以使用的,只是該欄位在使用時可能需要手動強轉,所以建議在知曉該欄位實際型別情況下儘量補全json字串後再點選'格式化',或者在型別下拉框中指定實際的基本資料型別
    3. 對於自定義物件型別(或者說字典/Map),'Fields'的對應輸入框將留空並設為紅色背景,需要您手動輸入型別名稱,並請注意:
      1. 任意一個欄位沒有輸入型別名時點選程式碼生成按鈕,都將彈出警告提示並拒絕生成程式碼
      2. 設定型別名時可以參考同一行'Name'欄的值進行設定以方便使用時識別欄位,一般情況下推薦直接將'Name'欄內容首字母大寫作為型別名
      3. 但是需要注意,型別名不可與'Name'欄內容完全相同,且不能是dart中的關鍵字,否則生成的程式碼將包含語法錯誤
      4. 一般情況下第一行的資料型別為物件且'Name'欄內容為空,設定第一列的'Types'即為生成的bean的頂級物件類名,推薦使用'該json的作用+Resp/Bean'形式進行命名以方便管理
    4. 對於陣列型別,'Types'欄將被自動設定,並且:
      1. 陣列的泛型型別取決於陣列的內容的型別,也就是下一行設定的型別;當陣列下一行的內容型別變化時泛型也會自動改變
      2. 支援陣列的巢狀泛型傳遞
      3. 支援空陣列,並且生成的程式碼中其泛型會被設定為dynamic
    5. 特殊的,如果json本身的頂層級不是物件而是陣列,那麼需要為第一行的'Name'欄設定型別名稱,獲取頂層級陣列資料的方式為物件bean.list
  3. 確認設定無誤後,點選'生成Bean'按鈕,左側json顯示欄的內容將被替換為生成的程式碼,可以使用滑鼠鍵盤全選複製,或者直接點選下方的'複製'按鈕,然後將程式碼貼上到IDE中,完成解析流程

2.生成程式碼說明 (參考錄屏:use.gif)

  1. 反序列化(json字串->物件)
    將生成的程式碼貼上到dart原始檔中後,即可以在任意地方導包使用,一般方法為(以http.get請求為例):

    var response = await HTTP.get(url);
    var resp = BeanResp(response.body);
    複製程式碼

    也就是說,將請求到的json內容作為引數傳遞給BeanResp的預設建構函式,這樣生成的resp物件即是請求到內容的實體。 需要說明的是,預設構造既可以傳入json的原始字串,也可以傳入已經用原生json.decode()方法解析過的json物件(這主要是為了照顧使用dio庫進行資料請求時結果資料會被自動解析成json物件的情況)。 只有頂級物件擁有預設構造方法,而其他子層級物件將使用xxx.fromJson()的命名構造進行物件建立。

  2. 序列化(物件->json字串)
    與官方樣例的處理方式不同,直接呼叫物件的toString()方法即可得到json字串完成序列化操作

  3. 手動建立物件
    為了方便大部分使用場景下的便利性,bean的預設建構函式被用來實現反序列化,所以如果想要在程式碼中手動傳參建立bean物件,可以使用xxx.fromParams()命名構造來完成。

簡易執行方式:

Release 頁面中,選擇下載對應平臺最新的二進位制檔案後——

linux:

在程式目錄開啟終端後執行:chmod u+x Formatter_linux && ./Formatter_linux

mac:

在程式目錄開啟終端後執行:chmod u+x Formatter_mac && ./Formatter_mac

windows:

直接雙擊執行 Formatter_win.exe

原始碼執行(以MAC為例)

沒有python執行環境的使用者需要先安裝python

mac中可以使用如下命令安裝

brew install python3
brew install pip3
複製程式碼

pip3是python3的包管理工具

brew 可以參考下面的連結

brew.sh/index_zh-cn

執行庫的時候會可能會提示

Traceback (most recent call last):
  File "formater.py", line 8, in <module>
    from mainwindow import *
  File "/Users/cjl/IdeaProjects/flutter/sxw-flutter-app/JSONFormat4Flutter/mainwindow.py", line 9, in <module>
    from PyQt5 import QtCore, QtGui, QtWidgets
ModuleNotFoundError: No module named 'PyQt5'

複製程式碼

這時候可以直接用 pip3 install PyQt5 pip3 install pyperclip 等待安裝完成

(注:brew安裝最新版python3可能會出現ssl模組丟失導致pip3無法正常使用,此時也可以考慮直接在python官網下載pkg包方式安裝python)

後面使用就是在命令列敲入 python3 formatter.py

一些問題的說明

如果有什麼問題,請在github上與我聯絡。下面列出幾個已有的問題:

  1. 為什麼不做成AS/idea外掛,而選擇PyQt編寫工具

    原因其一是我曾寫過簡單的AS/idea外掛,發現開發資料相當匱乏,除錯也很麻煩,雖然也有一些現成的外掛原始碼可供參考,但是開發這樣一個工具的週期預計會大幅超過我當時允許的時間(本工具第一個可用版本的實際開發時間為3天多一點);
    其二是由於Flutter既可以用AS/idea開發也可以用VSCode開發的特性(而且據我觀察兩者使用人數比例相仿),兩種IDE體系下開發外掛的語言工具及流程都完全不同,一旦我選擇了開發原生IDE外掛,那麼勢必需要編寫維護兩份程式碼,成本太高;
    其三是因為python強大的文字處理能力使得它特別適合這種模板生成的工作,而且正巧之前的工作中就有利用python解析excel資料生成其他語言原始碼的指令碼,可以快速借用已有的程式碼邏輯來實現功能;
    其四是得益於PyQt強大的跨平臺能力,可以方便快捷地打出三個桌面平臺的應用包,從而達到接近原生外掛的通用性(期待Flutter的桌面版專案早日成熟,這樣以後開發桌面工具也多一種選擇)
    另外,可以參考issue #9中的演示,將工具新增到IDE的外部工具中以方便開啟,這樣使用體驗就與IDE外掛更加相似了

  2. 生成的程式碼與官網上的樣例風格不符

    因為我寫這個玩意的時候官網文件還沒有現在的樣例模板(也可能單純是我沒注意到),所以完全是自己構思的方式來做的。後來看到樣例後也想過仿照那個格式改一下,但轉念一想其實也沒什麼本質區別,加上又忙就一直沒動。我做這個工具主要還是以能夠處理各種奇怪json為目標的,比如為了處理多層list巢狀,頂級結構為list,list內部元素為基本資料型別等,這些花了不少精力……而且生成的程式碼裡沒用樣例裡型別轉換,所以dart2升級強型別模式以後很多人根據那個樣板寫的json解析都報錯的,而這個工具生成的卻一直能用,既然能夠保證功能,具體的風格方式問題應該也沒必要太過糾結了

  3. 如何實現通過泛型返回不同的型別

    這主要是一些之前做android開發的同學習慣於android開發中流行的’網路請求封裝‘,就是假設後臺返回的JSON擁有相同的一級結構,比如都是
    {"code":0,"msg":"success","data":{}}
    這種結構,所有的請求結果都需要先進行code和msg欄位的判斷處理,然後要使用的實際資料全在data欄位中,所以希望有一個統一的處理函式可以進行code和msg欄位的處理,並通過指定泛型或class來返回指定型別的data內容物件。
    雖然我個人認為這只是個習慣問題,沒必要堅持,由於dart語法特性確實不好實現,那麼不如換種形式一樣能處理地很好,但是因為見到不止一個人有這種疑問,所以我試著用這種思路寫了個dart版本的demo僅供參考,希望有人能提供更好更優雅的方案:

Person介面返回的json: {"code":0,"msg":"success","data":{"name":"debuggerx","age":26}} Phone介面返回json: {"code":0,"msg":"success","data":{"model":"MI6X","price":1999}}

PersonResp.dart:

import 'dart:convert' show json;
class PersonResp {
  int code;
  String msg;
  Person data;

  PersonResp.fromParams({this.code, this.msg, this.data});

  factory PersonResp(jsonStr) => jsonStr is String ? PersonResp.fromJson(json.decode(jsonStr)) : PersonResp.fromJson(jsonStr);
  
  PersonResp.fromJson(jsonRes) {
    code = jsonRes['code'];
    msg = jsonRes['msg'];
    data = new Person.fromJson(jsonRes['data']);
  }

  @override
  String toString() {
    return '{"code": $code,"msg": ${msg != null?'${json.encode(msg)}':'null'},"data": $data}';
  }
}

class Person {
  int age;
  String name;
  
  Person.fromParams({this.age, this.name});
  
  Person.fromJson(jsonRes) {
    age = jsonRes['age'];
    name = jsonRes['name'];
  }

  @override
  String toString() {
    return '{"age": $age,"name": ${name != null?'${json.encode(name)}':'null'}}';
  }
}
複製程式碼

PhoneResp.dart:

import 'dart:convert' show json;
class PhoneResp {
  int code;
  String msg;
  Phone data;

  PhoneResp.fromParams({this.code, this.msg, this.data});

  factory PhoneResp(jsonStr) => jsonStr is String ? PhoneResp.fromJson(json.decode(jsonStr)) : PhoneResp.fromJson(jsonStr);
  
  PhoneResp.fromJson(jsonRes) {
    code = jsonRes['code'];
    msg = jsonRes['msg'];
    data = new Phone.fromJson(jsonRes['data']);
  }

  @override
  String toString() {
    return '{"code": $code,"msg": ${msg != null?'${json.encode(msg)}':'null'},"data": $data}';
  }
}

class Phone {
  int price;
  String model;

  Phone.fromParams({this.price, this.model});
  
  Phone.fromJson(jsonRes) {
    price = jsonRes['price'];
    model = jsonRes['model'];
  }

  @override
  String toString() {
    return '{"price": $price,"model": ${model != null?'${json.encode(model)}':'null'}}';
  }
}
複製程式碼

只需要編寫一個BaseResp.dart如下:

import 'dart:convert' show json;

class BaseResp<T> {
  int code;
  String msg;
  T data;

  factory BaseResp(jsonStr, Function buildFun) =>
      jsonStr is String ? BaseResp.fromJson(json.decode(jsonStr), buildFun) : BaseResp.fromJson(jsonStr, buildFun);

  BaseResp.fromJson(jsonRes, Function buildFun) {
    code = jsonRes['code'];
    msg = jsonRes['msg'];
    /// 這裡可以做code和msg的處理邏輯
    data = buildFun(jsonRes['data']);
  }
}
複製程式碼

那麼在解析的位置可以寫如下程式碼:

var response = await HTTP.get(url);
Person data = BaseResp<Person>(response.body, (res) => Person.fromJson(res)).data;
///或者
Phone data = BaseResp<Phone>(response.body, (res) => Phone.fromJson(res)).data;
複製程式碼

也就是說,通過構造BaseResp物件,指定data物件的泛型,並傳入data物件特有的fromJson建構函式,即可實現物件的建立,最後通過'.data'取值,即可得到想要的特定型別的data。

當然,那一長串挺長的,每次寫這麼一大段也有點麻煩,可以把這一行程式碼做成程式碼模板,只留下泛型部分作為模板變數即可。

更新:

有小夥伴指出,上面的BaseResp並不能很好地處理json中data的型別為陣列的情況,也就是是類似: {"code":0,"msg":"success","data":[{"name":"debuggerx","age":26},{"name":"dx","age":27}]} data的型別為List<Person>這種情況。此時可以參考issue 11擴充一個BaseRespList即可解決這個問題。

從 0 到 1:我的 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章