فلاتر برای وب — راهنمای مقدماتی
فلاتر از زمان معرفیش در اواخر سال 2018، به عنوان یک SDK برای توسعه موبایل محبوبیت زیادی کسب کرده است. با افزودن بخش فلاتر برای وب ، این SDK هماکنون در اختیار توسعهدهندگان وب نیز قرار گرفته است که با آن میتوانند تجربهای با کیفیت عالی در وب خلق کنند و از مزیت آخرین API-های وب بهرهمند شوند.
در این مقاله به بررسی شیوه ساخت یک صفحه وب ساده با فلاتر برای وب میپردازیم که شامل طرحبندی ساده، مقداری متن و تصویر و چند انیمیشن اسکرول میشود. این مثال ساده هیچ چالشی در زمینه طراحی UX ندارد، اما برای مقاصد آموزشی مورد نظر این مقاله مناسب است.
محیط
برای این که یک محیط برای توسعه وباپلیکیشنها با فلاتر برای وب آماده شود، باید ابزارهای زیر را روی سیستم نصب داشته باشید:
- فلاتر: از این صفحه (+) نصب کنید.
- Stagehand: از دستور pub global activate stagehand $ برای ایجاد اپلیکیشن جدید بهره بگیرید.
- IDE: یک IDE یا ادیتور کد مانند VS Code (+) با اکستنشن Dart (+).
برای تأیید این که نصب فلاتر شما عملیاتی است، دستور flutter doctor $ را اجرا کنید تا مطمئن شوید که هر گونه اکستنشن یا دیگر وابستگیهای ناموجود، دانلود میشوند. سورس کد این پروژه را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید. اطمینان حاصل کنید که دستور flutter pub get $ را در دایرکتوری پروژه اجرا میکنید.
ساختار پروژه
این پروژه با دستور زیر ایجاد شده است:
$ stagehand web-simple
بدین ترتیب یک پروژه وب خالی ایجاد میشود که هیچ پکیج flutter_web به عنوان وابستگی ندارد. از این رو باید کد زیر را به فایل pubspec.yaml اضافه کنید.
1name: flutter_web_example
2description: A basic example app demonstrating Flutter for Web
3author: Kenneth Reilly <kenneth@innovationgroup.tech>
4version: 1.0.0
5
6environment:
7 sdk: '>=2.1.0 <3.0.0'
8
9dependencies:
10 flutter_web: any
11 flutter_web_ui: any
12
13dev_dependencies:
14 build_runner: ^1.1.2
15 build_web_compilers: ^1.0.0
16 pedantic: ^1.0.0
17
18dependency_overrides:
19 flutter_web:
20 git:
21 url: https://github.com/flutter/flutter_web
22 path: packages/flutter_web
23 flutter_web_ui:
24 git:
25 url: https://github.com/flutter/flutter_web
26 path: packages/flutter_web_ui
این فایل pubspec.yaml برای یک پروژه فلاتر کاملاً عمومی است و چند نکته نیز برای ساخت وب اپلیکیشن به آن اضافه شده است. همچنین برخی وابستگیها با استفاده از url و مسیر ریپو override شدهاند، زیرا پکیج flutter_web هنوز روی ریپازیتوری pub.dartlang.org منتشر نشده است و pub بدون این override-ها ناموفق خواهد بود.
نقطه ورودی اپلیکیشن
در ادامه فایل سورس اصلی یعنی lib/main.dart را میبینید:
1import 'package:flutter_web/material.dart';
2import 'home.dart';
3
4class FlutterWebDemo extends StatelessWidget {
5
6 @override
7 Widget build(BuildContext context) {
8
9 return MaterialApp(
10 title: 'Flutter Web Demo',
11 theme: ThemeData(primarySwatch: Colors.deepPurple),
12 home: HomePage(title: 'Flutter Web Demo'),
13 );
14 }
15}
این فایل اپلیکیشن اصلی برای هر پروژه فلاتر برای وب است و با پیادهسازی StatelessWidget یک MaterialApp میسازد که ویجت HomePage درون آن قرارمی گیرد. در ادامه آن را بیشتر بررسی خواهیم کرد.
صفحه اصلی
در ادامه به بررسی صفحه اصلی وبسایت در آدرس lib/home.dart میپردازیم:
1import 'package:flutter_web/material.dart';
2import 'package:flutter_web_example/background.dart';
3import 'section-def.dart';
4import 'section.dart';
5
6class HomePage extends StatefulWidget {
7
8 HomePage({ Key key, this.title }) : super(key: key);
9 final String title;
10
11 _HomePageState createState() => _HomePageState();
12}
13
14class _HomePageState extends State<HomePage> {
15
16 List<Section> get _cards =>
17 List.generate(sections.length, (int x)
18 => Section(listenable: _controller, index: x, total: sections.length, item: sections[x]));
19
20 ScrollController _controller = ScrollController();
21
22 @override
23 Widget build(BuildContext context) {
24
25 return Scaffold(
26
27 backgroundColor: Colors.black,
28 body: Stack(
29
30 children: <Widget>[
31
32 Background(
33 image: AssetImage('images/image-01.jpg'),
34 listenable: _controller
35 ),
36 Container(
37 child: ListView(
38 padding: EdgeInsets.only(top: 16, bottom: 64),
39 children: _cards,
40 controller: _controller
41 )
42 )
43 ]
44 )
45 );
46 }
47}
اینجا بخشی است که عمده کدهای پروژه در آن نوشته شده است. کلاس HomePage اقدام به بسط یک StatefulWidget میکند که به ما امکان میدهد حالت درونی خود را با مشخصههای غیر نهایی (Non-Final) که تغییرپذیر هستند در آن نگهداری کنیم. یک مشخصه به نام _cards وجود دارد که بخش ایمپورت شده تعاریف را میگیرد و لیستی از شیءهای Section بازگشت میدهد که باید نمایش یابند. مشخصه _controller یک ارجاع به ScrollController نگهداری میکند که برای راهاندازی انیمیشنها در بقیه اپلیکیشن مورد استفاده قرار میگیرد.
متد build برای این ویجت یک Scaffold بازگشت میدهد که شامل یک Stack (+) است. این Stack برای پشته سازی ویجتها در محور z مورد استفاده میگیرد. همچنین این متد یک Background بازمیگرداند که وارد ScrollController میشود تا یک انیمیشن parallax را اجرا کند. در ادامه یک ListView بازگشت مییابد که فهرستی از بخشهای صفحه که باید اسکرول شوند را نشان میدهد. در این مورد از کنترلر ارائه شده برای حرکت لیست استفاده میکنیم. این تقریباً یک پیکربندی استاندارد برای مدیریت جلوههای اسکرول در فلاتر محسوب میشود، چون به کنترلر امکان میدهد که رفتار سفارشی ایجاد نماید و سپس از آن برای حرکت دادن مستقیم عنصر اسکرول شونده و همچنین هر تعداد از جلوههای انیمیشن از طریق AnimatedWidget و یا مفهوم مرتبطی مانند AnimatedBuilder بهره میگیرد.
پسزمینه
در ادامه پسزمینه وبسایت در فایل lib/background.dart قرار میگیرد:
1import 'package:flutter_web/material.dart';
2
3class Background extends AnimatedWidget {
4
5 Background({ Key key, @required this.image, @required this.listenable })
6 : super(key: key, listenable: listenable);
7
8 final AssetImage image;
9 final ScrollController listenable;
10
11 @override
12 Widget build(BuildContext context) {
13
14 double offset = listenable.hasClients ? listenable.offset : 0;
15 ScrollPosition position = listenable.hasClients ? listenable.position : null;
16 double extent = (position == null) ? 1 : position.maxScrollExtent * 1.2;
17 double align = (offset / extent);
18
19 return Container(
20 constraints: BoxConstraints.expand(),
21 child: Image(
22 image: image,
23 alignment: Alignment(0, align),
24 fit: BoxFit.cover
25 )
26 );
27 }
28}
ویجت Background یک پیادهسازی از AnimatedWidget است که یک Image (+) و یک شیء Listenable (+) میگیرد. در این مورد ما یک ScrollController ارسال میکنیم که یکی از کلاسهای فراوانی است که Listenable پیادهسازی میکند.
مقدار Listenable به کلاس بالاتر ارسال میشود تا اپلیکیشن امکان رفرش کردن ویجت انیمیت شده را در زمان رخداد اسکرول داشته باشد. در متد build محاسباتی برای تعیین میزان مسافتی که کاربر روی ListVew اسکرول کرده است انجام مییابد و این مقدار به مشخصه alignment روی تصویر به عنوان آفست y ارسال میشود که موجب حرکت کُند اسکرول میشود و جلوه parallax زیبایی در ترکیب با اسکرول شدن محتوا در پیشزمینه ایجاد میکند. ضمناً بررسیهای null مختلفی اجرا میشوند، چون کنترلر در نخستین رندر این ویجت نمیتواند کاملاً مقداردهی شده و آماده کار باشد.
تعاریف بخش صفحه
اکنون به بررسی تعاریف صفحه در فایل lib/section-def.dart میپردازیم:
1import 'package:flutter_web/material.dart';
2
3class SectionDef {
4
5 final String name;
6 final String description;
7 final AssetImage image;
8 const SectionDef(this.name, this.description, this.image);
9}
10
11List<SectionDef> sections = [
12 const SectionDef('Meditation', "Find your inner peace with meditation", AssetImage('images/image-02.jpg')),
13 const SectionDef('Beverages', "Relax with a beverage by the pool", AssetImage('images/image-03.jpg')),
14 const SectionDef('Aromatherapy', "Enjoy the aroma of pure essential oils", AssetImage('images/image-04.jpg')),
15 const SectionDef('Tea Time', "Have a conversation with friends over tea", AssetImage('images/image-05.jpg')),
16 const SectionDef('The Works', "Treat yourself to an all-day session", AssetImage('images/image-06.jpg'))
17];
کلاس SectionDef برای تعریف کردن یک بخش صفحه همراه با نام آن، توضیحات و تصویرش استفاده میشود. ضمناً این فایل لیستی از تعاریف بخشی را که از سوی HomePage برای ایجاد فهرست اشیای Section که باید رندر شوند شامل میشود.
کلاس Page Section
در نهایت به بررسی کد بخشهای صفحه در فایل lib/section.dart میپردازیم:
1import 'package:flutter_web/material.dart';
2import 'section-def.dart';
3
4class Content extends AnimatedWidget {
5
6 const Content({ Key key, this.listenable, this.children, this.opacity })
7 : super(key: key, listenable: listenable);
8
9 final ScrollController listenable;
10 final List<Widget> children;
11 final double opacity;
12
13 @override
14 Widget build(BuildContext context) {
15
16 return Opacity(
17 opacity: opacity,
18 child: Container(
19
20 padding: EdgeInsets.all(48).copyWith(bottom: 0 ),
21 constraints: BoxConstraints.expand(height: 720),
22 child: ClipRRect(
23
24 borderRadius: BorderRadius.all(Radius.circular(12)),
25 child: Container(
26
27 color: Color.fromARGB(64, 0, 16, 32),
28 child: Row(
29
30 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
31 crossAxisAlignment: CrossAxisAlignment.center,
32 children: children
33 )
34 )
35 )
36 )
37 );
38 }
39}
40
41class Section extends AnimatedWidget {
42
43 const Section({ Key key, this.index, this.total, this.item, @required this.listenable })
44 : super(key: key, listenable: listenable);
45
46 final int index;
47 final int total;
48 final SectionDef item;
49 final ScrollController listenable;
50
51 @override
52 Widget build(BuildContext context) {
53
54 Shadow shadow = const Shadow(color: Colors.grey, blurRadius: 24, offset: Offset(12, 12));
55 TextTheme theme = Theme.of(context).textTheme;
56 TextStyle _titleStyle = theme.display3.copyWith( color: Colors.black45, shadows: [shadow]);
57 TextStyle _descStyle = theme.display1.copyWith(fontSize: 18, color: Colors.black54, shadows: [shadow]);
58
59 double offset = listenable.hasClients ? listenable.offset : 0;
60 ScrollPosition position = listenable.hasClients ? listenable.position : null;
61 double extent = (position == null || position.maxScrollExtent == null) ? 1 : position.maxScrollExtent;
62 double diff = 1 - (index - ((offset / extent) * (total - 1))).abs();
63 double opacity = diff.clamp(0.2, 1);
64
65 return Content(
66 listenable: listenable,
67 opacity: opacity,
68 children: <Widget>[
69
70 Container(
71 constraints: BoxConstraints.expand(width: 400),
72 child: Column(
73
74 mainAxisAlignment: MainAxisAlignment.center,
75 crossAxisAlignment: CrossAxisAlignment.center,
76 children: <Widget>[
77 Text(item.name, style: _titleStyle),
78 Text(item.description, style: _descStyle)
79 ]
80 ),
81 ),
82 Container(
83 child: ClipRRect(
84
85 borderRadius: BorderRadius.all(Radius.circular(12)),
86 child: Container(
87
88 constraints: BoxConstraints.expand(width: 440, height: 440),
89 child: Image(image: item.image, fit: BoxFit.fitWidth),
90 )
91 ),
92 )
93 ]
94 );
95 }
96}
دو کلاس در این فایل وجود دارند که یکی کلاس Content است که اساساً ویجت کاربردی کوچکی است که برای جلوگیری از تودرتو شدن بیش از حد بلوکهای کد استفاده میشود. چون در این حالت همه چیز در پروژه فلاتر از کنترل خارج میشود. همچنین شامل خود کلاس Section است که بخشهای صفحه را به دست میآورد و یک کنترل شفافیت را به روشی مشابه جلوه اسکرول parallax در پسزمینه اجرا میکند.
سازنده کلاس Section اندیس آیتم جاری را میگیرد (موقعیت آن در لیست بخشها)، و همچنین مقدار total (تعداد بخشها)، item (تعریف بخش) و listenable (محرکه انیمیشن) را نیز دریافت میکند. این کلاس میزان مات بودن خود را بر اساس موقعیتش در لیست تعاریف بخش همراه با موقعیت کنونی اسکرول محاسبه میکند. هدف این است که مات بودن را بین 0.2 (برای آیتمی که هم اینک در لیست است) و 1.0 (برای آیتمی که به نما اسکرول میشود) تغییر دهیم. این وضعیت عملاً از کسر کردن عدد 1.0 (بیشینه مات بودن) از قدر مطلق مسافت بین آیتم و موقعیت کنونی اسکرول به دست میآید. بدین ترتیب آیتمها در هر دو سمت از نما خارج میشوند و مات بودن افزایش مییابد.
سخن پایانی
فلاتر برای وب هم اکنون در حال توسعه است، اما گوگل به شدت مشغول کار است تا آن را نیز مانند دیگر شاخههای iOS و اندروید در شاخه اصلی فلاتر ادغام کند. کدهای بررسی شده در این مقاله را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی جاوا
- مجموعه آموزشهای برنامهنویسی
- گوگل فلاتر (Flutter) از صفر تا صد — ساخت اپلیکیشن به کمک ویجت
- مفاهیم مقدماتی فلاتر (Flutter) — به زبان ساده
- استفاده از احراز هویت فایربیس با فلاتر — از صفر تا صد
==