基于Reacthooks实现的Flutterhooks。Flutterhooks用于管理FlutterWidgeet。有利于增加小部件之间的代码共享,可以代替StatefulWidget。
FlutterHooksAflutterimplementationofReacthooks:https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889
HooksareanewkindofobjectthatmanagesaWidgetlife-cycles.Theyexistforonereason:increasethecodesharingbetweenwidgetsandasacompletereplacementforStatefulWidget.
MotivationStatefulWidgetsufferfromabigproblem:itisverydifficulttoreusethelogicofsayinitStateordispose.AnobviousexampleisAnimationController:
class Example extends StatefulWidget { final Duration duration; const Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); @override _ExampleState createState() => _ExampleState();}class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller.duration = widget.duration; } } @override void dispose() { super.dispose(); _controller.dispose(); } @override Widget build(BuildContext context) { return Container(); }}AllwidgetsthatdesiretouseanAnimationControllerwillhavetoreimplementalmostofallthisfromscratch,whichisofcourseundesired.
Dartmixinscanpartiallysolvethisissue,buttheysufferfromotherproblems:
Agivenmixincanonlybeusedonceperclass.
Mixinsandtheclasssharesthesameobject.Thismeansthatiftwomixinsdefineavariableunderthesamename,theendresultmayvarybetweencompilationfailtounknownbehavior.
Thislibraryproposeathirdsolution:
class Example extends HookWidget { final Duration duration; const Example({Key key, @required this.duration}) : assert(duration != null), super(key: key); @override Widget build(BuildContext context) { final controller = useAnimationController(duration: duration); return Container(); }}Thiscodeisstrictlyequivalenttothepreviousexample.ItstilldisposestheAnimationControllerandstillupdatesitsdurationwhenExample.durationchanges.Butyou'reprobablythinking:
Wheredidallthelogicgo?
ThatlogicmovedintouseAnimationController,afunctionincludeddirectlyinthislibrary(seehttps://github.com/rrousselGit/flutter_hooks#existing-hooks).ItiswhatwecallaHook.
Hooksareanewkindofobjectswithsomespecificities:
TheycanonlybeusedinthebuildmethodofaHookWidget.
ThesamehookisreusableaninfinitenumberoftimesThefollowingcodedefinestwoindependentAnimationController,andtheyarecorrectlypreservedwhenthewidgetrebuild.
Widget build(BuildContext context) { final controller = useAnimationController(); final controller2 = useAnimationController(); return Container();}Hooksareentirelyindependentofeachotherandfromthewidget.Whichmeanstheycaneasilybeextractedintoapackageandpublishedonpubforotherstouse.
PrincipleSimilarilytoState,hooksarestoredontheElementofaWidget.ButinsteadofhavingoneState,theElementstoresaList<Hook>.ThentouseaHook,onemustcallHook.use.
Thehookreturnedbyuseisbasedonthenumberoftimesithasbeencalled.Thefirstcallreturnsthefirsthook;thesecondcallreturnsthesecondhook,thethirdreturnsthethirdhook,...
Ifthisisstillunclear,anaiveimplementationofhooksisthefollowing:
class HookElement extends Element { List<HookState> _hooks; int _hookIndex; T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this); @override performRebuild() { _hookIndex = 0; super.performRebuild(); }}Formoreexplanationofhowtheyareimplemented,here'sagreatarticleabouthowtheydiditinReact:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
RulesDuetohooksbeingobtainedfromtheirindex,therearesomerulesthatmustberespected:
DOcalluseunconditionallyWidget build(BuildContext context) { Hook.use(MyHook()); // ....}DON'TwrapuseintoaconditionWidget build(BuildContext context) { if (condition) { Hook.use(MyHook()); } // ....}DOalwayscallallthehooks:Widget build(BuildContext context) { Hook.use(Hook1()); Hook.use(Hook2()); // ....}DON'Tabortsbuildmethodbeforeallhookshavebeencalled:Widget build(BuildContext context) { Hook.use(Hook1()); if (condition) { return Container(); } Hook.use(Hook2()); // ....}Abouthot-reloadSincehooksareobtainedfromtheirindex,onemaythinkthathot-reloadwhilerefactoringwillbreaktheapplication.
Butworrynot,HookWidgetoverridesthedefaulthot-reloadbehaviortoworkwithhooks.Still,therearesomesituationsinwhichthestateofaHookmaygetreset.
Considerthefollowinglistofhooks:
Hook.use(HookA());Hook.use(HookB(0));Hook.use(HookC(0));Thenconsiderthatafterahot-reload,weeditedtheparameterofHookB:
Hook.use(HookA());Hook.use(HookB(42));Hook.use(HookC());Hereeverythingworksfine;allhookskeeptheirstates.
NowconsiderthatweremovedHookB.Wenowhave:
Hook.use(HookA());Hook.use(HookC());Inthissituation,HookAkeepsitsstatebutHookCgetsahardreset.Thishappensbecausewhenarefactoringisdone,allhooksafterthefirstlineimpactedaredisposed.SinceHookCwasplacedafterHookB,isgotdisposed.
HowtouseTherearetwowaystocreateahook:
Afunction
Functionsisbyfarthemostcommonwaytowriteahook.Thankstohooksbeingcomposablebynature,afunctionwillbeabletocombineotherhookstocreateacustomhook.Byconventionthesefunctionswillbeprefixedbyuse.
Thefollowingdefinesacustomhookthatcreatesavariableandlogsitsvalueontheconsolewheneverthevaluechanges:
ValueNotifier<T> useLoggedState<T>(BuildContext context, [T initialData]) { final result = useState<T>(initialData); useValueChanged(result.value, (_, __) { print(result.value); }); return result;}Aclass
Whenahookbecomestoocomplex,itispossibletoconvertitintoaclassthatextendsHook,whichcanthenbeusedusingHook.use.Asaclass,thehookwilllookverysimilartoaStateandhaveaccesstolife-cyclesandmethodssuchasinitHook,disposeandsetState.Itisusuallyagoodpracticetohidetheclassunderafunctionassuch:
Result useMyHook(BuildContext context) { return Hook.use(_MyHook());}ThefollowingdefinesahookthatprintsthetimeaStatehasbeenalive.
class _TimeAlive<T> extends Hook<void> { const _TimeAlive(); @override _TimeAliveState<T> createState() => _TimeAliveState<T>();}class _TimeAliveState<T> extends HookState<void, _TimeAlive<T>> { DateTime start; @override void initHook() { super.initHook(); start = DateTime.now(); } @override void build(BuildContext context) { // this hook doesn't create anything nor uses other hooks } @override void dispose() { print(DateTime.now().difference(start)); super.dispose(); }}ExistinghooksFlutter_hookscomeswithalistofreusablehooksalreadyprovided.Theyarestaticmethodsfreetousethatincludes:
useEffect
Usefultotriggersideeffectsinawidgetanddisposeobjects.Ittakesacallbackandcallsitimmediately.Thatcallbackmayoptionallyreturnafunction,whichwillbecalledwhenthewidgetisdisposed.
Bydefault,thecallbackiscalledoneverybuild,butitispossibletooverridethatbehaviorbypassingalistofobjectsasthesecondparameter.Thecallbackwillthenbecalledonlywhensomethinginsidethelisthaschanged.
ThefollowingcalltouseEffectsubscribestoaStreamandcancelthesubscriptionwhenthewidgetisdisposed:
Stream stream;useEffect(() { final subscribtion = stream.listen(print); // This will cancel the subscription when the widget is disposed // or if the callback is called again. return subscription.cancel; }, // when the stream change, useEffect will call the callback again. [stream],);useState
Defines+watchavariableandwheneverthevaluechange,callssetState.
ThefollowingcodeusesuseStatetomakeacounterapplication:
class Counter extends HookWidget { @override Widget build(BuildContext context) { final counter = useState(0); return GestureDetector( // automatically triggers a rebuild of Counter widget onTap: () => counter.value++, child: Text(counter.value.toString()), ); }}useReducer
AnalternativetouseStateformorecomplexstates.
useReducermanagesanreadonlystatethatcanbeupdatedbydispatchingactionswhichareinterpretedbyaReducer.
Thefollowingmakesacounterappwithbotha"+1"and"-1"button:
class Counter extends HookWidget { @override Widget build(BuildContext context) { final counter = useReducer(_counterReducer, initialState: 0); return Column( children: <Widget>[ Text(counter.state.toString()), IconButton( icon: const Icon(Icons.add), onPressed: () => counter.dispatch('increment'), ), IconButton( icon: const Icon(Icons.remove), onPressed: () => counter.dispatch('decrement'), ), ], ); } int _counterReducer(int state, String action) { switch (action) { case 'increment': return state + 1; case 'decrement': return state - 1; default: return state; } }}useMemoized
Takesacallback,callsitsynchronouslyandreturnsitsresult.Theresultisthenstoredtothatsubsequentcallswillreturnthesameresultwithoutcallingthecallback.
Bydefault,thecallbackiscalledonlyonthefirstbuild.Butitisoptionallypossibletospecifyalistofobjectsasthesecondparameter.Thecallbackwillthenbecalledagainwheneversomethinginsidethelisthaschanged.
ThefollowingsamplemakeanhttpcallandreturnthecreatedFuture.AndifuserIdchanges,anewcallwillbemade:
String userId;final Future<http.Response> response = useMemoized(() { return http.get('someUrl/$userId');}, [userId]);useValueChanged
Takesavalueandacallback,andcallthecallbackwheneverthevaluechanged.Thecallbackcanoptionallyreturnanobject,whichwillbestoredandreturnedastheresultofuseValueChanged.
Thefollowingexampleimplicitlystartsatweenanimationwhenevercolorchanges:
AnimationController controller;Color color;final colorTween = useValueChanged( color, (Color oldColor, Animation<Color> oldAnimation) { return ColorTween( begin: oldAnimation?.value ?? oldColor, end: color, ).animate(controller..forward(from: 0)); }, ) ?? AlwaysStoppedAnimation(color);useAnimationController,useStreamController,useSingleTickerProvider
Asetofhooksthathandlesthewholelife-cycleofanobject.Thesehookswilltakecareofbothcreating,disposingandupdatingtheobject.
TheyaretheequivalentofbothinitState,disposeanddidUpdateWidgetforthatspecificobject.
Duration duration;AnimationController controller = useAnimationController( // duration is automatically updates when the widget is rebuilt with a different `duration` duration: duration,);useStream,useFuture,useAnimation,useValueListenable,useListenable
AsetofhooksthatsubscribestoanobjectandcallssetStateaccordingly.
Stream<int> stream;// automatically rebuild the widget when a new value is pushed to the streamAsyncSnapshot<int> snapshot = useStream(stream);
评论