flutterflutter-providerflutter-pageviewflutter-textformfieldtexteditingcontroller

How to Manage TextEditingControllers Across Multiple Pages in a PageView Using Provider?


I am building a Flutter application that includes an extensive form for Technician registration. The form is split across multiple pages using a PageView widget, with each page containing several TextField widgets. Some of these fields are mandatory and others optional. Each page has its own set of TextEditingControllers.

I am using the provider package for state management. I want the controllers to be created anew each time I navigate into the form view, ensuring a fresh start each time while still managing state efficiently.

How can I achieve this? Below is the structure of my current implementation and the desired behavior:

Registration Page

class RegistrationPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Technician Registration')),
      body: Column(
        children: [
          Expanded(
            child: PageView(
              children: [
                Page1(),
                Page2(),
              ],
            ),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => AnotherPage(
                    controller1: Page1.page1Controller1,
                    controller2: Page1.page1Controller2,
                    controller3: Page2.page2Controller1,
                    controller4: Page2.page2Controller2,
                  ),
                ),
              );
            },
            child: Text('Go to Another Page'),
          ),
        ],
      ),
    );
  }
}

Page 1 and 2

class Page1 extends StatelessWidget {
  static final page1Controller1 = TextEditingController();  
  static final page1Controller2 = TextEditingController();

  const Page1({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: page1Controller1),
        TextField(controller: page1Controller2),
      ],
    );
  }
}

class Page2 extends StatelessWidget {
  static final page2Controller1 = TextEditingController();  
  static final page2Controller2 = TextEditingController();

  const Page2({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: page2Controller1),
        TextField(controller: page2Controller2),
      ],
    );
  }
}

I tried to keep the form data intact as I navigate between pages, I initially used static controllers. However, I faced issues when disposing of these controllers and reusing them after navigating to the form view, resulting in exceptions about disposed controllers being used.

I expect that

I am using provider for state management, I need to know the best way to achieve my desired result without getting bugged by annoying exceptions.


Solution

  • First thing first, you should never declare the controllers as static.

    Solution

    1. Create a ChangeNotifier for the form state:
    import 'package:flutter/material.dart';
    
    class RegistrationFormState extends ChangeNotifier {
      final TextEditingController page1Controller1 = TextEditingController();
      final TextEditingController page1Controller2 = TextEditingController();
      final TextEditingController page2Controller1 = TextEditingController();
      final TextEditingController page2Controller2 = TextEditingController();
    
      // Dispose all controllers
      void disposeControllers() {
        page1Controller1.dispose();
        page1Controller2.dispose();
        page2Controller1.dispose();
        page2Controller2.dispose();
      }
    
      @override
      void dispose() {
        disposeControllers();
        super.dispose();
      }
    }
    
    1. Provide the ChangeNotifier in the RegistrationPage:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    class RegistrationPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (context) => RegistrationFormState(),
          child: Scaffold(
            appBar: AppBar(title: Text('Technician Registration')),
            body: Column(
              children: [
                Expanded(
                  child: PageView(
                    children: [
                      Page1(),
                      Page2(),
                    ],
                  ),
                ),
                ElevatedButton(
                  onPressed: () {
                    final formState = Provider.of<RegistrationFormState>(context, listen: false);
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) => AnotherPage(
                          controller1: formState.page1Controller1,
                          controller2: formState.page1Controller2,
                          controller3: formState.page2Controller1,
                          controller4: formState.page2Controller2,
                        ),
                      ),
                    );
                  },
                  child: Text('Go to Another Page'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    1. Use the RegistrationFormState in Page1 and Page2:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    class Page1 extends StatelessWidget {
      const Page1({super.key});
    
      @override
      Widget build(BuildContext context) {
        final formState = Provider.of<RegistrationFormState>(context);
        return Column(
          children: [
            TextField(controller: formState.page1Controller1),
            TextField(controller: formState.page1Controller2),
          ],
        );
      }
    }
    
    class Page2 extends StatelessWidget {
      const Page2({super.key});
    
      @override
      Widget build(BuildContext context) {
        final formState = Provider.of<RegistrationFormState>(context);
        return Column(
          children: [
            TextField(controller: formState.page2Controller1),
            TextField(controller: formState.page2Controller2),
          ],
        );
      }
    }
    

    With this setup, every time you navigate to the RegistrationPage, a new instance of RegistrationFormState is created, providing fresh TextEditingController instances. These controllers are disposed of when the RegistrationPage is popped off the navigation stack, ensuring that no disposed controllers are reused.

    Note: I'm not sure why are you using passing the controllers to AnotherPage. But if you just wanna pass the input values you should pass it as:

    AnotherPage(
        controller1Text: formState.page1Controller1.text,
        controller2Text: formState.page1Controller2.text,
        controller3Text: formState.page2Controller1.text,
        controller4Text: formState.page2Controller2.text,
    )