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

۱. مقدمه

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

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

آنچه یاد خواهید گرفت

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

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

e9c6b402cd8003fd.png

و این فیلیپ است که شما را در کل آزمایشگاه کد همراهی می‌کند!

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

۲. محیط فلاتر خود را تنظیم کنید

ویرایشگر

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

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

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

228c71510a8e868.png

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

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

  • آی‌او‌اس
  • اندروید
  • ویندوز
  • مک‌او‌اس
  • لینوکس
  • وب

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

۱۶۶۹۵۷۷۷c07f18e5.png

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

ممکن است وسوسه شوید که وب را به عنوان هدف توسعه خود انتخاب کنید. نکته منفی این انتخاب این است که یکی از مفیدترین ویژگی‌های توسعه فلاتر را از دست می‌دهید: بارگذاری مجدد سریع با وضعیت. فلاتر نمی‌تواند برنامه‌های وب را به صورت خودکار بارگذاری مجدد کند.

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

نصب فلاتر

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

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

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

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

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

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

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

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

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

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

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

۲۶۰a۷d۹۷f۹۶۷۸۰۰۵.png

فلاتر اکنون پوشه پروژه شما را ایجاد می‌کند و 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

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

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

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

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

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

این مرحله یک دکمه‌ی «بعدی» برای ایجاد یک جفت‌سازی کلمات جدید اضافه می‌کند.

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

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

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

بعد از حدود یک دقیقه، برنامه شما در حالت اشکال‌زدایی (debug mode) اجرا می‌شود. هنوز چیز زیادی به نظر نمی‌رسد:

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 در فلاتر است. 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'),
          ),

        ],
      ),
    );

// ...

وقتی تغییر را ذخیره می‌کنید، برنامه دوباره به‌روزرسانی می‌شود: یک دکمه ظاهر می‌شود و وقتی روی آن کلیک می‌کنید، کنسول اشکال‌زدایی در 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 وضعیت... خب... برنامه را تعریف می‌کند. این اولین تجربه شما در فلاتر است، بنابراین این آزمایشگاه کد، آن را ساده و متمرکز نگه می‌دارد. روش‌های قدرتمند زیادی برای مدیریت وضعیت برنامه در فلاتر وجود دارد. یکی از ساده‌ترین روش‌ها برای توضیح، 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 کار کنید، اما یک ویجت مفید است و در اکثر قریب به اتفاق برنامه‌های Flutter در دنیای واقعی یافت می‌شود.
  4. Column یکی از اساسی‌ترین ویجت‌های طرح‌بندی در فلاتر است. این ویجت هر تعداد عنصر فرزند را می‌گیرد و آنها را از بالا به پایین در یک ستون قرار می‌دهد. به طور پیش‌فرض، ستون به صورت بصری فرزندان خود را در بالا قرار می‌دهد. به زودی این را تغییر خواهید داد تا ستون در مرکز قرار گیرد.
  5. شما این ویجت Text را در مرحله اول تغییر دادید.
  6. این ویجت Text دوم appState می‌گیرد و به تنها عضو آن کلاس، current (که یک WordPair است) دسترسی پیدا می‌کند. WordPair چندین getter مفید مانند asPascalCase یا asSnakeCase ارائه می‌دهد. در اینجا، ما از asLowerCase استفاده می‌کنیم، اما اگر یکی از گزینه‌های دیگر را ترجیح می‌دهید، می‌توانید اکنون آن را تغییر دهید.
  7. توجه کنید که کد فلاتر چگونه از کاماهای انتهایی به شدت استفاده می‌کند. این کاما خاص نیازی به اینجا ندارد، زیرا children آخرین (و همچنین تنها ) عضو این لیست پارامتر Column خاص است. با این حال، به طور کلی استفاده از کاماهای انتهایی ایده خوبی است: آنها اضافه کردن اعضای بیشتر را بی‌اهمیت می‌کنند و همچنین به عنوان یک اشاره برای قالب‌بندی خودکار Dart عمل می‌کنند تا یک خط جدید در آنجا قرار دهد. برای اطلاعات بیشتر، به بخش قالب‌بندی کد مراجعه کنید.

در مرحله بعد، دکمه را به وضعیت (state) متصل خواهید کرد.

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

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

فلاتر یک کمک‌کننده‌ی بازسازی برای استخراج ویجت‌ها ارائه می‌دهد، اما قبل از استفاده از آن، مطمئن شوید که خطی که استخراج می‌شود فقط به آنچه نیاز دارد دسترسی دارد. در حال حاضر، خط 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. روی قطعه کدی که می‌خواهید آن را بازسازی کنید (در اینجا Text ) کلیک راست کرده و از منوی کشویی، گزینه Refactor... را انتخاب کنید.

یا

  1. مکان‌نما را روی کدی که می‌خواهید اصلاح کنید (در این مورد Text ) ببرید و کلیدهای Ctrl+. (ویندوز/لینوکس) یا 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);
  }
}

// ...

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

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

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

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

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

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

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

این به شما امکان می‌دهد ویجت والد را مشخص کنید. عبارت "Card" را تایپ کرده و 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 در آنجا تغییر دهید.

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

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

قالب متنی

کارت هنوز یک مشکل دارد: متن خیلی کوچک است و رنگ آن به سختی خوانده می‌شود. برای رفع این مشکل، تغییرات زیر را در متد 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 یک استایل بزرگ است که برای نمایش متن در نظر گرفته شده است. کلمه display در اینجا به معنای تایپوگرافیک، مانند display typeface ، استفاده می‌شود. مستندات displayMedium می‌گوید که "سبک‌های نمایش برای متن کوتاه و مهم رزرو شده‌اند" - دقیقاً مورد استفاده ما.
  • از لحاظ تئوری، ویژگی displayMedium تم می‌تواند null باشد. دارت، زبان برنامه‌نویسی که شما این برنامه را با آن می‌نویسید، در برابر null امن است، بنابراین به شما اجازه نمی‌دهد متدهای اشیاء بالقوه null را فراخوانی کنید. در این حالت، می‌توانید از عملگر ! ("عملگر bang") استفاده کنید تا به دارت اطمینان دهید که می‌دانید چه کاری انجام می‌دهید. ( displayMedium در این مورد قطعاً null نیست . دلیل اینکه ما این را می‌دانیم، خارج از محدوده این آزمایشگاه کد است.)
  • فراخوانی copyWith() در displayMedium یک کپی از سبک متن را به همراه تغییراتی که شما تعریف می‌کنید، برمی‌گرداند. در این حالت، شما فقط رنگ متن را تغییر می‌دهید.
  • برای دریافت رنگ جدید، یک بار دیگر به تم برنامه دسترسی پیدا می‌کنید. ویژگی onPrimary در طرح رنگ، رنگی را تعریف می‌کند که برای استفاده روی رنگ اصلی برنامه مناسب است.

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

2405e9342d28c193.png

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

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

بهبود دسترسی

فلاتر برنامه‌ها را به طور پیش‌فرض قابل دسترسی می‌کند. برای مثال، هر برنامه فلاتر به درستی تمام متن و عناصر تعاملی موجود در برنامه را برای صفحه‌خوان‌هایی مانند 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 در مرکز قرار نگرفته است. می‌توانیم این را با استفاده از Widget Inspector تأیید کنیم.

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

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

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

۴۵۵۶۸۸d۹۳c۳۰d۱۵۴.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>[] ، با استفاده از genericها . این به قوی‌تر شدن برنامه شما کمک می‌کند - اگر سعی کنید چیزی غیر از WordPair به برنامه خود اضافه کنید، دارت حتی از اجرای آن خودداری می‌کند. در عوض، می‌توانید از لیست favorites استفاده کنید و بدانید که هرگز اشیاء ناخواسته‌ای (مانند null ) در آنجا پنهان نمی‌شوند.
  • همچنین یک متد جدید به نام toggleFavorite() اضافه کرده‌اید که یا جفت کلمه فعلی را از لیست علاقه‌مندی‌ها حذف می‌کند (اگر از قبل وجود داشته باشد)، یا آن را اضافه می‌کند (اگر هنوز وجود ندارد). در هر صورت، کد بعد از آن notifyListeners(); را فراخوانی می‌کند.

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

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

ابتدا، دکمه‌ی موجود را در یک Row قرار دهید. به متد build() در MyHomePage بروید، مکان‌نما را روی ElevatedButton قرار دهید، با استفاده از Ctrl+. یا Cmd+. منوی Refactor را فراخوانی کنید و 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

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

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

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 است. ویجت‌های Expanded در ردیف‌ها و ستون‌ها بسیار مفید هستند - آن‌ها به شما امکان می‌دهند طرح‌بندی‌هایی را بیان کنید که در آن برخی از فرزندان فقط به اندازه نیاز خود فضا اشغال می‌کنند (در این مورد SafeArea ) و سایر ویجت‌ها باید تا حد امکان از فضای باقی‌مانده استفاده کنند (در این مورد Expanded ). یک راه برای درک ویجت‌های Expanded این است که آن‌ها "حریص" هستند. اگر می‌خواهید درک بهتری از نقش این ویجت داشته باشید، سعی کنید ویجت SafeArea را با یک Expanded دیگر ترکیب کنید. طرح‌بندی حاصل چیزی شبیه به این خواهد بود:

6bbda6c1835a1ae.png

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

ویجت‌های بدون وضعیت در مقابل ویجت‌های دارای وضعیت

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

این در شرف تغییر است.

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

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

e52d9c0937cc0823.jpeg

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

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

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

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

تنظیم وضعیت

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

5685cf886047f6ec.png

  1. Applying the fail-fast principle , the switch statement also makes sure to throw an error if selectedIndex is neither 0 or 1. This helps prevent bugs down the line. If you ever add a new destination to the navigation rail and forget to update this code, the program crashes in development (as opposed to letting you guess why things don't work, or letting you publish a buggy code into production).

Now that page contains the widget you want to show on the right, you can probably guess what other change is needed.

Here's _MyHomePageState after that single remaining change:

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

// ...

The app now switches between our GeneratorPage and the placeholder that will soon become the Favorites page.

Responsiveness

Next, make the navigation rail responsive. That is to say, make it automatically show the labels (using extended: true ) when there's enough room for them.

a8873894c32e0d0b.png

Flutter provides several widgets that help you make your apps automatically responsive. For example, Wrap is a widget similar to Row or Column that automatically wraps children to the next "line" (called "run") when there isn't enough vertical or horizontal space. There's FittedBox , a widget that automatically fits its child into available space according to your specifications.

But NavigationRail doesn't automatically show labels when there's enough space because it can't know what is enough space in every context. It's up to you, the developer, to make that call.

Say you decide to show labels only if MyHomePage is at least 600 pixels wide.

The widget to use, in this case, is LayoutBuilder . It lets you change your widget tree depending on how much available space you have.

Once again, use Flutter's Refactor menu in VS Code to make the required changes. This time, though, it's a little more complicated:

  1. Inside _MyHomePageState 's build method, put your cursor on Scaffold .
  2. Call up the Refactor menu with Ctrl+. (Windows/Linux) or Cmd+. (Mac).
  3. Select Wrap with Builder and press Enter .
  4. Modify the name of the newly added Builder to LayoutBuilder .
  5. Modify the callback parameter list from (context) to (context, constraints) .

LayoutBuilder 's builder callback is called every time the constraints change. This happens when, for example:

  • The user resizes the app's window
  • The user rotates their phone from portrait mode to landscape mode, or back
  • Some widget next to MyHomePage grows in size, making MyHomePage 's constraints smaller

Now your code can decide whether to show the label by querying the current constraints . Make the following single-line change to _MyHomePageState 's build method:

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


// ...

Now, your app responds to its environment, such as screen size, orientation, and platform! In other words, it's responsive!.

The only work that remains is to replace that Placeholder with an actual Favorites screen. That's covered in the next section.

8. Add a new page

Remember the Placeholder widget we used instead of the Favorites page?

It's time to fix this.

If you feel adventurous, try to do this step by yourself. Your goal is to show the list of favorites in a new stateless widget, FavoritesPage , and then show that widget instead of the Placeholder .

Here are a few pointers:

  • When you want a Column that scrolls, use the ListView widget.
  • Remember, access the MyAppState instance from any widget using context.watch<MyAppState>() .
  • If you also want to try a new widget, ListTile has properties like title (generally for text), leading (for icons or avatars) and onTap (for interactions). However, you can achieve similar effects with the widgets you already know.
  • Dart allows using for loops inside collection literals. For example, if messages contains a list of strings, you can have code like the following:

f0444bba08f205aa.png

On the other hand, if you're more familiar with functional programming, Dart also lets you write code like messages.map((m) => Text(m)).toList() . And, of course, you can always create a list of widgets and imperatively add to it inside the build method.

The advantage of adding the Favorites page yourself is that you learn more by making your own decisions. The disadvantage is that you might run into trouble that you aren't yet able to solve by yourself. Remember: failing is okay, and is one of the most important elements of learning. Nobody expects you to nail Flutter development in your first hour, and neither should you.

252f7c4a212c94d2.png

What follows is just one way to implement the favorites page. How it's implemented will (hopefully) inspire you to play with the code—improve the UI and make it your own.

Here's the new FavoritesPage class:

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

Here's what the widget does:

  • It gets the current state of the app.
  • If the list of favorites is empty, it shows a centered message: No favorites yet.
  • Otherwise, it shows a (scrollable) list.
  • The list starts with a summary (for example, You have 5 favorites. ).
  • The code then iterates through all the favorites, and constructs a ListTile widget for each one.

All that remains now is to replace the Placeholder widget with a FavoritesPage . And voilá!

You can get the final code of this app in the codelab repo on GitHub.

9. Next steps

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

Look at you! You took a non-functional scaffold with a Column and two Text widgets, and made it into a responsive, delightful little app.

d6e3d5f736411f13.png

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

  • The basics of how Flutter works
  • Creating layouts in Flutter
  • Connecting user interactions (like button presses) to app behavior
  • Keeping your Flutter code organized
  • Making your app responsive
  • Achieving a consistent look & feel of your app

بعدش چی؟

  • Experiment more with the app you wrote during this lab.
  • Look at the code of this advanced version of the same app, to see how you can add animated lists, gradients, cross-fades, and more.