1

First, I need to say that I'm not sure if stackoverflow is the right place to ask this, but I have no one to ask except the community because we are working in small startup where there are no flutter developers besides two of us.

Me (about one year in development, and about a half of the year studying flutter) and one kinda "senior mobile developer with 10 years of experience" have fierce discussion.

The topic of the discussion is using non-final field in Stateless widget. He's doing it, he's writing such code. And he's saying that it is the best way to solve his problems. I'm saying that it is the bad idea and either he needs stateful widget or his design is bad and he doesn't need non-final field.

So my question is: Is there a situation in which the use of non-final field in stateless widget is justified?

His arguments:

  1. We are using BLoC pattern, and because in StatelessWidget we have BlocBuilder, this StatelessWidget has state.
  2. Stupid Dart linter doesn't know our "BLoC situation"
  3. If we will use stateful widget, readability of the code getting bad.
  4. If we will use stateful widget we are getting extra overhead.

I know that the first two arguments are silly and only 4th argument is worth to discuss.

Possible duplicate of this question also doesn't convince my colleague. Flutter: Mutable fields in stateless widgets

Please have a look at his code:

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);
  }
}
ivanesi
  • 1,603
  • 2
  • 15
  • 26
  • final in dart does not mean that the object is completely immutable it means only that it can not be re-assigned – Saed Nabil Feb 05 '20 at 04:24
  • Yes, thanks I replaced 'mutable' to 'non final' in question. – ivanesi Feb 05 '20 at 04:28
  • to make a variable non final means that you have intentions to re assign it which I don't see in the code , where you need to re assign your variable? – Saed Nabil Feb 05 '20 at 04:30
  • My colleague did it in code provided in this question. But question is more about classes that extend StatelessWidget marked as immutable – ivanesi Feb 05 '20 at 04:35
  • 2
    any reassigning to a mutable variable will be lost once a build for the parent widget happens hence the name stateless means that all variables have to be initialized at construction time. and @immutable meta is to give a warning if you are trying to use non final variable . I just wonder if he even do mutation which needed to be memorized ! – Saed Nabil Feb 05 '20 at 04:57

1 Answers1

1

Disclaimer: I am not good at explaining, hope you get something reading this trash explanation. I don't even think this can be called explanation

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);
  }
}
Federick Jonathan
  • 2,726
  • 2
  • 13
  • 26
  • Thanks! Actually he said that StatelessWidget has state if its build method returns BlocBuilder, that isn't true of course. – ivanesi Feb 05 '20 at 04:48