Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter setState of child widget without rebuilding parent

I have a parent that contain a listView and a floatingActionButton i would like to hide the floatingActionButton when the user starts scrolling i have managed to do this within the parent widget but this requires the list to be rebuilt each time.

I have moved the floatingActionButton to a separate class so i can update the state and only rebuild that widget the problem i am having is passing the data from the ScrollController in the parent class to the child this is simple when doing it through navigation but seams a but more awkward without rebuilding the parent!

like image 507
JWRich Avatar asked Aug 07 '18 10:08

JWRich


People also ask

Does setState rebuild all widgets?

When you run the application, the setState() will fail to rebuild the widget. In this example, the initState() is called only once. This means the results of clicking + or – icons won't get displayed on the screen. However, this clicking will always execute the setState() and pass the new value to the widget tree.

How do you call a method of a child widget from parent in Flutter?

Calling a method of child widget from a parent widget is discouraged in Flutter. Instead, Flutter encourages you to pass down the state of a child as constructor parameters. Hence instead of calling a method of the child, you just call setState in the parent widget to update its children.

How do I stop rebuild widget in Flutter?

When the instance of a widget stays the same; Flutter purposefully won't rebuild children. It implies that you can cache parts of your widget tree to prevent unnecessary rebuilds. Thanks to that const keyword, the instance of DecoratedBox will stay the same even if build were called hundreds of times.


4 Answers

A nice way to rebuild only a child widget when a value in the parent changes is to use ValueNotifier and ValueListenableBuilder. Add an instance of ValueNotifier to the parent's state class, and wrap the widget you want to rebuild in a ValueListenableBuilder.

When you want to change the value, do so using the notifier without calling setState and the child widget rebuilds using the new value.

import 'package:flutter/material.dart';

class Parent extends StatefulWidget {
  @override
  _ParentState createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  ValueNotifier<bool> _notifier = ValueNotifier(false);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(onPressed: () => _notifier.value = !_notifier.value, child: Text('toggle')),
        ValueListenableBuilder(
            valueListenable: _notifier,
            builder: (BuildContext context, bool val, Widget child) {
              return Text(val.toString());
            }),
      ],
    );
  }

  @override
  void dispose() {
      _notifier.dispose();

      super.dispose();
  }
}

like image 163
Tom Bowers Avatar answered Sep 21 '22 19:09

Tom Bowers


For optimal performance, you can create your own wrapper around Scaffold that gets the body as a parameter. The body widget will not be rebuilt when setState is called in HideFabOnScrollScaffoldState.

This is a common pattern that can also be found in core widgets such as AnimationBuilder.

import 'package:flutter/material.dart';

main() => runApp(MaterialApp(home: MyHomePage()));

class MyHomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  ScrollController controller = ScrollController();

  @override
  Widget build(BuildContext context) {
    return HideFabOnScrollScaffold(
      body: ListView.builder(
        controller: controller,
        itemBuilder: (context, i) => ListTile(title: Text('item $i')),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      controller: controller,
    );
  }
}

class HideFabOnScrollScaffold extends StatefulWidget {
  const HideFabOnScrollScaffold({
    Key key,
    this.body,
    this.floatingActionButton,
    this.controller,
  }) : super(key: key);

  final Widget body;
  final Widget floatingActionButton;
  final ScrollController controller;

  @override
  State<StatefulWidget> createState() => HideFabOnScrollScaffoldState();
}

class HideFabOnScrollScaffoldState extends State<HideFabOnScrollScaffold> {
  bool _fabVisible = true;

  @override
  void initState() {
    super.initState();
    widget.controller.addListener(_updateFabVisible);
  }

  @override
  void dispose() {
    widget.controller.removeListener(_updateFabVisible);
    super.dispose();
  }

  void _updateFabVisible() {
    final newFabVisible = (widget.controller.offset == 0.0);
    if (_fabVisible != newFabVisible) {
      setState(() {
        _fabVisible = newFabVisible;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: widget.body,
      floatingActionButton: _fabVisible ? widget.floatingActionButton : null,
    );
  }
}

Alternatively you could also create a wrapper for FloatingActionButton, but that will probably break the transition.

like image 28
boformer Avatar answered Sep 21 '22 19:09

boformer


I think using a stream is more simpler and also pretty easy.

You just need to post to the stream when your event arrives and then use a stream builder to respond to those changes.

Here I am showing/hiding a component based on the focus of a widget in the widget hierarchy.

I've used the rxdart package here but I don't believe you need to. also you may want to anyway because most people will be using the BloC pattern anyway.

import 'dart:async';
import 'package:rxdart/rxdart.dart';

class _PageState extends State<Page> {
  final _focusNode = FocusNode();

  final _focusStreamSubject = PublishSubject<bool>();
  Stream<bool> get _focusStream => _focusStreamSubject.stream;

  @override
  void initState() {
    super.initState();

    _focusNode.addListener(() {
      _focusStreamSubject.add(_focusNode.hasFocus);
    });
  }

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          _buildVeryLargeComponent(),
          StreamBuilder(
            stream: _focusStream,
            builder: ((context, AsyncSnapshot<bool> snapshot) {
              if (snapshot.hasData && snapshot.data) {
                return Text("keyboard has focus")
              }
              return Container();
            }),
          )
        ],
      ),
    );
  }
}
like image 24
David Rees Avatar answered Sep 19 '22 19:09

David Rees


You can use StatefulBuilder and use its setState function to build widgets under it.

Example:

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {

  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // put widget here that you do not want to update using _setState of StatefulBuilder
        Container(
          child: Text("I am static"),
        ),
        StatefulBuilder(builder: (_context, _setState) {
          // put widges here that you want to update using _setState
          return Column(
            children: [
              Container(
                child: Text("I am updated for $count times"),
              ),
              RaisedButton(
                  child: Text('Update'),
                  onPressed: () {
                    // Following only updates widgets under StatefulBuilder as we are using _setState
                    // that belong to StatefulBuilder
                    _setState(() {
                      count++;
                    });
                  })
            ],
          );
        }),
      ],
    );
  }
}
like image 40
bikram Avatar answered Sep 18 '22 19:09

bikram