Flutter: combining stateful widgets into a flutter-redux app architecture

Issue

Following this question, I understand why StatefulWidgets do make sense in the context of a Redux-based flutter app. And that’s what I’m trying to accomplish as well-

a Page that is "fed" information from the app-wide state (logged in user details, api data, etc) and can also dispatch actions from it’s connected ViewModel, but one that ALSO contains stateful widgets with a smaller scope, short-lived state. things like Animation, validating user input on the fly before submitting, and changing the UI based on user actions.

So I’m interested in the how and hope someone here can help me.
Currently all of my "pages" are stateless widgets connected to the app-state via the store, in the following manner:

class LoginPage extends StatelessWidget {
  final String TAG = "LoginPage";
  bool isGreen = false;

  LoginPage({Key key}) : super(key: key);

  void changeColor() {
    isGreen = !!isGreen;
  }


  @override
  Widget build(BuildContext context) {
    /// [StoreConnector] is used to convert store data (using the fromStore)
    /// into a ViewModel suitable for the page.
    return StoreConnector<AppState, LoginPageViewModel>(
        builder: (context, viewModel) {
          return Scaffold(
              body: Column(
               children: <Widget>[
                Container(...),
                Text(
                  text: viewModel.some_value_from_the_store,
                  color: isGreen ? Colors.green : Colors.red,
                ),
                ElevatedButton(
                  onPressed: () => changeColor(),
                  child: Text('Press to change color'),
                )
            ],
          ));
        },
        converter: LoginPageViewModel.fromStore);
  }
}

Here I’m just trying to simply change the text color inside the "LoginPage" widget, based on user clicks, while still remain connected to the store so that the UI keeps updating with new app-state information as it arrives.

Is there any reference for something like this our there? can anyone provide an example, or just the basic guidelines of how to achieve this?
seems simple, but I’m struggling with it.

Solution

Here are some ideas that might help.

Your widget needs to be Stateful. You are trying to keep track of the color that will change over time. Stateless widgets only allow final properties – i.e. you can only initiate their value on creation.

Your variable isGreen and method changeColor() should be part of the state object. Your build() method will also go there.

Next – when you call your method you should call:

void changeColor() {
    setState(() {isGreen = !isGreen;});
}

I think in your code you are not flipping the value (=!!iGreen is same as =isGreen). But more importantly – you are not telling the framework that your widget should be rebuilt.
You can test this for your self: click once (after you corrected ‘!!’). Nothing will happen. If you force refresh in the emulator – you should see that the color did change. This is because you manually refreshed it. setState() does it for you: it will run the code you provided, and then call Flutter to tell it to refresh your widget. See – flutter does not have a magic trigger, and does not observe your code to decide when to refresh your widget. setState() tells it to do so.

As a general rule: your application state – i.e. the data shared with multiple widgets (or pages) should be ‘lifted up’ the widget tree, and kept in Provider classes.

Stateful widgets should keep only the data directly related to them and only to them. Typically this is the data that helps render the provider data. For example:
-Provider will keep the list of items that you show. Your statefull widget will keep track on currently selected item
-Provider keeps the text to be displayed. Your Stateful widget keeps the font size, font color etc., allowing the user to change it on that specific widget, but not on all the widgets that use the same data.

In your example – you cloud have multiple Login widgets on the screen (for some reason). In that case:
-If you want all of your login widget to change color – keep the isGreen in your provider class. In that case, your widget can be stateless.
-If you want only the widget that you clicked to change the color – this belongs to your stateful widget since no one else cares about this value.

Let me update this with the code that should do what you want. Connecting the widget to the store works the same way for Stateful and Steteless widgets.

Note – I commented out your code connecting to the store, just to show how your widget will change the color. Uncomment the store code, and you should be good to go.

You can run this quickly in https://dartpad.dev/, just copy/paste the code.

// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: LoginPage(),
    );
  }
}

class LoginPage extends StatefulWidget {
  final String TAG = "LoginPage";
  LoginPage({Key key}) : super(key: key);
  
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  
  bool isGreen = false;

  void changeColor() {
    setState(() {
      isGreen = !isGreen;
    });
    
  }

  @override
  Widget build(BuildContext context) {
    /// [StoreConnector] is used to convert store data (using the fromStore)
    /// into a ViewModel suitable for the page.
    //return StoreConnector<AppState, LoginPageViewModel>(
        //builder: (context, viewModel) {
          return Scaffold(
              body: Column(
               children: <Widget>[
                Container(),
                Text(
                  'viewModel.some_value_from_the_store',
                  style: TextStyle(color: isGreen ? Colors.green : Colors.red),
                ),
                ElevatedButton(
                  onPressed: () => changeColor(),
                  child: Text('Press to change color'),
                )
            ],
          ));
        //},
        //converter: LoginPageViewModel.fromStore);
  //}
}
}

Answered By – Andrija

Answer Checked By – Terry (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *