When I call setState in StatefulWidget, other widgets refresh too

Viewed 694

The Goal

Use setState without redraw other widgets

What I Did

I'm using youtube_player_flutter to show YouTube video.

            String id;
            YoutubePlayerController _youtubeController =
                YoutubePlayerController(
              initialVideoId: id,
              flags: YoutubePlayerFlags(....);


            //Then call this:
                          YoutubePlayer(
                            controller: _youtubeController,
                            showVideoProgressIndicator: true,
                          ),

In same StatefulWidget I have a Text, Button and YoutubePlayer.

String str = "abc";

//in Column or Row
Text(str),
ElevatedButton(child: Text("Push"), onPressed: () { setState((){ str = "ABC" });}),
YoutubePlayer(
 controller: _youtubeController,
 showVideoProgressIndicator: true,
),

The Problem

When I call setState, YoutubePlayer is refreshed. This means Youtube video is loaded again, and progress is initialized.

I can't define YoutubePlayer as const because initialVideoId is indefinite.

flutter doctor

[√] Flutter (Channel beta, 2.6.0-5.2.pre, on Microsoft Windows [Version 10.0.22000.282], locale ja-JP)
    • Flutter version 2.6.0-5.2.pre at D:\src\flutter_windows_2.5.0-stable\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 400608f101 (7 weeks ago), 2021-09-15 15:50:26 -0700
    • Engine revision 1d521d89d8
    • Dart version 2.15.0 (build 2.15.0-82.2.beta)

[√] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
    • Android SDK at D:\Android
    • Platform android-30, build-tools 29.0.3
    • Java binary at: D:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6842174)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Android Studio (version 4.2)
    • Android Studio at D:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6842174)

[√] VS Code (version 1.61.2)
    • VS Code at C:\Users\yukik\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.27.0

[√] Connected device (3 available)
    • Android SDK built for x86 (mobile) • emulator-5554 • android-x86    • Android 10 (API 29) (emulator)
    • Chrome (web)                       • chrome        • web-javascript • Google Chrome 95.0.4638.54
    • Edge (web)                         • edge          • web-javascript • Microsoft Edge 95.0.1020.40

• No issues found!
4 Answers

setState refresh widget itself and it child widgets. So, if you put widget which you want to refresh on level beneath YouTube player, it won't refresh that player

Well, setState does exactly that, it redraws all the page. If you want to redraw only one widget, you have to use streams/state management. This is why using setState is not a good practice, the cost/benefit is too high. Try to use MobX, GetX, RxDart, Cubit for state management.

I solved this issue by using provider package. StatefulWidget was not suitable with this function.

class ChangeStr with ChangeNotifier{
    String str = "";  // Want to refresh widgets only which includes this string
    void changeStr(String s){
        str = s;
        notifyListeners();
    }
}

class Example extends StatelessWidget {
  const Example({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => ChangeStr(),
      child: _Example(),
    );
  }
}

class _Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
      .............
   Column(children: [ 
    YoutubePlayer(.....) // This won't refresh 
    Consumer<ChangeStr>(builder: (context, model, child) {
                          return Text(model.str); // Refresh only this Widget by using Consumer
      ]),
    }
  }
}

// And change value with like this
context.read<ChangeStr>().changeStr("difjws");

Also, if you use FutureBuilder, don't forget this

            WidgetsBinding.instance?.addPostFrameCallback((_) {
              context
                  .read<ChangeStr>()
                  .ChangeStr(snapshot .....); //Set with snapshot
            });

Here's a full code example in which 3 sub widgets are created and inserted in the main widget. Each widget displays in a local Text instance the value of its counter. Clicking on the first and second widget button increments all three counters, but as only the local setState() is called, only the widget Text instance is updated. On the contrary, the third widget button calls the main widget setState() as well as the local setState(), and so the third widget Text instance is updated as well as the main widget three Text instances; the first and second widget Text instances are not updated.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Title of Application',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State<MyHomePage> {
  int _counterSubWidgetOne = 0;
  int _counterSubWidgetTwo = 0;
  int _counterSubWidgetThree = 0;

  int get counterSubWidgetOne => _counterSubWidgetOne;
  int get counterSubWidgetTwo => _counterSubWidgetTwo;
  int get counterSubWidgetThree => _counterSubWidgetThree;

  late Widget _mySubWidgerOne;
  late Widget _mySubWidgetTwo;
  late Widget _mySubWidgetThree;

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

    _mySubWidgerOne = SubWidgetOne(parent: this);
    _mySubWidgetTwo = SubWidgetTwo(parent: this);
    _mySubWidgetThree = SubWidgetThree(parent: this);
  }

  void increaseAllCounters() {
    _counterSubWidgetOne++;
    _counterSubWidgetTwo++;
    _counterSubWidgetThree++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("setState sub widget explore"),
      ),
      body: getBody(),
    );
  }

  Widget getBody() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            const Text('Parent Text\'s'),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('$_counterSubWidgetOne'),
                const SizedBox(
                  width: 6,
                ),
                Text('$_counterSubWidgetTwo'),
                const SizedBox(
                  width: 6,
                ),
                Text('$_counterSubWidgetThree'),
              ],
            ),
          ],
        ),
        _mySubWidgerOne,
        _mySubWidgetTwo,
        _mySubWidgetThree,
      ],
    );
  }
}

class SubWidgetOne extends StatefulWidget {
  final _MyHomePageState _parent;

  const SubWidgetOne({Key? key, required _MyHomePageState parent})
      : _parent = parent,
        super(key: key);

  @override
  State<SubWidgetOne> createState() => _SubWidgetOneState();
}

class _SubWidgetOneState extends State<SubWidgetOne> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          const Text("SubWidgetOne"),
          Text('${widget._parent.counterSubWidgetOne}'),
          FloatingActionButton(
            onPressed: () {
              widget._parent.increaseAllCounters();
              setState(() {}); // updates local Text instance
            },
            child: const Icon(Icons.add),
            tooltip: 'Increases all counters, calls only local setState()',
          )
        ],
      ),
    );
  }
}

class SubWidgetTwo extends StatefulWidget {
  final _MyHomePageState _parent;

  const SubWidgetTwo({Key? key, required _MyHomePageState parent})
      : _parent = parent,
        super(key: key);

  @override
  State<SubWidgetTwo> createState() => _SubWidgetTwoState();
}

class _SubWidgetTwoState extends State<SubWidgetTwo> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          const Text("SubWidgetTwo"),
          Text('${widget._parent.counterSubWidgetTwo}'),
          FloatingActionButton(
            onPressed: () {
              widget._parent.increaseAllCounters();
              setState(() {}); // updates local Text instance
            },
            child: const Icon(Icons.add),
            tooltip: 'Increases all counters, calls only local setState()',
          )
        ],
      ),
    );
  }
}

class SubWidgetThree extends StatefulWidget {
  final _MyHomePageState _parent;

  const SubWidgetThree({Key? key, required _MyHomePageState parent})
      : _parent = parent,
        super(key: key);

  @override
  State<SubWidgetThree> createState() => _SubWidgetThreeState();
}

class _SubWidgetThreeState extends State<SubWidgetThree> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          const Text("SubWidgetThree"),
          Text('${widget._parent.counterSubWidgetThree}'),
          FloatingActionButton(
            onPressed: () {
              widget._parent.increaseAllCounters();
              widget._parent.setState(() {}); // updates parent Text instances
              setState(() {}); // updates local Text instance
            },
            child: const Icon(Icons.add),
            tooltip:
                'Increases all counters, calls parent setState() as well as local setState()',
          )
        ],
      ),
    );
  }
}
Related