اولین برنامه Flutter شما

۱. مقدمه

‫Flutter جعبه‌ابزار واسط کاربر Google برای ساختن برنامه‌های تلفن همراه، وب، و رایانه از یک پایگاه کد واحد است. در این codelab، برنامه Flutter زیر را خواهید ساخت:

این برنامه نام‌های جالبی مثل «newstay»،‏ «lightstream»،‏ «mainbrake»، یا «graypine» تولید می‌کند. کاربر می‌تواند نام بعدی را درخواست کند، نام فعلی را به فهرست موارد دلخواه اضافه کند، و فهرست موارد دلخواه را در صفحه‌ای جداگانه ببیند. برنامه به اندازه‌های مختلف صفحه‌نمایش واکنش نشان می‌دهد.

آنچه خواهید آموخت

  • مبانی نحوه عملکرد Flutter
  • ساختن چیدمان در Flutter
  • اتصال تعاملات کاربر (مانند فشار دادن دکمه) به رفتار برنامه
  • مرتب نگه داشتن کد Flutter
  • واکنش‌گرا کردن برنامه (برای صفحه‌نمایش‌های مختلف)
  • دستیابی به ظاهر و عملکردی یکپارچه برای برنامه‌تان

با یک داربست اولیه شروع خواهید کرد تا بتوانید مستقیماً به بخش‌های جالب بروید.

e9c6b402cd8003fd.png

و این هم فیلیپ که شما را در کل codelab راهنمایی می‌کند!

برای شروع آزمایش، روی «بعدی» کلیک کنید.

۲. راه‌اندازی محیط Flutter

ویرایشگر

برای اینکه این codelab تا حد امکان ساده باشد، فرض می‌کنیم که از Visual Studio Code (VS Code) به‌عنوان محیط توسعه نرم‌افزار خود استفاده خواهید کرد. این برنامه رایگان است و در همه پلاتفرم‌های اصلی کار می‌کند.

البته استفاده از هر ویرایشگری که دوست دارید مشکلی ندارد: Android Studio، سایر IntelliJ IDEها، Emacs،‏ Vim، یا Notepad++. همه آن‌ها با Flutter کار می‌کنند.

توصیه می‌کنیم برای این codelab از VS Code استفاده کنید زیرا دستورالعمل‌ها به‌طور پیش‌فرض از میان‌برهای مختص VS Code استفاده می‌کنند. بهتر است به‌جای چیزی مثل «کنش مناسب را در ویرایشگرتان برای انجام X انجام دهید»، چیزهایی مثل «اینجا کلیک کنید» یا «این کلید را فشار دهید» بگویید.

228c71510a8e868.png

انتخاب هدف توسعه

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

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • وب

بااین‌حال، معمولاً یک سیستم‌عامل انتخاب می‌شود که در درجه اول برای آن توسعه انجام می‌دهید. این «هدف توسعه» شما است—سیستم‌عاملی که برنامه شما درطول توسعه روی آن اجرا می‌شود.

16695777c07f18e5.png

برای مثال، فرض کنید از رایانه کیفی Windows برای توسعه برنامه Flutter استفاده می‌کنید. اگر Android را به‌عنوان هدف توسعه خود انتخاب کنید، معمولاً دستگاه Android را با کابل USB به رایانه کیفی Windows متصل می‌کنید و برنامه درحال توسعه شما در آن دستگاه Android متصل اجرا می‌شود. اما می‌توانید Windows را نیز به‌عنوان هدف توسعه انتخاب کنید، یعنی برنامه درحال توسعه شما به‌عنوان برنامه Windows درکنار ویرایشگرتان اجرا می‌شود.

شاید وسوسه شوید که وب را به‌عنوان هدف توسعه خود انتخاب کنید. نکته منفی این انتخاب این است که یکی از مفیدترین ویژگی‌های توسعه Flutter را ازدست می‌دهید: Stateful Hot Reload. Flutter نمی‌تواند برنامه‌های وب را بازآوری سریع کند.

اکنون انتخاب کنید. به‌خاطر داشته باشید: همیشه می‌توانید بعداً برنامه‌تان را در سیستم‌عامل‌های دیگر اجرا کنید. فقط اینکه داشتن یک هدف توسعه واضح در ذهن، گام بعدی را هموارتر می‌کند.

نصب Flutter

به‌روزترین دستورالعمل‌ها درباره نحوه نصب Flutter SDK همیشه در docs.flutter.dev دردسترس است.

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

  1. Flutter SDK
  2. Visual Studio Code با افزایه Flutter
  3. نرم‌افزار موردنیاز برای هدف توسعه انتخابی شما (برای مثال: Visual Studio برای هدف قرار دادن Windows، یا Xcode برای هدف قرار دادن macOS)

در بخش بعدی، اولین پروژه Flutter خود را ایجاد خواهید کرد.

اگر تاکنون با مشکلاتی مواجه شده‌اید، ممکن است برخی‌از این پرسش‌ها و پاسخ‌ها (از StackOverflow) برای عیب‌یابی مفید باشد.

پرسشگان

۳. ایجاد کردن پروژه

اولین پروژه Flutter خود را ایجاد کنید

Visual Studio Code را راه‌اندازی کنید و پالت فرمان را باز کنید (با F1 یا Ctrl+Shift+P یا Shift+Cmd+P). شروع به تایپ کردن «flutter new» کنید. فرمان Flutter: New Project را انتخاب کنید.

سپس، برنامه و سپس پوشه‌ای را که می‌خواهید پروژه‌تان در آن ایجاد شود انتخاب کنید. این می‌تواند دایرکتوری خانه شما یا چیزی مثل C:\src\ باشد.

درنهایت، نامی برای پروژه انتخاب کنید. چیزی مثل namer_app یا my_awesome_namer.

260a7d97f9678005.png

اکنون Flutter پوشه پروژه شما را ایجاد می‌کند و VS Code آن را باز می‌کند.

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

کپی و جای‌گذاری برنامه اولیه

در قاب سمت راست VS Code، مطمئن شوید که Explorer انتخاب شده باشد و فایل pubspec.yaml را باز کنید.

e2a5bab0be07f4f7.png

محتوای این فایل را با موارد زیر جایگزین کنید:

pubspec.yaml

name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter
  english_words: ^4.0.0
  provider: ^6.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

flutter:
  uses-material-design: true

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

سپس، فایل پیکربندی دیگری را در پروژه باز کنید، analysis_options.yaml.

a781f218093be8e0.png

محتوای آن را با موارد زیر جایگزین کنید:

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_final_fields: false
    unnecessary_breaks: true
    use_key_in_widget_constructors: false

این فایل تعیین می‌کند که Flutter هنگام تجزیه‌وتحلیل کد شما چقدر باید سخت‌گیر باشد. چون این اولین تجربه شما در Flutter است، به تحلیل‌گر می‌گویید که سخت نگیرد. همیشه می‌توانید بعداً این مورد را تنظیم کنید. درواقع، هرچه به انتشار برنامه تولید واقعی نزدیک‌تر می‌شوید، تقریباً به‌طور قطع می‌خواهید تحلیل‌گر را سخت‌گیرانه‌تر از این کنید.

درنهایت، فایل main.dart را در فهرست راهنمای lib/ باز کنید.

e54c671c9bb4d23d.png

محتوای این فایل را با موارد زیر جایگزین کنید:

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
      ),
    );
  }
}

این ۵۰ خط کد کل برنامه تا اینجا است.

در بخش بعدی، برنامه را در حالت اشکال‌زدایی اجرا کنید و توسعه را شروع کنید.

۴. افزودن دکمه

این مرحله دکمه بعدی را برای تولید جفت کلمه جدید اضافه می‌کند.

راه‌اندازی برنامه

ابتدا، lib/main.dart را باز کنید و مطمئن شوید دستگاه هدف را انتخاب کرده‌اید. در گوشه پایین سمت چپ VS Code، دکمه‌ای را خواهید دید که دستگاه هدف فعلی را نشان می‌دهد. برای تغییر دادن آن، کلیک کنید.

درحالی‌که lib/main.dart باز است، دکمه «پخش» b0a5d0200af5985d.png را در گوشه بالا سمت راست پنجره VS Code پیدا کنید و روی آن کلیک کنید.

پس‌از حدود یک دقیقه، برنامه‌تان در حالت اشکال‌زدایی راه‌اندازی می‌شود. هنوز چیز زیادی به‌نظر نمی‌رسد:

f96e7dfb0937d7f4.png

اولین بار کردن مجدد داغ

در پایین lib/main.dart، چیزی به رشته در اولین شیء Text اضافه کنید و فایل را ذخیره کنید (با Ctrl+S یا Cmd+S). برای مثال:

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

توجه کنید که چگونه برنامه بلافاصله تغییر می‌کند اما کلمه تصادفی ثابت می‌ماند. این همان بار کردن مجدد سریع با حفظ وضعیت معروف Flutter است که درحال کار است. وقتی تغییرات را در فایل منبع ذخیره می‌کنید، بار کردن مجدد سریع راه‌اندازی می‌شود.

پرسشگان

افزودن دکمه

سپس، دکمه‌ای در پایین Column، درست زیر نمونه دوم Text اضافه کنید.

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

وقتی تغییر را ذخیره می‌کنید، برنامه دوباره به‌روز می‌شود: دکمه‌ای ظاهر می‌شود و وقتی روی آن کلیک می‌کنید، کنسول اشکال‌زدایی در VS Code پیام دکمه فشار داده شد! را نشان می‌دهد.

دوره فشرده Flutter در ۵ دقیقه

هرچند تماشای کنسول اشکال‌زدایی جالب است، اما می‌خواهید دکمه کار معنادارتری انجام دهد. اما قبل‌از رسیدن به آن، نگاهی دقیق‌تر به کد در lib/main.dart بیندازید تا با نحوه عملکرد آن آشنا شوید.

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

در بالای فایل، تابع main() را خواهید دید. در شکل فعلی، فقط به Flutter می‌گوید برنامه تعریف‌شده در MyApp را اجرا کند.

lib/main.dart

// ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

کلاس MyApp کلاس StatelessWidget را گسترش می‌دهد. ابزاره‌ها عناصری هستند که با آن‌ها هر برنامه Flutter را می‌سازید. همان‌طور که می‌بینید، حتی خود برنامه یک ابزاره است.

کد موجود در MyApp کل برنامه را راه‌اندازی می‌کند. این کد وضعیت سراسری برنامه را ایجاد می‌کند (بعداً بیشتر درباره آن توضیح می‌دهیم)، برنامه را نام‌گذاری می‌کند، زمینه بصری را تعریف می‌کند، و ابزاره «صفحه اصلی» (نقطه شروع برنامه) را تنظیم می‌کند.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

سپس، کلاس MyAppState وضعیت برنامه را تعریف می‌کند. این اولین تجربه شما در Flutter است، بنابراین این codelab ساده و متمرکز خواهد بود. راه‌های قدرتمند زیادی برای مدیریت وضعیت برنامه در Flutter وجود دارد. یکی از ساده‌ترین روش‌ها برای توضیح دادن ChangeNotifier است، رویکردی که این برنامه اتخاذ کرده است.

  • MyAppState داده‌هایی را که برنامه برای عملکرد نیاز دارد تعریف می‌کند. درحال‌حاضر، فقط حاوی یک متغیر با جفت کلمه تصادفی فعلی است. بعداً به این اضافه خواهید کرد.
  • کلاس state،‏ ChangeNotifier را گسترش می‌دهد، یعنی می‌تواند دیگران را از تغییرات خود مطلع کند. برای مثال، اگر جفت واژه فعلی تغییر کند، برخی‌از ابزاره‌های برنامه باید مطلع شوند.
  • وضعیت بااستفاده از ChangeNotifierProvider ایجاد می‌شود و برای کل برنامه ارائه می‌شود (کد بالا را در MyApp ببینید). این کار به هر ابزاره‌ای در برنامه اجازه می‌دهد تا وضعیت را به‌دست آورد.

d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           // ← 1
    var appState = context.watch<MyAppState>();  // ← 2

    return Scaffold(                             // ← 3
      body: Column(                              // ← 4
        children: [
          Text('A random AWESOME idea:'),        // ← 5
          Text(appState.current.asLowerCase),    // ← 6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       // ← 7
      ),
    );
  }
}

// ...

درنهایت، MyHomePage وجود دارد، ابزاره‌ای که قبلاً اصلاح کرده‌اید. هر خط شماره‌گذاری‌شده در زیر به یک نظر شماره خط در کد بالا نگاشت می‌شود:

  1. هر ابزاره یک روش build() را تعریف می‌کند که هر بار که شرایط ابزاره تغییر می‌کند، به‌طور خودکار فراخوانی می‌شود تا ابزاره همیشه به‌روز باشد.
  2. MyHomePage تغییرات وضعیت فعلی برنامه را بااستفاده از روش watch ردیابی می‌کند.
  3. هر روش build باید یک ابزاره یا (معمولاً) یک درخت تودرتو از ابزاره‌ها را برگرداند. در این مورد، ابزاره سطح بالا Scaffold است. در این codelab با Scaffold کار نخواهید کرد، اما ابزاره مفیدی است و در اکثر برنامه‌های Flutter در دنیای واقعی یافت می‌شود.
  4. ‫Column یکی از ابتدایی‌ترین ابزاره‌های چیدمان در Flutter است. این مؤلفه هر تعداد فرزند را می‌گیرد و آن‌ها را در ستونی از بالا به پایین قرار می‌دهد. به‌طور پیش‌فرض، ستون به‌صورت بصری فرزندان خود را در بالا قرار می‌دهد. به‌زودی این را تغییر می‌دهید تا ستون در مرکز قرار گیرد.
  5. در مرحله اول، این ابزاره Text را تغییر دادید.
  6. این ابزاره Text دوم appState را می‌گیرد و به تنها عضو آن کلاس، current (که WordPair است) دسترسی پیدا می‌کند. WordPair چندین گیرنده مفید مانند asPascalCase یا asSnakeCase ارائه می‌دهد. در اینجا، از asLowerCase استفاده می‌کنیم اما اگر یکی از گزینه‌های جایگزین را ترجیح می‌دهید، می‌توانید آن را اکنون تغییر دهید.
  7. توجه کنید که کد Flutter چگونه به‌طور گسترده از ویرگول‌های پایانی استفاده می‌کند. این ویرگول خاص نباید اینجا باشد، زیرا children آخرین (و همچنین تنها) عضو این فهرست پارامتر Column خاص است. بااین‌حال، به‌طور کلی ایده خوبی است که از ویرگول‌های پایانی استفاده کنید: آن‌ها افزودن اعضای بیشتر را ساده می‌کنند و همچنین به‌عنوان راهنمایی برای قالب‌بندی خودکار Dart برای قرار دادن خط جدید در آنجا عمل می‌کنند. برای کسب اطلاعات بیشتر، به قالب‌بندی کد بروید.

سپس، دکمه را به وضعیت متصل می‌کنید.

اولین رفتار شما

به MyAppState پیمایش کنید و روش getNext را اضافه کنید.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

روش جدید getNext()، current را با WordPair تصادفی جدیدی دوباره اختصاص می‌دهد. همچنین notifyListeners() (روشی از ChangeNotifier) را فراخوانی می‌کند که تضمین می‌کند هرکسی که MyAppState را تماشا می‌کند مطلع شود.

تنها کاری که باقی مانده است فراخوانی روش getNext از فراخوانی دکمه است.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

اکنون برنامه را ذخیره و امتحان کنید. هر بار که دکمه بعدی را فشار می‌دهید، باید جفت کلمه تصادفی جدیدی تولید کند.

در بخش بعدی، واسط کاربر را زیباتر خواهید کرد.

۵. برنامه را زیباتر کنید

برنامه درحال‌حاضر این‌گونه به‌نظر می‌رسد.

3dd8a9d8653bdc56.png

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

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

2bbee054d81a3127.png

استخراج کردن ابزاره

خطی که مسئول نمایش جفت کلمه فعلی است اکنون به‌این‌صورت است: Text(appState.current.asLowerCase). برای تغییر آن به چیزی پیچیده‌تر، ایده خوبی است که این خط را به یک ابزاره جداگانه استخراج کنید. داشتن ابزارهایی جداگانه برای بخش‌های منطقی جداگانه در واسط کاربر روش مهمی برای مدیریت پیچیدگی در Flutter است.

Flutter یک کمک‌کننده بازسازی کد برای استخراج ابزاره‌ها ارائه می‌دهد، اما قبل‌از استفاده از آن، مطمئن شوید که خطی که استخراج می‌شود فقط به آنچه نیاز دارد دسترسی دارد. درحال‌حاضر، این خط به appState دسترسی دارد، اما واقعاً فقط باید بداند جفت کلمه فعلی چیست.

به همین دلیل، ابزارک MyHomePage را به‌صورت زیر بازنویسی کنید:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;                 // ← Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                // ← Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

خوب است. ابزاره Text دیگر به کل appState ارجاع نمی‌دهد.

اکنون، منو بازسازی کد را فراخوانی کنید. در VS Code، این کار را به یکی از دو روش زیر انجام می‌دهید:

  1. روی قطعه کدی که می‌خواهید بازسازی کنید (در این مورد Text) کلیک‌راست کنید و از منوِ کرکره‌ای، بازسازی کد… را انتخاب کنید،

یا

  1. مکان‌نما را به بخشی از کد که می‌خواهید بازسازی کنید (در این مورد، Text) ببرید و Ctrl+. (Win/Linux) یا Cmd+. (Mac) را فشار دهید.

در منو بازسازی کد، استخراج ابزاره را انتخاب کنید. نامی مثل BigCard اختصاص دهید و روی Enter کلیک کنید.

این کار به‌طور خودکار کلاس جدیدی (BigCard) در انتهای فایل فعلی ایجاد می‌کند. کلاس چیزی شبیه به زیر است:

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({super.key, required this.pair});

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

توجه کنید که برنامه حتی درطول این بازسازی کد همچنان کار می‌کند.

افزودن کارت

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

کلاس BigCard و روش build() را در آن پیدا کنید. مثل قبل، منو بازسازی کد را در ابزاره Text باز کنید. اما این بار قرار نیست ابزارک را استخراج کنید.

به‌جای آن، Wrap with Padding (پیچیدن با حاشیه داخلی) را انتخاب کنید. با این کار، ابزاره والد جدیدی به‌نام Padding در اطراف ابزاره Text ایجاد می‌شود. پس‌از ذخیره کردن، خواهید دید که کلمه تصادفی ازقبل فضای بیشتری دارد.

افزایش حاشیه داخلی از مقدار پیش‌فرض 8.0. برای مثال، برای حاشیه داخلی جادارتر از چیزی مثل 20 استفاده کنید.

سپس، یک سطح بالاتر بروید. مکان‌نما را روی ابزاره Padding قرار دهید، منو بازسازی کد را بالا بکشید، و Wrap with widget... (پیچیدن با ابزاره...) را انتخاب کنید.

این به شما امکان می‌دهد ابزاره اصلی را مشخص کنید. «کارت» را تایپ کنید و کلید Enter را فشار دهید.

این کار ابزاره Padding و درنتیجه Text را با ابزاره Card می‌پوشاند.

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({super.key, required this.pair});

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }
}

// ...

اکنون برنامه چیزی شبیه به این خواهد بود:

6031adbc0a11e16b.png

زمینه و سبک

برای اینکه کارت بیشتر متمایز شود، آن را با رنگی غنی‌تر رنگ‌آمیزی کنید. و چون همیشه ایده خوبی است که آرایش رنگ‌ها را ثابت نگه دارید، از Theme برنامه برای انتخاب رنگ استفاده کنید.

تغییرات زیر را در روش build() BigCard ایجاد کنید.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       // ← Add this.

    return Card(
      color: theme.colorScheme.primary,    // ← And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

این دو خط جدید کارهای زیادی انجام می‌دهند:

  • ابتدا، کد بااستفاده از Theme.of(context)، طرح زمینه فعلی برنامه را درخواست می‌کند.
  • سپس، کد رنگ کارت را به‌صورت همان ویژگی colorScheme زمینه تعریف می‌کند. آرایش رنگ‌ها شامل رنگ‌های زیادی است و primary برجسته‌ترین رنگی است که برنامه را تعریف می‌کند.

اکنون کارت با رنگ اصلی برنامه رنگ‌آمیزی شده است:

a136f7682c204ea1.png

می‌توانید این رنگ و آرایش رنگ‌ها را برای کل برنامه تغییر دهید. برای این کار، به بالا به MyApp پیمایش کنید و رنگ پایه را برای ColorScheme در آنجا تغییر دهید.

توجه کنید که رنگ چگونه به‌آرامی پویانمایی می‌شود. به این حالت پویانمایی ضمنی می‌گویند. بسیاری از ابزاره‌های Flutter به‌آرامی بین مقادیر درون‌یابی می‌کنند تا میانای کاربر فقط بین حالت‌ها «پرش» نکند.

دکمه برجسته زیر کارت نیز تغییر رنگ می‌دهد. این قدرت استفاده از Theme در سراسر برنامه درمقابل مقادیر کدگذاری‌شده است.

TextTheme

کارت هنوز مشکل دارد: نوشتار خیلی کوچک است و رنگ آن خوانا نیست. برای رفع این مشکل، تغییرات زیر را در روش build() BigCard ایجاد کنید.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // ↓ Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        // ↓ Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

دلیل این تغییر:

  • بااستفاده از theme.textTheme، به زمینه قلم برنامه دسترسی پیدا می‌کنید. این کلاس شامل اعضایی مانند bodyMedium (برای نوشتار استاندارد با اندازه متوسط)، caption (برای زیرنویس تصاویر)، یا headlineLarge (برای عناوین بزرگ) است.
  • ویژگی displayMedium سبکی بزرگ است که برای نمایش نوشتار درنظر گرفته شده است. واژه نمایش در اینجا به‌معنای تایپوگرافی استفاده شده است، مانند طرح حروف نمایشی. در اسناد displayMedium گفته شده است که «سبک‌های نمایش برای نوشتار کوتاه و مهم رزرو شده‌اند»—دقیقاً مورد استفاده ما.
  • ویژگی displayMedium زمینه ازنظر تئوری می‌تواند null باشد. Dart، زبان برنامه‌نویسی که این برنامه را با آن می‌نویسید، ازنظر مقدار تهی ایمن است، بنابراین به شما اجازه نمی‌دهد روش‌های اشیایی را که احتمالاً null هستند فراخوانی کنید. بااین‌حال، در این مورد می‌توانید از عملگر ! («عملگر bang») استفاده کنید تا به Dart اطمینان دهید که می‌دانید چه می‌کنید. (displayMedium قطعاً در این مورد تهی نیست. البته دلیل اینکه این را می‌دانیم فراتر از محدوده این codelab است.)
  • فراخوانی copyWith() در displayMedium کپی سبک نوشتار را با تغییراتی که تعریف می‌کنید برمی‌گرداند. در این مورد، فقط رنگ نوشتار را تغییر می‌دهید.
  • برای دریافت رنگ جدید، دوباره به زمینه برنامه دسترسی پیدا می‌کنید. ویژگی onPrimary آرایش رنگ‌ها رنگی را تعریف می‌کند که برای استفاده روی رنگ اصلی برنامه مناسب است.

اکنون برنامه باید چیزی شبیه به تصویر زیر باشد:

2405e9342d28c193.png

اگر مایلید، کارت را بیشتر تغییر دهید. چند ایده در اینجا آمده است:

  • copyWith() به شما امکان می‌دهد علاوه‌بر رنگ، چیزهای بیشتری را در سبک نوشتار تغییر دهید. برای دریافت فهرست کامل ویژگی‌هایی که می‌توانید تغییر دهید، مکان‌نما را در هر جایی داخل پرانتز copyWith() قرار دهید و Ctrl+Shift+Space (Win/Linux) یا Cmd+Shift+Space (Mac) را فشار دهید.
  • به‌طور مشابه، می‌توانید موارد بیشتری را درباره ابزاره Card تغییر دهید. برای مثال، می‌توانید با افزایش مقدار پارامتر elevation، سایه کارت را بزرگ‌تر کنید.
  • رنگ‌ها را امتحان کنید. علاوه‌بر theme.colorScheme.primary،‏ .secondary،‏ .surface، و تعداد بی‌شماری از موارد دیگر نیز وجود دارد. همه این رنگ‌ها معادل onPrimary خود را دارند.

بهبود دسترس‌پذیری

Flutter برنامه‌ها را به‌طور پیش‌فرض دسترس‌پذیر می‌کند. برای مثال، هر برنامه Flutter به‌درستی همه نوشتارها و عناصر تعاملی را در برنامه برای صفحه‌خوان‌هایی مانند TalkBack و VoiceOver نمایش می‌دهد.

d1fad7944fb890ea.png

اما گاهی اوقات، کمی کار لازم است. درمورد این برنامه، صفحه‌خوان ممکن است در تلفظ برخی جفت کلمه‌های تولیدشده مشکل داشته باشد. درحالی‌که انسان‌ها مشکلی در تشخیص دو کلمه در cheaphead ندارند، صفحه‌خوان ممکن است ph را در وسط کلمه به‌صورت f تلفظ کند.

یک راه‌حل این است که pair.asLowerCase را با "${pair.first} ${pair.second}" جایگزین کنید. مورد دوم از درون‌یابی رشته‌ای برای ایجاد رشته (مانند "cheap head") از دو کلمه موجود در pair استفاده می‌کند. استفاده از دو کلمه جداگانه به‌جای کلمه مرکب باعث می‌شود صفحه‌خوان‌ها آن‌ها را به‌درستی شناسایی کنند و تجربه بهتری برای کاربران دارای اختلال بینایی فراهم شود.

بااین‌حال، شاید بخواهید سادگی بصری pair.asLowerCase را حفظ کنید. از ویژگی semanticsLabel مربوط به Text استفاده کنید تا محتوای بصری ابزاره نوشتاری را با محتوای معنایی که برای صفحه‌خوان‌ها مناسب‌تر است لغو کنید:

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        // ↓ Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

اکنون صفحه‌خوان‌ها هر جفت کلمه تولیدشده را به‌درستی تلفظ می‌کنند، اما واسط کاربر همان می‌ماند. این ویژگی را با استفاده از صفحه‌خوان در دستگاهتان امتحان کنید.

میانای کاربر را در مرکز قرار دهید

اکنون که جفت کلمه تصادفی با استعداد بصری کافی ارائه شده است، وقت آن است که آن را در مرکز پنجره/صفحه برنامه قرار دهید.

ابتدا به‌خاطر داشته باشید که BigCard بخشی از Column است. به‌طور پیش‌فرض، ستون‌ها فرزندانشان را در بالا جمع می‌کنند، اما می‌توانیم این را لغو کنیم. به روش build() MyHomePage بروید و تغییر زیر را ایجاد کنید:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  // ← Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

این کار فرزندان را در داخل Column در امتداد محور اصلی (عمودی) آن متمرکز می‌کند.

b555d4c7f5000edf.png

فرزندان ازقبل در مرکز محور متقاطع ستون قرار دارند (به‌عبارت دیگر، ازقبل به‌صورت افقی در مرکز قرار دارند). اما Column خودش در مرکز Scaffold قرار ندارد. می‌توانیم این موضوع را بااستفاده از بازرس ابزاره درستی‌سنجی کنیم.

«بازرس ابزاره» به‌خودی‌خود فراتر از محدوده این codelab است، اما می‌توانید ببینید که وقتی Column برجسته می‌شود، کل عرض برنامه را اشغال نمی‌کند. فقط به همان اندازه فضای افقی را اشغال می‌کند که فرزندانش نیاز دارند.

می‌توانید فقط خود ستون را وسط‌چین کنید. مکان‌نما را روی Column قرار دهید، منو بازسازی کد را (با Ctrl+. یا Cmd+.) فراخوانی کنید، و Wrap with Center (پیچیدن با مرکز) را انتخاب کنید.

اکنون برنامه باید چیزی شبیه به تصویر زیر باشد:

455688d93c30d154.png

اگر بخواهید، می‌توانید این را کمی بیشتر تغییر دهید.

  • می‌توانید ابزارک Text را در بالای BigCard بردارید. می‌توان گفت که نوشتار توصیفی («یک ایده عالی تصادفی:») دیگر لازم نیست چون واسط کاربر حتی بدون آن هم منطقی است. و این‌طوری تمیزتر است.
  • همچنین می‌توانید ابزاره SizedBox(height: 10) را بین BigCard و ElevatedButton اضافه کنید. به‌این‌ترتیب، فاصله بین دو ابزاره کمی بیشتر می‌شود. ابزاره SizedBox فقط فضا اشغال می‌کند و به‌تنهایی چیزی را ارائه نمی‌دهد. معمولاً برای ایجاد «شکاف‌های» دیداری استفاده می‌شود.

با تغییرات اختیاری، MyHomePage حاوی این کد است:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

و برنامه به‌صورت زیر است:

3d53d2b071e2f372.png

در بخش بعدی، قابلیت افزودن به موارد دلخواه (یا «پسندیدن») کلمات تولیدشده را اضافه خواهید کرد.

‫۶. افزودن عملکرد

برنامه کار می‌کند و گاهی حتی جفت واژه‌های جالبی ارائه می‌دهد. اما هربار که کاربر روی بعدی کلیک می‌کند، هر جفت کلمه برای همیشه ناپدید می‌شود. بهتر است راهی برای «به‌خاطر سپردن» بهترین پیشنهادها وجود داشته باشد: مثلاً دکمه «پسندیدن».

e6b01a8c90df8ffa.png

افزودن منطق کسب‌وکار

به MyAppState پیمایش کنید و کد زیر را اضافه کنید:

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  // ↓ Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

تغییرات را بررسی کنید:

  • دارایی جدیدی به MyAppState با نام favorites اضافه کردید. این ویژگی با فهرست خالی مقداردهی اولیه شده است: [].
  • همچنین مشخص کردید که این فهرست فقط می‌تواند حاوی جفت کلمه باشد: <WordPair>[]، بااستفاده از عمومی‌ها. این کار به قوی‌تر شدن برنامه‌تان کمک می‌کند—اگر بخواهید چیزی غیراز WordPair به برنامه‌تان اضافه کنید، Dart حتی از اجرای آن امتناع می‌کند. به‌نوبه خود، می‌توانید از فهرست favorites استفاده کنید و مطمئن باشید که هیچ شیء ناخواسته‌ای (مثل null) در آن پنهان نشده است.
  • همچنین روش جدیدی به نام toggleFavorite() اضافه کردید که جفت واژه فعلی را از فهرست موارد دلخواه حذف می‌کند (اگر ازقبل در آنجا باشد) یا آن را اضافه می‌کند (اگر هنوز در آنجا نباشد). در هر دو صورت، کد بعداز آن notifyListeners(); را فراخوانی می‌کند.

افزودن دکمه

با کنار رفتن «منطق کسب‌وکار»، وقت آن است که دوباره روی میانای کاربر کار کنیم. قرار دادن دکمه «پسندیدن» در سمت چپ دکمه «بعدی» به Row نیاز دارد. ابزاره Row معادل افقی Column است که قبلاً دیدید.

ابتدا، دکمه موجود را در Row بپیچید. به روش build() مربوط به MyHomePage بروید، مکان‌نما را روی ElevatedButton قرار دهید، منو بازسازی کد را با Ctrl+. یا Cmd+. فراخوانی کنید، و Wrap with Row (پیچیدن با ردیف) را انتخاب کنید.

وقتی ذخیره می‌کنید، متوجه خواهید شد که Row عملکردی مشابه Column دارد—به‌طور پیش‌فرض، فرزندانش را در سمت چپ جمع می‌کند. (Column فرزندانش را در بالا جمع کرد.) برای رفع این مشکل، می‌توانید از همان رویکرد قبلی استفاده کنید، اما با mainAxisAlignment. اما برای اهداف آموزشی (یادگیری)، از mainAxisSize استفاده کنید. این به Row می‌گوید که همه فضای افقی دردسترس را نگیرد.

تغییر زیر را اعمال کنید:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   // ← Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

واسط کاربر به جایی که قبلاً بود برمی‌گردد.

3d53d2b071e2f372.png

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

e6b01a8c90df8ffa.png

اگر دقیقاً به همان روشی که در زیر انجام شده است انجام ندهید، مشکلی نیست. درواقع، نگران نماد قلب نباشید، مگراینکه واقعاً بخواهید چالش بزرگی داشته باشید.

اگر موفق نشدید هم هیچ اشکالی ندارد—به‌هرحال این اولین ساعت شما با Flutter است.

252f7c4a212c94d2.png

این یکی از روش‌های افزودن دکمه دوم به MyHomePage است. این‌بار، از سازنده ElevatedButton.icon() برای ایجاد دکمه‌ای با نماد استفاده کنید. و در بالای روش build، بسته به اینکه جفت واژه فعلی ازقبل در موارد دلخواه باشد یا نه، نماد مناسب را انتخاب کنید. همچنین، به استفاده دوباره از SizedBox توجه کنید تا دو دکمه کمی از هم فاصله داشته باشند.

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    // ↓ Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                // ↓ And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

برنامه باید به‌صورت زیر باشد:

متأسفانه کاربر نمی‌تواند موارد دلخواه را ببیند. وقت آن است که یک صفحه کاملاً جداگانه به برنامه‌مان اضافه کنیم. در بخش بعدی می‌بینمتان!

‫۷. افزودن ریل پیمایش

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

f62c54f5401a187.png

برای اینکه هرچه زودتر به اصل این مرحله برسید، MyHomePage را به ۲ ابزاره جداگانه تقسیم کنید.

همه MyHomePage را انتخاب کنید، آن را حذف کنید، و با کد زیر جایگزین کنید:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

وقتی ذخیره شد، خواهید دید که بخش بصری واسط کاربر آماده است—اما کار نمی‌کند. کلیک کردن روی ♥︎ (قلب) در ریل پیمایش کاری انجام نمی‌دهد.

388bc25fe198c54a.png

تغییرات را بررسی کنید.

  • ابتدا، توجه کنید که کل محتوای MyHomePage در ابزاره جدیدی به‌نام GeneratorPage استخراج شده است. تنها بخشی از ابزاره قدیمی MyHomePage که استخراج نشده است Scaffold است.
  • ‫MyHomePage جدید حاوی Row با دو فرزند است. ابزاره اول SafeArea است و ابزاره دوم Expanded است.
  • SafeArea تضمین می‌کند که فرزندش توسط بریدگی سخت‌افزاری یا نوار وضعیت پوشانده نشود. در این برنامه، ابزاره دور NavigationRail می‌پیچد تا مثلاً دکمه‌های پیمایش توسط نوار وضعیت تلفن همراه پوشانده نشود.
  • می‌توانید خط extended: false را در NavigationRail به true تغییر دهید. با این کار، برچسب‌ها در کنار نمادها نشان داده می‌شود. در مرحله‌ای در آینده، یاد خواهید گرفت که چگونه این کار را به‌طور خودکار انجام دهید وقتی برنامه فضای افقی کافی دارد.
  • ریل پیمایش دو مقصد (صفحه اصلی و موارد دلخواه) با نمادها و برچسب‌های مربوطه دارد. همچنین selectedIndex فعلی را تعریف می‌کند. نمایه انتخاب‌شده صفر مقصد اول را انتخاب می‌کند، نمایه انتخاب‌شده یک مقصد دوم را انتخاب می‌کند، و غیره. فعلاً، به‌صورت کد ثابت روی صفر تنظیم شده است.
  • ریل پیمایش همچنین تعیین می‌کند که وقتی کاربر یکی از مقصدها را با onDestinationSelected انتخاب می‌کند چه اتفاقی می‌افتد. درحال‌حاضر، برنامه فقط مقدار شاخص درخواست‌شده را با print() برونداد می‌کند.
  • فرزند دوم Row، ابزاره Expanded است. ابزاره‌های گسترده در ردیف‌ها و ستون‌ها بسیار مفید هستند—آن‌ها به شما امکان می‌دهند چیدمان‌هایی را بیان کنید که در آن‌ها برخی‌از فرزندان فقط به‌اندازه نیازشان فضا می‌گیرند (SafeArea، در این مورد) و ابزاره‌های دیگر باید تا حد امکان از فضای باقی‌مانده استفاده کنند (Expanded، در این مورد). یک راه برای فکر کردن درباره ابزاره‌های Expanded این است که آن‌ها «حریص» هستند. اگر می‌خواهید نقش این ابزاره را بهتر درک کنید، ابزاره SafeArea را با Expanded دیگری بپوشانید. چیدمان حاصل چیزی شبیه به این است:

6bbda6c1835a1ae.png

  • دو ابزاره Expanded کل فضای افقی دردسترس را بین خودشان تقسیم می‌کنند، هرچند ریل پیمایش واقعاً فقط به بخش کوچکی در سمت چپ نیاز دارد.
  • در داخل ابزاره Expanded، یک Container رنگی وجود دارد و در داخل ظرف، GeneratorPage.

ابزاره‌های بدون حالت درمقابل ابزاره‌های با حالت

تاکنون، MyAppState همه نیازهای ایالتی شما را پوشش می‌داد. به همین دلیل است که همه ابزاره‌هایی که تاکنون نوشته‌اید بدون حالت هستند. آن‌ها هیچ حالت تغییرپذیری از خود ندارند. هیچ‌کدام از ابزاره‌ها نمی‌توانند خودشان را تغییر دهند—باید ازطریق MyAppState انجام شود.

این موضوع درحال تغییر است.

باید راهی برای نگه‌داشتن مقدار selectedIndex ریل پیمایش داشته باشید. همچنین می‌خواهید بتوانید این مقدار را از داخل callback onDestinationSelected تغییر دهید.

می‌توانید selectedIndex را به‌عنوان دارایی دیگری از MyAppState اضافه کنید. و این کارساز خواهد بود. اما می‌توانید تصور کنید که اگر هر ابزاره مقادیر خود را در آن ذخیره کند، وضعیت برنامه به‌سرعت از حد منطقی فراتر می‌رود.

e52d9c0937cc0823.jpeg

برخی‌از حالت‌ها فقط به یک ابزاره مربوط می‌شوند، بنابراین باید با همان ابزاره بمانند.

StatefulWidget را وارد کنید، نوعی ابزاره که State دارد. ابتدا، MyHomePage را به ابزاره باوضعیت تبدیل کنید.

مکان‌نما را روی خط اول MyHomePage (خطی که با class MyHomePage... شروع می‌شود) قرار دهید و بااستفاده از Ctrl+. یا Cmd+. منو بازسازی کد را فراخوانی کنید. سپس تبدیل به StatefulWidget را انتخاب کنید.

IDE کلاس جدیدی برای شما ایجاد می‌کند، _MyHomePageState. این کلاس State را گسترش می‌دهد و بنابراین می‌تواند مقادیر خودش را مدیریت کند. (می‌تواند خودش را تغییر دهد.) همچنین توجه داشته باشید که روش build از ابزاره قدیمی بدون حالت به _MyHomePageState منتقل شده است (به‌جای اینکه در ابزاره باقی بماند). این کد به‌صورت کلمه‌به‌کلمه منتقل شده است — هیچ‌چیز در روش build تغییر نکرده است. اکنون فقط در جای دیگری زندگی می‌کند.

setState

ابزاره جدید باوضعیت فقط باید یک متغیر را ردیابی کند: selectedIndex. ۳ تغییر زیر را در _MyHomePageState ایجاد کنید:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

تغییرات را بررسی کنید:

  1. متغیر جدیدی به نام selectedIndex معرفی می‌کنید و آن را با مقدار 0 مقداردهی اولیه می‌کنید.
  2. از این متغیر جدید در تعریف NavigationRail به‌جای 0 کدگذاری‌شده که تا الان آنجا بود استفاده می‌کنید.
  3. وقتی فراخوانی برگشتی onDestinationSelected فراخوانده می‌شود، به‌جای اینکه صرفاً مقدار جدید را در کنسول چاپ کنید، آن را به selectedIndex در فراخوانی setState() اختصاص می‌دهید. این فراخوانی مشابه روش notifyListeners() است که قبلاً استفاده می‌شد—این فراخوانی مطمئن می‌شود که واسط کاربر به‌روز می‌شود.

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

استفاده از selectedIndex

کد زیر را در بالای روش build مربوط به _MyHomePageState، درست قبل‌از return Scaffold قرار دهید:

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

این قطعه کد را بررسی کنید:

  1. کد متغیر جدیدی به‌نام page از نوع Widget را اعلام می‌کند.
  2. سپس، یک عبارت سوئیچ صفحه‌ای را براساس مقدار فعلی در selectedIndex به page اختصاص می‌دهد.
  3. ازآنجایی‌که هنوز FavoritesPage وجود ندارد، از Placeholder استفاده کنید؛ ابزارک مفیدی که هرجا آن را قرار دهید مستطیلی با خطوط متقاطع می‌کشد و آن بخش از واسط کاربر را به‌عنوان ناتمام علامت‌گذاری می‌کند.

5685cf886047f6ec.png

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

اکنون که page حاوی ابزاره‌ای است که می‌خواهید در سمت راست نشان داده شود، احتمالاً می‌توانید حدس بزنید چه تغییر دیگری لازم است.

این _MyHomePageState پس‌از آن یک تغییر باقی‌مانده است:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  // ← Here.
            ),
          ),
        ],
      ),
    );
  }
}

// ...

اکنون برنامه بین GeneratorPage ما و جای‌بان که به‌زودی به صفحه موارد دلخواه تبدیل می‌شود جابه‌جا می‌شود.

پاسخ‌گویی

سپس، نرده‌ای پیمایش را واکنش‌گرا کنید. یعنی کاری کنید که وقتی فضای کافی وجود دارد، برچسب‌ها به‌طور خودکار (بااستفاده از extended: true) نشان داده شوند.

a8873894c32e0d0b.png

‫Flutter چندین ابزاره ارائه می‌دهد که به شما کمک می‌کند برنامه‌هایتان را به‌طور خودکار واکنش‌گرا کنید. برای مثال، Wrap ابزاره‌ای شبیه به Row یا Column است که وقتی فضای عمودی یا افقی کافی وجود نداشته باشد، به‌طور خودکار فرزندان را به «خط» بعدی (به‌نام «اجرا») می‌پیچد. FittedBox وجود دارد، ابزاره‌ای که به‌طور خودکار فرزندش را طبق مشخصات شما در فضای دردسترس قرار می‌دهد.

اما NavigationRail وقتی فضای کافی وجود دارد، برچسب‌ها را به‌طور خودکار نشان نمی‌دهد زیرا نمی‌تواند تشخیص دهد که در هر بافت، فضای کافی چیست. این تصمیم به شما، یعنی توسعه‌دهنده، بستگی دارد.

فرض کنید تصمیم می‌گیرید برچسب‌ها را فقط درصورتی نشان دهید که عرض MyHomePage حداقل ۶۰۰ پیکسل باشد.

ابزاره‌ای که باید استفاده شود، در این مورد، LayoutBuilder است. این ویژگی به شما امکان می‌دهد درخت ابزاره را بسته به مقدار فضای دردسترس تغییر دهید.

یک‌بار دیگر، از منو بازسازی کد Flutter در VS Code برای اعمال تغییرات لازم استفاده کنید. اما این‌بار کمی پیچیده‌تر است:

  1. در روش build _MyHomePageState، مکان‌نما را روی Scaffold قرار دهید.
  2. منو بازسازی کد را با Ctrl+. (Windows/Linux) یا Cmd+. (Mac) فراخوانی کنید.
  3. Wrap with Builder را انتخاب کنید و Enter را فشار دهید.
  4. نام Builder جدیداً اضافه‌شده را به LayoutBuilder تغییر دهید.
  5. فهرست پارامترهای فراخوانی را از (context) به (context, constraints) تغییر دهید.

فراخوانی برگشتی builder LayoutBuilder هربار که محدودیت‌ها تغییر می‌کند فراخوانی می‌شود. این اتفاق زمانی می‌افتد که، برای مثال:

  • کاربر اندازه پنجره برنامه را تغییر می‌دهد
  • کاربر تلفنش را از حالت عمودی به حالت افقی یا برعکس می‌چرخاند
  • اندازه ابزاره‌ای در کنار MyHomePage بزرگ‌تر می‌شود و محدودیت‌های MyHomePage را کوچک‌تر می‌کند

اکنون کد شما می‌تواند با پرسمان کردن constraints فعلی تصمیم بگیرد که برچسب را نشان دهد یا نه. تغییر تک‌خطی زیر را در روش build مربوط به _MyHomePageState ایجاد کنید:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  // ← Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

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

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

‫۸. افزودن صفحه جدید

ابزارک Placeholder را که به‌جای صفحه موارد دلخواه استفاده کردیم به‌خاطر دارید؟

وقت آن است که این مشکل را برطرف کنیم.

اگر ماجراجو هستید، این مرحله را خودتان انجام دهید. هدف شما این است که فهرست favorites را در یک ابزاره بی‌حالت جدید، FavoritesPage، نشان دهید و سپس آن ابزاره را به‌جای Placeholder نشان دهید.

در اینجا چند نکته آورده شده است:

  • وقتی می‌خواهید Column پیمایش شود، از ابزاره ListView استفاده کنید.
  • به‌یاد داشته باشید که بااستفاده از context.watch<MyAppState>() از هر ابزارکی به نمونه MyAppState دسترسی پیدا کنید.
  • اگر می‌خواهید ابزاره جدیدی را هم امتحان کنید، ListTile ویژگی‌هایی مثل title (معمولاً برای نوشتار)، leading (برای نمادها یا چهرک‌ها)، و onTap (برای تعاملات) دارد. بااین‌حال، می‌توانید با ابزارهایی که ازقبل می‌شناسید به جلوه‌های مشابهی دست پیدا کنید.
  • Dart اجازه می‌دهد از حلقه‌های for در لیترال‌های مجموعه استفاده شود. برای مثال، اگر messages حاوی فهرستی از رشته‌ها باشد، می‌توانید کدی مانند کد زیر داشته باشید:

f0444bba08f205aa.png

ازسوی دیگر، اگر با برنامه‌نویسی تابعی بیشتر آشنا هستید، Dart به شما امکان می‌دهد کدی مانند messages.map((m) => Text(m)).toList() بنویسید. و البته همیشه می‌توانید فهرستی از ابزاره‌ها ایجاد کنید و به‌طور امری در روش build به آن اضافه کنید.

مزیت اینکه خودتان صفحه موردعلاقه‌ها را اضافه کنید این است که با تصمیم‌گیری‌های خودتان بیشتر یاد می‌گیرید. نکته منفی این است که ممکن است با مشکلی مواجه شوید که هنوز نمی‌توانید خودتان آن را حل کنید. به‌یاد داشته باشید: شکست خوردن اشکالی ندارد و یکی از مهم‌ترین عناصر یادگیری است. هیچ‌کس از شما انتظار ندارد که در اولین ساعت توسعه Flutter را کاملاً یاد بگیرید، و شما هم نباید چنین انتظاری داشته باشید.

252f7c4a212c94d2.png

آنچه در ادامه می‌آید فقط یک روش برای پیاده‌سازی صفحه موارد دلخواه است. نحوه پیاده‌سازی آن (امیدواریم) به شما الهام دهد که با کد بازی کنید—رابط کاربری را بهبود دهید و آن را ازآن خود کنید.

این کلاس جدید FavoritesPage است:

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

این ابزاره این کارها را انجام می‌دهد:

  • این مجوز وضعیت فعلی برنامه را دریافت می‌کند.
  • اگر فهرست موارد دلخواه خالی باشد، این پیام در مرکز نشان داده می‌شود: هنوز مورد دلخواهی وجود ندارد.
  • درغیراین‌صورت، فهرستی (پیمایش‌کردنی) نشان می‌دهد.
  • فهرست با خلاصه‌ای شروع می‌شود (برای مثال، ۵ مورد دلخواه دارید).
  • سپس کد در همه موارد دلخواه تکرار می‌شود و برای هرکدام یک ابزاره ListTile می‌سازد.

اکنون تنها کاری که باقی مانده است جایگزین کردن ابزاره Placeholder با FavoritesPage است. و بفرمایید!

می‌توانید کد نهایی این برنامه را در مخزن codelab در GitHub دریافت کنید.

‫۹. مراحل بعدی

تبریک می‌گوییم!

خودتان را ببینید! شما یک داربست غیرعملکردی با یک Column و دو ابزاره Text را گرفتید و آن را به یک برنامه کوچک لذت‌بخش و واکنش‌گرا تبدیل کردید.

d6e3d5f736411f13.png

آنچه پوشش داده‌ایم

  • مبانی نحوه عملکرد Flutter
  • ساختن چیدمان در Flutter
  • اتصال تعاملات کاربر (مانند فشار دادن دکمه) به رفتار برنامه
  • مرتب نگه داشتن کد Flutter
  • درحال واکنش‌گرا کردن برنامه شما
  • دستیابی به ظاهر و عملکردی یکپارچه برای برنامه‌تان

گام بعدی چیست؟

  • با برنامه‌ای که درطول این کارگاه نوشتید بیشتر آزمایش کنید.
  • کد این نسخه پیشرفته از همان برنامه را ببینید تا متوجه شوید چگونه می‌توانید فهرست‌های پویانمایی‌شده، گرادیان‌ها، محو شدن متقاطع، و موارد دیگر را اضافه کنید.
  • با رفتن به flutter.dev/learn، سفر یادگیری خود را دنبال کنید.