Widgetها در Flutter
«همه چیز در Flutter یک Widget است.» این جمله طلایی Flutter است. در این فصل با مفهوم Widget، انواع آن، چرخه حیات و BuildContext آشنا میشویم.
۳.۱ مقدمه
«همه چیز در Flutter یک Widget است.» این جمله طلایی Flutter است. در این فصل با مفهوم Widget، انواع آن، چرخه حیات و BuildContext آشنا میشویم.
۳.۲ Widget چیست؟
Widget یک توضیح غیرقابل تغییر (immutable description) از بخشی از UI است. Flutter آن را به یک Element و سپس RenderObject تبدیل میکند.
لایههای Flutter:
Widget (immutable description)
↓ creates
Element (mutable, lifecycle)
↓ creates
RenderObject (layout, paint)
دو نوع Widget اصلی
StatelessWidget
بدون state داخلی. وقتی parameter ها تغییر کنند، widget جدید ساخته میشود.
مثل: Text، Icon، Image
StatefulWidget
state داخلی دارد. میتواند خودش را با setState() rebuild کند.
مثل: Checkbox، Slider، فرم
۳.۳ StatelessWidget
import "package:flutter/material.dart";
class GreetingCard extends StatelessWidget {
final String name;
final String message;
// const constructor — performance بهتر
const GreetingCard({
super.key,
required this.name,
this.message = "خوش آمدید!",
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"سلام $name",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(message),
],
),
),
);
}
}
// استفاده
class MyHome extends StatelessWidget {
const MyHome({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("صفحه اصلی")),
body: Column(
children: const [
GreetingCard(name: "علی"),
GreetingCard(name: "زهرا", message: "روز خوبی داشته باشید"),
],
),
);
}
}
چرا const؟
اگر یک widget در زمان compile قابل ساخت است (همه پارامترهایش const)، با const یک نسخه canonical آن ساخته میشود. این یعنی Flutter آن را rebuild نمیکند — performance بهتر!
۳.۴ StatefulWidget
class Counter extends StatefulWidget {
final int initialValue;
const Counter({super.key, this.initialValue = 0});
@override
State createState() => _CounterState();
}
class _CounterState extends State {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue; // دسترسی به widget با this.widget
print("Counter initialized");
}
void _increment() {
setState(() {
_count++;
});
}
void _decrement() {
setState(() {
_count--;
});
}
@override
void dispose() {
print("Counter disposed");
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: _decrement,
),
Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _increment,
),
],
);
}
}
چرا State جداست از Widget؟
چون Widget باید immutable باشد. State در یک object جدا نگهداری میشود که بین rebuilds حفظ میشود. این طراحی Flutter را بسیار کارآمد میکند.
۳.۵ Lifecycle یک StatefulWidget
1. createState() ← Flutter صدا میزند
↓
2. initState() ← فقط یک بار، شروع
↓
3. didChangeDependencies() ← اگر InheritedWidget تغییر کند
↓
4. build() ← بارها اجرا میشود
↓ (rebuild)
5. didUpdateWidget() ← اگر parent rebuild کند
↓
6. setState() → build() again
↓
7. deactivate() ← قبل از حذف از tree
↓
8. dispose() ← یک بار، پایان
class LifecycleDemo extends StatefulWidget {
final String title;
const LifecycleDemo({super.key, required this.title});
@override
State createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State {
late TextEditingController _controller;
Timer? _timer;
@override
void initState() {
super.initState();
print("1. initState");
_controller = TextEditingController();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
print("tick");
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("2. didChangeDependencies");
// مثلاً MediaQuery، Theme در دسترس است
}
@override
void didUpdateWidget(covariant LifecycleDemo oldWidget) {
super.didUpdateWidget(oldWidget);
print("3. didUpdateWidget: ${oldWidget.title} -> ${widget.title}");
// اگر title عوض شده، کاری انجام دهیم
}
@override
Widget build(BuildContext context) {
print("4. build");
return TextField(controller: _controller);
}
@override
void deactivate() {
print("5. deactivate");
super.deactivate();
}
@override
void dispose() {
print("6. dispose");
// ⚠️ حتماً resources را آزاد کنید
_controller.dispose();
_timer?.cancel();
super.dispose();
}
}
dispose() برای Controller، AnimationController، StreamSubscription و Timer منجر به memory leak میشود!
۳.۶ BuildContext
BuildContext در واقع reference به Element فعلی در tree است. از طریق آن:
- به Theme دسترسی دارید:
Theme.of(context) - به MediaQuery دسترسی:
MediaQuery.of(context).size - Navigation:
Navigator.of(context).push(...) - Inherited Widgets:
Provider.of<X>(context) - Localization:
AppLocalizations.of(context)
class ResponsiveText extends StatelessWidget {
const ResponsiveText({super.key});
@override
Widget build(BuildContext context) {
// اطلاعات از context
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final fontSize = mediaQuery.size.width < 600 ? 16.0 : 24.0;
return Text(
"متن واکنشگرا",
style: TextStyle(
fontSize: fontSize,
color: isDark ? Colors.white : Colors.black,
),
);
}
}
۳.۷ Widgetهای ضروری
Text — نمایش متن
Text("سلام")
Text(
"متن استایلدار",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
fontFamily: "Vazirmatn",
letterSpacing: 0.5,
height: 1.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
// متن غنی با چند استایل
RichText(
text: const TextSpan(
style: TextStyle(color: Colors.black),
children: [
TextSpan(text: "Flutter "),
TextSpan(
text: "خیلی",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red),
),
TextSpan(text: " قدرتمند است"),
],
),
)
Container — جعبه چندمنظوره
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black, width: 2),
gradient: const LinearGradient(
colors: [Colors.blue, Colors.purple],
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Center(child: Text("Container", style: TextStyle(color: Colors.white))),
)
Image — نمایش تصویر
// از Asset
Image.asset("assets/images/logo.png")
// از URL
Image.network(
"https://example.com/photo.jpg",
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return Center(
child: CircularProgressIndicator(
value: progress.expectedTotalBytes != null
? progress.cumulativeBytesLoaded / progress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.error);
},
fit: BoxFit.cover,
)
// با cached_network_image (پیشنهاد)
CachedNetworkImage(
imageUrl: "https://example.com/photo.jpg",
placeholder: (_, __) => const CircularProgressIndicator(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
)
// از File
Image.file(File("/path/to/image.jpg"))
Icon — آیکونهای Material و Cupertino
const Icon(Icons.home, size: 32, color: Colors.blue)
const Icon(Icons.favorite, color: Colors.red)
const Icon(Icons.shopping_cart)
// Cupertino (iOS-style)
const Icon(CupertinoIcons.heart_fill)
Buttonها
// 1. ElevatedButton — بارز
ElevatedButton(
onPressed: () => print("کلیک"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text("ثبتنام"),
)
// 2. TextButton — مسطح
TextButton(
onPressed: () {},
child: const Text("لغو"),
)
// 3. OutlinedButton — با حاشیه
OutlinedButton(
onPressed: () {},
child: const Text("بیشتر"),
)
// 4. IconButton
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
tooltip: "جستجو",
)
// 5. FloatingActionButton
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
)
// 6. با icon و label
ElevatedButton.icon(
icon: const Icon(Icons.send),
label: const Text("ارسال"),
onPressed: () {},
)
// disabled کردن: onPressed: null
ElevatedButton(
onPressed: null, // disabled
child: const Text("غیرفعال"),
)
Scaffold — ساختار صفحه
Scaffold(
appBar: AppBar(
title: const Text("عنوان"),
actions: [
IconButton(icon: const Icon(Icons.search), onPressed: () {}),
IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
],
),
body: const Center(child: Text("بدنه اصلی")),
drawer: Drawer(
child: ListView(
children: const [
DrawerHeader(child: Text("منو")),
ListTile(title: Text("خانه")),
ListTile(title: Text("تنظیمات")),
],
),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "خانه"),
BottomNavigationBarItem(icon: Icon(Icons.search), label: "جستجو"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "پروفایل"),
],
currentIndex: 0,
onTap: (index) {},
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
)
۳.۸ Keys — کِی استفاده کنیم؟
Key به Flutter میگوید کدام widget با کدام Element مطابقت دارد. در اکثر موارد لازم نیست، اما در موارد خاص حیاتی است:
// مشکل بدون Key:
class TodoList extends StatefulWidget {
// وقتی item های لیست reorder میشوند، state اشتباه میشود
}
// راه حل: Key
ListView(
children: items.map((item) => TodoTile(
key: ValueKey(item.id), // ← مهم
item: item,
)).toList(),
)
انواع Key
- ValueKey: از یک value (مثل id)
- UniqueKey: همیشه unique (هر rebuild جدید)
- ObjectKey: از reference یک object
- GlobalKey: در همه widget tree unique
کِی Key استفاده کنیم؟
- وقتی لیستی از StatefulWidget های مشابه دارید
- وقتی reorder/insert/delete در لیست انجام میدهید
- برای دسترسی به State از خارج (با GlobalKey)
۳.۹ بهترین تجربیات Widget
- const هر کجا ممکن. performance بهتر، rebuild کمتر.
- Widget کوچک نگه دارید. هر method جدا = widget جدا.
- extract widget با Refactor. به جای method، widget بسازید.
- StatelessWidget اگر state ندارد. سبکتر است.
- dispose را فراموش نکنید. Controller، Timer، Stream.
- Key وقتی نیاز است. در لیستهای قابل reorder.
- BuildContext را به async نسپارید. اگر widget unmount شود، crash میشود. با
mountedچک کنید. - Theme و MediaQuery از context. hardcode نکنید.
- final برای فیلدهای widget. چون immutable است.
- required برای پارامترهای ضروری.
۳.۱۰ خلاصه فصل
آنچه آموختیم:
- «همه چیز Widget است» — flutter philosophy
- StatelessWidget: بدون state، از parameter ها
- StatefulWidget: state داخلی با setState
- Lifecycle: initState، didUpdateWidget، dispose
- BuildContext: دسترسی به Theme، MediaQuery، Navigation
- Widgetهای ضروری: Text، Container، Image، Icon، Buttons، Scaffold
- Keys برای identify widget ها در لیست