آموزش گوگل فلاتر (Flutter ): ساخت اپلیکیشن دستورهای آشپزی — (بخش دوم)
ما در مقاله قبلی که در مورد آموزش گوگل فلاتر در بلاگ فرادرس منتشر ساختیم به معرفی «مفاهیم ابتدایی یک اپلیکیشن دستور آشپزی» پرداختیم. در آن راهنما با ویجتهای بیحالت، لیآوتها، قالبها و چگونگی ناوبری در Dart و فلاتر آشنا شدید. پیشنیاز مطالعه این سری مقالات آموزش فلاتر، داشتن اطلاعاتی اولیه از برنامهنویسی شیءگرا و دانش اندکی از خط فرمان است. اگر مقاله قبلی را مطالعه نکردهاید، بهتر است مطالعه این سری را از آنجا آغاز کنید.
در این مقاله قصد داریم با نماهای فهرستی (list views) آشنا شویم، یک مدل داده ایجاد کنیم و به کسب اعتماد به نفس بیشتر در پیادهسازی ویجتهای سفارشی با استفاده از قالبها بپردازیم.
نتیجه نهایی این بخش مقاله را میتوانید در این ریپو گیتهاب (+) ملاحظه کنید.
مدل داده
در نخستین گام باید یک مدل داده برای دستورهای آشپزی به عنوان یک کلاس Dart ایجاد کنیم. این مدل را Recipe مینامیم و فیلدهای id, type, name, duration, ingredients, preparation و imageURL را در آن پیادهسازی میکنیم، چون قصد داریم از آن برای ذخیرهسازی دستورهای آشپزی زیادی استفاده کنیم.
مانند همیشه باید ابتدا یک ذهنیت روشن از ساختار پروژه داشته باشیم. یک دایرکتوری به نام model در دایرکتوری lib پروژه ایجاد کنیم. در این دایرکتوری هر چیزی که در مورد مدلهای داده درون پروژه اهمیت دارد را قرار میدهیم.
import 'package:duration/duration.dart'; enum RecipeType { food, drink, } class Recipe { final String id; final RecipeType type; final String name; final Duration duration; final List<String> ingredients; final List<String> preparation; final String imageURL; const Recipe({ this.id, this.type, this.name, this.duration, this.ingredients, this.preparation, this.imageURL, }); }
ما از یک شمارش (enumeration) به نام RecipeType برای جلوگیری از بروز خطا روی وهلهسازی از شیءهای Recipe استفاده میکنیم. به علاوه در این مسیر مشخص میسازیم که چه نوع دادههایی منتظر پارامتر type در سازنده کلاس Recipe هستند.
تأمین دادهها
پیش از آن که به صورت عملی شروع به ساخت نمای فهرستی بکنیم به دادههایی برای دستور آشپزی نیاز داریم. برای ذخیرهسازی و دریافت دادهها در ادامه از Google Firestore استفاده خواهیم کرد. از آنجا که ما در این مرحله از پروژه روی رابط کاربری تمرکز داریم، تعدادی داده ساختگی تأمین میکنیم تا به پیادهسازی UI خود ادامه دهیم.
یک دایرکتوری به نام utils در دایرکتوری lib خود ایجاد کنید و کد زیر را در فایل جدیدی به نام store.dart در آن دایرکتوری قرار دهید. شما میتوانید دادههای دستورهای آشپزی مورد علاقه خود را در آن قرار دهید.
import 'package:recipes_app/model/recipe.dart'; List<Recipe> getRecipes() { return [ Recipe( id: '0', type: RecipeType.food, name: 'Oatmeal with Fruits', duration: Duration(minutes: 15), ingredients: [ '100g of oats', '100ml of milk', 'Fruits of your choice', 'Honey', 'Cinnamon', ], preparation: [ 'Step 1', 'Step 2', 'Step 3', ], imageURL: 'https://images.unsplash.com/photo-1517673400267-0251440c45dc?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f197f4922b3f26ed3f4e3e66a113b67b&auto=format&fit=crop&w=1050&q=80', ), Recipe( id: '1', type: RecipeType.food, name: 'Pancakes with Maple Syrup', duration: Duration(minutes: 20), ingredients: [ '2 eggs', '100ml of milk', '50g of flour', '10g of butter', 'Baking powder', 'Maple syrup', ], preparation: [ 'Step 1', 'Step 2', 'Step 3', ], imageURL: 'https://images.unsplash.com/photo-1517741991040-91338b176129?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f65c4032c1b3131f829d464fb979f5e8&auto=format&fit=crop&w=675&q=80', ), Recipe( id: '2', type: RecipeType.drink, name: 'Strawberry Juice', duration: Duration(minutes: 10), ingredients: [ '100g of strawberries', '500ml of water', '2 teaspoons of sugar', 'Juice of half a lemon', ], preparation: [ 'Step 1', 'Step 2', 'Step 3', ], imageURL: 'https://images.unsplash.com/photo-1506802913710-40e2e66339c9?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=c8ffc5bbb3719b5a46ee703acb0a0ac5&auto=format&fit=crop&w=634&q=80', ), Recipe( id: '3', type: RecipeType.drink, name: 'Blueberry Smoothie', duration: Duration(minutes: 10), ingredients: [ '100g of fresh blueberries', '200g of plain yoghurt', '100g of milk', '1 banana', ], preparation: [ 'Step 1', 'Step 2', 'Step 3', ], imageURL: 'https://images.unsplash.com/photo-1532301791573-4e6ce86a085f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=c0d9fe8ce9033db3a46ddf00bba92240&auto=format&fit=crop&w=1050&q=80', ), ]; } List<String> getFavoritesIDs() { return [ '0', '2', '3', ]; }
اکنون ما آماده هستیم که نمای فهرستی دستورهای آشپزی خود را پیادهسازی کنیم.
نمای فهرستی
برای این که یک فهرست قابل اسکرول ارائه کنیم که حاوی چندین ویجت سفارشی باشد، باید از کلاس ListView استفاده کنیم. اما قبل از آن باید ویجت بیحالت از قبل موجود HomeScreen خود را به ویجت باحالت تبدیل کنیم و از کلاسهای فلاتر به نام StatefulWidget و State استفاده نماییم. از آنجا که دادههایی که با آن سر و کار داریم، ممکن است در طی چرخه عمر ویجت HomeScreen تغییر یابند، پس StateWidget دیگر انتخاب مناسبی محسوب نمیشود. در ادامه نگاهی دقیقتر به ویجتهای باحالت خواهیم داشت.
ویجتهای باحالت
در صورتی که بدانیم ویجتی که قصد پیادهسازیاش را داریم باید از نوع دینامیک باشد، میبایست از یک ویجت باحالت به جای ویجتهای بیحالت استفاده کنیم. همان طور که از نام آن مشخص است ویجت باحالت دارای یک حالت است. این حالت میتواند در طی چرخه عمر ویجت تغییر یابد. چنین تغییراتی ممکن است از سوی کاربر که با اجزای رابط کاربری تعامل دارند ایجاد شوند. به محض این که حالت ویجت تغییر یابد، ویجت دوباره ترسیم میشود.
پیادهسازی ویجت باحالت در دو کلاس فرعی زیر صورت میگیرد:
- کلاس فرعی StatefulWidget - شامل تعریف ویجت است.
- کلاس فرعی State – ویجت را ساخته و حالت آن را تعیین میکند.
پیادهسازی
در این مرحله قصد داریم الگوی زیر را پیادهسازی کنیم.
ویجت HomeScreen ما قرار است ویجت والدی باشد که به مدیریت حالت فهرست دستورهای آشپزی و فهرست دستورهای محبوب بپردازد. این ویجت قرار است ویجت فعال ما باشد. اگر حالت تغییر یابد، این ویجت فعال است که به ویجتهای غیر فعال اعلام میکند بهروزرسانی شوند.
در انتهای این پیادهسازی، کلاس HomeScreen به مدیریت حالتی که ویجتهای RecipeCard بر مبنای آن طراحی شدهاند خواهد پرداخت. به علاوه این ویجت قرار است زمانی که کاربر فهرست دستورهای محبوب خود را در تعامل با رابط کاربری ویجت RecipeCard تغییرمی دهد، بهروزرسانی کند. با زدن روی دکمه favorites میتوانیم فهرست دستورهای محبوب را با متد handleFavoritesListChanged_ بهروزرسانی کنیم. کد آن را در ادامه مشاهده کنید.
این مراحل را به صورت گام به گام در ادامه انجام میدهیم. پیادهسازی ابتدایی نمای فهرستی به صورت زیر خواهد بود.
import 'package:flutter/material.dart'; import 'package:recipes_app/model/recipe.dart'; import 'package:recipes_app/utils/store.dart'; class HomeScreen extends StatefulWidget { @override State<StatefulWidget> createState() => new HomeScreenState(); } class HomeScreenState extends State<HomeScreen> { // New member of the class: List<Recipe> recipes = getRecipes(); List<String> userFavorites = getFavoritesIDs(); // New method: // Inactive widgets are going to call this method to // signalize the parent widget HomeScreen to refresh the list view. void _handleFavoritesListChanged(String recipeID) { // Set new state and refresh the widget: setState(() { if (userFavorites.contains(recipeID)) { userFavorites.remove(recipeID); } else { userFavorites.add(recipeID); } }); } @override Widget build(BuildContext context) { // New method: Column _buildRecipes(List<Recipe> recipesList) { return Column( children: <Widget>[ Expanded( child: ListView.builder( itemCount: recipesList.length, itemBuilder: (BuildContext context, int index) { return ListTile( title: Text(recipesList[index].name), ); }, ), ), ], ); } const double _iconSize = 20.0; return DefaultTabController( length: 4, child: Scaffold( appBar: PreferredSize( // We set Size equal to passed height (50.0) and infinite width: preferredSize: Size.fromHeight(50.0), child: AppBar( backgroundColor: Colors.white, elevation: 2.0, bottom: TabBar( labelColor: Theme.of(context).indicatorColor, tabs: [ Tab(icon: Icon(Icons.restaurant, size: _iconSize)), Tab(icon: Icon(Icons.local_drink, size: _iconSize)), Tab(icon: Icon(Icons.favorite, size: _iconSize)), Tab(icon: Icon(Icons.settings, size: _iconSize)), ], ), ), ), body: Padding( padding: EdgeInsets.all(5.0), child: TabBarView( // Replace placeholders: children: [ // Display recipes of type food: _buildRecipes(recipes .where((recipe) => recipe.type == RecipeType.food) .toList()), // Display recipes of type drink: _buildRecipes(recipes .where((recipe) => recipe.type == RecipeType.drink) .toList()), // Display favorite recipes: _buildRecipes(recipes .where((recipe) => userFavorites.contains(recipe.id)) .toList()), Center(child: Icon(Icons.settings)), ], ), ), ), ); } }
همان طور که میبینید ما از ListView.builder در متد قبلی buildRecipes_ استفاده کردهایم. سازنده builder برای کلاس ListView کاملاً شبیه حلقهها عمل میکند. این سازنده تعداد ویجتهایی که باید بسازد را در پارامتر itemsCount میگیرد تا بداند که builder چند بار باید ویجت را به نمای فهرستی اضافه کند.
شیء ListView به وسیله یک شیء Expanded پوشش یافته است تا از همه فضای موجود در ستون استفاده کند. نتیجه آن چنین است:
نتیجه قطعاً ظرفیت بهبود بیشتری دارد.
در ادامه قصد داریم یک ویجت Card سفارشی برای جایگزینی ListTile با آن بنویسیم و نمای فهرستی واکنشپذیری برای تعاملهای کاربران و ارائه ظاهری بهتر طراحی کنیم.
ویجت Card سفارشی
ویجت Card سفارشی ما باید شامل تصویر و اطلاعات اولیهای در مورد دستور آشپزی باشد. در مرحله بعدی قصد داریم از آن در نمای فهرستی که قرار است شامل چندین مورد از ویجتهای کارتی سفارشیمان باشد، استفاده کنیم.
پیادهسازی
در ادامه پیادهسازی ویجت RecipeCard را میبینید:
import 'package:flutter/material.dart'; import 'package:recipes_app/model/recipe.dart'; class RecipeCard extends StatelessWidget { final Recipe recipe; final bool inFavorites; final Function onFavoriteButtonPressed; RecipeCard( {@required this.recipe, @required this.inFavorites, @required this.onFavoriteButtonPressed}); @override Widget build(BuildContext context) { RawMaterialButton _buildFavoriteButton() { return RawMaterialButton( constraints: const BoxConstraints(minWidth: 40.0, minHeight: 40.0), onPressed: () => onFavoriteButtonPressed(recipe.id), child: Icon( // Conditional expression: // show "favorite" icon or "favorite border" icon depending on widget.inFavorites: inFavorites == true ? Icons.favorite : Icons.favorite_border, ), elevation: 2.0, fillColor: Colors.white, shape: CircleBorder(), ); } Padding _buildTitleSection() { return Padding( padding: EdgeInsets.all(15.0), child: Column( // Default value for crossAxisAlignment is CrossAxisAlignment.center. // We want to align title and description of recipes left: crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( recipe.name, ), // Empty space: SizedBox(height: 10.0), Row( children: [ Icon(Icons.timer, size: 20.0), SizedBox(width: 5.0), Text( recipe.duration.toString(), ), ], ), ], ), ); } return GestureDetector( onTap: () => print("Tapped!"), child: Padding( padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), child: Card( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // We overlap the image and the button by // creating a Stack object: Stack( children: <Widget>[ AspectRatio( aspectRatio: 16.0 / 9.0, child: Image.network( recipe.imageURL, fit: BoxFit.cover, ), ), Positioned( child: _buildFavoriteButton(), top: 2.0, right: 2.0, ), ], ), _buildTitleSection(), ], ), ), ), ); } }
برای این که در نهایت با کد آشفتهای مملو از پرانتز مواجه نشویم، از متدهای خصوصی buildTitleSectionـ و buildFavoriteButtonـ استفاده میکنیم. دقت کنید که چگونه یک حاشیه (padding) در متدهای buildTitleSectionـ و build ارائه کردهایم. ما به این منظور از ویجت padding استفاده کردهایم. خصوصیت padding در ویجت Padding شیءهای کلاس EdgeInsets را میپذیرد که کاربرد بسیار انعطافپذیری دارند:
- EdgeInsets.all – یک سازنده که یک حاشیه روی همه اضلاع ویجت کارت قرار میدهد (متدbuildTitleSectionـ)
- EdgeInsets.symmetric - یک سازنده که یک حاشیه افقی و عمودی درج میکند (متد build).
در مستندات رسمی فلاتر (+) اطلاعات بیشتری در مورد EdgeInsets میتوانید بیابید.
همه ویجتهایی که در متد build گنجانده شده بودند، در یک شیء GestureDetector (+) پوشش یافتهاند. بدین ترتیب میتوانیم همه نوع رویداد را روی این ویجتها شناسایی کنیم. در حال حاضر متد print را روی رویداد onTap فراخوانی میکنیم:
در نهایت ListTile را با در متد buildRecipes_ در ویجت HomeScreenState جایگزین میکنیم:
سلام وقت بخیر ، در نهایت اپلیکیشن ما شبیه اسکرین هایی که در ابتدا نشون دادید نمیشه چرا؟ جزئیات دستور غذا و دکمه ی لاگ اوت در تنظیمات رو نتونستم اوکی کنم 🙁