# InheritedWidget vs Provider ### InheritedWidget 是一個可以讓資料可以在 widget tree 中向下傳遞、共享的 widget,因為他是 widget,所以可以在任意節點中插入此 `InheritedWidget`,在這個底下的所有 widget 都可以訪問到這個 `InheritedWidget` 內的資料。 * 通過 `context.dependOnInheritedWidgetOfExactType<T>` 取得父節點中相同類型的最近節點並返回,如果找不到相同類型的節點,返回 null。 * `updateShouldNotify` 定義資料改動的時機並發送通知給觀察者 #### Usage * 定義 `ShareDataWidget` 並繼承 `InheritedWidget`。 * 定義靜態方法 `of` 用來查找此物件。 * 在 `oldWidget.count != count`,count 數量改變時時發送通知。 ``` import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class ShareDataWidget extends InheritedWidget { const ShareDataWidget({ Key? key, required this.selectedColorIndex, required this.selectedSizeIndex, required Widget child, }) : super(key: key, child: child); final int selectedColorIndex; final int selectedSizeIndex; static ShareDataWidget? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>(); } @override bool updateShouldNotify(ShareDataWidget oldWidget) { return oldWidget.selectedColorIndex != selectedColorIndex; } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int selectedColorIndex = -1; int selectedSizeIndex = -1; @override Widget build(BuildContext context) { return ShareDataWidget( selectedColorIndex: selectedColorIndex, selectedSizeIndex: selectedSizeIndex, child: Scaffold( body: SizedBox( width: double.infinity, height: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('顏色', style: TextStyle(color: Colors.black, fontSize: 16)), const SizedBox(width: 10), Container(color: const Color(0x19333333), height: 18, width: 1), const SizedBox(width: 5), SingleSelectColor( colorList: const [Colors.green, Colors.deepOrange], onSelect: (selectedColorIndex) { setState(() { this.selectedColorIndex = selectedColorIndex; selectedSizeIndex = -1; }); }, ), ], ), const SizedBox(height: 15), Row( children: [ const Text('尺寸', style: TextStyle(color: Colors.black, fontSize: 16)), const SizedBox(width: 10), Container(color: const Color(0x19333333), height: 18, width: 1), const SizedBox(width: 10), SingleSelectSize( sizeList: const ['S', 'M', 'L'], onSelect: (index) { setState(() { selectedSizeIndex = index; }); }, ), ], ), ], ), ], ), ), ), ); } } class SingleSelectColor extends StatelessWidget { final List<MaterialColor> colorList; final Function(int) onSelect; const SingleSelectColor({super.key, required this.colorList, required this.onSelect}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate( colorList.length, (int index) { return Row( children: [ OutlinedButton( onPressed: () { onSelect(index); }, style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, minimumSize: MaterialStateProperty.all(const Size(40, 40)), padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.all(0)), side: MaterialStateProperty.all(BorderSide(color: getColor(context, index), width: 1.0, style: BorderStyle.solid)), shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), ), child: Container(color: colorList[index], height: 18, width: 18)), ], ); }, ), ); } Color getColor(BuildContext context, int index) { if (ShareDataWidget.of(context) == null) { return Colors.transparent; } else { return ShareDataWidget.of(context)!.selectedColorIndex == index ? Colors.black : Colors.transparent; } } } class SingleSelectSize extends StatelessWidget { final List<String> sizeList; final Function(int) onSelect; final grey = const Color.fromARGB(255, 87, 99, 108); final white = Colors.white; const SingleSelectSize({super.key, required this.sizeList, required this.onSelect}); @override Widget build(BuildContext context) { Color getBackgroundColor(int index) { if (ShareDataWidget.of(context) == null) { return white; } else { return ShareDataWidget.of(context)!.selectedSizeIndex == index ? white : grey; } } Color getTextColor(int index) { if (ShareDataWidget.of(context) == null) { return white; } else { return ShareDataWidget.of(context)!.selectedSizeIndex == index ? grey : white; } } return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate( sizeList.length, (int index) { return Row( children: [ ElevatedButton( onPressed: () { onSelect(index); }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.only(top: 3, bottom: 3, left: 10, right: 10), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: getBackgroundColor(index), ), child: Text(sizeList[index], style: TextStyle(color: getTextColor(index), fontSize: 12)), ), const SizedBox(width: 10), ], ); }, ), ); } } ``` ### Provider 類似觀察者模式,具有觀察者和被觀察者,widget 透過觀察者模式觀察數據,一旦資料來源變動,widget 會自動重新渲染,更新介面。 #### Usage * 定義 `MainViewModel` 並繼承 `ChangeNotifier`,裡面存在需要變更的數據,在資料變更後呼叫 `notifyListeners()` 發送通知。 * 使用 `ChangeNotifierProvider` 創建 `MainViewModel`。 * 使用 `Consumer` 取得 `MainViewModel` 內的資料。 ``` import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'main_view_model.dart'; void main() { runApp(ChangeNotifierProvider( create: (context) => MainViewModel(), child: const MyApp(), )); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( body: SizedBox( width: double.infinity, height: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('顏色', style: TextStyle(color: Colors.black, fontSize: 16)), const SizedBox(width: 10), Container(color: const Color(0x19333333), height: 18, width: 1), const SizedBox(width: 5), const SingleSelectColor( colorList: [Colors.green, Colors.deepOrange], ), ], ), const SizedBox(height: 15), Row( children: [ const Text('尺寸', style: TextStyle(color: Colors.black, fontSize: 16)), const SizedBox(width: 10), Container(color: const Color(0x19333333), height: 18, width: 1), const SizedBox(width: 10), const SingleSelectSize( sizeList: ['S', 'M', 'L'], ), ], ), ], ), ], ), ), ); } } class SingleSelectColor extends StatelessWidget { final List<MaterialColor> colorList; const SingleSelectColor({super.key, required this.colorList}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate( colorList.length, (int index) { return Consumer<MainViewModel>( builder: (context, cartModel, child) => Row( children: [ OutlinedButton( onPressed: () { context.read<MainViewModel>().onColorSelected(index); }, style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, minimumSize: MaterialStateProperty.all(const Size(40, 40)), padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.all(0)), side: MaterialStateProperty.all(BorderSide(color: getColor(index, cartModel.selectedColorIndex), width: 1.0, style: BorderStyle.solid)), shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), ), child: Container(color: colorList[index], height: 18, width: 18)), ], ), ); }, ), ); } Color getColor(int index, int selectedColorIndex) { return selectedColorIndex == index ? Colors.black : Colors.transparent; } } class SingleSelectSize extends StatelessWidget { final List<String> sizeList; final grey = const Color.fromARGB(255, 87, 99, 108); final white = Colors.white; const SingleSelectSize({super.key, required this.sizeList}); @override Widget build(BuildContext context) { Color getBackgroundColor(int index, int selectedSizeIndex) { return selectedSizeIndex == index ? white : grey; } Color getTextColor(int index, int selectedSizeIndex) { return selectedSizeIndex == index ? grey : white; } return Consumer<MainViewModel>( builder: (context, cartModel, child) => Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List<Widget>.generate( sizeList.length, (int index) { return Row( children: [ ElevatedButton( onPressed: () { context.read<MainViewModel>().onSizeSelected(index); }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.only(top: 3, bottom: 3, left: 10, right: 10), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: getBackgroundColor(index, cartModel.selectedSizeIndex), ), child: Text(sizeList[index], style: TextStyle(color: getTextColor(index, cartModel.selectedSizeIndex), fontSize: 12)), ), const SizedBox(width: 10), ], ); }, ), ) ); } } ``` ``` import 'package:flutter/cupertino.dart'; class MainViewModel extends ChangeNotifier { int selectedColorIndex = -1; int selectedSizeIndex = -1; void onColorSelected(int index) { selectedColorIndex = index; selectedSizeIndex = -1; notifyListeners(); } void onSizeSelected(int index) { selectedSizeIndex = index; notifyListeners(); } } ```