0

I followed this link at an attempt to update the parent widget from its child using a callback function. How to update the state of Parent Widget from its Child Widget while also updating the Child's state in Flutter?

However I am encountering lots of bugs. You can see it in this GIF of my work: enter image description here

You can see in the Muscles pane, there are two buttons to select the view type. The goal is when switching between panes (Overview, Exercises, Muscles), I want to preserve the state of the view type that has been selected in the Muscles pane.

The issues: Table view is selected by default and displays text underneath, but when I select Heatmap diagram, the text below changes but the colour of the button does not. Then when I switch from Exercises pane and back to Muscles pane, the colour of the button now updates but the text goes back to default. Basically, lots of things are going wrong.

Code of the parent widget:

import 'package:workout_log/screens/select_workout.dart';
import 'package:workout_log/screens/settings/settings.dart';
import '../../widgets/page/pane_button.dart';
import 'exercises_pane.dart';
import 'muscles/muscles_pane.dart';
import 'overview_pane.dart';

Widget pageSection = const OverviewPane();

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

  @override
  State<LogWorkout> createState() => _LogWorkoutState();
}

enum LogWorkoutPanes { overview, exercises, muscles }

enum MuscleViewType { table, heatmapDiagram }

class _LogWorkoutState extends State<LogWorkout> {
  EdgeInsets padding = const EdgeInsets.all(25);
  LogWorkoutPanes selectedPane = LogWorkoutPanes.overview;

  MuscleViewType muscleViewType = MuscleViewType.table;
  updateMuscleViewType(newViewType) {
    setState(() {
      muscleViewType = newViewType;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: Scaffold(
          backgroundColor: Colors.white,
          body: SingleChildScrollView(
            child: SafeArea(
                child: Center(
              child: Column(children: [
                const LogWorkoutNavBar(),
                const LogWorkoutHeader(),
                Container(
                    width: double.infinity,
                    height: 50,
                    margin: const EdgeInsets.symmetric(
                        vertical: 16.0, horizontal: 25.0),
                    alignment: Alignment.center,
                    child: ElevatedButton(
                        onPressed: () {},
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: const [
                            Icon(Icons.electric_bolt),
                            Text("Start workout"),
                          ],
                        ))),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    InkWell(
                      onTap: () => {
                        setState(() {
                          pageSection = const OverviewPane();
                          selectedPane = LogWorkoutPanes.overview;
                        })
                      },
                      child: PaneButton(
                          title: "Overview",
                          isActive: selectedPane == LogWorkoutPanes.overview),
                    ),
                    InkWell(
                        onTap: () => {
                              setState(() {
                                pageSection = const ExercisesPane();
                                selectedPane = LogWorkoutPanes.exercises;
                              })
                            },
                        child: PaneButton(
                            title: "Exercises",
                            isActive:
                                selectedPane == LogWorkoutPanes.exercises)),
                    InkWell(
                        onTap: () => {
                              setState(() {
                                pageSection = MusclesPane(
                                    updateViewType: updateMuscleViewType,
                                    viewType: muscleViewType);
                                selectedPane = LogWorkoutPanes.muscles;
                              })
                            },
                        child: PaneButton(
                            title: "Muscles",
                            isActive: selectedPane == LogWorkoutPanes.muscles)),
                  ],
                ),
                Padding(padding: padding, child: pageSection),
              ]),
            )),
          )),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(top: 24.0, left: 24.0, right: 24.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("Pull Hypertrophy",
              style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: Theme.of(context).textTheme.headline2?.fontSize)),
          Container(
              margin: const EdgeInsets.only(top: 5),
              child: const Text("21 October 2022 - 5:30PM"))
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    const double padding = 20;
    const EdgeInsets rowPadding =
        EdgeInsets.only(top: padding, left: padding, right: padding);

    return Padding(
      padding: rowPadding,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(
              flex: 2,
              child: Align(
                  alignment: Alignment.topLeft,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                        minimumSize: const Size(40, 40), primary: Colors.white),
                    onPressed: () {
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) => const SelectWorkout()));
                    },
                    child: const Icon(
                      Icons.navigate_before,
                      color: Colors.black,
                    ),
                  ))),
          Expanded(
              flex: 2,
              child: Align(
                  alignment: Alignment.topLeft,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                        minimumSize: const Size(40, 40), primary: Colors.white),
                    onPressed: () {
                      showDialog(
                          context: context,
                          builder: (context) {
                            return const AlertDialog(
                              content: Text("This is a timer"),
                            );
                          });
                    },
                    child: const Icon(
                      Icons.timer,
                      color: Colors.black,
                    ),
                  ))),
          const Spacer(),
          Expanded(
            flex: 2,
            child: Center(
                child: ElevatedButton(
              style: ElevatedButton.styleFrom(
                  minimumSize: const Size(40, 40), primary: Colors.white),
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => const Settings()));
              },
              child: const Icon(
                Icons.settings,
                color: Colors.black,
              ),
            )),
          ),
          Expanded(
            flex: 2,
            child: Center(
                child: ElevatedButton(
              style: ElevatedButton.styleFrom(
                  minimumSize: const Size(40, 40), primary: Colors.white),
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Icon(
                Icons.check,
                color: Colors.black,
              ),
            )),
          )
        ],
      ),
    );
  }
}

Code of the child widget (Muscles pane):

import 'package:flutter/material.dart';
import '../log_workout.dart';
import 'muscles_heatmap_diagram.dart';
import 'muscles_table_view.dart';

class MusclesPane extends StatefulWidget {
  const MusclesPane(
      {Key? key, required this.updateViewType, required this.viewType})
      : super(key: key);
  final Function updateViewType;
  final MuscleViewType viewType;

  @override
  State<MusclesPane> createState() => _MusclesPaneState();
}

class _MusclesPaneState extends State<MusclesPane> {
  EdgeInsets padding = const EdgeInsets.all(25);
  Widget pageSection = const MusclesTableView();

  selectTableView() {
    setState(() {
      pageSection = const MusclesTableView();
      widget.updateViewType(MuscleViewType.table);
    });
  }

  selectHeatmapView() {
    setState(() {
      pageSection = const MusclesHeatmapDiagram();
      widget.updateViewType(MuscleViewType.heatmapDiagram);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            ViewTypeButton(
                title: "Table view",
                isActive: widget.viewType == MuscleViewType.table,
                callback: selectTableView),
            ViewTypeButton(
                title: "Heatmap diagram",
                isActive: widget.viewType == MuscleViewType.heatmapDiagram,
                callback: selectHeatmapView)
          ],
        ),
        Padding(padding: padding, child: pageSection),
      ],
    );
  }
}

class ViewTypeButton extends StatelessWidget {
  const ViewTypeButton(
      {Key? key,
      required this.title,
      required this.isActive,
      required this.callback})
      : super(key: key);
  final String title;
  final bool isActive;
  final Function callback;

  @override
  Widget build(BuildContext context) {
    return Container(
        margin: const EdgeInsets.symmetric(horizontal: 4.0),
        child: ElevatedButton(
            onPressed: () => callback(),
            style: ElevatedButton.styleFrom(
                primary: isActive ? Colors.blue : Colors.grey),
            child: Text(title)));
  }
}

You can see I am using updateMuscleViewType as the callback, and the value being changed is the muscleViewType. Is this the correct way to update the parent state from the child?

tomtomdam
  • 67
  • 2
  • 9

1 Answers1

1

In general, it is ok to manage your state in this way to start with. However, you have a smart UI here, which is bad practice. You should try to separate logic and UI in the long run. There are various state management systems for this, such as Bloc: https://docs.flutter.dev/data-and-backend/state-mgmt/options.

If you adopt one of these systems, you can manage your state independently of the UI and then use this state across several widgets without any problems. This way you wouldn't have to work with relatively error-prone approaches like callbacks (where it's easy to forget a widget).

Mäddin
  • 1,070
  • 6
  • 11