برنامه نویسی 720 بازدید

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

// ...
            child: ListView.builder(
              itemCount: recipesList.length,
              itemBuilder: (BuildContext context, int index) {
                // New code:
                return new RecipeCard(
                  recipe: recipesList[index],
                  inFavorites: userFavorites.contains(recipesList[index].id),
                  onFavoriteButtonPressed: _handleFavoritesListChanged,
                );
              },
            ),
// ...

نتیجه کار تا به اینجا چنین است:

می‌بینید که ظاهر اپلیکیشن ما بسیار بهبود یافته است؛ اما همچنان دو چیز هست که باید اصلاح کنیم:

  • حاشیه گذاری عمودی در ابتدا و انتهای نمای فهرستی خیلی کوچک است.
  • رشته مدت‌زمان دستورهای آشپزی که در ویجت‌های RecipeCard نمایش می‌یابد، چندان زیبا نیست.

ابتدا به اصلاح حاشیه‌بندی می‌پردازیم.

حاشیه‌بندی که در اپلیکیشن ما به وسیله RecipeCard پیاده‌سازی شده است برابر با 10.0 (نسبت پیکسل دستگاه) در سمت افقی و 5.0 در سمت عمودی است. نتیجه کار این است که 5.0 پیش و پس از ویجت ListView که در متد _buildRecipes در کلاس HomeScreen ایجاد شده مفقود است. اینجا حاشیه گذاری مفقود را به صورت زیر اضافه می‌کنیم:

// ...
    Padding _buildRecipes(List<Recipe> recipesList) { // New code
      return Padding( // New code
          // Padding before and after the list view:
          padding: const EdgeInsets.symmetric(vertical: 5.0), // New code
          child: Column(
            children: <Widget>[
              Expanded(
                child: ListView.builder(
                  itemCount: recipesList.length,
                  itemBuilder: (BuildContext context, int index) {
                    return new RecipeCard(
                      recipe: recipesList[index],
                      inFavorites:
                          userFavorites.contains(recipesList[index].id),
                      onFavoriteButtonPressed: _handleFavoritesListChanged,
                    );
                  },
                ),
              ),
            ],
          ),
        ); // New code
    }
// ...

اکنون به زیباسازی رشته مدت‌زمان دستور آشپزی می‌پردازیم. برای انجام این کار باید مراحل زیر را طی کنیم:

بسته duration را در بخش dependencies در فایل pubspec.yaml اضافه کنید تا بتوانید از این بسته به صورت یک کتابخانه در پروژه خود استفاده کنید:

dependencies:
  flutter:
    sdk: flutter
  duration: ^2.0.0 # New
# ...

یک متد getter اضافه کنید که از متد prettyDuration در کتابخانه Duration برای کلاس Recipe در recipe.dart استفاده کند:

// ...
String get getDurationString => prettyDuration(this.duration);
// ...

از getDurationString در متد buildTitleSectionـ در RecipeCard استفاده کنید:

// ...
                Icon(Icons.timer, size: 20.0),
                SizedBox(width: 5.0),
                Text(
                  recipe.getDurationString, // New code
                ),
// ...

از دستور flutter packages get روی خط فرمان در ریشه پروژه استفاده کنید تا بسته جدیدی را دریافت و اجرا کرده و اپلیکیشن را بارگذاری مجدد کنید تا نتیجه را مشاهده کنید:

قالب

در این مقاله کد جدید زیادی نوشتیم؛ اما هنوز به قالب اپلیکیشن خود توجه نکرده‌ایم. آیا از بخش قبلی این مورد را به خاطر دارید؟ ما می‌خواهیم کاری کنیم که اپلیکیشن‌مان ظاهر جذابی داشته باشد. بدین منظور قالب را گسترش می‌دهیم و با افزودن سبک‌های متنی و رنگ‌بندی، حس و حالی منحصر به فرد و شیک به آن می‌بخشیم. در ادامه پیاده‌سازی بسط یافته متد buildTheme را در theme.dart می‌بینید:

// ...

ThemeData buildTheme() {
  // We're going to define all of our font styles
  // in this method:
  TextTheme _buildTextTheme(TextTheme base) {
    return base.copyWith(
      headline: base.headline.copyWith(
        fontFamily: 'Merriweather',
        fontSize: 40.0,
        color: const Color(0xFF807A6B),
      ),
      // New code:
      // Used for the recipes' title:
      title: base.title.copyWith(
        fontFamily: 'Merriweather',
        fontSize: 15.0,
        color: const Color(0xFF807A6B),
      ),
      // Used for the recipes' duration:
      caption: base.caption.copyWith(
        color: const Color(0xFFCCC5AF),
      ),
    );
  }

  // We want to override a default light blue theme.
  final ThemeData base = ThemeData.light();

  // And apply changes on it:
  return base.copyWith(
    textTheme: _buildTextTheme(base.textTheme),
    // New code:
    primaryColor: const Color(0xFFFFF8E1),
    indicatorColor: const Color(0xFF807A6B),
    scaffoldBackgroundColor: const Color(0xFFF5F5F5),
    accentColor: const Color(0xFFFFF8E1),
    iconTheme: IconThemeData(
      color: const Color(0xFFCCC5AF),
      size: 20.0,
    ),
    buttonColor: Colors.white,
  );
}

در ادامه قصد داریم سبک‌های جدیدی را روی ویجت‌های خودمان به کار بگیریم. متد buildFavoriteButton_ را در کلاس RecipeCard به‌روزرسانی کنید:

// ...
    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,
          color: Theme.of(context).iconTheme.color, // New code
        ),
        elevation: 2.0,
        fillColor: Theme.of(context).buttonColor, // New code
        shape: CircleBorder(),
      );
    }
// ...

سپس سبک‌های متنی جدیدی را به متد buildTitleSectionـ اضافه می‌کنیم:

          children: <Widget>[
            Text(
              recipe.name,
              style: Theme.of(context).textTheme.title, // New code
            ),
            // Empty space:
            SizedBox(height: 10.0),
            Row(
              children: [
                Icon(Icons.timer, size: 20.0),
                SizedBox(width: 5.0),
                Text(
                  recipe.getDurationString,
                  // New code
                  style: Theme.of(context).textTheme.caption,
                ),
              ],
            ),
],

در گام آخر، انتساب Colors.white به خصوصیت backgroundColor در زمان ایجاد شیء AppBar در home.dart را حذف می‌کنیم. این انتساب باعث می‌شد که رنگ بخش‌های اصلی اپلیکیشن ما شامل نوار اپلیکیشن، که در مشخصات primaryColor در قالب خود تعریف می‌کنیم، لغو شوند.

می‌توانید به بررسی کلاس ThemeData (+) پرداخته و قالب را بر اساس ایده‌های خودتان تغییر دهید. اپلیکیشن ما اکنون چنین ظاهری یافته است.

سخن پایانی

در این مقاله مطالب زیادی را آموختیم که آن‌ها را در فهرست زیر می‌توانیم جمع‌بندی کنیم:

  • چگونگی ساخت نمای فهرستی با استفاده از ListView.builder.
  • دانستیم که ویجت‌های باحالت دینامیک هستند و شامل کلاس فرعی StatefulWidget و State هستند.
  • به‌روزرسانی حالت با استفاده از متد setState را آموختیم.
  • با چگونگی افزودن بسته‌ها در بخش dependencies فایل pubspec.yaml برای استفاده از آن‌ها به صورت کتابخانه آشنا شدیم.

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

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

بر اساس رای 2 نفر

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *