Faire un setState d'un autre widget

Bonsoir à tous,

On va mettre ça sur le compte de la chaleur…mais je n’arrive pas à faire ce que je veux, et surtout, je ne comprends pas la mécanique.
En fait, je souhaite faire un setState d’un widget à partir d’un autre widget.

Pour expliquer ma situation, j’ai un premier widget AdministrationScreen qui retourne un Scaffold, ce dernier contenant avec un body (widget AdminWinTypesTab) et un Floating Action Button.

Le Floating Action Button ouvre une modal bottom dont le contenu est le widget ModalBottomContent.
Ce dernier contient une colonne avec le titre et un autre widget, AdminWinTypesColumn.

Ce que je souhaite faire, c’est un setState de ma liste (widget AdminWinTypesTab) une fois que je valide et que la fonction qui modifie mes données est exécutée.

Je vous mets le code ci-dessous, en espérant qu’il ne soit pas trop indigeste.

import 'package:flutter/material.dart';

import '../datas/datas.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test Refresh State',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AdministrationScreen(),
    );
  }
}

class AdministrationScreen extends StatefulWidget {
  const AdministrationScreen({super.key});
  @override
  State<AdministrationScreen> createState() => _AdministrationScreenState();
}

class _AdministrationScreenState extends State<AdministrationScreen>
    with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: const Text('Test'),
      ),
      body: const AdminWinTypesTab(),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showModalBottomSheet(
            isScrollControlled: true,
            context: context,
            builder: (BuildContext context) {
              return SizedBox(
                height: MediaQuery.of(context).size.height / 1.75,
                child: const Scaffold(
                  body: SingleChildScrollView(
                    child: SizedBox(
                      child: Padding(
                        padding: EdgeInsets.all(20.0),
                        child: ModalBottomSheetContent(
                          add: true,
                        ),
                      ),
                    ),
                  ),
                ),
              );
            },
          );
        },
        backgroundColor: Colors.green,
        child: const Icon(
          Icons.add,
        ),
      ),
    );
  }
}

class ModalBottomSheetContent extends StatefulWidget {
  final bool add;
  final String? winType;

  const ModalBottomSheetContent({
    super.key,
    required this.add,
    this.winType,
  });

  @override
  State<ModalBottomSheetContent> createState() =>
      _ModalBottomSheetContentState();
}

class _ModalBottomSheetContentState extends State<ModalBottomSheetContent> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          '${widget.add ? 'Ajouter' : 'Modifier'} dans les types de gain',
          style: const TextStyle(
            fontSize: 30,
            fontWeight: FontWeight.bold,
          ),
          textAlign: TextAlign.center,
        ),
        const SizedBox(
          height: 10,
        ),
        AdminWinTypesColumn(add: widget.add, winType: widget.winType),
      ],
    );
  }
}

class AdminWinTypesColumn extends StatefulWidget {
  final bool add;
  final String? winType;

  const AdminWinTypesColumn({
    super.key,
    required this.add,
    this.winType,
  });

  @override
  State<AdminWinTypesColumn> createState() => _AdminWinTypesColumnState();
}

class _AdminWinTypesColumnState extends State<AdminWinTypesColumn> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _nameController = TextEditingController();
  int? indexToModify;

  void _submit(bool add, int? indexToModify, String? winTypeToModify) async {
    if (_formKey.currentState!.validate()) {
      String action;
      if (add) {
        action = 'add';
      } else {
        action = 'modify';
      }

      modifyWinType(winTypeToModify!, action, indexToModify);

      if (context.mounted) {
        Navigator.of(context).pop();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(action),
            backgroundColor: Colors.green,
          ),
        );
      }
    }
    setState(() {});
  }

  @override
  void initState() {
    // Récupération des informations si modification
    if (!widget.add) {
      _nameController.text = widget.winType!;
      indexToModify = winTypes.indexOf(widget.winType!);
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: Column(
        children: [
          Form(
            key: _formKey,
            child: Column(
              children: [
                Row(
                  children: [
                    const SizedBox(
                      width: 100,
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            'Type de gain',
                          ),
                          Text(
                            '(au singulier) :',
                          ),
                        ],
                      ),
                    ),
                    Flexible(
                      child: TextFormField(
                        controller: _nameController,
                        validator: (value) {
                          if (value == null || value.trim().isEmpty) {
                            return 'Entre le nom';
                          }
                          if (value == widget.winType) {
                            return 'Le nom est inchangé';
                          }
                          return null;
                        },
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
          const SizedBox(height: 30),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  textStyle: const TextStyle(color: Colors.white),
                ),
                child: const Text('Annuler'),
              ),
              ElevatedButton(
                onPressed: () {
                  int? indexTomodify = widget.add ? null : indexToModify;
                  String? winTypeToModify = _nameController.text.trim();
                  _submit(widget.add, indexTomodify, winTypeToModify);
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green,
                  textStyle: const TextStyle(color: Colors.black),
                ),
                child: const Text('Valider'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class AdminWinTypesTab extends StatefulWidget {
  const AdminWinTypesTab({
    super.key,
  });

  @override
  State<AdminWinTypesTab> createState() => _AdminWinTypesTabState();
}

class _AdminWinTypesTabState extends State<AdminWinTypesTab> {
  Future<void> modifyWinTypesAndRefresh(
      String winType, String action, int indexToModify) async {
    Color? snackBarColor;
    String? snackBarMessage;

    await modifyWinType(winType, action, indexToModify);
    if (action == 'delete') {
      snackBarColor = Colors.red;
      snackBarMessage = 'Type de gain "$winType" supprimé';
    }
    if (context.mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            snackBarMessage!,
          ),
          backgroundColor: snackBarColor,
        ),
      );
    }
    setState(() {});
  }

  void refreshWinTypesTab() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<String>?>(
      future: getWinTypes(),
      builder: (context, snapshot) {
        if (snapshot.hasData && snapshot.data != null) {
          List<String> fetchedWinTypes = snapshot.data!;
          return ListView.builder(
            itemCount: fetchedWinTypes.length,
            itemBuilder: (context, index) {
              String winType = fetchedWinTypes[index];
              return ListTile(
                tileColor: index.isOdd ? null : Colors.grey.shade300,
                title: Text(
                  winType,
                  style: const TextStyle(
                      fontSize: 20, fontWeight: FontWeight.bold),
                ),
                trailing: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    IconButton(
                        onPressed: () {
                          showModalBottomSheet(
                            isScrollControlled: true,
                            context: context,
                            builder: (BuildContext context) {
                              return SizedBox(
                                height:
                                    MediaQuery.of(context).size.height / 1.75,
                                child: Scaffold(
                                  body: SingleChildScrollView(
                                    child: Padding(
                                      padding: const EdgeInsets.all(20.0),
                                      child: ModalBottomSheetContent(
                                        add: false,
                                        winType: winType,
                                      ),
                                    ),
                                  ),
                                ),
                              );
                            },
                          );
                        },
                        icon: const Icon(Icons.edit, color: Colors.green)),
                    IconButton(
                      onPressed: () async {
                        await showDialog(
                          context: context,
                          builder: (context) {
                            return AlertDialog(
                              title: Column(
                                children: [
                                  const Icon(
                                    Icons.dangerous,
                                    color: Colors.red,
                                  ),
                                  Text(
                                    'Supprimer le type de gain "$winType" ?',
                                    style: const TextStyle(
                                      color: Colors.red,
                                      fontSize: 20,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ],
                              ),
                              actions: <Widget>[
                                TextButton(
                                  onPressed: () {
                                    Navigator.of(context).pop();
                                  },
                                  child: const Text(
                                    'Annuler',
                                    style: TextStyle(
                                      color: Colors.black,
                                      fontSize: 16,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ),
                                TextButton(
                                  onPressed: () {
                                    modifyWinTypesAndRefresh(
                                        winType, 'delete', index);
                                    Navigator.of(context).pop();
                                  },
                                  child: const Text(
                                    'Supprimer',
                                    style: TextStyle(
                                      color: Colors.red,
                                      fontSize: 16,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ),
                              ],
                            );
                          },
                        );
                      },
                      icon: const Icon(
                        Icons.delete_forever,
                        color: Colors.red,
                      ),
                    ),
                  ],
                ),
              );
            },
          );
        } else if (snapshot.hasError) {
          return Center(child: Text('Erreur: ${snapshot.error}'));
        } else {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }
}

Merci beaucoup pour votre aide.

Fabien

Salut @Fabien !

Ton soucis cache un problème plus profond : le State management dans Flutter.
La majorité des tutos en ligne couvrent la création d’écrans individuels omniscients : l’écran du tuto connait toutes les données et n’a pas à communiquer avec les autres parties de l’app.
C’est pratique pour expliquer un concept précis, mais lorsqu’on doit ensuite créer une app avec plusieurs écrans ça devient plus compliqué :slight_smile:

En règle générale ce que tu veux faire, est absolument interdit : un widget ne doit jamais appeler le setState d’un autre widget.

Pour trouver une solution viable à ton problème, il y a 2 solutions que je peux te recommander :

  • soit ton 2eme écran est plutôt un petit widget et non un écran à part entière et tu peux voir le dernier chapitre que j’ai ajouté dans le cours Flutter : Créer un widget réutilisable | Purple Giraffe . Tu y verras la notion de callback et tu as besoin de ça pour permettre à ton écran principal d’appeler lui-même son setState au bon moment
  • soit ton 2eme écran est un véritable écran et tu veux pouvoir communiquer efficacement entre plusieurs écrans de ton app et je te conseille ce cours : Flutter : Architecture et Navigation | Purple Giraffe

J’espère que ça pourra t’aider :slight_smile:

Happy coding!

1 « J'aime »

Bonjour,

Merci @mbritto pour ta réponse.
Je vais partir sur la solution la plus rapide à mettre en place (la première je pense), quitte à revenir dessus un peu plus tard.
Bonne journée.

Fabien

1 « J'aime »