Неокончательное поле в виджете без сохранения состояния

Во-первых, я должен сказать, что я не уверен, что stackoverflow - подходящее место, чтобы спросить об этом, но мне не у кого спросить, кроме сообщества, потому что мы работаем в небольшом стартапе, где нет разработчиков флаттера, кроме нас двоих.

Я (около года в разработке и около полугода изучаю флаттер) и один вроде как «старший мобильный разработчик с 10-летним опытом» ведут ожесточенные дискуссии.

Тема обсуждения - использование незавершенного поля в виджете без сохранения состояния. Он это делает, он такой код пишет. И он говорит, что это лучший способ решить его проблемы. Я говорю, что это плохая идея, и либо ему нужен виджет с отслеживанием состояния, либо его дизайн плохой и ему не нужно неокончательное поле.

Итак, мой вопрос: существует ли ситуация, в которой оправдано использование неокончательного поля в виджете без состояния?

Его аргументы:

  1. Мы используем шаблон BLoC, и поскольку в StatelessWidget у нас есть BlocBuilder, этот StatelessWidget имеет состояние.
  2. Глупый линтер Dart не знает нашей "ситуации с BLoC"
  3. Если мы будем использовать виджет с отслеживанием состояния, читаемость кода ухудшится.
  4. Если мы будем использовать виджет с отслеживанием состояния, мы получим дополнительные накладные расходы.

Я знаю, что первые два аргумента глупы и стоит обсудить только 4-й аргумент.

Возможный дубликат этого вопроса тоже не убеждает моего коллегу. Flutter: изменяемые поля в виджетах без сохранения состояния

Взгляните на его код:

class GameDiscussThePicture extends StatelessWidget {

  GameDiscussThePicture();

  CarouselSlider _slider;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: BlocProvider.of<ChatBloc>(context),
      condition: (previousState, state) {
        return previousState != GameClosed();
      },
      builder: (context, state) {
        if (state is GameDiscussTopicChanged) {
          _showPictureWith(context, state.themeIndex);
        } else if (state is GameClosed) {
          Navigator.of(context).pop();
          return Container();
        }
      final _chatBloc = BlocProvider.of<ChatBloc>(context);
      return Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromARGB(255, 255, 255, 255),
          leading: BackButton(
            color: Color.fromARGB(255, 12, 12, 13),
            onPressed: () => BlocProvider.of<ChatBloc>(context).add(GameCancel()),
          ),
        ),
        //SafeArea
        body: DecoratedBox(
          decoration: BoxDecoration(color: Color.fromARGB(255, 240, 240, 240)),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              Expanded(
                child: Column(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    SizedBox(height: 15),
                    _carouselSlider(context),
                    Container(
                      height: 88,
                      child: DecoratedBox(
                        decoration: BoxDecoration(color: Color.fromARGB(255, 255, 255, 255)),
                        child: Row(
                          mainAxisSize: MainAxisSize.max,
                          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                          children: [
                            if (_chatBloc.partnerAvatar() != null) Image.network(_chatBloc.partnerAvatar(), fit: BoxFit.cover, width: 75.0),
                            if (_chatBloc.partnerAvatar() == null) Text('RU', style: TextStyle(fontSize: 22)),
                          Padding(
                            padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(_chatBloc.partnerName(), style: TextStyle(fontSize: 20, fontWeight: FontWeight.normal),),
                                ChatStopwatch(),
                                // Text('До конца 06:33', style: TextStyle(fontSize: 14, fontWeight: FontWeight.normal),),
                              ],
                            )
                          ),
                          // FlatButton(
                          //   child: Image.asset('assets/images/mic_off.png', width: 30, height: 30,),
                          //   onPressed: () => print('mic off pressed'),
                          // ),
                          FlatButton(
                            child: Image.asset('assets/images/hang_off.png', width: 60, height: 60,),
                            onPressed: () => ChatHelper.confirmEndingDialog(context)
                          ),
                        ]),
                    ))
                  ],
                ),
              ),
            ],
          ),
        ),
      );
    });
  }

  @widget
  Widget _carouselSlider(BuildContext context) {    
    final chatBloc = BlocProvider.of<ChatBloc>(context);
    _slider = CarouselSlider(
      height: 600.0,
      viewportFraction: 0.9,
      reverse: false,
      enableInfiniteScroll: false,
      initialPage: chatBloc.gameDiscussCurrentIdx,
      onPageChanged: (index) {
        final chatBloc = BlocProvider.of<ChatBloc>(context);
        if (chatBloc.gameDiscussCurrentIdx < index) {
          chatBloc.add(GameDiscussTopicChange(themeIndex: index));
        } else {
          _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
        }
      },
      items: chatBloc.gameDiscussPictures.map((item) {
        return Builder(
          builder: (BuildContext context) {
            return Container(
              width: MediaQuery.of(context).size.width,
              margin: EdgeInsets.symmetric(horizontal: 5.0),
              child: Column(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(item.titleEn, style: Styles.h3),
                  SizedBox(height: 15.0,),
                  ClipRRect(
                    borderRadius: BorderRadius.all(Radius.circular(15.0)),
                    child: Image.network(item.getImageUrl(), fit: BoxFit.cover, width: MediaQuery.of(context).size.width),
                  )
                ]
              ),
            );
          },
        );
      }).toList(),
    );
    return _slider;
  }

  _onPictureChanged(BuildContext context, int index) {
    final chatBloc = BlocProvider.of<ChatBloc>(context);
    if (chatBloc.gameDiscussCurrentIdx < index) {
      chatBloc.add(GameDiscussTopicChange(themeIndex: index));
    } else {
      _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
    }
  }

  _showPictureWith(BuildContext context, int index) {
      final chatBloc = BlocProvider.of<ChatBloc>(context);
      chatBloc.gameDiscussCurrentIdx = index;
      _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
  }
}

person ivanesi    schedule 05.02.2020    source источник
comment
final in dart не означает, что объект полностью неизменяем, это означает только то, что его нельзя переназначить   -  person Saed Nabil    schedule 05.02.2020
comment
Да, спасибо, я заменил «изменяемый» на «не окончательный» в вопросе.   -  person ivanesi    schedule 05.02.2020
comment
сделать переменную не окончательной означает, что у вас есть намерения повторно присвоить ее, чего я не вижу в коде, где вам нужно повторно назначить свою переменную?   -  person Saed Nabil    schedule 05.02.2020
comment
Мой коллега сделал это с помощью кода, приведенного в этом вопросе. Но вопрос больше о классах, расширяющих StatelessWidget, отмеченных как неизменяемые.   -  person ivanesi    schedule 05.02.2020
comment
любое переназначение изменяемой переменной будет потеряно после сборки родительского виджета, поэтому имя без сохранения состояния означает, что все переменные должны быть инициализированы во время создания. и @immutable meta - это предупреждение, если вы пытаетесь использовать не конечную переменную. Мне просто интересно, сделал ли он вообще мутацию, которую нужно было запомнить!   -  person Saed Nabil    schedule 05.02.2020


Ответы (1)


Заявление об отказе от ответственности: я не умею объяснять, надеюсь, вы что-то поняли, прочитав это бессмысленное объяснение. Я даже не думаю, что это можно назвать объяснением

class GameDiscussThePicture extends StatelessWidget {

  GameDiscussThePicture();

  /// As he said, BLoC is the one holding state, therefore if he wants a non final field 
  /// declare it in ChatBloc not here.
  /// class ChatBloc extends Bloc {
  ///   CarouselSlider _slider;
  ///   CarouselSlider get slider => _slider;
  /// }
  /// To access it, BlocProvider.of<ChatBloc>(context).slider;
  CarouselSlider _slider;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: BlocProvider.of<ChatBloc>(context),
      condition: (previousState, state) {
        return previousState != GameClosed();
      },
      builder: (context, state) {
        if (state is GameDiscussTopicChanged) {
          _showPictureWith(context, state.themeIndex);
        } else if (state is GameClosed) {
          Navigator.of(context).pop();
          return Container();
        }
      final _chatBloc = BlocProvider.of<ChatBloc>(context);
      return Scaffold(
        appBar: AppBar(
          backgroundColor: Color.fromARGB(255, 255, 255, 255),
          leading: BackButton(
            color: Color.fromARGB(255, 12, 12, 13),
            /// he can just write _chatBloc.add(GameCancel()) here.
            onPressed: () => BlocProvider.of<ChatBloc>(context).add(GameCancel()),
          ),
        ),
        //SafeArea
        body: DecoratedBox(
          decoration: BoxDecoration(color: Color.fromARGB(255, 240, 240, 240)),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              Expanded(
                child: Column(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    SizedBox(height: 15),
                    _carouselSlider(context),
                    Container(
                      height: 88,
                      child: DecoratedBox(
                        decoration: BoxDecoration(color: Color.fromARGB(255, 255, 255, 255)),
                        child: Row(
                          mainAxisSize: MainAxisSize.max,
                          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                          children: [
                            if (_chatBloc.partnerAvatar() != null) Image.network(_chatBloc.partnerAvatar(), fit: BoxFit.cover, width: 75.0),
                            if (_chatBloc.partnerAvatar() == null) Text('RU', style: TextStyle(fontSize: 22)),
                          Padding(
                            padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(_chatBloc.partnerName(), style: TextStyle(fontSize: 20, fontWeight: FontWeight.normal),),
                                ChatStopwatch(),
                                // Text('До конца 06:33', style: TextStyle(fontSize: 14, fontWeight: FontWeight.normal),),
                              ],
                            )
                          ),
                          // FlatButton(
                          //   child: Image.asset('assets/images/mic_off.png', width: 30, height: 30,),
                          //   onPressed: () => print('mic off pressed'),
                          // ),
                          FlatButton(
                            child: Image.asset('assets/images/hang_off.png', width: 60, height: 60,),
                            onPressed: () => ChatHelper.confirmEndingDialog(context)
                          ),
                        ]),
                    ))
                  ],
                ),
              ),
            ],
          ),
        ),
      );
    });
  }

  @widget
  /// Also rather than passing BuildContext, passing the _chatBloc is better.
  /// I am not sure why, but I've read somewhere BuildContext is not meant to be passed
  /// around. And you don't need to make another final field for BlocProvider.of<ChatBloc> 
  /// (context)
  /// Widget _carouselSlider(ChatBloc chatBloc) {
  ///   and here you can do something like chatBloc.slider = CarouselSlider(); in case
  ///   that slider field will be used again somehow.
  /// }
  /// Tho just return CarouselSlider instead is better in this scenario IMO.
  Widget _carouselSlider(BuildContext context) {    
    final chatBloc = BlocProvider.of<ChatBloc>(context);
    _slider = CarouselSlider(
      height: 600.0,
      viewportFraction: 0.9,
      reverse: false,
      enableInfiniteScroll: false,
      initialPage: chatBloc.gameDiscussCurrentIdx,
      onPageChanged: (index) {
        final chatBloc = BlocProvider.of<ChatBloc>(context);
        if (chatBloc.gameDiscussCurrentIdx < index) {
          chatBloc.add(GameDiscussTopicChange(themeIndex: index));
        } else {
          _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
        }
      },
      items: chatBloc.gameDiscussPictures.map((item) {
        return Builder(
          builder: (BuildContext context) {
            return Container(
              width: MediaQuery.of(context).size.width,
              margin: EdgeInsets.symmetric(horizontal: 5.0),
              child: Column(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(item.titleEn, style: Styles.h3),
                  SizedBox(height: 15.0,),
                  ClipRRect(
                    borderRadius: BorderRadius.all(Radius.circular(15.0)),
                    child: Image.network(item.getImageUrl(), fit: BoxFit.cover, width: MediaQuery.of(context).size.width),
                  )
                ]
              ),
            );
          },
        );
      }).toList(),
    );
    return _slider;
  }

  _onPictureChanged(BuildContext context, int index) {
    final chatBloc = BlocProvider.of<ChatBloc>(context);
    if (chatBloc.gameDiscussCurrentIdx < index) {
      chatBloc.add(GameDiscussTopicChange(themeIndex: index));
    } else {
      _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
    }
  }

  _showPictureWith(BuildContext context, int index) {
      final chatBloc = BlocProvider.of<ChatBloc>(context);
      chatBloc.gameDiscussCurrentIdx = index;
      _slider.animateToPage(chatBloc.gameDiscussCurrentIdx, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
  }
}
person Federick Jonathan    schedule 05.02.2020
comment
Спасибо! На самом деле он сказал, что StatelessWidget имеет состояние, если его метод сборки возвращает BlocBuilder, что, конечно же, неверно. - person ivanesi; 05.02.2020