flutter_hooks React hooks 的 Flutter 版本开源项目

我要开发同款
匿名用户2019年01月03日
78阅读
开发技术Dart
所属分类手机开发工具、手机/移动开发
授权协议MIT

作品详情

基于Reacthooks实现的Flutterhooks。Flutterhooks用于管理FlutterWidgeet。有利于增加小部件之间的代码共享,可以代替StatefulWidget。

FlutterHooks

AflutterimplementationofReacthooks:https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889

HooksareanewkindofobjectthatmanagesaWidgetlife-cycles.Theyexistforonereason:increasethecodesharingbetweenwidgetsandasacompletereplacementforStatefulWidget.

Motivation

StatefulWidgetsufferfromabigproblem: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.

Principle

SimilarilytoState,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

Rules

Duetohooksbeingobtainedfromtheirindex,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-reload

Sincehooksareobtainedfromtheirindex,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.

Howtouse

Therearetwowaystocreateahook:

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();  }}Existinghooks

Flutter_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);
声明:本文仅代表作者观点,不代表本站立场。如果侵犯到您的合法权益,请联系我们删除侵权资源!如果遇到资源链接失效,请您通过评论或工单的方式通知管理员。未经允许,不得转载,本站所有资源文章禁止商业使用运营!
下载安装【程序员客栈】APP
实时对接需求、及时收发消息、丰富的开放项目需求、随时随地查看项目状态

评论