JWRich
JWRich

Reputation: 551

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!

Upvotes: 28

Views: 38947

Answers (4)

Tom Bowers
Tom Bowers

Reputation: 5140

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

Upvotes: 40

bikram
bikram

Reputation: 7935

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++;
                    });
                  })
            ],
          );
        }),
      ],
    );
  }
}

Upvotes: 9

David Rees
David Rees

Reputation: 7312

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

Upvotes: 12

boformer
boformer

Reputation: 30103

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.

Upvotes: 14

Related Questions