Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Flutter] Flutter Provider 看似簡單卻又不簡單的狀態管理工具

[Flutter] Flutter Provider 看似簡單卻又不簡單的狀態管理工具

當 App 開發到一定的規模時,就需要一個狀態管理框架與工具, 用 Provider 有什麼好處?要解決什麼問題? 用 Provider 可以大大減少冗餘的程式碼和複雜性? 用 Bloc 狀態管理還是可以一統 Flutter 的天下嗎? 在這場演講告訴您

#flutter #provider #bloc #development

Johnny Sung

November 25, 2023
Tweet

More Decks by Johnny Sung

Other Decks in Programming

Transcript

  1. Full stack developer Johnny Sung (宋岡諺) https://fb.com/j796160836 https://blog.jks.co ff ee/

    https://www.slideshare.net/j796160836 https://github.com/j796160836
  2. class FrogColor extends InheritedWidget { const FrogColor({ super.key, required this.color,

    required super.child, }); final Color color; static FrogColor? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<FrogColor>(); } static FrogColor of(BuildContext context) { final FrogColor? result = maybeOf(context); assert(result != null, 'No FrogColor found in context'); return result!; } @override bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color; } https://api. fl utter.dev/ fl utter/widgets/InheritedWidget-class.html 背後宣告
  3. // continuing from previous example... class MyPage extends StatelessWidget {

    const MyPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: FrogColor( color: Colors.green, child: Builder( builder: (BuildContext innerContext) { return Text( 'Hello Frog', style: TextStyle(color: FrogColor.of(innerContext).color), ); }, ), ), ); } } https://api. fl utter.dev/ fl utter/widgets/InheritedWidget-class.html 呼叫使⽤
  4. class FrogColor extends InheritedWidget { const FrogColor({ super.key, required this.color,

    required super.child, }); final Color color; static FrogColor? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<FrogColor>(); } static FrogColor of(BuildContext context) { final FrogColor? result = maybeOf(context); assert(result != null, 'No FrogColor found in context'); return result!; } @override bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color; } https://api. fl utter.dev/ fl utter/widgets/InheritedWidget-class.html (剛剛看到的段落)
  5. class Provider<T> extends InheritedProvider<T> { Provider({ Key? key, required Create<T>

    create, Dispose<T>? dispose, bool? lazy, TransitionBuilder? builder, Widget? child, }) : super( key: key, lazy: lazy, builder: builder, create: create, dispose: dispose, debugCheckInvalidValueType: kReleaseMode ? null : (T value) => Provider.debugCheckInvalidValueType?.call<T>(value), child: child, ); static T of<T>(BuildContext context, {bool listen = true}) { assert( context.owner!.debugBuilding || listen == false || debugIsInInheritedProviderUpdate, ''' Tried to listen to a value exposed with provider, from outside of the widget tree. … 略 ''', ); final inheritedElement = _inheritedElementOf<T>(context); if (listen) { context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>(); } final value = inheritedElement?.value; if (_isSoundMode) { if (value is! T) { throw ProviderNullException(T, context.widget.runtimeType); } return value; } return value as T; } } Provider 的原始碼
  6. Provider 家族 • Provider:提供遠端存取的服務 • ChangeNoti fi erProvider:它會監聽 model 的變化。當資料發⽣變更,它會重繪

    Consumer 的⼦ widget。 • FutureProvider:是 FutureBuilder 的⼀個封裝,提供⼀個 Future,FutureProvider 會在 Future 完成的時候 通知 Consumer 重繪它的 Widget ⼦樹。 • StreamProvider:是 StreamBuilder 的⼀個封裝,提供⼀個 Stream,然後 Consumer 會在 Stream 收到事 件時更新它的 Widget⼦樹。
  7. import 'package:flutter/foundation.dart'; class Counter with ChangeNotifier { int _count =

    0; int get count => _count; void increment() { _count++; notifyListeners(); } }
  8. import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'counter.dart'; 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: ChangeNotifierProvider<Counter>( create: (context) => Counter(), child: Builder(builder: (context) { final counter = context.watch<Counter>().count; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('$counter'), ElevatedButton( onPressed: () { final counter = context.read<Counter?>(); counter?.increment(); print('count=${counter?.count ?? 0}'); }, child: const Text('Push me')), ], ), ); }), ), ); } }
  9. Consumer<Counter>(builder: (context, myModel, child) { return Text('${myModel.count}'); }), 或 三種寫法都可以

    註:Consumer 的部份 Text('${context.watch<Counter>().count}') Text('${Provider.of<Counter>(context).count}') 或
  10. /// Exposes the [read] method. extension ReadContext on BuildContext {

    T read<T>() { return Provider.of<T>(this, listen: false); } } Provider 原始碼中 context.read() 對應到 Provider.of()
  11. import 'package:flutter/foundation.dart'; class TabProvider with ChangeNotifier { int _currentIndex =

    0; int get currentIndex => _currentIndex; set currentIndex(int index) { _currentIndex = index; notifyListeners(); } }
  12. mixin & with Mixins are a way of reusing a

    class’s code in multiple class hierarchies. 簡單來說就是: 
 我提供⼀些⽅法給你使⽤,但我不⽤成為你的⽗類別。 mixin ShowName { void showName() { print('mixin ShowName'); } } class Quadrilateral with ShowName { void showAllSideLength() { showName(); } } https://ithelp.ithome.com.tw/articles/10268368?sc=iThelpR
  13. import 'package:flutter/material.dart'; import 'package:flutter_provider_tab_demo/tab_provider.dart'; import 'package:flutter_provider_tab_demo/tab_screen.dart'; import 'package:provider/provider.dart'; void main()

    { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => TabProvider()), ], child: TabScreen(), ), ); } }
  14. import 'package:flutter/material.dart'; import 'package:flutter_provider_tab_demo/tab_provider.dart'; import 'package:provider/provider.dart'; class TabItem { final

    String name; final int index; TabItem(this.name, this.index); } class TabScreen extends StatelessWidget { final List<TabItem> tabItems = [ TabItem('Tab 1', 0), TabItem('Tab 2', 1), TabItem('Tab 3', 2), ]; TabScreen({super.key}); @override Widget build(BuildContext context) { // build } } return Scaffold( appBar: AppBar( title: const Text('Tab Example'), ), body: Column( children: [ ListView.builder( shrinkWrap: true, itemCount: tabItems.length, itemBuilder: (context, index) { TabItem tabItem = tabItems[index]; return ListTile( title: Text(tabItem.name), onTap: () { TabProvider p = context.read<TabProvider>(); p.currentIndex = tabItem.index; }, ); }, ), Expanded( child: IndexedStack( index: context.watch<TabProvider>().currentIndex, children: const [ Center(child: Text('Tab 1')), Center(child: Text('Tab 2')), Center(child: Text('Tab 3')), ], ), ), ], ), ); 選單按鈕 分⾴內⾴
  15. MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home:

    BlocProvider<SigninCubit>( create: (context) => SigninCubit(), child: BlocProvider<UpdateCubit>( create: (context) => UpdateCubit(), child: BlocProvider<ProfileBloc>( create: (context) => ProfileBloc(), child: BlocProvider<VoteBloc>( create: (context) => VoteBloc(), child: BlocProvider<MessageBloc>( create: (context) => MessageBloc(), child: BlocProvider<LeaderboardBloc>( create: (context) => LeaderboardBloc(), child: BlocProvider<CommentBloc>( create: (context) => CommentBloc(), child: BlocProvider<EditbioCubit>( create: (context) => EditbioCubit(), child: BlocProvider<CommentCubit>( create: (context) => CommentCubit(), child: BlocProvider<SearchBloc>( create: (context) => SearchBloc(), child: BlocProvider<SwipeBloc>( create: (context) => SwipeBloc(), child: Scaffold( body: Builder(builder: (context) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Sample'), ], ), ); }), ), ), ), ), ), ), ), ), ), ), ), ), ); 不免俗的,Flutter 沒寫好的話,很容易寫出⼀堆波動拳 https://captown.capcom.com/zh-TW/museums/street fi ghter/5/images/125#group=0&photo=0
  16. 部分的波動拳可以⽤ MultiXXXProvider 解決部分的問題 但這範例還有權責不明確問題 MaterialApp( title: 'Flutter Demo', theme: ThemeData(

    primarySwatch: Colors.blue, ), home: MultiBlocProvider( providers: [ BlocProvider<SigninCubit>(create: (context) => SigninCubit()), BlocProvider<UpdateCubit>(create: (context) => UpdateCubit()), BlocProvider<ProfileBloc>(create: (context) => ProfileBloc()), BlocProvider<VoteBloc>(create: (context) => VoteBloc()), BlocProvider<MessageBloc>(create: (context) => MessageBloc()), BlocProvider<LeaderboardBloc>(create: (context) => LeaderboardBloc()), BlocProvider<CommentBloc>(create: (context) => CommentBloc()), BlocProvider<EditbioCubit>(create: (context) => EditbioCubit()), BlocProvider<CommentCubit>(create: (context) => CommentCubit()), BlocProvider<SearchBloc>(create: (context) => SearchBloc()), BlocProvider<SwipeBloc>(create: (context) => SwipeBloc()), ], child: Scaffold( body: Builder(builder: (context) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Sample'), ], ), ); }), ), ), );
  17. BlocProvider<LoginBloc>( create: (BuildContext context) => LoginBloc()), child: …, ), 在

    Widget Tree 最頂層處使⽤ BlocProvider 並宣告⾃訂的 Bloc (以下是⼀些 pseudo code) BlocProvider 使⽤⽅式 (1/4)
  18. BlocBuilder<LoginBloc, LoginState>(builder: (context, state) { if (state is LoginStateLoggedIn) {

    return …; } if (state is LoginStateInitial) { return …; } return Container(); } 在所需 Widget 的 build() 處使⽤ BlocBuilder (以下是⼀些 pseudo code) BlocProvider 使⽤⽅式 (2/4) 這裡沒指定 bloc 參數時,預設會 Provider 往上找
  19. 在需要聆聽事件的地⽅使⽤ BlocListener (以下是⼀些 pseudo code) BlocProvider 使⽤⽅式 (3/4) 這裡沒指定 bloc

    參數時,預設會 Provider 往上找 BlocListener<LoginBloc, LoginState>( listener: (context, state) { if (state is LoginStateError) { // Do Something } if (state is LoginStateLoggedIn) { // Do Something } }, child: … );
  20. 在需要變更狀態的地⽅使⽤ context.read() 並操作 Bloc (以下是⼀些 pseudo code) BlocProvider 使⽤⽅式 (4/4)

    ElevatedButton( child: const Text('Login'), onPressed: () { context.read<LoginBloc>().add(LoginEventLogin()); }, )
  21. Q&A