آموزش فلاتر ( Flutter ) - تبدیل Json

سلام به همگی چند وقتی میشه با فلاتر آشنا شدم. نسبت به تجربه ای که تو اندروید داشتم طراحی UI تو فلاتر خیلی آسونتره واسه همین تصمیم گرفتم یه پروژه با فلاتر درست کنم و تصمیم دارم پروژمو این جا با شما به اشتراک بزارم. تو این مجموعه از پست ها چند تا از مهم ترین موضوعات فلاتر رو باهم بررسی می‌کنیم و در نهایت یک پروژه کامل رو به پایان میرسونیم. این پست سومین پست از این سری هستش واسه دیدن قسمت قبل به این لینک مراجعه کنید.


چیز هایی که تو این مجموعه یاد میگیریم به چند قسمت تقسیم میشن:


تعریف Json:

تعریف Wikipedia:

جی‌سان (به انگلیسی: JSON) مخفف JavaScript Object Notation (نشانه‌گذاری شیء جاوااسکریپت)، یک استاندارد باز متنیِ سبک برای انتقال داده‌ها است به گونه‌ای که برای انسان نیز خوانا باشد. با وجود ارتباط عمیقی که با جاوااسکریپت دارد، جی‌سان مستقل از زبان است و مفسرهایش تقریباً برای هر زبانی موجود هستند.

اما اگر بخوام به زبان ساده تربگم Json یه زبان (Notation) که برای ذخیره و با تبادل اطلاعات استفاده میشه. ما معمولا زمانی از Json استفاده میکنیم که بخواهیم اطلاعاتی رو از سمت سرور بگیرم و یا برای سرور ارسال کنیم.


ساخت مدل اولیه:

واسه این که کار با داده ها رو آسون کنیم مدل ها رو میسازیم. مدل ها معمولا کلاس هایی هستند که صرفا داده رو نگه میدارن و معمولا منطق دیگه ای درون خودشون ندارن. اگه با Java کار کرده باشید به این کلاس ها Pojo میگن. همچنین اگه با کاتلین کار کرده باشید بهشون Data Class میگن که کار باهاشون خیلی هم آسونه.

اما بریم سراغ ساختن مدل خودمون تو Dart. واسه ساختن مدل تو فلاتر مراحل زیر رو انجام بدین:

  • تو پوشه lib پروژتون یه پوشه درست کنید و اسمش رو بزارید model.
  • حالا فایل مدلتون را داخل این پوشه بسازید که با توجه به این پروژه اسم مدلمون food هستش. پس یه فایل دارت درست کنید و اسمش رو بزارید food.dart و ذخیرش کنید.
  • داخل فایلی که درست کردید یه کلاس به نام Food درست کنید.

بیاید برای شروع فرض بگیریم که کلاس Food ما دوتا خصوصیت بیشتر نداره. یه name که برای نمایش اسم غذا و یه image که ادرس عکس غذا رو درون خودش نگه میداره. پس کد کلاسمون چیزی شبیه به این میشه:

class Food {
  final String name;
  final String image;
  //constructor
  Food({
    this.name,
    this.image,
    });
}

سوال اینجاست که چطور این مدل رو تبدیل کنیم به Json و بر عکس مثلا وقتی دیتا Json از سرور بگیریم چطور تبدیلش کنیم به مدلمون؟

تو این مقاله به دو روش معمول که تو فلاتر (دارت) پیشنهاد شده رو مورد بررسی قرار میدیم:

  • سریال سازی (تبدیل) دستی.
  • سریال سازی خودکار توسط code generation.

معمولا از روش اول واسه داده های ساده و از دومی برای داده های پیچیده که مدیریتشون سخته استفاده میشه.


سریال سازی دستی - Manual

تبدیل دستی تو فلاتر خیلی سادست. فلاتر یه کتاب خونه داخلی از پیش تعریف شده تو پکیج dart:convert داره که کار باهاش خیلی راحته. فرض کنید json نمونه ما به این شکله:

{
    &quotname&quot : &quotpizza&quot,
    &quotimage&quot : &quoturl&quot
}

کافیه پکیج dart:convert رو اضافه کنید و بعد کد زیر رو اجرا کنید.

Map<String, dynamic> food = jsonDecode(jsonString);
print('pizza name , ${food['name']}! ');
print('image url ${food['email']}.');

همونطور که میبینید متد jsonDecode یه ورودی از نوع String میگیره و خروجی از نوع Map به ما میده. شما به راحتی میتونید با پیمایش تو Map به اطلاعات دست پیدا کنید. اما ما هدفمون اینه که از json یک مدل درس کنیم ولی اینجا یه خروجی مپ داریم.

پس باید یکسری کد ها به مدلمون اضافه کنیم. فایل مدلتون رو به شکل زیر تغییر بدین:

class Food {
  final String name;
  final String image;
  //constructor
  Food({
    this.name,
    this.image,
    });
    
//named constructor
  Food.fromJson(Map<String , dynamic> json)
    : name = json['name'],
      image= json['image'];
    
}

اگه به تیکه کد بالا نگاه کنید میبینید که ما یه Constructor جدید به کلاس مدلمون اضافه کردیم تو دارت به این شکل از constructor ها Named Constructor میگن اگه اطلاعاتی درموردش ندارید میتونید به این لینک مراجعه کنید.

در واقع کاری که Constructor جدیدمون انجام میده اینه که یه پارامتر Map رو میگیره و مقادیر مدلمون رو مقداردهی میکنه. شاید حدس زده باشید ما با ترکیب Constructor جدیدمون و متد از پیش تعریف شده jsonDecode میتونیم به راحتی json رو به مدل تبدیل کنیم.

Map foodMap = jsonDecode(jsonString);
var food= Food.fromJson(foodMap );
print('pizza name , ${food.name}!');
print('image url ${food.email}.');

به این فرایند میگن Decode کردن. در واقع ما یه json داریم اول تبدیلش میکنیم به یه Map بعدش با فرستادن Map به مدل اونو تبدیل به مدل میکنیم.

اما حالت برعکس چی؟ اگه بخوایم یه مدل رو به json تبدیل کنیم چی؟ فلاتر و دارت باز هم یه متد واسه این کار دارن. jsonEncode یه متد که یه پارامتر Map میگیره و اونو به string تبدیل میکنه همون json ماست. واسه استفاده از این متد لازمه بازم کلاس مدلمون رو یکم تغییر بدیم. پس کد کلاستون رو به کد زیر تبدیل کنید:

class Food {
  final String name;
  final String image;
  //constructor
  Food({
    this.name,
    this.image,
    });
    
//named constructor
  Food.fromJson(Map<String , dynamic> json)
    : name = json['name'],
      image= json['image'];

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

واسه استفاده از jsonEncode لازمه به این شکل ازش استفاده کنید.

var food=Food(&quotname&quot,&quoturl&quot);
String json = jsonEncode(food.toJson());

ما تو کد بالا یه مدل ساختیم پاسش کردیم به متد jsonEncode و یه خروجی json رو دریافت کردیم. یه نکته باید بگم اونم این که نیاز نیست متد toJson مربوط به مدل مون رو صدا بزنیم. در واقع خود متد jsonEncode این کار رو برامون میکنه. چه عالییی...

پس کدمون به این شکل تغییر میکنه:

var food=Food(&quotname&quot,&quoturl&quot);
String json = jsonEncode(food);

تا اینجا encode و decode کردن جیسون رو به صورت دستی یاد گرفتیم. اما یه سوال بزرگ اونم این که اگر مدلمون 20 تا فیلد داشت چی؟ قراره کلی کد بزنیم؟ این جاست که کتابخونه json_serializable به کمکون میاد.


سریال سازی خودکار - Automatic

در واقع کاری که که کتابخونه json_serializable انجام میده اینه که کدهایی اضافی و تکراری(boilerplate code) شما رو هر دفعه که کلاس مدلتون تغییر میکنه از نو میسازه ( Generate میکنه) .

واسه استفاده از این کتابخونه نیازه که اول وابستگی های اون رو به فایل 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

وابستگی های بالا رو مثل کد بالا به فایل pubspec.yaml اضافه کنید. شاید این فایل هم بتونه کمکتون کنه.

مرحله بعد اضافه کردن کتابخونه json_serializable به مدلتونه. کد کلاس مدلتون رو به کد زیر تغییر بدید.

//1
import 'package:json_annotation/json_annotation.dart';

//2
part 'food.g.dart';

//3
@JsonSerializable()
class Food {
  final String name;
  final String image;
  //constructor
  Food({
    this.name,
    this.image,
    });
    
//4
  Food.fromJson(Map<String , dynamic> json) => _$FoodFromJson(json);

//5
  Map<String, dynamic> toJson() => _$FoodToJson(this);
  
}

اضافه کردن این کتابخونه به مدل به 4 بخش تقسیم میشه:

  1. ایمپورت کردن فایل json_annotation.dart به مدل.
  2. اضافه کردن food.g.dart . این همون فایلیه که توسط کتابخونه تولید(generate) میشه .
  3. اضافه کردن انوتیشن @JsonSerializable کتابخونه json_serializable از طریق این انوتیشن می فهمه که برای چه کلاس هایی باید کد تولید کنه.
  4. متد _$FoodFromJson(json); که توسط json_serializable تولید شده و در فایل food.g.dart قرار داده شده یه متده که کد های اضافی برای تبدیل json به مدلمون رو داخل خودش نگه داری میکنه.
  5. متد _$FoodToJson(this) این متد هم مثل متد بالا تولید و در فایل food.g.dart قرار داده شده و کارش برعکس کار متد بالا، تبدیل مدل به json هست.

اما اگه کد بالا رو اجرا کنید هنوز هم یه مشکلی هست و حتما به یه ارور مثل ارور زیر بر میخورید.

عکس استفاده شده در flutter.dev
عکس استفاده شده در flutter.dev


مشکل اینجاست که ما کدمون رو تغییر دادیم اما generator رو فعال نکردیم. و به ما ارور میده که فایل مورد نظر generate نشده.

واسه فعال کردن generator در فلاتر 2 روش وجود داره:

تولید کد یکباره(One-time code generation)

تو این روش ما generator رو برای یک اجرا میکنیم و اگه تغییراتی رو فایل مدلمون اعمال کنیم باید باز هم اجراش کنیم . برای اجرا generator به این روش کد flutter pub run build_runner build رو در ریشه پروژتون در terminal اجرا کنید. اگه از cmd استفاده میکنید یادتون نره که به آدرس ریشه پروژتون برید.


تولید کد به صورت مداوم(Generating code continuously)

با این روش هر وقت که ما تغییری رو فایلای مورد نظرمون ایجاد میکنیم کد مرتبط باهاش بلافاصله تولید میشه. واسه استفاده از این روش کد flutter pub run build_runner watch رو در ریشه پروژتون اجرا کنید. واسه اطلاعات بیشتر تو این مورد میتونید برید به این لینک. و اما یه نکته یادتون نره که generator رو وقتی لازم نداشتید متوقف کنید واسه متوقف کردنش CTRL + C رو همزمان فشار بدید.


سخن پایانی و مهم

مثال بالا یه مثال ساده بود اگر میخواید پروژرو ادامه بدید باید بگم که مدل ما یکم پیچیده تر از این کدیه که زدیم. میتونید به عنوان تمرین مدل رو خودتون کامل کنید. لینک مدل کامل شده روی گیت هاب من هست. اگرم حوصله نداشتید میتونید فایلش رو کپی کنید.


منابع

پیشنهاد میکنم لینک های بالا رو مطالعه کنید.


لینک گیت هاب

https://github.com/payam-zahedi/featured-food-sample
https://www.linkedin.com/in/payam-zahedi