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

1. مقدمه

Flutter ابزار UI گوگل برای ساخت برنامه های کاربردی برای موبایل، وب و دسکتاپ از یک پایگاه کد واحد است. در این نرم افزار کد، اپلیکیشن فلاتر زیر را خواهید ساخت:

این برنامه نام‌های جذابی مانند «newstay»، «lightstream»، «mainbrake» یا «greypine» تولید می‌کند. کاربر می تواند نام بعدی را بپرسد، نام فعلی را مورد علاقه خود قرار دهد، و لیست نام های مورد علاقه را در یک صفحه جداگانه بررسی کند. این برنامه به اندازه های مختلف صفحه نمایش پاسخگو است.

چیزی که یاد خواهید گرفت

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

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

e9c6b402cd8003fd.png

و در اینجا فیلیپ شما را از طریق کل کدها می برد!

برای شروع آزمایشگاه روی next کلیک کنید.

2. محیط Flutter خود را تنظیم کنید

ویرایشگر

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

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

توصیه می‌کنیم از VS Code برای این کد لبه استفاده کنید زیرا دستورالعمل‌ها به‌طور پیش‌فرض روی میان‌برهای ویژه کد VS هستند. گفتن جملاتی مانند "اینجا کلیک کنید" یا "این کلید را فشار دهید" به جای عبارتی مانند "عمل مناسب را در ویرایشگر خود برای انجام X انجام دهید" آسان تر است.

228c71510a8e868.png

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

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

  • iOS
  • اندروید
  • ویندوز
  • macOS
  • لینوکس
  • وب

با این حال، انتخاب یک سیستم عامل واحد که در درجه اول برای آن توسعه می دهید، معمول است. این "هدف توسعه" شما است - سیستم عاملی که برنامه شما در طول توسعه روی آن اجرا می شود.

16695777c07f18e5.png

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

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

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

فلاتر را نصب کنید

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

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

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

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

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

سوالات متداول

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

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

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

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

در نهایت نام پروژه خود را بنویسید. چیزی مانند namer_app یا my_awesome_namer .

260a7d97f9678005.png

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

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

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

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

e2a5bab0be07f4f7.png

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

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.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

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

در نهایت فایل 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(
          useMaterial3: true,
          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),
        ],
      ),
    );
  }
}

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

در قسمت بعدی برنامه را در حالت دیباگ اجرا کرده و شروع به توسعه کنید.

4. یک دکمه اضافه کنید

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

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

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

وقتی lib/main.dart باز است، «play» را پیدا کنید 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),
        ],
      ),
    );

// ...

توجه کنید که چگونه برنامه بلافاصله تغییر می کند اما کلمه تصادفی یکسان می ماند. این همان Hot Reload معروف فلاتر در محل کار است. هنگامی که تغییرات را در یک فایل منبع ذخیره می کنید، بارگذاری مجدد داغ فعال می شود.

سوالات متداول

اضافه کردن دکمه

سپس، یک دکمه در پایین 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'),
          ),

        ],
      ),
    );

// ...

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

یک دوره تصادف فلاتر در 5 دقیقه

به همان اندازه که تماشای Debug Console سرگرم کننده است، می خواهید این دکمه کاری معنادارتر انجام دهد. با این حال، قبل از رسیدن به آن، نگاهی دقیق‌تر به کد موجود در 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(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

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

کد موجود در MyApp کل برنامه را تنظیم می کند. حالت گسترده برنامه را ایجاد می کند (در ادامه در این مورد بیشتر توضیح می دهیم)، برنامه را نامگذاری می کند، تم بصری را تعریف می کند، و ویجت "home" - نقطه شروع برنامه شما را تنظیم می کند.

lib/main.dart

// ...

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

// ...

در مرحله بعد، کلاس MyAppState وضعیت...خوب...وضعیت برنامه را تعریف می کند. این اولین تلاش شما در 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 است. قرار نیست با Scaffold در این Codelab کار کنید، اما یک ویجت مفید است و در اکثریت قریب به اتفاق برنامه های 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'),
    ),

// ...

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

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

5. اپلیکیشن را زیباتر کنید

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

3dd8a9d8653bdc56.png

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

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

2bbee054d81a3127.png

یک ویجت را استخراج کنید

خطی که مسئول نمایش جفت کلمه فعلی است اکنون به این شکل است: Text(appState.current.asLowerCase) . برای تغییر آن به چیزی پیچیده تر، ایده خوبی است که این خط را در یک ویجت جداگانه استخراج کنید. داشتن ویجت‌های مجزا برای بخش‌های منطقی مجزا از UI شما یک راه مهم برای مدیریت پیچیدگی در 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 اشاره نمی کند.

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

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

یا

  1. مکان نما خود را به کد قطعه ای که می خواهید تغییر دهید (در این مورد Text ) ببرید و Ctrl+. (Win/Linux) یا Cmd+. (مک).

در منوی Refactor ، Extract Widget را انتخاب کنید. نامی مانند 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);
  }
}

// ...

توجه داشته باشید که برنامه حتی از طریق این refactoring چگونه به کار خود ادامه می دهد.

یک کارت اضافه کنید

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

کلاس BigCard و متد build() در آن پیدا کنید. مانند قبل، منوی Refactor را در ویجت Text فراخوانی کنید. با این حال، این بار قرار نیست ویجت را استخراج کنید.

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

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

بعد، یک سطح بالاتر بروید. مکان نما خود را روی ویجت Padding قرار دهید، منوی Refactor را بالا بکشید و Wrap with widget... را انتخاب کنید.

این به شما امکان می دهد ویجت والد را مشخص کنید. "Card" را تایپ کرده و Enter را فشار دهید.

این ویجت Padding و بنابراین Text با ویجت Card می‌پیچد.

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 باشد. دارت، زبان برنامه نویسی که در آن این برنامه را می نویسید، از نظر تهی ایمن است، بنابراین به شما اجازه نمی دهد متدهایی از اشیاء بالقوه null را فراخوانی کنید. اما در این مورد، می توانید از ! اپراتور ("اپراتور بنگ") برای اطمینان از اینکه دارت می دانید چه کاری انجام می دهید. (در این مورد displayMedium قطعاً تهی نیست . دلیل اینکه ما این را می دانیم فراتر از محدوده این نرم افزار کد است.)
  • فراخوانی 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}",
        ),
      ),
    );
  }

// ...

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

UI را در مرکز قرار دهید

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

ابتدا، به یاد داشته باشید که 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 نیست. ما می‌توانیم این موضوع را با استفاده از ابزارک بازرس تأیید کنیم.

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

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

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

455688d93c30d154.png

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

  • می‌توانید ویجت Text بالای BigCard را حذف کنید. می توان استدلال کرد که متن توصیفی ("یک ایده تصادفی عالی:") دیگر مورد نیاز نیست زیرا UI حتی بدون آن نیز منطقی است. و به این ترتیب تمیزتر است.
  • همچنین می توانید یک ویجت 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

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

6. قابلیت اضافه کنید

این برنامه کار می کند، و حتی گاهی اوقات جفت کلمات جالبی را ارائه می دهد. اما هر زمان که کاربر روی Next کلیک کند، هر جفت کلمه برای همیشه ناپدید می شود. بهتر است راهی برای "به خاطر سپردن" بهترین پیشنهادها داشته باشید: مانند دکمه "پسندیدن".

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 قرار دهید، منوی Refactor را با 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

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

e6b01a8c90df8ffa.png

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

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

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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

برنامه باید به شکل زیر باشد:

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

7. راه آهن ناوبری را اضافه کنید

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

f62c54f5401a187.png

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

تمام 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 به یک ویجت Stateful تبدیل کنید.

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

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

setState

ویجت stateful جدید فقط باید یک متغیر را ردیابی کند: selectedIndex . 3 تغییر زیر را در _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 فراخوانی می شود، به جای اینکه صرفاً مقدار جدید را در کنسول چاپ کنید، آن را در داخل یک فراخوانی setState() به selectedIndex اختصاص می دهید. این فراخوانی شبیه به متد notifyListeners() است که قبلاً استفاده می‌شد - مطمئن می‌شود که رابط کاربری به‌روزرسانی می‌شود.

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

از selectIndex استفاده کنید

کد زیر را در بالای متد 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. سپس، یک دستور switch یک صفحه را با توجه به مقدار فعلی در selectedIndex به page اختصاص می‌دهد.
  3. از آنجایی که هنوز FavoritesPage وجود ندارد، از Placeholder استفاده کنید. یک ویجت مفید که در هر کجا که قرار دهید یک مستطیل متقاطع می کشد و آن قسمت از رابط کاربری را به عنوان ناتمام علامت گذاری می کند.

5685cf886047f6ec.png

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

اکنون آن 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 است که در صورت کمبود فضای عمودی یا افقی کافی، کودکان را به‌طور خودکار به «خط» بعدی (به نام «run») می‌پیچد. FittedBox وجود دارد، ویجتی که به طور خودکار فرزند خود را با توجه به مشخصات شما در فضای موجود قرار می دهد.

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

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

ویجت مورد استفاده در این مورد LayoutBuilder است. این به شما امکان می دهد درخت ویجت خود را بسته به میزان فضای موجود تغییر دهید.

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

  1. در روش build _MyHomePageState ، مکان نما را روی Scaffold قرار دهید.
  2. منوی Refactor را با Ctrl+. (ویندوز/لینوکس) یا Cmd+. (مک).
  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 با صفحه Favorites واقعی جایگزین کنید. که در بخش بعدی پوشش داده شده است.

8. یک صفحه جدید اضافه کنید

ویجت Placeholder را که به جای صفحه Favorites استفاده کردیم، به خاطر دارید؟

وقت آن است که این را درست کنیم.

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

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

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

f0444bba08f205aa.png

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

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

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),
          ),
      ],
    );
  }
}

کاری که ویجت انجام می دهد به شرح زیر است:

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

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

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

9. مراحل بعدی

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

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

d6e3d5f736411f13.png

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

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

بعدش چی؟

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