Hooks In Flutter. Welcome to one of the best ways of…
By: Apoorv Wadhwa
Tags: Flutter Widget, Flutter, Mobile App Development, Dart, Hooks

Welcome to one of the best ways of managing UI logic, forget all about stateful widgets and the amount of unreadability as you pile hundreds of functions into initState() or call setState() hundred times.

So, that brings us to a question of what hooks are exactly?

Hooks came from React Native, if you want to learn more about them

With them in react you could use state features without writing a class, So what do they do in flutter then?

well, their main use is for readability and clear the clutter in your state classes, they use something called HOOKWIDGET, now with stateful widgets, you can only have one state but with Hookwidget you can have multiple hooks attached to one HookWidget each containing its own logic.

Hooks are kept inside a List and are managed on their own so you don’t need to initialize them or dispose of them(well not the prebuilt ones anyway)

There are prebuilt Hooks and Custom hooks that we can use.To get a better understanding of Hooks lets just jump into code

Without using a HookWidget :

import 'package:flutter/animation.dart';

import 'package:flutter/material.dart';

import 'package:flutter_hooks/flutter_hooks.dart';



void main()=>runApp(MaterialApp(

title: "Hooks",

home: Home(),

debugShowCheckedModeBanner: false,

));



class Home extends StatefulWidget {

@override

_HomeState createState() => _HomeState();

}



class _HomeState extends State<Home> with SingleTickerProviderStateMixin{

AnimationController animationController;

Animation<double> animation;





@override

void initState() {

super.initState();

animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 1000),value: 1)..addListener(() {

if(animationController.status == AnimationStatus.completed) {

animationController.repeat();

}

setState(() {



});

});

}



@override

Widget build(BuildContext context) {

return Scaffold(

body: Container(

color: Colors.blueGrey,

child: GestureDetector(

child: FadeTransition(opacity: animationController,child: Center(child: FlutterLogo(size: 300,))),

onTap: animationController.forward,

onDoubleTap: animationController.reverse,

),

),

);

} @override

void dispose() {

super.dispose();

animationController.dispose();

}

}

we are simply trying to Fade the flutter logo when the user taps on Screen, just create an animation controller and an Animation and call _animationController.forward(), to make it run.

Now the equivalent of this in Hooks would be

import 'package:flutter/animation.dart';

import 'package:flutter/material.dart';

import 'package:flutter_hooks/flutter_hooks.dart';



void main()=>runApp(MaterialApp(

title: "Hooks",

home: Home(),

debugShowCheckedModeBanner: false,

));







class Home extends HookWidget {

@override

Widget build(BuildContext context) {

final hookAnimation = useAnimationController(duration: kThemeAnimationDuration, initialValue: 1);



return Scaffold(

body: GestureDetector(

onTap: () {

hookAnimation.forward();

},

onDoubleTap: () {

hookAnimation.reverse();

},

child: Container(

color: Colors.blueGrey,

child: Center(child: FadeTransition(opacity: hookAnimation,child: ScaleTransition(scale: hookAnimation,child: FlutterLogo(colors: Colors.blue,size: 500,)))),



),

),

);

}

}

see the lack of initState(), or even the dispose()? So why is it that we need to declare them in stateful widget yet we can freely use useAnimationController() in the HookWidget?

Well, the answer to that question is simple, in HookWidget all the different hooks we create like the useAnimationController they are managed by Hook that is to say that their intialization and dispose is taken care of and we don't need to get our hands dirty by writing them on our own:

Suppose if I had to create 2 TextEditingControllers in this project as well I would’ve had to dispose them and then the dispose method would’ve been cluttered, now imagine this for a project on a huge scale where multiple people are working on it, imagine you had to put so many functions in the initState or call setState so many times that it takes a while just to read and understand the code and then make changes to a particular part of the program, there is a chance you may even break something trying to fix something else which would consume even more time.

So that is why Hooks are the perfect solution provided to us so that we do not run into this particular problem. Another benefit of using hooks is re-usability, after creating your custom hook you can reuse it as much as you would like.

NOTE: Always create the hooks in build method.

CREATING CUSTOM HOOKS

let's say you want the animation you just created to show up when you scroll the page?

Simple right you will just create a scroll controller and in initState just define it to do that, but what if you had to do the same thing on like 5 or 6 different screens? nobody would write the same stuff 5–6 times just for it to work, that’s where hook comes in and saves your day. Lemme show you how :

How would you do that in normal Stateful Widget?

class Home extends StatefulWidget {

@override

_HomeState createState() => _HomeState();

}



class _HomeState extends State<Home> with SingleTickerProviderStateMixin{

AnimationController animationController;

Animation<double> animation;

ScrollController scroller;





@override

void initState() {

super.initState();



animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 1000),value: 1);

scroller = ScrollController()..addListener(

() {

if(scroller.position.userScrollDirection ==ScrollDirection.forward) {

animationController.forward();

}

else if(scroller.position.userScrollDirection ==ScrollDirection.reverse) {

animationController.reverse();

}

else {

print("not scrolloing");

}

}

);

}



@override

Widget build(BuildContext context) {

return Scaffold(

body: Container(

color: Colors.blueGrey,

child: ListView(

controller: scroller,

children: List.generate(20, (index) {

return Container(

margin: EdgeInsets.all(10),

decoration: BoxDecoration(

border: Border.all(

color: Colors.white,

width: 10

)

),

child: GestureDetector(

child: FadeTransition(opacity: animationController,child: Center(child: FlutterLogo(size: 300,))),

onTap: animationController.forward,

onDoubleTap: animationController.reverse,

),

);

}),

)

),

);

}



@override

void dispose() {

super.dispose();

animationController.dispose();

scroller.dispose();

}





}

So, I have made a simple example of when you scroll the animation plays, and when you scroll in reverse direction the animation also plays in reverse, its simple code but if you have no practice with ScrollController then you might wanna check out :

Alright now that you are familiar with ScrollController class we can focus on how to get this done with the help of a CustomHook

While it may sound difficult thing to do its really not,

so i have created another file called customScroll where i have created a class called HookScroll which extends Hook<ScrollController>, now just like in a normal stateful class you need to create an createState().

TIP : Just create a normal stateful class in your project then modify it with the code i m about to paste, will save a lot of time

Now create a _HookScrollState which will HookState which will have 2 generics one of ScrollController and the other of the class it will be the state of HookScroll , So it will go like this

class _HookScrollerState extends HookState<ScrollController,HookScroller> {

Now we will create a normal ScrollController instance, and in initState for a hook called the initHook we will define it

@override

void initHook() {

// TODO: implement initHook

super.initHook();

scroller = ScrollController()..addListener((){

scrollAnimation(hook.controller, scroller);

});

}

the scrollAnimation is a function defined by me but you can just create it inside scrollController’s addListener i just like to keep my functions seperate, here is the code for it

void scrollAnimation(AnimationController controler, ScrollController scrollController) {

if(scrollController.position.userScrollDirection == ScrollDirection.forward) {

controler.forward();

}

else if(scrollController.position.userScrollDirection == ScrollDirection.reverse){

controler.reverse();

}

else {

print("not scrolling rn");

}

}

now after making an initHook just like an initState we are going to need a dispose for the scroll controller

@override

void dispose() {

// TODO: implement dispose

super.dispose();

scroller.dispose();

}

Alright, now we can wire it up so that HookScroll takes an animation controller value which we can access by hook.controller

class HookScroller extends Hook<ScrollController> {

AnimationController controller;

HookScroller(this.controller); @override

_HookScrollerState createState() => _HookScrollerState();

}

Now after all that we need a build method so that it can all work together so for that just paste the below code

@override

ScrollController build(BuildContext context) {

return scroller;

}

Body is of type ScrollerController meaning it returns the scroller that we created so that

in the end it would look like this

class HookScroller extends Hook<ScrollController> {

AnimationController controller;

HookScroller(this.controller);

@override

_HookScrollerState createState() => _HookScrollerState();

}



class _HookScrollerState extends HookState<ScrollController,HookScroller> {

ScrollController scroller;



void scrollAnimation(AnimationController controler, ScrollController scrollController) {

if(scrollController.position.userScrollDirection == ScrollDirection.forward) {

controler.forward();

}

else if(scrollController.position.userScrollDirection == ScrollDirection.reverse){

controler.reverse();

}

else {

print("not scrolling rn");

}

}



@override

void initHook() {

// TODO: implement initHook

super.initHook();

scroller = ScrollController()..addListener((){

scrollAnimation(hook.controller, scroller);

});

}



@override

ScrollController build(BuildContext context) {

return scroller;

}



@override

void dispose() {

// TODO: implement dispose

super.dispose();

scroller.dispose();

}

}

With this we can make a scroll work, just one little problem tho if you try to make an instance of it, It will give you an error, why is that?

Well when you call a HookWidget inside a class that extends Hook,it does that by calling Hook.use, so to fix this you can click on useAnimationController() method we created while holding ctrl button on the keyboard to visit this function, as you can see how that function returns a Hook.use with _AnimationControllerHook, so we have do perform the same with our custom Hook.

To do this outside of the classes of our HookScroller and _HookScrollState create a method that will return a ScrollController which will need the value of an animation controller called controller

ScrollController scrollController(AnimationController controller) {

return Hook.use(HookScroller(controller));

}

After creating this the final would customScroll file would look like this :

import 'package:flutter/material.dart';

import 'package:flutter/rendering.dart';

import 'package:flutter_hooks/flutter_hooks.dart';





class HookScroller extends Hook<ScrollController> {

AnimationController controller;

HookScroller(this.controller);

@override

_HookScrollerState createState() => _HookScrollerState();

}



class _HookScrollerState extends HookState<ScrollController,HookScroller> {

ScrollController scroller;



void scrollAnimation(AnimationController controler, ScrollController scrollController) {

if(scrollController.position.userScrollDirection == ScrollDirection.forward) {

controler.forward();

}

else if(scrollController.position.userScrollDirection == ScrollDirection.reverse){

controler.reverse();

}

else {

print("not scrolling rn");

}

}



@override

void initHook() {

// TODO: implement initHook

super.initHook();

scroller = ScrollController()..addListener((){

scrollAnimation(hook.controller, scroller);

});

}



@override

ScrollController build(BuildContext context) {

return scroller;

}



@override

void dispose() {

// TODO: implement dispose

super.dispose();

scroller.dispose();

}

}



ScrollController scrollController(AnimationController controller) {

return Hook.use(HookScroller(controller));

}

Finally, we can wire it all up together by calling the method we just created in our Home class

class Home extends HookWidget {

@override

Widget build(BuildContext context) {

final hookAnimation = useAnimationController(duration: Duration(milliseconds: 500), initialValue: 0);

final hookScroll = scrollController(hookAnimation);



return Scaffold(

body: GestureDetector(

onTap: () {

hookAnimation.reverse();

},

onDoubleTap: () {

hookAnimation.forward();

},

child: Container(

color: Colors.blueGrey,

child: ListView(

controller: hookScroll,

children: List.generate(20, (index) {

return Container(margin: EdgeInsets.all(20),decoration: BoxDecoration(

border: Border.all(

color: Colors.white,

width: 2,

style: BorderStyle.solid

)

),child: Center(child: FadeTransition(opacity: hookAnimation,child: ScaleTransition(scale: hookAnimation,child: FlutterLogo(colors: Colors.blue,size: 500,)))));

})

)



),

),

);

}



}

See the highlighted above and do the same in your code, or you can copy-paste it, I leave that decision up to you

With this, we are done. now we can have it play our animation every time we scroll

If you wanna say that this is too much boilerplate code compared to what we had in StatefulWidget…. well, I agree, if I just have to make it for one screen alone then I won't make a hook for it and just work with a stateful widget, but if I have to use that animation in like 3–4 screens nothing would be better than a CustomHook.

If you are facing any problem implementing Hook, contact me i will try to help to the best of my ability