۱. مقدمه
فلاتر (Flutter) ابزار رابط کاربری گوگل برای ساخت برنامههای کاربردی برای موبایل، وب و دسکتاپ از یک کدبیس واحد است. در این آزمایشگاه کد، شما برنامه فلاتر زیر را خواهید ساخت:
این برنامه نامهای جذابی مانند "newstay"، "lightstream"، "mainbrake" یا "graypine" تولید میکند. کاربر میتواند نام بعدی را بپرسد، نام فعلی را به لیست علاقهمندیهای خود اضافه کند و لیست نامهای مورد علاقه را در یک صفحه جداگانه مرور کند. این برنامه نسبت به اندازههای مختلف صفحه نمایش واکنشگرا است.
آنچه یاد خواهید گرفت
- اصول اولیه نحوه کار فلاتر
- ایجاد طرحبندیها در فلاتر
- اتصال تعاملات کاربر (مانند فشردن دکمه) به رفتار برنامه
- مرتب نگه داشتن کد فلاتر شما
- واکنشگرا کردن برنامه (برای صفحه نمایشهای مختلف)
- دستیابی به ظاهر و حس یکپارچه برای برنامه شما
شما با یک چارچوب اولیه شروع خواهید کرد تا بتوانید مستقیماً به قسمتهای جالب بروید.

و این فیلیپ است که شما را در کل آزمایشگاه کد همراهی میکند!
برای شروع آزمایشگاه، روی بعدی کلیک کنید.
۲. محیط فلاتر خود را تنظیم کنید
ویرایشگر
برای اینکه این آزمایشگاه کد تا حد امکان ساده باشد، فرض میکنیم که شما از ویژوال استودیو کد (VS Code) به عنوان محیط توسعه خود استفاده خواهید کرد. این محیط رایگان است و روی همه پلتفرمهای اصلی کار میکند.
البته استفاده از هر ویرایشگری که دوست دارید اشکالی ندارد: اندروید استودیو، سایر IDEهای IntelliJ، Emacs، Vim یا Notepad++. همه آنها با Flutter کار میکنند.
ما استفاده از VS Code را برای این آزمایشگاه کد توصیه میکنیم زیرا دستورالعملها به طور پیشفرض به میانبرهای مخصوص VS Code اشاره دارند. گفتن چیزهایی مانند «اینجا کلیک کنید» یا «این کلید را فشار دهید» به جای چیزی مانند «برای انجام X، اقدام مناسب را در ویرایشگر خود انجام دهید» آسانتر است.

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

برای مثال، فرض کنید از یک لپتاپ ویندوزی برای توسعه یک برنامه Flutter استفاده میکنید. اگر اندروید را به عنوان هدف توسعه خود انتخاب کنید، معمولاً یک دستگاه اندروید را با کابل USB به لپتاپ ویندوزی خود متصل میکنید و برنامه در حال توسعه شما روی آن دستگاه اندروید متصل اجرا میشود. اما میتوانید ویندوز را نیز به عنوان هدف توسعه انتخاب کنید، به این معنی که برنامه در حال توسعه شما به عنوان یک برنامه ویندوزی در کنار ویرایشگر شما اجرا میشود.
ممکن است وسوسه شوید که وب را به عنوان هدف توسعه خود انتخاب کنید. نکته منفی این انتخاب این است که یکی از مفیدترین ویژگیهای توسعه فلاتر را از دست میدهید: بارگذاری مجدد سریع با وضعیت. فلاتر نمیتواند برنامههای وب را به صورت خودکار بارگذاری مجدد کند.
همین حالا انتخاب خود را انجام دهید. به یاد داشته باشید: همیشه میتوانید برنامه خود را بعداً روی سیستم عاملهای دیگر اجرا کنید. فقط داشتن یک هدف توسعه مشخص در ذهن، گام بعدی را هموارتر میکند.
نصب فلاتر
جدیدترین دستورالعملها در مورد نحوه نصب SDK فلاتر همیشه در docs.flutter.dev موجود است.
دستورالعملهای موجود در وبسایت Flutter نه تنها نصب خود SDK، بلکه ابزارهای مرتبط با هدف توسعه و افزونههای ویرایشگر را نیز پوشش میدهد. به یاد داشته باشید که برای این آزمایشگاه کد، فقط باید موارد زیر را نصب کنید:
- کیت توسعه نرمافزار فلاتر
- ویژوال استودیو کد با افزونه فلاتر
- نرمافزار مورد نیاز برای هدف توسعه انتخابی شما (برای مثال: ویژوال استودیو برای ویندوز یا Xcode برای macOS)
در بخش بعدی، اولین پروژه فلاتر خود را ایجاد خواهید کرد.
اگر تا الان با مشکلاتی مواجه شدهاید، ممکن است برخی از این پرسش و پاسخها (از StackOverflow) برای عیبیابی مفید باشند.
سوالات متداول
- چگونه میتوانم مسیر SDK فلاتر را پیدا کنم؟
- وقتی دستور Flutter پیدا نشد، چه کاری باید انجام دهم؟
- چگونه میتوانم مشکل «منتظر دستور دیگری برای آزادسازی قفل راهاندازی هستم» را برطرف کنم؟
- چگونه به Flutter بگویم که محل نصب SDK اندروید من کجاست؟
- چگونه میتوانم با خطای جاوا هنگام اجرای
flutter doctor --android-licensesمقابله کنم؟ - چگونه با خطای «ابزار
sdkmanagerاندروید یافت نشد» (Android sdkmanager tool not found) برخورد کنم؟ - چگونه با خطای "
cmdline-toolscomponent is missing" مقابله کنم؟ - چگونه میتوانم CocoaPods را روی Apple Silicon (M1) اجرا کنم؟
- چگونه میتوانم قالببندی خودکار هنگام ذخیره در VS Code را غیرفعال کنم؟
۳. ایجاد یک پروژه
اولین پروژه فلاتر خود را ایجاد کنید
ویژوال استودیو کد را اجرا کنید و پالت دستورات را باز کنید (با استفاده از کلیدهای F1 یا Ctrl+Shift+P یا Shift+Cmd+P ). شروع به تایپ کردن عبارت "flutter new" کنید. دستور Flutter: New Project را انتخاب کنید.
سپس، Application و سپس پوشهای که میخواهید پروژه خود را در آن ایجاد کنید را انتخاب کنید. این میتواند پوشه اصلی شما یا جایی مانند C:\src\ باشد.
در نهایت، نامی برای پروژه خود انتخاب کنید. چیزی شبیه namer_app یا my_awesome_namer .

فلاتر اکنون پوشه پروژه شما را ایجاد میکند و VS Code آن را باز میکند.
اکنون محتویات ۳ فایل را با یک چارچوب اولیه از برنامه بازنویسی خواهید کرد.
برنامه اولیه را کپی و جایگذاری کنید
در پنل سمت چپ VS Code، مطمئن شوید که Explorer انتخاب شده است و فایل pubspec.yaml باز کنید.

محتویات این فایل را با موارد زیر جایگزین کنید:
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 باز کنید.

محتویات آن را با موارد زیر جایگزین کنید:
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/ باز کنید.

محتویات این فایل را با موارد زیر جایگزین کنید:
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" را پیدا کنید.
روی دکمهی «در گوشهی بالا سمت راست پنجرهی VS Code» کلیک کنید.
بعد از حدود یک دقیقه، برنامه شما در حالت اشکالزدایی (debug mode) اجرا میشود. هنوز چیز زیادی به نظر نمیرسد:

اولین بارگذاری مجدد داغ
در پایین 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 زمانی فعال میشود که تغییرات را در یک فایل منبع ذخیره میکنید.
سوالات متداول
- اگر قابلیت Hot Reload در VSCode کار نکند، چه میشود؟
- آیا برای بارگذاری سریع در VSCode باید دکمه 'r' را فشار دهم؟
- آیا قابلیت 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مراجعه کنید). این به هر ویجتی در برنامه اجازه میدهد تا به وضعیت دسترسی داشته باشد.

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 را داریم که قبلاً آن را تغییر دادهاید. هر خط شمارهگذاری شده زیر، به یک کامنت با شماره خط در کد بالا اشاره دارد:
- هر ویجت یک متد
build()تعریف میکند که هر بار که شرایط ویجت تغییر میکند، به طور خودکار فراخوانی میشود تا ویجت همیشه بهروز باشد. -
MyHomePageبا استفاده از متدwatchتغییرات وضعیت فعلی برنامه را ردیابی میکند. - هر متد
buildباید یک ویجت یا (معمولاً) یک درخت تو در تو از ویجتها را برگرداند. در این مورد، ویجت سطح بالاScaffoldاست. شما قرار نیست در این آزمایشگاه کد باScaffoldکار کنید، اما یک ویجت مفید است و در اکثر قریب به اتفاق برنامههای Flutter در دنیای واقعی یافت میشود. -
Columnیکی از اساسیترین ویجتهای طرحبندی در فلاتر است. این ویجت هر تعداد عنصر فرزند را میگیرد و آنها را از بالا به پایین در یک ستون قرار میدهد. به طور پیشفرض، ستون به صورت بصری فرزندان خود را در بالا قرار میدهد. به زودی این را تغییر خواهید داد تا ستون در مرکز قرار گیرد. - شما این ویجت
Textرا در مرحله اول تغییر دادید. - این ویجت
TextدومappStateمیگیرد و به تنها عضو آن کلاس،current(که یکWordPairاست) دسترسی پیدا میکند.WordPairچندین getter مفید مانندasPascalCaseیاasSnakeCaseارائه میدهد. در اینجا، ما ازasLowerCaseاستفاده میکنیم، اما اگر یکی از گزینههای دیگر را ترجیح میدهید، میتوانید اکنون آن را تغییر دهید. - توجه کنید که کد فلاتر چگونه از کاماهای انتهایی به شدت استفاده میکند. این کاما خاص نیازی به اینجا ندارد، زیرا
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'),
),
// ...
برنامه را ذخیره و امتحان کنید. هر بار که دکمهی «بعدی» را فشار دهید، باید یک جفت کلمهی تصادفی جدید ایجاد کند.
در بخش بعدی، رابط کاربری را زیباتر خواهید کرد.
۵. برنامه را زیباتر کنید
این ظاهر فعلی برنامه است.

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

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

تم و سبک
برای اینکه کارت بیشتر به چشم بیاید، آن را با رنگی غنیتر رنگآمیزی کنید. و از آنجایی که همیشه ایده خوبی است که یک طرح رنگی ثابت داشته باشید، 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، برجستهترین و تعیینکنندهترین رنگ برنامه است.
اکنون کارت با رنگ اصلی برنامه رنگآمیزی شده است:

شما میتوانید این رنگ و طرح رنگی کل برنامه را با اسکرول کردن به 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در طرح رنگ، رنگی را تعریف میکند که برای استفاده روی رنگ اصلی برنامه مناسب است.
حالا برنامه باید چیزی شبیه به تصویر زیر باشد:

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

با این حال، گاهی اوقات، به مقداری کار نیاز است. در مورد این برنامه، صفحهخوان ممکن است در تلفظ برخی از جفت کلمات تولید شده مشکل داشته باشد. در حالی که انسانها در شناسایی دو کلمه در 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 را در امتداد محور اصلی (عمودی) آن قرار میدهد.

عناصر فرزند از قبل در امتداد محور عرضی ستون در مرکز قرار گرفتهاند (به عبارت دیگر، آنها از قبل به صورت افقی در مرکز قرار گرفتهاند). اما خود Column درون Scaffold در مرکز قرار نگرفته است. میتوانیم این را با استفاده از Widget Inspector تأیید کنیم.
خودِ Widget Inspector فراتر از محدودهی این کد است، اما میتوانید ببینید که وقتی Column هایلایت میشود، کل عرض برنامه را اشغال نمیکند. فقط به اندازهی فضای افقی که فرزندانش نیاز دارند، فضا اشغال میکند.
شما میتوانید خود ستون را در مرکز قرار دهید. مکاننما را روی Column قرار دهید، منوی Refactor را (با Ctrl+. یا Cmd+. ) فراخوانی کنید و Wrap with Center را انتخاب کنید.
حالا برنامه باید چیزی شبیه به تصویر زیر باشد:

اگر بخواهید، میتوانید این را کمی بیشتر تنظیم کنید.
- میتوانید ویجت
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'),
),
],
),
),
);
}
}
// ...
و ظاهر برنامه به شکل زیر است:

در بخش بعدی، قابلیت افزودن کلمات تولید شده به علاقهمندیها (یا «لایک») را اضافه خواهید کرد.
۶. افزودن قابلیتها
این برنامه کار میکند و گاهی اوقات حتی جفت کلمات جالبی را ارائه میدهد. اما هر زمان که کاربر روی «بعدی » کلیک کند، هر جفت کلمه برای همیشه ناپدید میشود. بهتر است راهی برای «به خاطر سپردن» بهترین پیشنهادات وجود داشته باشد: مانند دکمه «لایک».

منطق کسب و کار را اضافه کنید
به 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'),
),
],
),
],
),
),
);
}
}
// ...
رابط کاربری به حالت قبل برگشته است.

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

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

در اینجا یک راه برای اضافه کردن دکمه دوم به 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 خود را پیادهسازی خواهید کرد.

برای اینکه هر چه سریعتر به اصل مطلب این مرحله برسید، 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'),
),
],
),
],
),
);
}
}
// ...
وقتی ذخیره شد، خواهید دید که بخش بصری رابط کاربری آماده است—اما کار نمیکند. کلیک کردن روی ♥︎ (قلب) در نوار ناوبری هیچ کاری انجام نمیدهد.

تغییرات را بررسی کنید.
- ابتدا توجه کنید که کل محتوای
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دیگر ترکیب کنید. طرحبندی حاصل چیزی شبیه به این خواهد بود:

- دو ویجت
Expandedتمام فضای افقی موجود را بین خود تقسیم کردند، اگرچه ریل ناوبری فقط به یک برش کوچک در سمت چپ نیاز داشت. - درون ویجت
Expanded، یکContainerرنگی وجود دارد و درون این container،GeneratorPage.
ویجتهای بدون وضعیت در مقابل ویجتهای دارای وضعیت
تا الان، MyAppState تمام نیازهای مربوط به state شما را پوشش میداد. به همین دلیل است که تمام ویجتهایی که تاکنون نوشتهاید، بدون state هستند. آنها هیچ state قابل تغییری از خودشان ندارند. هیچکدام از ویجتها نمیتوانند خودشان را تغییر دهند - آنها باید از طریق MyAppState این کار را انجام دهند.
این در شرف تغییر است.
شما به روشی نیاز دارید تا مقدار selectedIndex مربوط به ریل ناوبری را نگه دارید. همچنین میخواهید بتوانید این مقدار را از درون تابع onDestinationSelected تغییر دهید.
شما میتوانید selectedIndex به عنوان یک ویژگی دیگر از MyAppState اضافه کنید. و این کار میکند. اما میتوانید تصور کنید که اگر هر ویجت مقادیر خود را در آن ذخیره کند، وضعیت برنامه به سرعت و به طور غیرمنطقی افزایش مییابد.

برخی از حالتها فقط به یک ویجت مربوط میشوند، بنابراین باید در همان ویجت باقی بمانند.
عبارت 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(),
),
),
],
),
);
}
}
// ...
بررسی تغییرات:
- شما یک متغیر جدید به
selectedIndexمعرفی میکنید و مقدار اولیه آن را برابر با0قرار میدهید. - شما از این متغیر جدید در تعریف
NavigationRailبه جای مقدار ثابت0که تاکنون وجود داشت، استفاده میکنید. - وقتی تابع
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');
}
// ...
این قطعه کد را بررسی کنید:
- این کد یک متغیر جدید به
pageاز نوعWidgetتعریف میکند. - سپس، یک دستور switch، یک صفحه نمایش را بر اساس مقدار فعلی در
selectedIndexبهpageاختصاص میدهد. - از آنجایی که هنوز
FavoritesPageوجود ندارد،Placeholderاستفاده کنید؛ یک ویجت کاربردی که هر جا که آن را قرار دهید، یک مستطیل متقاطع رسم میکند و آن بخش از رابط کاربری را به عنوان ناتمام علامتگذاری میکند.

- Applying the fail-fast principle , the switch statement also makes sure to throw an error if
selectedIndexis 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.

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:
- Inside
_MyHomePageState'sbuildmethod, put your cursor onScaffold. - Call up the Refactor menu with
Ctrl+.(Windows/Linux) orCmd+.(Mac). - Select Wrap with Builder and press Enter .
- Modify the name of the newly added
BuildertoLayoutBuilder. - 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
MyHomePagegrows in size, makingMyHomePage'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
Columnthat scrolls, use theListViewwidget. - Remember, access the
MyAppStateinstance from any widget usingcontext.watch<MyAppState>(). - If you also want to try a new widget,
ListTilehas properties liketitle(generally for text),leading(for icons or avatars) andonTap(for interactions). However, you can achieve similar effects with the widgets you already know. - Dart allows using
forloops inside collection literals. For example, ifmessagescontains a list of strings, you can have code like the following:

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.

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
ListTilewidget 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.

آنچه ما پوشش دادهایم
- 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.
- Follow your learning journey by going to flutter.dev/learn .