مدیریت state برنامه در flutter به کمک provider 4/5 (1)

0
35
flutter-banner

فرض کنید می‌خواهیم یک برنامه فروشگاهی با flutter بنویسیم. این برنامه دارای دو صفحه جداگانه است. یک صفحه برای نمایش لیست آیتم‌ها(MyCatalog) داریم که می‌توان هر آیتم را به سبد خرید اضافه کنیم. صفحه‌ی لیست آیتم‌ها از دو بخش AppBar سفارشی(MyAppBar)  و لیست اقلام فروشگاه به نام MyListItems تشکیل شده است. صفحه‌ی دیگر برنامه MyCard نام دارد که موارد انتخاب شده را درون سبد خرید نمایش می‌دهد. ساختار درخت ویجت این برنامه مانند شکل زیر است:

simple-widget-tree

بنابراین مطابق تصویر بالا ما حداقل ۵ زیر کلاس از ویجت‌ها داریم. بسیاری از آنها نیاز به وضعیتی(state) دارند که “متعلق” به جای دیگری است تا به درستی کار کنند. برای مثال، هر MyListItem باید بتواند خود را به سبد خرید(MyCart) اضافه کند. همچنین ممکن است بخواهید ببینید که آیا مورد نمایش داده شده در MyListItems در حال حاضر در سبد خرید وجود دارد یا خیر.

این موضوع ما را به اولین سوال می برد: وضعیت فعلی سبد را در کجا باید قرار دهیم؟


روش کشیدن وضعیت به بالا

در Flutter، منطقی است که وضعیت را بالاتر از ویجت هایی که از آن استفاده می کنند نگه داریم. چرا؟ زیرا در فریمورک های اعلامی مانند Flutter، اگر می خواهید رابط کاربری را تغییر دهید، باید آن را دوباره بسازید. هیچ راه آسانی برای داشتن MyCart.updateWith (somethingNew) وجود ندارد. به عبارت دیگر، تغییر ضروری یک ویجت از خارج، با فراخوانی متدی روی آن، دشوار است. و حتی اگر بتوانید این کار را انجام دهید، به جای اینکه اجازه دهید فریم‌ورک به شما کمک کند، با آن مبارزه می‌کنید. به نمونه کد نادرست زیر دقت کنید:

// BAD: DO NOT DO THIS
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

حتی اگر کد بالا را وارد کنید، باید با موارد زیر در ویجت MyCart مقابله کنید:

// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
  return SomeWidget(
    // The initial state of the cart.
  );
}

void updateWith(Item item) {
  // Somehow you need to change the UI from here.
}

شما باید وضعیت فعلی UI را در نظر بگیرید و داده های جدید را روی آن اعمال کنید. اجتناب از بروز اشکالات در این روش دشوار است.

در Flutter، هر بار که محتوای ویجت تغییر می کند، یک ویجت جدید می سازید. به جای MyCart.updateWith(somethingNew) (یک متد فراخوانی) از MyCart(contents) (یک سازنده) استفاده می کنید. در واقع از آنجایی که شما فقط می‌توانید ویجت‌های جدید را در متد build والدین آنها بسازید، اگر می‌خواهید محتوا را تغییر دهید، ,وضعیت را باید در والد MyCart یا بالاتر از آن قرار دهید. نمونه کد زیر حالت درست قرار گیری وضعیت را نشان می‌دهد.

// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}

اکنون MyCart تنها یک مسیر کد برای ساخت هر نسخه از UI دارد.

// GOOD
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // Just construct the UI once, using the current state of the cart.
    // ···
  );
}

در مثال ما، محتوا باید در MyApp قرار گیرد. هر زمان که محتوا تغییر می‌کند، MyCart را از بالا بازسازی می کند (در ادامه در مورد آن بیشتر یاد میگیرید). به همین دلیل، MyCart نیازی به نگرانی در مورد چرخه عمر ندارد – فقط اعلام می کند که برای هر محتوایی چه چیزی را نشان دهد. هنگامی که وضعیت تغییر می کند، ویجت MyCart قدیمی ناپدید می شود و به طور کامل با ویجت جدید جایگزین می شود.

simple-widget-tree-with-cart

وقتی می گوییم ویجت ها تغییر ناپذیر هستند، منظور ما همین است. آنها تغییر نمی کنند – آنها جایگزین می شوند.

اکنون که می دانیم وضعیت سبد را در کجا قرار دهیم، بیایید نحوه دسترسی به آن را ببینیم.


دسترسی به وضعیت

هنگامی که کاربر روی یکی از موارد موجود در کاتالوگ کلیک می کند، به سبد خرید اضافه می شود. اما از آنجایی که سبد خرید بالاتر از MyListItem قرار دارد، چگونه این کار را انجام دهیم؟

یک گزینه ساده این است که یک callback قرار دهید که MyListItem می تواند با کلیک روی آن تماس برقرار کند. توابع دارت اشیاء درجه یک هستند، بنابراین می توانید آنها را به هر طریقی که می خواهید منتقل کنید. بنابراین، در MyCatalog می توانید موارد زیر را تعریف کنید:

@override
Widget build(BuildContext context) {
  return SomeWidget(
    // Construct the widget, passing it a reference to the method above.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}

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

خوشبختانه، Flutter مکانیسم‌هایی برای ویجت‌ها دارد که داده‌ها و خدماتی را به فرزندانشان ارائه می‌کنند (به عبارت دیگر، نه فقط فرزندانشان، بلکه هر ویجت زیر آنها). همانطور که از Flutter انتظار دارید، جایی که همه چیز یک ویجت است، این مکانیسم‌ها فقط انواع خاصی از ویجت‌ها هستند – InheritedWidget، InheritedNotifier، InheritedModel و غیره. اما آنها را در این آموزش پوشش نمی دهیم، زیرا آنها برای کاری که ما می خواهیم انجام دهیم کمی سطح پایین هستند و یادگیری آن‌ها کار را کمی دشوار می‌کند.

در عوض، ما از بسته‌ای استفاده می‌کنیم که با ویجت‌های سطح پایین کار می‌کند و استفاده از آن آسان است.نام این بسته provider است.

قبل از کار با provider، فراموش نکنید که وابستگی به آن را به فایل pubspec.yaml پروژه‌ی خود اضافه کنید.

name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0.0

dev_dependencies:
  # ...

 

اکنون می توانید ‘package:provider/provider.dart’ را به پروژه‌ی خود import  کنید و شروع به ساختن پروژه کنید.

با provider، نیازی نیست نگران تماس‌های برگشتی یا InheritedWidget ها باشید. در عوض شما باید ۳ مفهوم را درک کنید:

  1. ChangeNotifier
  2. ChangeNotifierProvider
  3. Consumer

ChangeNotifier

ChangeNotifier یک کلاس ساده است که در Flutter SDK گنجانده شده است و برای کار با آن هیچ پکیجی نیاز نیست. این کلاس اعلان تغییر را به شنوندگان خود ارائه می دهد. به عبارت دیگر، اگر کلاسی از نوع ChangeNotifier است، می توانید در تغییرات آن مشترک شوید. (این یک شکل از Observable برای کسانی است که با این اصطلاح آشنا هستند. چیزی شبیه الگوی برنامه نویسی Observer)

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

در مثال برنامه خرید خود، می‌خواهیم وضعیت سبد خرید را در ChangeNotifier مدیریت کنیم. ما یک کلاس جدید ایجاد می کنیم که آن را گسترش(extends) می دهد، مانند:

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

تنها کدی که مختص کلاس ChangeNotifier است فراخوانی به notifyListeners() است. هر زمان که مدل به گونه ای تغییر کرد که ممکن است رابط کاربری برنامه شما تغییر کند، این متد را فراخوانی کنید. هر چیز دیگری در کلاس CartModel مربوط به مدل و منطق آن است.

ChangeNotifier بخشی از flutter:foundation است و به هیچ کلاس سطح بالاتری در Flutter وابسته نیست. به راحتی قابل آزمایش است (حتی نیازی به استفاده از تست ویجت برای آن ندارید). برای مثال، در اینجا یک تست واحد ساده از CartModel آمده است:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});

ChangeNotifierProvider

ChangeNotifierProvider ویجتی است که نمونه ای از ChangeNotifier را در اختیار فرزندان خود قرار می دهد و از بسته provider می آید. ما قبلاً می دانیم که ChangeNotifierProvider را در بالای ویجت هایی که باید به آن دسترسی داشته باشند، قرار دهیم. در مورد CartModel، جایی بالاتر از MyCart و MyCatalog مکان مناسبی خواهد بود.

همچنین باید دقت کنید که شما نباید ChangeNotifierProvider را بالاتر از حد لازم قرار دهید (زیرا نمی خواهید محدوده وسیع‌تری را آلوده کنید). اما در مورد مثال ما، تنها ویجتی که در بالای MyCart و MyCatalog قرار دارد، MyApp است.

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

توجه داشته باشید که ما سازنده ای را تعریف می کنیم که نمونه جدیدی از CartModel را ایجاد می کند. ChangeNotifierProvider به اندازه کافی هوشمند است که CartModel را بازسازی نکند مگر اینکه کاملا ضروری باشد. همچنین زمانی که دیگر به این مورد نمونه نیاز نیست، به طور خودکار dispose() را در CartModel فراخوانی می کند.

اگر می خواهید بیش از یک کلاس ارائه دهید، می توانید از MultiProvider استفاده کنید:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}

Consumer

اکنون که CartModel از طریق ChangeNotifierProvider در بالا به ویجت های برنامه ما ارائه شده است، می توانیم استفاده از آن را شروع کنیم.

این کار از طریق ویجت Consumer انجام می شود.

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

باید نوع مدلی که می خواهیم به آن دسترسی داشته باشیم را مشخص کنیم. در این حالت ما CartModel را می خواهیم، ​​بنابراین Consumer<CartModel> را می نویسیم. اگر نوع (<CartModel>) را مشخص نکنید، بسته provider نمی تواند به شما کمک کند. زیرا provider بر اساس نوع کار می‌کند و بدون نوع، نمی داند شما چه می خواهید.

تنها آرگومان مورد نیاز ویجت Consumer سازنده است. Builder تابعی است که هر زمان که ChangeNotifier تغییر کند فراخوانی می شود. (به عبارت دیگر، هنگامی که در مدل خود notifyListeners() را فراخوانی می کنید، تمام متدهای سازنده تمامی ویجت های Consumer مربوطه فراخوانی می شوند.)

دقت کنید که سازنده‌ی Consumer با سه آرگومان فراخوانی می شود. اولین مورد context است که در هر متد ساخت آن را نیز دریافت می کنید.

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

آرگومان سوم فرزند است که برای بهینه سازی وجود دارد. اگر یک زیردرخت ویجت بزرگ در زیر Consumer خود دارید که با تغییر مدل تغییر نمی کند، می توانید یک بار آن را بسازید و از طریق سازنده دریافت کنید.

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text("Total price: ${cart.totalPrice}"),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

بهترین راهکار این است که ویجت های Consumer خود را تا حد امکان در عمق درخت قرار دهید. شما نمی خواهید فقط به این دلیل که برخی از جزئیات در جایی تغییر کرده است، بخش های بزرگی از UI را بازسازی کنید.

// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

به جای آن از روش درست زیر استفاده کنید:

// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);

Provider.of

گاهی اوقات، برای تغییر رابط کاربری به داده های مدل نیازی ندارید، اما همچنان باید به آن دسترسی داشته باشید. به عنوان مثال، یک دکمه ClearCart می خواهد به کاربر اجازه دهد همه چیز را از سبد خرید حذف کند. نیازی به نمایش محتویات سبد خرید نیست، فقط باید متد clear() را فراخوانی کند.

ما می‌توانیم از Consumer<CartModel> برای این کار استفاده کنیم، اما این کار بیهوده خواهد بود. ما از فریم‌ورک می خواهیم ویجتی را بازسازی کند که نیازی به بازسازی ندارد.

برای این مورد، می‌توانیم از Provider.of با پارامتر listen روی false استفاده کنیم.

Provider.of<CartModel>(context, listen: false).removeAll();

استفاده از خط بالا در متد ساخت باعث نمی شود که این ویجت در هنگام فراخوانی notifyListeners بازسازی شود.

به این مطلب امتیاز بدهید

ارسال یک پاسخ

لطفا دیدگاه خود را وارد کنید!
لطفا نام خود را در اینجا وارد کنید