3.9.1. توسعه سمت کلاینت با دارت (Frontend Development with Dart)

3.9.1.1. درس اول: مقدمه

به آموزش زبان دارت خوش آمدید! این آموزش به شما یاد می‌دهد که چگونه با ساخت یک برنامه کاربردی خط فرمان (CLI) تعاملی، کدنویسی به زبان دارت را بیاموزید. شما از همان ابتدا شروع خواهید کرد: ایجاد یک برنامه خالی در دارت. در پایان این دوره، اپلیکیشنی ساخته‌اید که ویژگی‌های ضروری توسعه در دارت را نشان می‌دهد؛ مواردی مانند متغیرها، توابع، کلاس‌ها، برنامه‌نویسی غیرهمگام (Async)، تست‌نویسی و موارد دیگر!

3.9.1.1.1. تاریخچه

توسعه در اکوسیستم «دیتائیست» بر پایه چند ستون اصلی استوار است: زبان برنامه‌نویسی دارت (Dart) و چارچوب توسعه رابط کاربری فلاتر (Flutter). هر دوی این تکنولوژی‌ها توسط شرکت گوگل (Google) توسعه داده شده‌اند و با هدف بهینه‌سازی تجربه توسعه‌دهنده و کارایی بالا طراحی شده‌اند.

زبان دارت (Dart): یک زبان شیءگرا، بهینه و با نوع‌دهی قوی (Strongly Typed) است که ابتدا در سال ۲۰۱۱ معرفی شد. دارت برای توسعه‌دهندگان زبان‌های C-style بسیار آشنا بوده و یادگیری آن سریع و ساده است. این زبان از قابلیت‌هایی نظیر کامپایل به کد ماشین (AOT) برای سرعت فوق‌العاده و کامپایل به جاوا اسکریپت برای وب بهره می‌برد.

فلاتر (Flutter) و وعده «یک بار کد بزن، همه جا اجرا کن»: در سال ۲۰۱۷، گوگل فلاتر را معرفی کرد که انقلابی در توسعه رابط کاربری به پا کرد. فلاتر این امکان را فراهم می‌آورد که با نوشتن یک کد واحد (Single Codebase)، خروجی‌های کاملاً بومی (Native) برای سیستم‌عامل‌های اندروید، iOS، ویندوز، لینوکس، مک و وب دریافت کنید.

چرا این ترکیب؟

  • بهره‌وری حداکثری: با قابلیت Hot Reload در فلاتر و دارت، تغییرات کد در لحظه روی خروجی اعمال می‌شود.

  • سادگی در یادگیری: دارت به دلیل نحو (Syntax) تمیز و مدرن، پیچیدگی‌های مدیریت حافظه را حذف کرده است.

  • کارایی نزدیک به بومی: فلاتر رابط کاربری را مستقیماً با استفاده از موتور گرافیکی خود (Impeller) رسم می‌کند که نتیجه آن، کارایی مشابه برنامه‌های بومی است.

3.9.1.1.1.1. موتور رندرینگ (Impeller) در فلاتر

موتور «ایمپلر» (Impeller) تحول بزرگی در نحوه رندرینگ یا رسم برنامه‌های فلاتر است که گوگل برای حل یکی از چالش‌های اصلی این فریم‌ورک، یعنی پرش یا افت فریم (Jank)، آن را جایگزین موتور رندرینگ قدیمی یعنی Skia کرد. در موتور قبلی یعنی Skia، فلاتر در زمان اجرا (Runtime) باید دستورات گرافیکی را به کدهای سطح پایین مانند OpenGL یا Metal ترجمه می‌کرد. مشکل این بود که ترجمه این دستورات به صورت لحظه‌ای (Compile on-the-fly) انجام می‌شد. هنگامی که کاربر به صفحه جدیدی می‌رفت، فلاتر باید شیدرهای (Shader) جدیدی تولید می‌کرد که قبلاً ساخته نشده بودند. این «تولید لحظه‌ای»، باعث می‌شد کارت گرافیک برای چند میلی‌ثانیه درگیر شود و کاربر یک پرش یا افت فریم را حس کند. به این پدیده در اصطلاح فنی، Shader Compilation Jank گفته می‌شود.

موتور ایمپلر برای حل این مشکل، از رویکرد «پیش‌تولید» استفاده می‌کند:

  • پیش‌کامپایل (Pre-compilation): ایمپلر تمام شیدرهای مورد نیاز را در زمان ساخت برنامه (Build Time) تولید می‌کند. وقتی کاربر برنامه را روی گوشی اجرا می‌کند، دیگر نیازی به تولید شیدر در لحظه نیست.

  • استفاده از گرافیک مدرن: برخلاف Skia که مجبور بود برای سازگاری با همه چیز از لایه‌های میانی استفاده کند، ایمپلر مستقیماً با APIهای سطح پایین و مدرن گرافیکی مانند Metal در iOS و Vulkan در اندروید ارتباط برقرار می‌کند.

3.9.1.1.2. مزایای کلیدی ایمپلر برای توسعه‌دهندگان

  • حذف پرش‌ها: به دلیل حذف تولید لحظه‌ای شیدرها، اجرای انیمیشن‌ها در اولین برخورد کاربر، کاملاً نرم و بدون افت فریم است.

  • عملکرد بهینه در انیمیشن‌های پیچیده: به دلیل استفاده هوشمندانه از پردازش موازی (Multi-threading)، انیمیشن‌های سنگین بسیار روان‌تر اجرا می‌شوند.

  • پایداری و بهینگی: این موتور برای سخت‌افزارهای موبایل مدرن بهینه‌سازی شده است و بار پردازشی را از روی CPU برداشته و مستقیماً به سمت GPU هدایت می‌کند.

3.9.1.1.3. نتیجه‌گیری

با انتقال فلاتر به موتور ایمپلر، این فریم‌ورک یک گام بلند به سمت ارائه تجربه کاربری مشابه برنامه‌های بومی (Native) برداشته است. در حال حاضر ایمپلر به موتور رندر پیش‌فرض در فلاتر تبدیل شده و گوگل همچنان در حال توسعه آن برای پشتیبانی بهتر از سخت‌افزارهای متنوع است.

3.9.1.1.4. پیش از شروع

این آموزش فرض می‌کند که شما موارد زیر را دارا هستید:

  • یک محیط دارت آماده‌سازی شده؛ در غیر این صورت، برای دریافت Dart SDK دستورالعمل‌ها را دنبال کنید.

  • آشنایی مقدماتی با مفاهیم برنامه‌نویسی. این آموزش برای مبتدیان مناسب است، اما مبانی برنامه‌نویسی را پوشش نمی‌دهد.

3.9.1.1.5. درس‌ها

01. ساخت اولین اپلیکیشن خود 02. افزودن تعامل به اپلیکیشن 03. نوشتن کد غیرهمگام (Asynchronous) 04. سازماندهی کد با بسته‌ها (Packages) و کتابخانه‌ها 05. تعریف روابط با کلاس‌ها 06. مدیریت خطاها به صورت اصولی 07. گسترش اپلیکیشن با استفاده از Enumها و Extensionها 08. صیقل دادن (بهینه‌سازی) اپلیکیشن CLI 09. کار با داده‌های JSON 10. تست کردن اپلیکیشن و کدها 11. دریافت داده از اینترنت 12. افزودن لاگ برای دیباگ کردن و مانیتورینگ

3.9.1.1.6. ساخت اولین اپلیکیشن

برنامه خط فرمان (CLI) خود را در دارت بسازید، اجرا کنید و اولین تغییرات را در آن اعمال کنید. به دارت خوش آمدید! در این فصل، شما اطمینان حاصل می‌کنید که پیش‌نیازها کامل هستند و سپس اولین برنامه دارت خود را می‌سازید. این فصل ساده شروع می‌شود اما با سرعت پیش می‌رود!

3.9.1.1.7. آنچه به دست خواهید آورد

  • تأیید نصب Dart SDK

  • ایجاد یک پروژه CLI با دستور dart create

  • اجرا و تغییر اولین برنامه خود

3.9.1.1.8. پیش‌نیازها

پیش از شروع این فصل، اطمینان حاصل کنید که: * Dart SDK را نصب کرده‌اید. * اگر در دارت تازه‌کار هستید، مروری بر کلیات دارت داشته‌اید.

3.9.1.1.9. وظایف (Tasks)

برای شروع پروژه، اولین برنامه «سلام دنیا» (Hello World) را در دارت بسازید.

3.9.1.1.10. وظیفه ۱: تأیید راه‌اندازی دارت

ابتدا با دنبال کردن این مراحل، مطمئن شوید که دارت روی سیستم شما آماده است.

  1. یک ترمینال (یا خط فرمان) باز کنید.

  2. دستور زیر را برای بررسی نسخه Dart SDK اجرا کنید:

    dart --version
    

    خروجی: Dart SDK version: 3.9.2 (stable) (Wed Aug 27 03:49:40 2025 -0700) on "linux_x64"

    اگر خطایی مانند "command not found" مشاهده کردید، برای تنظیم محیط خود به راهنمای نصب دارت مراجعه کنید.

3.9.1.1.11. وظیفه ۲: ایجاد یک پروژه جدید دارت

حالا اولین برنامه کاربردی خط فرمان دارت خود را بسازید.

  1. در همان ترمینال، یک پوشه جدید به نام dartpedia برای نگهداری پروژه خود ایجاد کرده و سپس وارد آن شوید:

    mkdir dartpedia
    cd dartpedia
    
  2. پروژه را ایجاد کنید:

    dart create cli
    

    این دستور یک پروژه پایه دارت به نام "cli" (مخفف Command Line Interface) ایجاد می‌کند و فایل‌ها و پوشه‌های ضروری را برای شما تنظیم می‌کند.

توجه

دستور dart create فایل‌های متعددی ایجاد می‌کند. نگران آن‌ها نباشید؛ جزئیات آن‌ها در فصل‌های آینده پوشش داده خواهد شد.

3.9.1.1.12. وظیفه ۳: اجرای اولین برنامه دارت

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

  1. در ترمینال، به پوشه پروژه جدید خود بروید:

    cd cli
    
  2. برنامه را اجرا کنید:

    dart run
    

    شما باید خروجی مشابه زیر را ببینید که اجرای موفقیت‌آمیز اولین برنامه دارت را تأیید می‌کند: Hello world: 42!

3.9.1.1.13. وظیفه ۴: اولین تغییر در کد

حالا کدی که خروجی Hello world: 42! را تولید می‌کرد، تغییر دهید.

  1. در یک ویرایشگر کد، فایل bin/cli.dart را باز کنید. * پوشه bin/ محل قرارگیری کد اجرایی شماست. * فایل cli.dart نقطه ورود اپلیکیشن شماست. * در داخل آن، تابع main را می‌بینید. هر برنامه دارت از تابع main شروع به اجرا می‌کند.

  2. فایل bin/cli.dart را به شکل زیر ویرایش کنید:

    void main(List<String> arguments) {
      print('Hello, Dart!');
    }
    
  3. فایل را ذخیره کنید. سپس در ترمینال، دوباره برنامه خود را اجرا کنید:

    dart run
    

    شما باید خروجی زیر را مشاهده کنید: Hello, Dart!

شما با موفقیت اولین برنامه دارت خود را تغییر دادید و دوباره اجرا کردید!

3.9.1.1.13.1. مرور

در این درس یاد گرفتید که چگونه یک محیط توسعه دارت را تأیید کنید، یک پروژه جدید بسازید و کدهای پایه را تغییر دهید.

3.9.1.1.13.2. کوئیز

توجه

در طول این آموزش با این کوئیزها مواجه خواهید شد. اگر تمایل دارید، می‌توانید از آن‌ها عبور کنید.

کدام دستور یک پروژه جدید دارت با فایل‌ها و ساختار پوشه‌بندی ضروری ایجاد می‌کند؟

  • dart init: نادرست است.

  • dart new: نادرست است، چنین دستوری وجود ندارد.

  • dart build: نادرست است؛ برای ساخت برنامه‌های موجود جهت استقرار استفاده می‌شود.

  • dart create: درست است! این دستور پروژه جدید را ایجاد و آماده‌سازی (scaffold) می‌کند.

توجه

راهنمایی: اصلاح خودکار کد با dart fix

دستور dart fix مانند یک دستیار هوشمند، کدهای منسوخ یا غیربهینه را شناسایی کرده و آن‌ها را به استانداردهای جدید دارت ارتقا می‌دهد.

  • dart fix --dry-run: در این حالت، هیچ تغییری در فایل‌ها اعمال نمی‌شود. این دستور صرفاً کدهای پروژه را اسکن کرده و لیستی از اصلاحات پیشنهادی را به شما نشان می‌دهد.

  • dart fix --apply: این دستور اصلاحات شناسایی شده را مستقیماً در فایل‌های پروژه شما اعمال و ذخیره می‌کند.

پیشنهاد: همیشه ابتدا از --dry-run استفاده کنید تا تغییرات را بررسی کنید و سپس با اجرای --apply آن‌ها را نهایی کنید.

نکته مهم: پیش از اجرای دستور --apply حتما از کامیت (Commit) بودن کدهای خود در Git مطمئن شوید تا در صورت نیاز، امکان بازگشت به نسخه قبلی وجود داشته باشد.

در درس بعدی، یاد می‌گیرید که چگونه با معرفی آرگومان‌های خط فرمان و کلمه کلیدی const، برنامه خود را به دستورات خاص پاسخگو کنید.

3.9.1.2. درس دوم: افزودن قابلیت تعامل به اپلیکیشن

دستورات ساده‌ای را به اپلیکیشن خط فرمان (CLI) خود اضافه کنید. با مبانی نحو (Syntax) زبان دارت، از جمله کنترل جریان، مجموعه‌ها، متغیرها، توابع و موارد دیگر آشنا شوید.

در این فصل، تجربه عملی با نحو دارت کسب خواهید کرد. یاد می‌گیرید که چگونه ورودی کاربر را بخوانید، اطلاعات راهنما (Usage) را چاپ کنید و یک تعامل ساده خط فرمان ایجاد کنید.

3.9.1.2.1. آنچه به دست خواهید آورد

  • ساخت مدیریت‌کننده‌های دستور (Command handlers) با استفاده از عبارات if

  • کار با لیست‌ها و متغیرها

  • مدیریت قابلیت پوچی (Nullability) در کد

  • تعریف و فراخوانی توابع

  • خواندن و مدیریت ورودی کاربر

3.9.1.2.2. پیش‌نیازها

پیش از شروع این فصل، اطمینان حاصل کنید که: * فصل اول را به پایان رسانده و محیط توسعه دارت فعال دارید. * با مفاهیم پایه برنامه‌نویسی (متغیرها، انواع داده، کنترل جریان) آشنایی دارید.

3.9.1.3. وظایف (Tasks)

برخی قابلیت‌های پایه را به اپلیکیشن Dartpedia اضافه کنید و نحو دارت مربوط به آن‌ها را بررسی نمایید.

3.9.1.3.1. وظیفه ۱: پیاده‌سازی دستورات version و help

  1. یک متغیر ثابت version در بالای تابع main تعریف کنید. مقدار یک متغیر const پس از تعیین، هرگز قابل تغییر نیست:

    const version = '0.0.1';
    
  2. تابع main را برای بررسی آرگومان ورودی تغییر دهید:

    void main(List<String> arguments) {
      if (arguments.isEmpty) {
        print('Hello, Dart!');
      } else if (arguments.first == 'version') {
        print('Dartpedia CLI version $version');
      }
    }
    
    • نکته: سینتکس $version را درون‌گذاری رشته (String Interpolation) می‌نامند که مقدار متغیر را مستقیماً در متن قرار می‌دهد.

  3. برای کاربرپسندتر کردن خروجی، تابع printUsage را در زیر تابع main تعریف کنید:

    void printUsage() {
      print("The following commands are valid: 'help', 'version', 'search <ARTICLE-TITLE>'");
    }
    
  4. تابع main را جهت استفاده از دستور help و مدیریت دستورات ناشناس اصلاح کنید:

    void main(List<String> arguments) {
      if (arguments.isEmpty || arguments.first == 'help') {
        printUsage();
      } else if (arguments.first == 'version') {
        print('Dartpedia CLI version $version');
      } else {
        printUsage();
      }
    }
    

3.9.1.3.3. مرور

  • از if/else برای هدایت جریان کنترل برنامه استفاده کردید.

  • با کلمات کلیدی var، final و const متغیر تعریف کردید.

  • Null Safety را با استفاده از علامت ? و اپراتور ?? مدیریت کردید.

  • با استفاده از dart:io ورودی کاربر را از ترمینال دریافت کردید.

3.9.1.3.4. کوئیز

کدام کلمه کلیدی برای تعریف یک متغیر ثابت استفاده می‌شود که مقدار آن در زمان کامپایل مشخص است؟

  • var: نادرست.

  • static: نادرست.

  • final: نادرست (مقدار آن در زمان اجرا تعیین می‌شود).

  • const: درست است!

3.9.1.3.5. درس بعدی

در درس بعدی، وارد برنامه‌نویسی غیرهمگام (Asynchronous) خواهید شد و یاد می‌گیرید که چگونه با استفاده از پکیج http، داده‌ها را از Wikipedia API دریافت کنید.

3.9.1.4. درس سوم: نوشتن کد غیرهمگام (Asynchronous)

با برنامه‌نویسی غیرهمگام در دارت آشنا شوید و یاد بگیرید که چگونه درخواست‌های HTTP ارسال کنید.

در این فصل، برنامه‌نویسی غیرهمگام را در دارت بررسی می‌کنید که به اپلیکیشن‌های شما اجازه می‌دهد چندین کار را به‌طور هم‌زمان انجام دهند. شما یاد می‌گیرید که چگونه با استفاده از پکیج http، داده‌ها را از اینترنت دریافت کرده و خلاصه مقالات ویکی‌پدیا را بازیابی کنید.

3.9.1.5. آنچه به دست خواهید آورد

  • نوشتن کد غیرهمگام با استفاده از Future و await

  • نصب و استفاده از پکیج‌های خارجی

  • دریافت داده‌های زنده از Wikipedia API

3.9.1.6. پیش‌نیازها

  • پایان فصل دوم و داشتن محیط توسعه فعال دارت.

  • آشنایی با مفهوم API به عنوان راهی برای دریافت داده‌ها.

3.9.1.7. وظایف (Tasks)

در این فصل، اپلیکیشن CLI فعلی خود را تغییر می‌دهید تا با استفاده از پکیج http و تکنیک‌های برنامه‌نویسی غیرهمگام، خلاصه مقاله را دریافت و نمایش دهد.

3.9.1.7.1. وظیفه ۱: افزودن وابستگی http

قبل از ارسال درخواست‌های HTTP، باید پکیج http را به پروژه خود اضافه کنید.

  1. فایل dartpedia/cli/pubspec.yaml را باز کنید. این فایل (pubspec) مدیریت وابستگی‌ها و متادیتای پروژه شما را بر عهده دارد.

  2. در بخش dependencies، پکیج http را اضافه کنید:

    dependencies:
      http: ^1.4.0
    
  3. در ترمینال و در مسیر dartpedia/cli، دستور زیر را اجرا کنید تا پکیج دانلود و اضافه شود:

    dart pub get
    

3.9.1.7.2. وظیفه ۲: ایمپورت کردن پکیج http

  1. فایل bin/cli.dart را باز کنید.

  2. خط زیر را به بالای فایل، کنار ایمپورت‌های دیگر اضافه کنید:

    import 'package:http/http.dart' as http;
    

    استفاده از ``as http`` یک قرارداد استاندارد است تا از تداخل نام‌های احتمالی جلوگیری شود.

3.9.1.7.3. وظیفه ۳: پیاده‌سازی تابع getWikipediaArticle

این تابع داده‌ها را از API دریافت می‌کند. چون درخواست‌های شبکه زمان‌بر هستند، این تابع باید async باشد.

  1. تابع زیر را در پایین فایل تعریف کنید:

    Future<String> getWikipediaArticle(String articleTitle) async {
      final url = Uri.https(
        'en.wikipedia.org',
        '/api/rest_v1/page/summary/$articleTitle',
      );
      final response = await http.get(url);
    
      if (response.statusCode == 200) {
        return response.body;
      }
      return 'Error: Failed to fetch article "$articleTitle". Status code: ${response.statusCode}';
    }
    
    • Future<String>: نشان می‌دهد که این تابع در نهایت یک رشته برمی‌گرداند، اما نه بلافاصله.

    • await: اجرای تابع را تا زمان دریافت پاسخ از سرور متوقف می‌کند.

3.9.1.7.4. وظیفه ۴: یکپارچه‌سازی API در searchWikipedia

  1. تابع searchWikipedia را برای استفاده از قابلیت غیرهمگام به‌روز کنید:

    void searchWikipedia(List<String>? arguments) async {
      final String articleTitle;
      if (arguments == null || arguments.isEmpty) {
        print('Please provide an article title.');
        final inputFromStdin = stdin.readLineSync();
        if (inputFromStdin == null || inputFromStdin.isEmpty) {
          print('No article title provided. Exiting.');
          return;
        }
        articleTitle = inputFromStdin;
      } else {
        articleTitle = arguments.join(' ');
      }
    
      print('Looking up articles about "$articleTitle". Please wait.');
      var articleContent = await getWikipediaArticle(articleTitle);
      print(articleContent);
    }
    

3.9.1.7.5. وظیفه ۵: به‌روزرسانی main

در تابع main، شرط مربوط به search را به wikipedia تغییر دهید تا تابع جدید فراخوانی شود:

} else if (arguments.first == 'wikipedia') {
  final inputArgs = arguments.length > 1 ? arguments.sublist(1) : null;
  searchWikipedia(inputArgs);
}

3.9.1.7.6. وظیفه ۶: تست نهایی

اکنون برنامه را اجرا کنید:

dart run bin/cli.dart wikipedia "Dart_(programming_language)"

شما با موفقیت داده‌های واقعی را از اینترنت دریافت کردید!

3.9.1.8. مرور

  • Future: نشان‌دهنده مقداری است که در آینده در دسترس قرار می‌گیرد.

  • async/await: کلمه async تابع را غیرهمگام می‌کند و await منتظر پایان عملیات غیرهمگام می‌ماند.

  • pubspec.yaml: مدیریت وابستگی‌های خارجی پروژه را بر عهده دارد.

3.9.1.9. کوئیز

کدام کلمه کلیدی برای مشخص کردن یک تابع به عنوان غیرهمگام در دارت استفاده می‌شود؟

  • async: درست است!

  • asynchronous: نادرست.

  • future: نادرست.

  • await: نادرست.

3.9.1.10. درس بعدی

در فصل بعد، بر سازماندهی کد در قالب کتابخانه‌ها و پکیج‌های قابل استفاده مجدد تمرکز خواهیم کرد.

3.9.1.11. سازماندهی کد دارت با پکیج‌ها و کتابخانه‌ها

یاد بگیرید چگونه کد دارت خود را در قالب کتابخانه‌ها و پکیج‌های قابل استفاده مجدد سازماندهی کنید.

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

3.9.1.12. آنچه به دست خواهید آورد

  • ایجاد یک پکیج قابل استفاده مجدد در دارت

  • استفاده از دستور export برای تعریف یک API عمومی

  • افزودن وابستگی‌های محلی (Local path dependencies)

3.9.1.13. پیش‌نیازها

  • پایان فصل سوم (برنامه‌نویسی غیرهمگام و درخواست‌های HTTP).

3.9.1.14. وظایف (Tasks)

در این فصل، شما با استخراج منطق تحلیل آرگومان‌های خط فرمان به یک پکیج جداگانه به نام command_runner، اپلیکیشن dartpedia را بازسازی (Refactor) می‌کنید.

توجه

کلاس command_runner بخشی از پکیج رسمی args است. در این آموزش ما کلاس اختصاصی خودمان را می‌سازیم، اما در پروژه‌های واقعی معمولاً از پکیج رسمی استفاده می‌شود.

3.9.1.14.1. وظیفه ۱: ایجاد پکیج command_runner

  1. به دایرکتوری ریشه پروژه (/dartpedia) بروید.

  2. دستور زیر را برای ایجاد ساختار پکیج اجرا کنید:

    dart create -t package command_runner
    

3.9.1.14.2. وظیفه ۲: پیاده‌سازی کلاس CommandRunner

  1. در فایل command_runner/lib/command_runner.dart، کد زیر را قرار دهید:

    library;
    export 'src/command_runner_base.dart';
    
  2. در فایل command_runner/lib/src/command_runner_base.dart، کلاس خود را پیاده کنید:

    class CommandRunner {
      Future<void> run(List<String> input) async {
        print('CommandRunner received arguments: $input');
      }
    }
    
  • Future<void> نشان می‌دهد که این متد ممکن است عملیات غیرهمگام انجام دهد اما مقداری برنمی‌گرداند.

3.9.1.14.3. وظیفه ۳: افزودن command_runner به عنوان وابستگی

  1. فایل cli/pubspec.yaml را باز کنید و در بخش dependencies، پکیج محلی را اضافه کنید:

    dependencies:
      http: ^1.4.0
      command_runner:
        path: ../command_runner
    
  2. در ترمینال (در مسیر cli) دستور dart pub get را اجرا کنید.

3.9.1.14.4. وظیفه ۴: استفاده از پکیج command_runner

  1. در فایل cli/bin/cli.dart، این پکیج را ایمپورت کنید:

    import 'package:command_runner/command_runner.dart';
    
  2. تابع main را برای استفاده از کلاس جدید تغییر دهید:

    void main(List<String> arguments) async {
      var runner = CommandRunner();
      await runner.run(arguments);
    }
    

با این تغییر، منطق‌های قدیمی (مثل ``searchWikipedia`` و غیره) حذف شده و کنترل برنامه به پکیج جدید منتقل می‌شود.

3.9.1.14.5. وظیفه ۵: اجرای اپلیکیشن

دستور زیر را در ترمینال اجرا کنید:

dart run bin/cli.dart wikipedia Computer_programming

خروجی باید به این صورت باشد: CommandRunner received arguments: [wikipedia, Computer_programming]

3.9.1.15. مرور

  • Library Packages: روش استاندارد سازماندهی کد برای استفاده مجدد.

  • Export: فایل‌های موجود در lib/src/ را برای مصرف‌کنندگان عمومی در دسترس قرار می‌دهد.

  • Path Dependency: اجازه می‌دهد پکیج‌های محلی در یک فضای کاری به هم متصل شوند.

3.9.1.16. کوئیز

هدف از دستور `export` در کتابخانه دارت چیست؟

  • ایجاد API عمومی: درست است!

  • تعیین نسخه SDK: نادرست (در pubspec است).

  • مخفی کردن جزئیات پیاده‌سازی: نادرست (برعکس عمل می‌کند).

3.9.1.17. درس بعدی

در فصل بعد، به مفاهیم شیءگرایی (OOP) در دارت می‌پردازیم تا یک چارچوب تحلیل آرگومان قوی‌تر بسازیم.

3.9.1.18. تعریف روابط با استفاده از کلاس‌ها

با قدرت برنامه‌نویسی شیءگرا (OOP) در دارت آشنا شوید. یاد بگیرید چگونه کلاس‌ها را ایجاد کرده و روابط بین آن‌ها را با استفاده از ارث‌بری (Inheritance) و کلاس‌های انتزاعی (Abstract Classes) تعریف کنید.

در این فصل، زیربنای یک اپلیکیشن CLI با معماری قوی را می‌سازید که می‌تواند دستورات پیچیده مانند $ dartpedia help --verbose --command=search را مدیریت کند.

3.9.1.19. آنچه به دست خواهید آورد

  • طراحی و درک کلاس‌های انتزاعی

  • گسترش (Extend) کلاس‌های والد و Override کردن متدها

  • استفاده از Enumها برای نمایش مجموعه‌ای از مقادیر ثابت

  • ادامه ساخت چارچوب (Framework) اپلیکیشن CLI

3.9.1.20. وظایف (Tasks)

3.9.1.20.1. وظیفه ۱: تعریف سلسله‌مراتب آرگومان‌ها

  1. فایل command_runner/lib/src/arguments.dart را ایجاد کنید.

  2. یک enum برای نوع گزینه‌ها تعریف کنید:

    enum OptionType { flag, option }
    
  3. کلاس انتزاعی Argument را تعریف کنید:

    abstract class Argument {
      String get name;
      String? get help;
      Object? get defaultValue;
      String? get valueHelp;
      String get usage;
    }
    

    کلمه کلیدی ``abstract`` یعنی این کلاس مستقیماً قابل نمونه‌سازی نیست.

  4. کلاس‌های Option و Command را بر اساس Argument گسترش دهید:

    class Option extends Argument {
      // ... پیاده‌سازی متدها و خصوصیات
    }
    
    abstract class Command extends Argument {
      late CommandRunner runner; // late به معنی قولِ مقداردهی قبل از استفاده است
      // ...
    }
    
  5. کلاس ArgResults را برای نگهداری نتایج تحلیل آرگومان‌ها اضافه کنید. این کلاس از متدهای کمکی برای تشخیص ``flag``ها و ``option``ها استفاده می‌کند.

3.9.1.20.2. وظیفه ۲: به‌روزرسانی کلاس CommandRunner

در command_runner/lib/src/command_runner_base.dart، کلاس CommandRunner را طوری تغییر دهید که از کلاس‌های جدید استفاده کند:

class CommandRunner {
  final Map<String, Command> _commands = <String, Command>{};

  Future<void> run(List<String> input) async {
    final ArgResults results = parse(input);
    if (results.command != null) {
      Object? output = await results.command!.run(results);
      print(output.toString());
    }
  }
  // ...
}

3.9.1.20.3. وظیفه ۳: ایجاد HelpCommand

یک کلاس برای چاپ راهنمای برنامه در command_runner/lib/src/help_command.dart ایجاد کنید که از کلاس Command ارث‌بری می‌کند:

class HelpCommand extends Command {
  HelpCommand() {
    addFlag('verbose', abbr: 'v', help: '...');
  }
  @override
  String get name => 'help';
  // ...
  @override
  FutureOr<Object?> run(ArgResults args) async {
    // منطق چاپ راهنما
  }
}

3.9.1.20.4. وظیفه ۴: استفاده از CommandRunner جدید در cli.dart

فایل cli/bin/cli.dart را بسیار ساده کنید:

import 'package:command_runner/command_runner.dart';

void main(List<String> arguments) {
  var commandRunner = CommandRunner()..addCommand(HelpCommand());
  commandRunner.run(arguments);
}

عملگر ``..`` (cascade operator) اجازه می‌دهد متدها را پشت سر هم روی یک شیء فراخوانی کنید.

3.9.1.21. مرور

  • Abstract Classes: قراردادهایی را تعریف می‌کنند که زیرکلاس‌ها باید از آن پیروی کنند.

  • Inheritance (extends): اجازه می‌دهد کدهای مشترک در کلاس‌های والد مدیریت شوند.

  • @override: برای اطمینان از اینکه متدهای والد به درستی بازنویسی شده‌اند.

  • Encapsulation: استفاده از _ برای خصوصی‌سازی فیلدها و محافظت از وضعیت داخلی کلاس.

3.9.1.22. کوئیز

هدف از استفاده از انوتیشن `@override` در کلاس Option چیست؟

  • برای اجرای پیاده‌سازی متد تعریف شده در کلاس والد.

  • درست است! نشان می‌دهد که شما در حال ارائه پیاده‌سازی مشخص برای یک متد انتزاعی یا جایگزینی پیاده‌سازی موروثی هستید.

3.9.1.23. درس بعدی

در درس بعدی، یاد می‌گیرید که چگونه خطاها و استثناها (Exceptions) را در کدهای خود مدیریت کنید و کلاس‌های سفارشی برای مدیریت خطا بسازید.

3.9.1.24. مدیریت اصولی خطاها

اپلیکیشن خود را با مدیریت اصولی خطاها، مقاوم‌تر کنید. با مفاهیم استثناها (Exceptions)، خطاها (Errors)، بلوک‌های try/catch و کلمات کلیدی throw و rethrow آشنا شوید.

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

3.9.1.25. آنچه به دست خواهید آورد

  • تشخیص تفاوت بین خطاها (Errors) و استثناها (Exceptions)

  • استفاده از try/catch برای مدیریت شکست‌ها

  • ایجاد و پرتاب (Throw) استثناهای سفارشی

3.9.1.26. وظایف (Tasks)

3.9.1.26.1. وظیفه ۱: ایجاد استثنای سفارشی ArgumentException

  1. فایل command_runner/lib/src/exceptions.dart را ایجاد کنید.

  2. کلاس ArgumentException را تعریف کنید که از FormatException ارث‌بری می‌کند:

    class ArgumentException extends FormatException {
      final String? command;
      final String? argumentName;
    
      ArgumentException(super.message, [this.command, this.argumentName, super.source, super.offset]);
    
      @override
      String toString() => 'ArgumentException: $message';
    }
    

3.9.1.26.2. وظیفه ۲: پیاده‌سازی مدیریت خطا در CommandRunner

  1. فایل command_runner/lib/src/command_runner_base.dart را باز کرده و مدیریت خطا را به متد run اضافه کنید:

    Future<void> run(List<String> input) async {
      try {
        final ArgResults results = parse(input);
        if (results.command != null) {
          Object? output = await results.command!.run(results);
          print(output.toString());
        }
      } on Exception catch (exception) {
        if (onError != null) {
          onError!(exception);
        } else {
          rethrow;
        }
      }
    }
    
  2. متد parse را به‌روزرسانی کنید تا در صورت ورود آرگومان‌های نامعتبر (مانند دستورات ناشناس یا گزینه‌های اشتباه)، ArgumentException را پرتاب کند.

3.9.1.26.3. وظیفه ۳: به‌روزرسانی cli.dart

در cli/bin/cli.dart، تابع onError را به سازنده CommandRunner پاس دهید تا خطاهای نوع Exception را چاپ کند:

var commandRunner = CommandRunner(
  onError: (Object error) {
    if (error is Error) throw error;
    if (error is Exception) print(error);
  },
)..addCommand(HelpCommand());

3.9.1.26.4. وظیفه ۴: صادرات (Export) استثنا

در command_runner/lib/command_runner.dart خط زیر را اضافه کنید:

export 'src/exceptions.dart';

3.9.1.26.5. وظیفه ۵: تست مدیریت خطا

دستور زیر را اجرا کنید تا عملکرد مدیریت خطا را ببینید:

dart run bin/cli.dart invalid_command

خروجی باید این باشد: ArgumentException: The first word of input must be a command.

3.9.1.27. مرور

  • Errors vs Exceptions: زیرمجموعه‌های Error نشان‌دهنده باگ‌های برنامه‌نویسی هستند (نباید catch شوند) و Exception نشان‌دهنده شکست‌های قابل بازیابی است (باید مدیریت شوند).

  • try/catch: برای گرفتن استثناها و جلوگیری از کرش کردن برنامه.

  • rethrow: استثنا را دوباره پرتاب می‌کند در حالی که Stack Trace اصلی حفظ می‌شود.

3.9.1.28. کوئیز

هنگام نوشتن تابعی که ورودی کاربر را تحلیل می‌کند، بهترین راه برای اطلاع‌رسانی خطا به کدهای فراخواننده چیست؟

  • پرتاب یک استثنا (Exception) که دلیل خطا را توصیف می‌کند: درست است! این کار باعث توقف مسیر اشتباه شده و کدهای فراخواننده را مجبور به مدیریت خطا می‌کند.

  • بازگرداندن null: نادرست (مستعد فراموشی و ایجاد باگ است).

3.9.1.29. درس بعدی

در درس بعدی، به ویژگی‌های پیشرفته شیءگرایی شامل Enumهای بهبودیافته و Extensionها می‌پردازیم تا خروجی CLI خود را رنگی و زیباتر کنیم.

3.9.1.30. گسترش اپلیکیشن با Enumهای پیشرفته و Extensionها

مهارت‌های خود در دارت را با کاوش در ویژگی‌های پیشرفته مانند Enumهای پیشرفته و Extensionها ارتقا دهید. خروجی اپلیکیشن خود را با افزودن رنگ و فرمت‌دهی بهتر، کاربرپسندتر کنید.

در این فصل، یاد می‌گیرید که چگونه با استفاده از Enumهای پیشرفته، رنگ‌های کنسول را مدیریت کنید و با Extensionها، قابلیت‌های جدیدی به انواع داده‌های موجود اضافه نمایید.

3.9.1.31. آنچه به دست خواهید آورد

  • استفاده از Enumهای پیشرفته با فیلدها و متدها

  • گسترش (Extend) انواع داده‌های موجود با Extensionها

  • افزودن خروجی رنگی به کنسول

3.9.1.32. وظایف (Tasks)

3.9.1.32.1. وظیفه ۱: بهبود Enum رنگ‌های کنسول

  1. فایل command_runner/lib/src/console.dart را ایجاد کنید.

  2. یک Enum پیشرفته برای رنگ‌ها تعریف کنید. Enumهای پیشرفته در دارت می‌توانند دارای سازنده، خصوصیات و متد باشند:

    enum ConsoleColor {
      lightBlue(184, 234, 254),
      red(242, 93, 80),
      yellow(249, 248, 196),
      grey(240, 240, 240),
      white(255, 255, 255);
    
      const ConsoleColor(this.r, this.g, this.b);
      final int r, g, b;
    
      // متدی برای اعمال رنگ به متن با استفاده از کدهای ANSI
      String applyForeground(String text) {
        return '\x1B[38;2;$r;$g;${b}m$text\x1B[0m';
      }
    }
    

3.9.1.32.2. وظیفه ۲: ایجاد Extension برای String

Extensionها اجازه می‌دهند بدون تغییر یا ارث‌بری از یک کلاس، متدهای جدیدی به آن اضافه کنید:

extension TextRenderUtils on String {
  String get errorText => ConsoleColor.red.applyForeground(this);
  String get instructionText => ConsoleColor.yellow.applyForeground(this);
  String get titleText => ConsoleColor.lightBlue.applyForeground(this);
}

3.9.1.32.3. وظیفه ۳: به‌روزرسانی پکیج command_runner

در فایل command_runner/lib/command_runner.dart خط زیر را برای اکسپورت کردن کنسول اضافه کنید:

export 'src/console.dart';

3.9.1.32.4. وظیفه ۴: پیاده‌سازی دستور Echo رنگی

در فایل example/command_runner_example.dart یک دستور جدید به نام PrettyEcho بسازید که از متدهای رنگی استفاده می‌کند:

class PrettyEcho extends Command {
  // ...
  @override
  FutureOr<String> run(ArgResults arg) {
    var words = arg.commandArg!.split(' ');
    // اعمال رنگ‌های مختلف به کلمات
    return words.map((w) => w.titleText).join(' ');
  }
}

3.9.1.33. مرور

  • Enhanced Enums: علاوه بر مقادیر ثابت، می‌توانند سازنده و متد داشته باشند.

  • Extensions: راهی برای افزودن قابلیت به کلاس‌های موجود (مانند String) بدون ارث‌بری.

  • ANSI Escape Codes: استانداردی برای تغییر رنگ و استایل متن در ترمینال.

3.9.1.34. کوئیز

Enum پیشرفته در دارت چیست؟

  • Enumi که فقط مقادیر رشته‌ای دارد.

  • Enumi که می‌تواند متد و ویژگی (Property) داشته باشد. (درست است!)

  • Enumi که فقط در فلاتر قابل استفاده است.

3.9.1.35. درس بعدی

در درس آخر، پکیج command_runner را با تکمیل HelpCommand و ساختار نهایی، صیقل داده و نهایی می‌کنیم.

3.9.1.36. صیقل دادن اپلیکیشن CLI

در این فصل، آخرین تغییرات را روی پکیج command_runner اعمال می‌کنیم. HelpCommand را برای نمایش اطلاعات دقیق‌تر بهبود می‌دهیم و آرگومان onOutput را برای مدیریت انعطاف‌پذیر خروجی اضافه می‌کنیم.

3.9.1.37. آنچه به دست خواهید آورد

  • یادگیری نحوه ساخت بهینه رشته‌ها با استفاده از StringBuffer

  • بهبود اطلاعات راهنمای برنامه (Help Usage)

  • افزایش انعطاف‌پذیری CommandRunner با کال‌بک‌های خروجی

3.9.1.38. وظایف (Tasks)

3.9.1.38.1. وظیفه ۱: بهبود خروجی HelpCommand

  1. فایل command_runner/lib/src/help_command.dart را باز کرده و ایمپورت‌های لازم را اضافه کنید:

    import 'dart:async';
    import 'package:command_runner/command_runner.dart';
    import 'console.dart';
    import 'exceptions.dart';
    
  2. متد run را برای پشتیبانی از حالت‌های verbose و نمایش جزئیات دستورات خاص تغییر دهید. استفاده از StringBuffer برای ساخت رشته‌ها در حلقه‌ها کارآمدتر از عملگر + است:

    @override
    FutureOr<String> run(ArgResults args) async {
      final buffer = StringBuffer();
      buffer.writeln(runner.usage.titleText);
    
      if (args.flag('verbose')) {
        for (var cmd in runner.commands) {
          buffer.write(_renderCommandVerbose(cmd));
        }
        return buffer.toString();
      }
      // ... (سایر منطق‌ها)
      return buffer.toString();
    }
    

3.9.1.38.2. وظیفه ۲: افزودن کال‌بک onOutput

در فایل command_runner/lib/src/command_runner_base.dart، قابلیت مدیریت خروجی را اضافه کنید تا کاربر بتواند نحوه چاپ خروجی را شخصی‌سازی کند (مثلاً برای لاگ‌گیری یا تغییر نحوه چاپ):

class CommandRunner {
  CommandRunner({this.onOutput, this.onError});

  FutureOr<void> Function(String)? onOutput;
  FutureOr<void> Function(Object)? onError;

  Future<void> run(List<String> input) async {
    // ...
    if (onOutput != null) {
      await onOutput!(output.toString());
    } else {
      print(output.toString());
    }
  }
}

3.9.1.38.3. وظیفه ۳: استفاده از قابلیت جدید در cli.dart

در فایل cli/bin/cli.dart، تابع onOutput را به CommandRunner پاس دهید تا از متد write (که در فصل قبل ساختیم) برای چاپ استفاده کند:

void main(List<String> arguments) {
  var commandRunner = CommandRunner(
    onOutput: (String output) async {
      await write(output);
    },
    // ...
  )..addCommand(HelpCommand());
  commandRunner.run(arguments);
}

3.9.1.39. مرور

  • StringBuffer: ابزاری بهینه برای الحاق (Concatenation) رشته‌ها در حلقه‌ها.

  • Flexibility: با افزودن کال‌بک onOutput به CommandRunner، پکیج شما اکنون برای سناریوهای پیچیده‌تر (مانند تغییر مقصد خروجی یا اضافه کردن وقفه در چاپ) آماده است.

  • Verbosity: با استفاده از پرچم --verbose و گزینه‌های دقیق‌تر، تجربه کاربری CLI بهبود یافت.

3.9.1.40. کوئیز

هدف از کلاس `StringBuffer` در دارت چیست؟

  • رمزنگاری رشته‌ها.

  • ساخت بهینه رشته‌ها با الحاق بخش‌های متعدد. (درست است!)

  • ذخیره یک رشته با طول ثابت.

3.9.1.41. پایان آموزش

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

3.9.1.42. کار با داده‌های JSON

با نحوه تبدیل داده‌های JSON در دارت (Deserialization) آشنا شوید و یاد بگیرید چگونه از dart:convert و jsonDecode و قابلیت قدرتمند Pattern Matching برای کار با داده‌های Wikipedia API استفاده کنید.

در این فصل، یاد می‌گیرید که چگونه داده‌های JSON (مخفف JavaScript Object Notation) را که فرمت رایج تبادل داده در وب است، به کلاس‌های دارت تبدیل کنید. این کار باعث می‌شود کار با داده‌های API بسیار ساده‌تر و ایمن‌تر شود.

3.9.1.43. آنچه به دست خواهید آورد

  • ایجاد کلاس‌های مدل داده برای JSON

  • استفاده از dart:convert برای کار با داده‌های JSON

  • استفاده از Pattern Matching برای استخراج داده از آبجکت‌های JSON

  • راه‌اندازی یک فضای کاری چند-پکیجی (Multi-package Workspace)

3.9.1.44. وظایف (Tasks)

3.9.1.44.1. وظیفه ۱: ایجاد پکیج wikipedia

در ریشه پروژه (/dartpedia) دستور زیر را اجرا کنید تا پکیج جدید ایجاد شود:

dart create wikipedia

3.9.1.44.2. وظیفه ۲: پیکربندی Workspace در دارت

برای مدیریت بهتر وابستگی‌ها بین پکیج‌های cli ، command_runner و wikipedia، در ریشه پروژه فایل pubspec.yaml را ایجاد کنید:

name: dartpedia_root
publish_to: none
workspace:
  - cli
  - command_runner
  - wikipedia

سپس در فایل pubspec.yaml هر یک از پکیج‌های فرعی، خط resolution: workspace را اضافه کنید.

3.9.1.44.3. وظیفه ۳: ایجاد مدل داده Summary

فایل wikipedia/lib/src/model/summary.dart را ایجاد کنید. در اینجا از Pattern Matching برای استخراج مستقیم مقادیر از JSON استفاده می‌کنیم:

static Summary fromJson(Map<String, Object?> json) {
  return switch (json) {
    {
      'titles': final Map<String, Object?> titles,
      'pageid': final int pageid,
      'extract': final String extract,
      // ...
    } => Summary(titles: TitlesSet.fromJson(titles), pageid: pageid, /* ... */),
    _ => throw FormatException('Could not deserialize Summary'),
  };
}

3.9.1.44.4. وظیفه ۴: ایجاد کلاس‌های TitlesSet و Article و SearchResults

کلاس‌های مدل داده را برای ساختار پاسخ‌های API ویکی‌پدیا تعریف کنید. استفاده از if case در متد fromJson باعث می‌شود که اگر ساختار JSON تغییر کرد، برنامه به جای کرش کردن، استثنای (Exception) معناداری پرتاب کند:

// نمونه در Article
static List<Article> listFromJson(Map<String, Object?> json) {
  if (json case {'query': {'pages': final Map<String, Object?> pages}}) {
    // ... تبدیل صفحات به لیست Article
  }
  throw FormatException('Could not deserialize Article');
}

3.9.1.45. مرور

  • JSON: فرمت متنی سبک برای تبادل داده که مستقل از زبان برنامه‌نویسی است.

  • jsonDecode: تابعی در dart:convert که رشته JSON را به Map یا List تبدیل می‌کند.

  • Pattern Matching: استفاده از switch و case برای اعتبارسنجی و استخراج همزمان مقادیر از داخل Mapهای پیچیده JSON.

  • Workspace: قابلیت جدید دارت که اجازه می‌دهد چندین پکیج مرتبط را در یک پروژه واحد مدیریت کنید.

3.9.1.46. کوئیز

JSON چیست؟

  • یک پایگاه داده اختصاصی برای جاوا اسکریپت.

  • یک فرمت سبک تبادل داده که برای انسان و ماشین قابل خواندن است. (درست است!)

  • یک زبان برنامه‌نویسی برای طراحی وب.

3.9.1.47. درس بعدی

در درس بعدی، یاد می‌گیرید که چگونه برای کدهای خود تست بنویسید (با استفاده از پکیج test) تا مطمئن شوید منطقِ تبدیلِ JSON شما به درستی کار می‌کند.

3.9.1.48. تست اپلیکیشن و کدها

یاد بگیرید چگونه برای کدهای دارت خود با استفاده از کتابخانه package:test تست بنویسید. تست‌نویسی برای اطمینان از رفتار صحیح اپلیکیشن و پایداری آن هنگام ایجاد تغییرات، حیاتی است.

در این فصل، با استفاده از فریم‌ورک محبوب test در دارت، برای مدل‌های داده‌ای که در فصل قبل ایجاد کردیم، تست‌های واحد (Unit Tests) می‌نویسیم.

3.9.1.49. آنچه به دست خواهید آورد

  • افزودن پکیج test به عنوان وابستگی توسعه (Dev Dependency)

  • نوشتن تست‌های ساختاریافته با استفاده از test و matchers

  • تست منطق Deserialization داده‌های JSON

3.9.1.50. وظایف (Tasks)

3.9.1.50.1. وظیفه ۱: افزودن وابستگی تست

در فایل wikipedia/pubspec.yaml بررسی کنید که پکیج تست در بخش dev_dependencies موجود باشد:

dev_dependencies:
  test: ^1.24.0

سپس دستور dart pub get را در ترمینال اجرا کنید.

3.9.1.50.2. وظیفه ۲: ایجاد فایل تست و ایمپورت‌ها

فایل wikipedia/test/model_test.dart را ایجاد کنید و ایمپورت‌های زیر را در آن قرار دهید:

import 'dart:convert';
import 'dart:io';
import 'package:test/test.dart';
import 'package:wikipedia/src/model/article.dart';
import 'package:wikipedia/src/model/search_results.dart';
import 'package:wikipedia/src/model/summary.dart';

3.9.1.50.3. وظیفه ۳: ایجاد فایل‌های داده تست

در دایرکتوری wikipedia/test/test_data/ فایل‌های JSON نمونه (مانند dart_lang_summary.json و open_search_response.json) را ایجاد کنید تا تست‌های شما از داده‌های واقعی ویکی‌پدیا شبیه‌سازی شده استفاده کنند.

3.9.1.50.4. وظیفه ۴: نوشتن تست‌های Deserialization

با استفاده از توابع group ، test و expect تست‌ها را سازماندهی کنید:

void main() {
  group('deserialize example JSON responses', () {
    test('Summary object deserialization', () async {
      final String input = await File(dartLangSummaryJson).readAsString();
      final Map<String, Object?> map = jsonDecode(input);
      final Summary summary = Summary.fromJson(map);

      expect(summary.titles.canonical, 'Dart_(programming_language)');
    });

    test('SearchResults length test', () async {
      final String input = await File(openSearchResponse).readAsString();
      final List<Object?> list = jsonDecode(input);
      final SearchResults results = SearchResults.fromJson(list);

      expect(results.results.length, greaterThan(1));
    });
  });
}

3.9.1.50.5. وظیفه ۵: اجرای تست‌ها

در مسیر wikipedia ترمینال را باز کرده و دستور زیر را اجرا کنید:

dart test

3.9.1.51. مرور

  • Dev Dependencies: وابستگی‌هایی که فقط برای محیط توسعه و تست استفاده می‌شوند و در خروجی نهایی برنامه لحاظ نمی‌شوند.

  • Test Structure:
    • group(): برای دسته‌بندی تست‌های مرتبط.

    • test(): برای تعریف هر مورد تست مجزا.

    • expect(): برای بررسی درستی خروجی (Assertion) با استفاده از matchers.

3.9.1.52. کوئیز

رابطه بین group، test و expect چیست؟

  • group تست‌های مرتبط را دسته‌بندی می‌کند، test هر تست را تعریف می‌کند و expect برای درستی نتیجه ادعا (Assertion) صادر می‌کند. (درست است!)

  • تست و گروپ جایگزین هم هستند.

  • expect مسئول اجرای تست‌هاست.

3.9.1.53. درس بعدی

در درس بعدی، نوبت به پیاده‌سازی تماس‌های واقعی با API ویکی‌پدیا در اپلیکیشن اصلی می‌رسد. از پکیج http برای دریافت داده‌ها استفاده خواهیم کرد.

3.9.1.54. دریافت داده از اینترنت

اپلیکیشن خود را برای استفاده از یک کلاینت API قدرتمند و آماده برای تولید (Production-ready) در CLI ویکی‌پدیا بازنویسی کنید.

در این فصل، یاد می‌گیرید که چگونه درخواست‌های HTTP پیچیده را مدیریت کنید، پارامترهای پرس‌وجو (Query Parameters) را به درستی بسازید و پاسخ‌های JSON را به مدل‌های داده‌ای که قبلاً ساختیم، نگاشت (Map) کنید.

3.9.1.55. آنچه به دست خواهید آورد

  • ساخت URIهای پیچیده با پارامترهای پرس‌وجو

  • ایجاد یک کلاینت HTTP استاندارد و حرفه‌ای

  • اکسپورت کردن توابع برای استفاده در پکیج‌های دیگر

3.9.1.56. وظایف (Tasks)

3.9.1.56.1. وظیفه ۱: افزودن وابستگی http

در فایل wikipedia/pubspec.yaml پکیج http را اضافه کنید:

dependencies:
  http: ^1.3.0

سپس در مسیر wikipedia دستور dart pub get را اجرا کنید.

3.9.1.56.2. وظیفه ۲: پیاده‌سازی تماس‌های API ویکی‌پدیا

توابع API را در دایرکتوری wikipedia/lib/src/api/ پیاده‌سازی کنید. برای مثال، تابعی برای دریافت خلاصه یک مقاله بر اساس عنوان:

Future<Summary> getArticleSummaryByTitle(String articleTitle) async {
  final client = http.Client();
  try {
    final Uri url = Uri.https(
      'en.wikipedia.org',
      '/api/rest_v1/page/summary/$articleTitle',
    );
    final response = await client.get(url);
    if (response.statusCode == 200) {
      return Summary.fromJson(jsonDecode(response.body));
    }
    throw HttpException('Error: ${response.statusCode}');
  } finally {
    client.close();
  }
}

نکته: همیشه `client.close()` را در بلاک `finally` فراخوانی کنید تا منابع شبکه آزاد شوند.

3.9.1.56.3. وظیفه ۳: اکسپورت کردن توابع API

در فایل wikipedia/lib/wikipedia.dart تمام توابع API و مدل‌های داده‌ای را اکسپورت کنید تا در پکیج cli در دسترس باشند:

export 'src/api/get_article.dart';
export 'src/api/search.dart';
export 'src/api/summary.dart';
export 'src/model/article.dart';
// ...

3.9.1.56.4. وظیفه ۴: تایید عملکرد با تست‌ها

با اجرای دستور dart test در مسیر wikipedia مطمئن شوید که تغییرات جدید، تست‌های قبلی را خراب نکرده است.

3.9.1.57. مرور

  • Uri.https(): بهترین روش برای ساخت URL در دارت است. با پاس دادن پارامتر سوم به صورت یک Map، دارت به‌طور خودکار کار URL Encoding (تبدیل کاراکترهای خاص) را انجام می‌دهد.

  • API Client Lifecycle: مدیریت صحیح کلاینت HTTP با استفاده از try-finally برای اطمینان از بسته شدن کلاینت، باعث جلوگیری از نشت حافظه و اتلاف منابع می‌شود.

  • Modularization: جدا کردن منطق API از مدل‌های داده، باعث می‌شود کد شما در آینده برای تست‌نویسی یا تغییرات ساده‌تر باشد.

3.9.1.58. کوئیز

چگونه با استفاده از Uri.https پارامترهای پرس‌وجو (Query Parameters) را به URL اضافه کنیم؟

  • با استفاده از الحاق رشته (String Concatenation) با +.

  • با پاس دادن یک Map به عنوان آرگومان سوم. (درست است!)

  • با استفاده از متد .addQueryParam().

3.9.1.59. درس بعدی

در درس آخر، پکیج wikipedia را به پکیج cli متصل می‌کنیم و دستورات واقعی برای جستجو و نمایش مقالات ویکی‌پدیا را در ترمینال پیاده‌سازی خواهیم کرد.

3.9.1.60. افزودن سیستم لاگ‌نویسی (Logging)

در این فصل، برای کمک به عیب‌یابی (Debugging) و مانیتورینگ اپلیکیشن، سیستم لاگ‌نویسی را به Dartpedia CLI اضافه می‌کنیم. لاگ‌نویسی ابزاری حیاتی برای درک رفتار برنامه در محیط‌های مختلف است.

3.9.1.61. آنچه به دست خواهید آورد

  • استفاده از پکیج logging برای ثبت رویدادها

  • پیکربندی سطوح لاگ (Log Levels) و ذخیره آن‌ها در فایل

  • ادغام لاگ‌نویسی در دستورات CLI با استفاده از تزریق وابستگی (Dependency Injection)

3.9.1.62. وظایف (Tasks)

3.9.1.62.1. وظیفه ۱: افزودن پکیج logging

در فایل cli/pubspec.yaml پکیج را اضافه کنید:

dependencies:
  logging: ^1.2.0

سپس dart pub get را اجرا کنید.

3.9.1.62.2. وظیفه ۲: ایجاد Logger

فایل cli/lib/src/logger.dart را ایجاد کنید. این تابع یک فایل لاگ در پوشه logs ایجاد می‌کند و هر رکورد را در آن می‌نویسد:

Logger initFileLogger(String name) {
  hierarchicalLoggingEnabled = true;
  final logger = Logger(name);
  // تنظیمات مسیر فایل لاگ و مدیریت خروجی
  logger.onRecord.listen((record) {
    final msg = '[${record.time}] ${record.level.name}: ${record.message}';
    logFile.writeAsStringSync('$msg \n', mode: FileMode.append);
  });
  return logger;
}

3.9.1.62.3. وظیفه ۳: استفاده از Logger در cli.dart

در cli/bin/cli.dart لاگر را مقداردهی اولیه کرده و به دستورات پاس می‌دهیم:

void main(List<String> arguments) async {
  final errorLogger = initFileLogger('errors');
  final app = CommandRunner(
    onError: (Object error) {
      if (error is Error) errorLogger.severe(error.toString());
      if (error is Exception) errorLogger.warning(error.toString());
      throw error;
    },
  )
  ..addCommand(SearchCommand(logger: errorLogger))
  ..addCommand(GetArticleCommand(logger: errorLogger));
  app.run(arguments);
}

3.9.1.62.4. وظیفه ۴ و ۵: پیاده‌سازی دستورات Search و Article

دستورات SearchCommand و GetArticleCommand را طوری تغییر دهید که در صورت بروز خطا (مانند HttpException) از لاگر برای ثبت جزئیات استفاده کنند:

on HttpException catch (e) {
  logger..warning(e.message)..info(usage);
  return e.message;
}

3.9.1.63. مرور

  • Logger: پکیجی برای ثبت متمرکز رویدادها، هشدارها و خطاها.

  • Dependency Injection: با پاس دادن نمونه Logger به سازنده دستورات (Commands)، آن‌ها بدون نیاز به دانستن جزئیات پیاده‌سازی فایل لاگ، می‌توانند لاگ ثبت کنند.

  • Levels: استفاده از severe برای خطاهای بحرانی و warning برای مشکلات غیربحرانی، به فیلتر کردن بهتر لاگ‌ها کمک می‌کند.

3.9.1.64. کوئیز

هدف از پکیج `logging` در دارت چیست؟

  • مدیریت وابستگی‌های پروژه در pubspec.yaml.

  • ثبت رویدادها و خطاها در طول اجرای اپلیکیشن. (درست است!)

  • مدیریت درخواست‌های شبکه (HTTP).

3.9.1.65. پایان آموزش

تبریک می‌گویم! شما تمام فصل‌های اصلی این آموزش را به پایان رساندید. اکنون شما یک CLI حرفه‌ای در دارت دارید که دارای ساختار چند-پکیجی، مدیریت خطای اصولی، تست‌های واحد و سیستم لاگ‌نویسی است.

آماده‌اید برای قدم بعدی؟ می‌توانید به وب‌سایت Flutter مراجعه کرده و مسیر یادگیری «دارت و فلاتر» را ادامه دهید!