どのようなときにflutterでsetstat ()とは何を使用するか?


フロントエンドアプリケーションで動作する場合、一般的なユースケースは「画面のUIを動的に更新」します.setState フラッターでこれを達成する方法の一つです.

概要
  • Tech Terms

  • What is a State Object in flutter
  • Fun Fact
  • Example code
  • When to use setState

  • Going one step ahead
  • Summary
  • Final Note

  • 技術用語

    These are some common terms used in flutter. The concepts apply to other frameworks also, although each framework has its own technical term for them.

    • Widget: Any UI component on the screen is called a Widget in flutter. A Widget can have its own Widgets, like a tree structure.

    • StatefulWidget: A Widget that can change dynamically. Generally used when we want to modify something on the screen's UI.


    フラッターの状態オブジェクトは何ですか? setState is called inside a State class. Let's understand this in detail.

    State is simply the information of a StatefulWidget. Every
    StatefulWidget にはstateオブジェクトがあります.この状態オブジェクトは、statefulwidget内で定義する変数と関数の追跡を行います.

    State Object is actually managed by corresponding Element Object of the Widget, but for this blog, we will only focus on the Widget part. If you don't know what Element Object is, I'll encourage you to read about it. It's not required to know for this blog though.


    class MyWidget extends StatefulWidget { // immutable Widget
      @override
      _MyWidgetState createState() => _MyWidgetState();
      // creating State Object of MyWidget
    }
    
    class _MyWidgetState extends State<MyWidget> { // State Object
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    
    StateFileWidget自体は不変です(変更できません)、我々は使用しますState Object UIを変更するには
    私たちは、この状態オブジェクトに、私たちのスクリーンのUIをsetState() .

    関数定義
    void setState(VoidCallback fn) {
      ...
    }
    
    このsetState() 関数をパラメータとして受け取ります.VoidCallback ちょっとおしゃれなやり方です.void Function()
    typedef VoidCallback = void Function();
    


    我々は、単純なカウンタアプリを作成します(私はそれは非常に一般的ですが、私と一緒に負担)知っている.
  • これには変数がありますcounter で初期化0 .
  • これを表示しますcounter 画面の内側にText ウィジェット.
  • 次に、これを増やすボタンがありますcounter 's価値1 .
  • 新しいUIの値でUIを更新する必要がありますcounter .

  • 一歩上に書かれた手順を見ましょう.

    ウィジェットが作成されると、カウンタの値は0です
    int counter = 0;
    

    画面上のカウンタの値を表示する
    Widget build(BuildContext context){
      return 
           ...
           Text(`counter value: $counter`)
           ...
    }
    

    ボタンをクリックすると、カウンタの値がインクリメントされます
    onTap: () {
    
      // passing an anonymous function to setState
      // that increments counter's value by 1
      counter++;
    },
    

    UIを更新
    onTap: () {
    
      // passing an anonymous function to setState
      // that increments counter's value by 1
      // and update the UI
      setState(() {
        counter++;
      });
    },
    

    楽しい事実

    We can update our variables first and then call the setState() function, since setState just informs the underlying framework that

    "update this Widget's UI in next frame" (marks it dirty).

    The underlying framework will use the last value that is defined before calling the setState function.


    これは上と同じです
    onTap: () {
      counter++;
      setState(() {});
    },
    

    コード例
    import 'package:flutter/material.dart';
    
    class MyWidget extends StatefulWidget {   // widget class
    
      const MyWidget({Key? key}) : super(key: key);
    
      @override
      _MyWidgetState createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {  // state class
      int counter = 0; // initializing counter
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
    
              // displaing the counter
              Text(
                'counter value: $counter',
                textAlign: TextAlign.center,
              ),
              TextButton(
                onPressed: () {
    
                  //incrementing counter
                  setState(() {
                    counter++;
                  });
                },
                child: const Text('Tap here to increment counter'),
              )
            ],
          ),
        );
      }
    }
    

    setstat ()を使用する場合は?

    When we want to change the UI of the screen.

    We don't need to call setState every time we change a variable. We call setState only when we want the change in a variable to reflect on the UI of the screen.

    For instance, say you have a form containing a text field and a button to submit it.

    import 'package:flutter/material.dart';
    
    class MyForm extends StatefulWidget {
      const MyForm({Key? key}) : super(key: key);
    
      @override
      _MyFormState createState() => _MyFormState();
    }
    
    class _MyFormState extends State<MyForm> {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
    
            // text field
            Form(
              child: TextFormField(),
            ),
    
            // submit button
            OutlinedButton(
              onPressed: () {},
              child: const Text('Submit'),
            ),
          ],
        );
      }
    }
    

    User types in the text field and clicks on submit button. Then we display that text field's text below the submit button.

    import 'package:flutter/material.dart';
    
    class MyForm extends StatefulWidget {
      const MyForm({Key? key}) : super(key: key);
    
      @override
      _MyFormState createState() => _MyFormState();
    }
    
    class _MyFormState extends State<MyForm> {
      // we'll use this to save the text user types
      String userText = "";
    
      // this will keep track of submit button's tapped action/event
      // and display the userText below submit button
      bool hasSubmitted = false;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Form(
              child: TextFormField(
    
                // triggered when we save the form
                onSaved: (value) {
                  // store the text-field's value into
                  // the userText variable
                },
              ),
            ),
            OutlinedButton(
    
              // this is triggered whenever we click on button  
              onPressed: () {
                // validate and save the form
                // display the text below the button
              },
              child: const Text('Submit'),
            ),
    
            // this will display the userText only
            // if user has clicked on submit button
            if (hasSubmitted) Text(userText)
          ],
        );
      }
    }
    

    Steps:

    1. User types in the text-field
    2. User clicks submit button
    3. onPressed function of submit button is triggered
    4. Inside onPressed function:
      1. Validate and save the form
      2. This will trigger the onSaved function in the TextFormField
      3. Inside onSaved function:
        1. Store the text field's value in the userText variable
      4. Update hasSubmitted variable with true

    Implementation:

    import 'package:flutter/material.dart';
    
    class MyForm extends StatefulWidget {
      const MyForm({Key? key}) : super(key: key);
    
      @override
      _MyFormState createState() => _MyFormState();
    }
    
    class _MyFormState extends State<MyForm> {
      String userText = "";
    
      bool hasSubmitted = false;
    
      // for getting access to form
      final _formKey = GlobalKey<FormState>();
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Form(
              // attaching key to form
              key: _formKey,
              child: TextFormField(
                onSaved: (value) {
                  /// updating userText variable
                  if (value != null) userText = value;
                },
              ),
            ),
            OutlinedButton(
              onPressed: () {
                // validating form
                if (!_formKey.currentState!.validate()) {
                  return;
                }
    
                // saving form
                _formKey.currentState!.save();
    
                // updating hasSubmitted
                hasSubmitted = true;
              },
              child: const Text('Submit'),
            ),
            if (hasSubmitted) Text(userText)
          ],
        );
      }
    }
    

    There's one small step left.
    When you run this program, you'll notice that nothing happens when we click on submit button.

    Here setState comes to the rescue!

    Now the question is where should we call it?

    There are two places in the program where we are updating variables.

    • inside onSaved function
    • insided onPressed function

    So either one of these or both places should be the answer.

    Let's ask one question to ourselves.

    "On which variable update, do I want to update the UI of the screen?"

    Is it userText inside onSaved function

    or

    hasSubmitted inside onPressed function?

    You got it right!

    It's inside the onPressed function after the hasSubmitted variable has been updated.

    ...
    onPressed: () {
      // validating form
      if (!_formKey.currentState!.validate()) {
        return;
      }
    
      // saving form
      _formKey.currentState!.save();
    
      // updating hasSubmitted
      hasSubmitted = true;
    
      setState(() {});
    },
    ...
    

    Again, this is same as below:

    onPressed: () {
      // validating form
      if (!_formKey.currentState!.validate()) {
        return;
      }
    
      // saving form
      _formKey.currentState!.save();
    
      setState(() {
        // updating hasSubmitted
        hasSubmitted = true;
      });
    },
    

    Why use setState here?

    In our logic, we used hasSubmitted variable as a condition to show the userText . So only after updating hasSubmitted value, does the UI show our desired result.


    一歩前進:
    • What happens when you use the setState inside the onSaved function only?
    ...
    onSaved: (value) {
          /// updating userText variable
          if (value != null) userText = value;
          setState(() {});
        },
      ),
    ),
    OutlinedButton(
      onPressed: () {
    // validating form
        if (!_formKey.currentState!.validate()) {
          return;
        }
    
    // saving form
        _formKey.currentState!.save();
    
    // updating hasSubmitted
        hasSubmitted = true;
      },
    ...
    

    It works here also. Surprise!!

    But why? This goes against everything we've read so far in this blog.

    So here's what happens.

    When we call setState , the Widget inside we called it is marked as dirty .

    Now whenever the framework actually rebuilds the UI of the screen, it will take into account all the latest values of the respective variables and paint the pixels on the screen.

    This happens 60 times per second usually, which is the frame per second (fps) rate of flutter. That means approximately every 16ms (1000/60 ms).


    次のフレームがレンダリングされるまで他の変更がある場合は、それらの変更も画面のUIに反映されます.
    変化hasSubmitted 変数はこのような場合に該当します.
    どうやってそれを確かめるの?
    印刷文を追加して、UIが実際にいつ再構築されるかを正確に見ましょう.
    import 'package:flutter/material.dart';
    
    class MyForm extends StatefulWidget {
      const MyForm({Key? key}) : super(key: key);
    
      @override
      _MyFormState createState() => _MyFormState();
    }
    
    class _MyFormState extends State<MyForm> {
      String userText = "";
    
      bool hasSubmitted = false;
    
      // for getting access to form
      final _formKey = GlobalKey<FormState>();
    
      @override
      Widget build(BuildContext context) {
        print('Widget build called');
    
        return Column(
          children: [
            Form(
              // attaching key to form
              key: _formKey,
              child: TextFormField(
                onSaved: (value) {
                  print('inside save');
    
                  if (value != null) userText = value;
    
                  print('hasSubmitted value before setState: $hasSubmitted');
    
                  setState(() {});
    
                  print('hasSubmitted value after setState: $hasSubmitted');
                },
              ),
            ),
            OutlinedButton(
              onPressed: () async {
                print('button clicked: ->');
    
                // validating form
                if (!_formKey.currentState!.validate()) {
                  return;
                }
    
                print('before calling save');
    
                // saving form
                _formKey.currentState!.save();
    
                print('after calling save');
    
                print('hasSubmitted value after calling save: $hasSubmitted');
    
                // updating hasSubmitted
                hasSubmitted = true;
    
                print(
                    'hasSubmitted value after updating hasSubmitted: $hasSubmitted');
              },
              child: const Text('Submit'),
            ),
            if (hasSubmitted) Text(userText)
          ],
        );
      }
    }
    
    これらの印刷文で、フラッタフレームワークがUIを更新する順序を見ることができます.
    テキストフィールドに何かを書きましょう.
    インサイドコンソール
    Widget build called
    button clicked: ->
    before calling save
    inside save
    hasSubmitted value before setState: false
    hasSubmitted value after setState: false
    after calling save
    hasSubmitted value after calling save: false
    hasSubmitted value after updating hasSubmitted: true
    Widget build called
    
    明らかに、ウィジェットはhasSubmittedtrue .
    だから大きな問題は次のとおりですsetState 内部onSaved 関数の内部ではなくonPressed 機能がうまくいくようです."
    このメソッドはすべてのユースケースでは動作しません.そしてそれも論理的に間違っている.
    コードをリファクタ(新しい機能を追加するように)に戻ると、物事は期待通りに動作しないかもしれません.問題が古いコードにあるので、コードをデバッグするのも難しいでしょう.
    そのようなユースケースの例を見ましょう.

    Okay, we're almost done. This is the most important part. We've been building up to this point since the starting of this blog.


    この例の手順に戻りましょう.
  • フォームを保存した後、ユーザーがサーバーに入力したデータを送信するAPI呼び出しを作成したいとします.簡単にするために、私たちはuserText データは、最小20 ms(これはあなたの選択の任意の期間)することができます.
  • これが実用例である.通常、我々はアプリケーションのバックエンドサーバーと通信します.
    我々の望ましい結果は、まだ起こりますか?
    onPressed: () async {
      print('button clicked: ->');
    
      // validating form
      if (!_formKey.currentState!.validate()) {
        return;
      }
    
      print('before calling save');
    
      // saving form
      _formKey.currentState!.save();
    
      print('after calling save');
    
      print('hasSubmitted value after calling save: $hasSubmitted');
    
      /// some computation that takes 20ms
      await Future.delayed(const Duration(milliseconds: 20), () {});
    
      // updating hasSubmitted after 20ms
      hasSubmitted = true;
    
      print(
          'hasSubmitted value after updating hasSubmitted: $hasSubmitted');
    },
    
    内部を見るonPressed 関数.我々はawating エーFuture そして、hasSubmitted 値.

    If you don't know what await, async and Future means, then just think that we're basically saying to program, "Hey flutter framework, we're gonna do some task that'll probably take some time. Please do it in the next iteration."
    I'll encourage you to read about asynchronous programming. This concept is not exclusive to dart.



    私たちはまたTimer 同じ効果を得るには、ここで使用しますFuture .
    インサイドコンソール
    Widget build called
    button clicked: ->
    before calling save
    inside save
    hasSubmitted value before setState: false
    hasSubmitted value after setState: false
    after calling save
    hasSubmitted value after calling save: false
    Widget build called
    hasSubmitted value after updating hasSubmitted: true
    
    今、我々は画面上でUIの更新を表示されません.およびprint文の順序に従ってhasSubmitted 変数は、ウィジェットが再構築された後に更新されます.
    理由

    Warning: This includes some advanced topics. I'll try to explain it as simply as possible.


    があるqueue マイクロタスクつずつすべてのタスクはダートで行われます.使用するときawait , 私たちは最初にダーツを教えて、現在のタスク(20 msを待って)を完了し、次にキュー内の次のいずれかに移動します.
    いくつかのタスク(画面のレンダリングと20 ms待ち)が同時に実行されているが、以下のタスクはawait キーワード更新hasSubmitted は、現在のタスクが完了するまで実行されません.
    だから、フレームワークが実際にレンダリングされたときdirty ウィジェット( myform )hasSubmitted 変数の値は更新されませんでした.したがって、送信ボタンの下に入力されたテキストが表示されません.
    探すべきもの
    また、あるevent queue . 我々は未来が完了するのを待つが、次のマイクロタスクに継続したい場合は、将来的にタスクが追加されますevent queue .
    もっと実験したいですか?
    期間を0秒に変更してみてください.
    UIはまだ必要に応じて更新されません.

    If you wanna dig deep into why after encountering the await keyword, does the code below it doesn't run synchronously even if the duration is 0 seconds, then read about the event loop in dart. There are other resources also that you can easily find on the internet. This concept is not exclusive to dart.


    この新しいユースケース(将来を使用する)で、我々の望ましい出力は達成されません.以下は短い要約です.
       /// Approach 1: this is good (recommended)
       setState((){
         hasSubmitted = true;
       });
    
       ...
    
       /// Approach 2: this is also good
       hasSubmitted = true;
       setState((){});
    
       ...
    
       /// Approach 3: this is not good
       setState((){});
       hasSubmitted = true;  
    
  • 我々が使うとき、何が起こりますかsetState 両方の機能の中?
  • この事件は危険だ.
    私たちの目的の結果が達成されるので、我々はsetState インサイドonSaved 関数.
    私は今あなたがいつ使用しないかという考えを得たことを望みますsetState .
    これは2つの変数だけを考えた非常に簡単な例でしたので、コードにロジックを実装するのは簡単です.
    プログラムが大きく複雑になると、変数とUIの更新の追跡が面倒になります.
    それから、我々はstatefulwidgetのものの混合を使いますsetState その他のstate management 解決法
    フラッタのオフィシャルサイトをお読みしましょうdocs そして、この上でグリップを得るためにアプリをビルドします.

    概要
    • setState is a way to dynamically change the UI.
    • We call it inside the State Object class of the StatefulWidget.
    • Calling setState marks the corresponding Widget dirty . When flutter builds the next frame (approx. every 16ms), it renders the Widget according to the latest values of the State Object.
    • Where we call setState matters a lot.
    • There are other state management solutions also.

    ファイナルノート

    Thank you for reading this article. If you enjoyed it, consider sharing it with other people.

    If you find any mistakes, please let me know.

    Feel free to share your opinions below.