App/Flutter

Flutter Hook - Summary

Agrafenaaa 2023. 2. 1. 17:27

Hooks ?

- let us organize the logic inside a component into reusable isolated units. 

- let use functions instead of having to constantly switch between functions, classes, higher-order components, and render props. 

- are fully encapsulated and work as a isolated local state within the currently running component. 

- redeems statefulWidget's difficulty of logic reusability.

(Ref -> https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889)

 

Prerequisites : Add the dependency - "flutter_hooks"

Principles && Rules:

- use it only in the "build" method. 

- the same hook can be reused arbitrarily many times. 

- hooks are totally independent even from the widget. 

- use it with prefix "use". (ex: useHook();)

- do not wrap "use" into a condition. (ex: if (flag) {useHook();})

 

(Tutorial Ref -> https://www.youtube.com/watch?v=XsbxM1Aztpo)

Most-Used Hooks

1. useState

-> creates a variable and subscribe to it and based in ValueNotifier. 

2. useEffect

-> useful for side-effects and optionally canceling them. 

3. useFuture

-> subscribes to a future and returns its current state as an AsyncSnapshot, but doesn't hold any state in memory. 

 @override
  Widget build(BuildContext context) {
    final image = useFuture(
      NetworkAssetBundle(Uri.parse(url))
          .load(url)
          .then((data) => data.buffer.asUint8List())
          .then((data) => Image.memory(data)),
    );

    return Column(
          children: [
            image.hasData
            ? image.data!
            : null
          ].compactMap().toList(),
    );
   }

** Image will be flickering every time when it is loaded as useFuture doesn't hold any state of it. 

4. useMemoized

-> caches the instance of a complex object. 

extension CompactMap<T> on Iterable<T?> {
  Iterable<T> compactMap<E>([
    E? Function(T?)? transform,
  ]) =>
      map(
        transform ?? (e) => e,
      ).where((e) => e != null).cast();
}

Widget build(BuildContext context) {
    final future = useMemoized(() {
      NetworkAssetBundle(Uri.parse(url))
          .load(url)
          .then((data) => data.buffer.asUint8List())
          .then((data) => Image.memory(data));
    });

    final snapshot = useFuture(future);

    return Column(
          children: <Widget>[snapshot.data].compactMap().toList(),
    );
  }

** Image will be downloaded only once and be cached further by useMemoised. 

5. useListenable

-> subscribes to a Listenable rebuilds the widget once a new values is detected.

class CountDown extends ValueNotifier<int> {
  late StreamSubscription sub;
  CountDown({required int from}) : super(from) {
    sub = Stream.periodic(const Duration(seconds: 1), (v) => from - v)
        .takeWhile((e) => e >= 0)
        .listen((event) {
      value = event;
    });
  }

  @override
  void dispose() {
    sub.cancel();
    super.dispose();
  }
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final counter = useMemoized(() => CountDown(from: 20));
    final notifier = useListenable(counter);

    return Text(notifier.value.toString());
  }

** useListenable was attached to counter, as useMemoized caches the countdown, not renew the value.

5. useReducer

-> handles more complex states than useState

enum Action {
  rotateLeft,
  rotateRight,
  moreVisible,
  lessVisible,
}

@immutable
class State {
  final double rotationDeg;
  final double alpha;

  const State({
    required this.rotationDeg,
    required this.alpha,
  });

  const State.zero()
      : rotationDeg = 0.0,
        alpha = 1.0;

  // reducers
  State rotateRight() => State(
        alpha: alpha,
        rotationDeg: rotationDeg + 10.0,
      );

  State rotateLeft() => State(
        alpha: alpha,
        rotationDeg: rotationDeg - 10.0,
      );

  State moreVisible() => State(
        alpha: min(alpha + 0.1, 1.0),
        rotationDeg: rotationDeg,
      );

  State lessVisible() => State(
        alpha: max(alpha - 0.1, 0.0),
        rotationDeg: rotationDeg,
      );
}

State reducer(State oldState, Action? action) {
  switch (action) {
    case Action.rotateLeft:
      return oldState.rotateLeft();
    case Action.rotateRight:
      return oldState.rotateRight();
    case Action.moreVisible:
      return oldState.moreVisible();
    case Action.lessVisible:
      return oldState.lessVisible();
    case null:
      return oldState;
  }
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final store = useReducer<State, Action?>(
      reducer,
      initialState: const State.zero(),
      initialAction: null,
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: Action.values
                .map((e) => TextButton(
                      onPressed: () {
                        store.dispatch(e);
                      },
                      child: Text(e.name),
                    ))
                .toList(),
          ),
          const SizedBox(height: 50),
          Opacity(
            opacity: store.state.alpha,
            child: RotationTransition(
              turns: AlwaysStoppedAnimation(
                store.state.rotationDeg / 360.0,
              ),
              child: Image.network(url),
            ),
          ),
        ],
      ),
    );
  }

 

6. useAppLifecycleState

-> returns the current lifecycle state and rebuilds the widget once it gets changed. (!= useOnAppLifecycleStateChange -> listens to AppLifeCycleState's changes and triggers a callback on change)

 final state = useAppLifecycleState();

    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: Opacity(
        opacity: state == AppLifecycleState.resumed ? 1.0 : 0.0,
        child: Container(
          margin: const EdgeInsets.all(20),

 

 

 

 

 

Other Hooks

1. useTextEditingController();

 Widget build(BuildContext context) {
    final controller = useTextEditingController();
    final text = useState('');

    useEffect(() {
      controller.addListener(() {
        text.value = controller.text;
      });
      return null;
    }, [controller]); // controller serves as a key

    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(20, 100, 20, 0),
        child: Column(children: [
          TextFormField(
            controller: controller,
          ),
          Text('You typed - ${text.value}'),
        ]),
      ),
    );
  }

 

2. useAnimationController() && useScrollController();

const url =
    "https://thumbs.dreamstime.com/b/ozero-malaya-ritsa-abkhazia-early-evening-lake-small-108638026.jpg";
const imgHeight = 320.0;

extension Normalize on num {
  num normalized(
    num selfRangeMin,
    num selfRangeMax, [
    num normalizedRangeMin = 0.0,
    num normalizedRangeMax = 1.0,
  ]) =>
      (normalizedRangeMax - normalizedRangeMin) *
          ((this - selfRangeMin) / (selfRangeMax - selfRangeMin)) +
      normalizedRangeMin;
}

class HomeScreen extends HookWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final opacity = useAnimationController(
      duration: const Duration(seconds: 1),
      initialValue: 1.0,
      lowerBound: 0.0,
      upperBound: 1.0,
    );

    final size = useAnimationController(
      duration: const Duration(seconds: 1),
      initialValue: 1.0,
      lowerBound: 0.0,
      upperBound: 1.0,
    );

    final controller = useScrollController();
    useEffect(() {
      controller.addListener(() {
        final newOpacity = max(imgHeight - controller.offset, 0.0);
        final normalized = newOpacity.normalized(0.0, imgHeight).toDouble();
        opacity.value = normalized;
        size.value = normalized;
      });
      return null;
    }, [controller]);

    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: Column(
        children: [
          SizeTransition(
            sizeFactor: size,
            axis: Axis.vertical,
            axisAlignment: -1.0,
            child: FadeTransition(
              opacity: opacity,
              child: Image.network(
                url,
                height: imgHeight,
                fit: BoxFit.cover,
              ),
            ),
          ),
          Expanded(
            child: ListView.builder(
              controller: controller,
              itemCount: 100,
              itemBuilder: (ctx, idx) => ListTile(
                title: Text('IDX - ${idx + 1}'),
              ),
            ),
          )
        ],
      ),
    );
  }

 

3. useStreamController();

 late final StreamController<double> controller;
    controller = useStreamController<double>(onListen: () {
      controller.sink.add(0.0);
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: StreamBuilder<double>(
        stream: controller.stream,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const CircularProgressIndicator();
          } else {
            final rotation = snapshot.data ?? 0.0;

            return GestureDetector(
              onTap: () {
                controller.sink.add(rotation + 10.0);
              },
              child: RotationTransition(
                turns: AlwaysStoppedAnimation(rotation / 360.0),
                child: Center(
                  child: Image.network(url),
                ),
              ),
            );
          }
        },
      ),
    );