Flutter實戰(zhàn) Json轉(zhuǎn)Dart Model類

2021-03-09 10:11 更新

在實戰(zhàn)中,后臺接口往往會返回一些結(jié)構(gòu)化數(shù)據(jù),如 JSON、XML 等,如之前我們請求 Github API 的示例,它返回的數(shù)據(jù)就是 JSON 格式的字符串,為了方便我們在代碼中操作 JSON,我們先將 JSON 格式的字符串轉(zhuǎn)為 Dart 對象,這個可以通過dart:convert中內(nèi)置的 JSON 解碼器 json.decode() 來實現(xiàn),該方法可以根據(jù) JSON 字符串具體內(nèi)容將其轉(zhuǎn)為 List 或 Map,這樣我們就可以通過他們來查找所需的值,如:

//一個JSON格式的用戶列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
//將JSON字符串轉(zhuǎn)為Dart對象(此處是List)
List items=json.decode(jsonStr);
//輸出第一個用戶的姓名
print(items[0]["name"]);

通過 json.decode() 將 JSON 字符串轉(zhuǎn)為 List/Map 的方法比較簡單,它沒有外部依賴或其它的設(shè)置,對于小項目很方便。但當(dāng)項目變大時,這種手動編寫序列化邏輯可能變得難以管理且容易出錯,例如有如下 JSON:

{
  "name": "John Smith",
  "email": "john@example.com"
}

我們可以通過調(diào)用json.decode方法來解碼 JSON ,使用 JSON 字符串作為參數(shù):

Map<String, dynamic> user = json.decode(json);


print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

由于json.decode()僅返回一個Map<String, dynamic>,這意味著直到運行時我們才知道值的類型。 通過這種方法,我們失去了大部分靜態(tài)類型語言特性:類型安全、自動補(bǔ)全和最重要的編譯時異常。這樣一來,我們的代碼可能會變得非常容易出錯。例如,當(dāng)我們訪問nameemail字段時,我們輸入的很快,導(dǎo)致字段名打錯了。但由于這個 JSON 在 map 結(jié)構(gòu)中,所以編譯器不知道這個錯誤的字段名,所以編譯時不會報錯。

其實,這個問題在很多平臺上都會遇到,而也早就有了好的解決方法即“Json Model 化”,具體做法就是,通過預(yù)定義一些與 Json 結(jié)構(gòu)對應(yīng)的 Model 類,然后在請求到數(shù)據(jù)后再動態(tài)根據(jù)數(shù)據(jù)創(chuàng)建出 Model 類的實例。這樣一來,在開發(fā)階段我們使用的是 Model 類的實例,而不再是 Map/List,這樣訪問內(nèi)部屬性時就不會發(fā)生拼寫錯誤。例如,我們可以通過引入一個簡單的模型類(Model class)來解決前面提到的問題,我們稱之為User。在 User 類內(nèi)部,我們有:

  • 一個User.fromJson 構(gòu)造函數(shù), 用于從一個 map 構(gòu)造出一個 User實例 map structure
  • 一個toJson 方法, 將 User 實例轉(zhuǎn)化為一個 map.

這樣,調(diào)用代碼現(xiàn)在可以具有類型安全、自動補(bǔ)全字段(name 和 email)以及編譯時異常。如果我們將拼寫錯誤字段視為int類型而不是String, 那么我們的代碼就不會通過編譯,而不是在運行時崩潰。

user.dart

class User {
  final String name;
  final String email;


  User(this.name, this.email);


  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];


  Map<String, dynamic> toJson() =>
    <String, dynamic>{
      'name': name,
      'email': email,
    };
}

現(xiàn)在,序列化邏輯移到了模型本身內(nèi)部。采用這種新方法,我們可以非常容易地反序列化 user.

Map userMap = json.decode(json);
var user = new User.fromJson(userMap);


print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');

要序列化一個 user,我們只是將該User對象傳遞給該json.encode方法。我們不需要手動調(diào)用toJson這個方法,因為`JSON.encode 內(nèi)部會自動調(diào)用。

String json = json.encode(user);

這樣,調(diào)用代碼就不用擔(dān)心 JSON 序列化了,但是,Model 類還是必須的。在實踐中,User.fromJsonUser.toJson方法都需要單元測試到位,以驗證正確的行為。

另外,實際場景中,JSON 對象很少會這么簡單,嵌套的 JSON 對象并不罕見,如果有什么能為我們自動處理 JSON 序列化,那將會非常好。幸運的是,有!

#自動生成Model

盡管還有其他庫可用,但在本書中,我們介紹一下官方推薦的json_serializable package (opens new window)包。 它是一個自動化的源代碼生成器,可以在開發(fā)階段為我們生成 JSON 序列化模板,這樣一來,由于序列化代碼不再由我們手寫和維護(hù),我們將運行時產(chǎn)生 JSON 序列化異常的風(fēng)險降至最低。

#在項目中設(shè)置json_serializable

要包含json_serializable到我們的項目中,我們需要一個常規(guī)和兩個開發(fā)依賴項。簡而言之,開發(fā)依賴項是不包含在我們的應(yīng)用程序源代碼中的依賴項,它是開發(fā)過程中的一些輔助工具、腳本,和 node 中的開發(fā)依賴項相似。

pubspec.yaml

dependencies:
  # Your other regular dependencies here
  json_annotation: ^2.0.0


dev_dependencies:
  # Your other dev_dependencies here
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

在您的項目根文件夾中運行 flutter packages get (或者在編輯器中點擊 “Packages Get”) 以在項目中使用這些新的依賴項.

#以json_serializable的方式創(chuàng)建model類

讓我們看看如何將我們的User類轉(zhuǎn)換為一個json_serializable。為了簡單起見,我們使用前面示例中的簡化 JSON model。

user.dart

import 'package:json_annotation/json_annotation.dart';


// user.g.dart 將在我們運行生成命令后自動生成
part 'user.g.dart';


///這個標(biāo)注是告訴生成器,這個類是需要生成Model類的
@JsonSerializable()


class User{
  User(this.name, this.email);


  String name;
  String email;
  //不同的類使用不同的mixin即可
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);  
}

有了上面的設(shè)置,源碼生成器將生成用于序列化nameemail字段的 JSON 代碼。

如果需要,自定義命名策略也很容易。例如,如果我們正在使用的 API 返回帶有 _snakecase 的對象,但我們想在我們的模型中使用 lowerCamelCase, 那么我們可以使用 @JsonKey 標(biāo)注:

//顯式關(guān)聯(lián)JSON字段名與Model屬性的對應(yīng)關(guān)系 
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

#運行代碼生成程序

json_serializable第一次創(chuàng)建類時,您會看到與圖11-4類似的錯誤。

ide_warning

這些錯誤是完全正常的,這是因為 Model 類的生成代碼還不存在。為了解決這個問題,我們必須運行代碼生成器來為我們生成序列化模板。有兩種運行代碼生成器的方法:

#一次性生成

通過在我們的項目根目錄下運行:

flutter packages pub run build_runner build

這觸發(fā)了一次性構(gòu)建,我們可以在需要時為我們的 Model 生成 json 序列化代碼,它通過我們的源文件,找出需要生成 Model 類的源文件(包含@JsonSerializable 標(biāo)注的)來生成對應(yīng)的 .g.dart 文件。一個好的建議是將所有 Model 類放在一個單獨的目錄下,然后在該目錄下執(zhí)行命令。

雖然這非常方便,但如果我們不需要每次在 Model 類中進(jìn)行更改時都要手動運行構(gòu)建命令的話會更好。

#持續(xù)生成

使用 _watcher _可以使我們的源代碼生成的過程更加方便。它會監(jiān)視我們項目中文件的變化,并在需要時自動構(gòu)建必要的文件,我們可以通過flutter packages pub run build_runner watch在項目根目錄下運行來啟動 watcher。只需啟動一次觀察器,然后它就會在后臺運行,這是安全的。

#自動化生成模板

上面的方法有一個最大的問題就是要為每一個 json 寫模板,這是比較枯燥的。如果有一個工具可以直接根據(jù) JSON 文本生成模板,那我們就能徹底解放雙手了。筆者自己用 dart 實現(xiàn)了一個腳本,它可以自動生成模板,并直接將 JSON 轉(zhuǎn)為 Model 類,下面我們看看怎么做:

  1. 定義一個"模板的模板",名為"template.dart":

   import 'package:json_annotation/json_annotation.dart';
   %t
   part '%s.g.dart';
   @JsonSerializable()
   class %s {
       %s();

   
       %s
       factory %s.fromJson(Map<String,dynamic> json) => _$%sFromJson(json);
       Map<String, dynamic> toJson() => _$%sToJson(this);
   }

模板中的“%t”、“%s”為占位符,將在腳本運行時動態(tài)被替換為合適的導(dǎo)入頭和類名。

  1. 寫一個自動生成模板的腳本(mo.dart),它可以根據(jù)指定的 JSON 目錄,遍歷生成模板,在生成時我們定義一些規(guī)則:

  • 如果 JSON 文件名以下劃線“_”開始,則忽略此 JSON 文件。
  • 復(fù)雜的 JSON 對象往往會出現(xiàn)嵌套,我們可以通過一個特殊標(biāo)志來手動指定嵌套的對象(后面舉例)。

腳本我們通過 Dart 來寫,源碼如下:

   import 'dart:convert';
   import 'dart:io';
   import 'package:path/path.dart' as path;
   const TAG="\$";
   const SRC="./json"; //JSON 目錄
   const DIST="lib/models/"; //輸出model目錄

   
   void walk() { //遍歷JSON目錄生成模板
     var src = new Directory(SRC);
     var list = src.listSync();
     var template=new File("./template.dart").readAsStringSync();
     File file;
     list.forEach((f) {
       if (FileSystemEntity.isFileSync(f.path)) {
         file = new File(f.path);
         var paths=path.basename(f.path).split(".");
         String name=paths.first;
         if(paths.last.toLowerCase()!="json"||name.startsWith("_")) return ;
         if(name.startsWith("_")) return;
         //下面生成模板
         var map = json.decode(file.readAsStringSync());
         //為了避免重復(fù)導(dǎo)入相同的包,我們用Set來保存生成的import語句。
         var set= new Set<String>();
         StringBuffer attrs= new StringBuffer();
         (map as Map<String, dynamic>).forEach((key, v) {
             if(key.startsWith("_")) return ;
             attrs.write(getType(v,set,name));
             attrs.write(" ");
             attrs.write(key);
             attrs.writeln(";");
             attrs.write("    ");
         });
         String  className=name[0].toUpperCase()+name.substring(1);
         var dist=format(template,[name,className,className,attrs.toString(),
                                   className,className,className]);
         var _import=set.join(";\r\n");
         _import+=_import.isEmpty?"":";";
         dist=dist.replaceFirst("%t",_import );
         //將生成的模板輸出
         new File("$DIST$name.dart").writeAsStringSync(dist);
       }
     });
   }

   
   String changeFirstChar(String str, [bool upper=true] ){
     return (upper?str[0].toUpperCase():str[0].toLowerCase())+str.substring(1);
   }

   
   //將JSON類型轉(zhuǎn)為對應(yīng)的dart類型
    String getType(v,Set<String> set,String current){
     current=current.toLowerCase();
     if(v is bool){
       return "bool";
     }else if(v is num){
       return "num";
     }else if(v is Map){
       return "Map<String,dynamic>";
     }else if(v is List){
       return "List";
     }else if(v is String){ //處理特殊標(biāo)志
       if(v.startsWith("$TAG[]")){
         var className=changeFirstChar(v.substring(3),false);
         if(className.toLowerCase()!=current) {
           set.add('import "$className.dart"');
         }
         return "List<${changeFirstChar(className)}>";

   
       }else if(v.startsWith(TAG)){
         var fileName=changeFirstChar(v.substring(1),false);
         if(fileName.toLowerCase()!=current) {
           set.add('import "$fileName.dart"');
         }
         return changeFirstChar(fileName);
       }
       return "String";
     }else{
       return "String";
     }
    }

   
   //替換模板占位符
   String format(String fmt, List<Object> params) {
     int matchIndex = 0;
     String replace(Match m) {
       if (matchIndex < params.length) {
         switch (m[0]) {
           case "%s":
             return params[matchIndex++].toString();
         }
       } else {
         throw new Exception("Missing parameter for string format");
       }
       throw new Exception("Invalid format string: " + m[0].toString());
     }
     return fmt.replaceAllMapped("%s", replace);
   }

   
   void main(){
     walk();
   }

  1. 寫一個 shell(mo.sh),將生成模板和生成 model 串起來:

   dart mo.dart
   flutter packages pub run build_runner build --delete-conflicting-outputs

至此,我們的腳本寫好了,我們在根目錄下新建一個 json 目錄,然后把 user.json 移進(jìn)去,然后在 lib 目錄下創(chuàng)建一個 models 目錄,用于保存最終生成的 Model 類?,F(xiàn)在我們只需要一句命令即可生成 Model 類了:

./mo.sh  

運行后,一切都將自動執(zhí)行,現(xiàn)在好多了,不是嗎?

#嵌套JSON

我們定義一個 person.json 內(nèi)容修改為:

{
  "name": "John Smith",
  "email": "john@example.com",
  "mother":{
    "name": "Alice",
    "email":"alice@example.com"
  },
  "friends":[
    {
      "name": "Jack",
      "email":"Jack@example.com"
    },
    {
      "name": "Nancy",
      "email":"Nancy@example.com"
    }
  ]
}

每個 Person 都有name 、email 、 motherfriends四個字段,由于mother也是一個 Person,朋友是多個 Person(數(shù)組),所以我們期望生成的 Model 是下面這樣:

import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';


@JsonSerializable()
class Person {
    Person();

    
    String name;
    String email;
    Person mother;
    List<Person> friends;


    factory Person.fromJson(Map<String,dynamic> json) => _$PersonFromJson(json);
    Map<String, dynamic> toJson() => _$PersonToJson(this);
}

這時,我們只需要簡單修改一下 JSON,添加一些特殊標(biāo)志,重新運行 mo.sh 即可:

{
  "name": "John Smith",
  "email": "john@example.com",
  "mother":"$person",
  "friends":"$[]person"
}

我們使用美元符“$”作為特殊標(biāo)志符(如果與內(nèi)容沖突,可以修改 mo.dart 中的TAG常量,自定義標(biāo)志符),腳本在遇到特殊標(biāo)志符后會先把相應(yīng)字段轉(zhuǎn)為相應(yīng)的對象或?qū)ο髷?shù)組,對象數(shù)組需要在標(biāo)志符后面添加數(shù)組符“[]”,符號后面接具體的類型名,此例中是 person。其它類型同理,加入我們給 User 添加一個 Person 類型的 boss字段:

{
  "name": "John Smith",
  "email": "john@example.com",
  "boss":"$person"
}

重新運行 mo.sh,生成的 user.dart 如下:

import 'package:json_annotation/json_annotation.dart';
import "person.dart";
part 'user.g.dart';


@JsonSerializable()


class User {
    User();


    String name;
    String email;
    Person boss;

    
    factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
    Map<String, dynamic> toJson() => _$UserToJson(this);
}

可以看到,boss字段已自動添加,并自動導(dǎo)入了“person.dart”。

#Json_model 包

如果每個項目都要構(gòu)建一個上面這樣的腳本顯然很麻煩,為此,我們將上面腳本和生成模板封裝了一個包,已經(jīng)發(fā)布到了 Pub 上,包名為 Json_model (opens new window),開發(fā)者把該包加入開發(fā)依賴后,便可以用一條命令,根據(jù) Json 文件生成 Dart 類。另外 Json_model (opens new window)處于迭代中,功能會逐漸完善,所以建議讀者直接使用該包(而不是手動復(fù)制上面的代碼)。

#使用IDE插件生成model

目前 Android Studio(或IntelliJ)有幾個插件,可以將 json 文件轉(zhuǎn)成 Model 類,但插件質(zhì)量參差不齊,甚至還有一些沾染上了抄襲風(fēng)波,故筆者在此不做優(yōu)先推薦,讀者有興趣可以自行了解。但是,我們還是要了解一下 IDE 插件和 Json_model (opens new window)的優(yōu)劣:

  1. Json_model (opens new window)需要單獨維護(hù)一個存放 Json 文件的文件夾,如果有改動,只需修改 Json 文件便可重新生成 Model 類;而 IDE 插件一般需要用戶手動將 Json 內(nèi)容拷貝復(fù)制到一個輸入框中,這樣生成之后 Json 文件沒有存檔的化,之后要改動就需要手動。
  2. Json_model (opens new window)可以手動指定某個字段引用的其它 Model 類,可以避免生成重復(fù)的類;而 IDE 插件一般會為每一個 Json 文件中所有嵌套對象都單獨生成一個 Model 類,即使這些嵌套對象可能在其它 Model 類中已經(jīng)生成過。
  3. Json_model (opens new window)提供了命令行轉(zhuǎn)化方式,可以方便集成到 CI 等非 UI 環(huán)境的場景。

#FAQ

很多人可能會問 Flutter 中有沒有像 Java 開發(fā)中的 Gson/Jackson 一樣的 Json 序列化類庫?答案是沒有!因為這樣的庫需要使用運行時反射,這在 Flutter 中是禁用的。運行時反射會干擾 Dart 的 tree shaking,使用 tree shaking,可以在 release 版中“去除”未使用的代碼,這可以顯著優(yōu)化應(yīng)用程序的大小。由于反射會默認(rèn)應(yīng)用到所有代碼,因此_ tree shaking _會很難工作,因為在啟用反射時很難知道哪些代碼未被使用,因此冗余代碼很難剝離,所以 Flutter 中禁用了 Dart 的反射功能,而正因如此也就無法實現(xiàn)動態(tài)轉(zhuǎn)化 Model 的功能。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號