Flutter Dynamic TextField creation with Riverpod

eye-catch Dart and Flutter

I have posted the following article before. The code uses neither Riverpod nor Provider. It is implemented by using StatefulWidget. I implement it with Riverpod this time, so I will show you how to do it.

The Riverpod version is 1.0.0. If you want to check the code for 0.14.0+3, open the toggle.

Sponsored links

NG example to handle List data with Provider

Look at the following code first. This code doesn’t work properly.

class _View1 extends ConsumerWidget {
  final _provider =
      Provider.autoDispose<List<TextEditingController>>((ref) => []);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          context.read(_provider).add(TextEditingController());
        },
        icon: Icon(Icons.add),
      ),
    );

    final listView = ListView.builder(
      itemCount: watch(_provider).length,
      itemBuilder: (context, index) {
        return ListTile(
          leading: IconButton(
            onPressed: () {
              final list = context.read(_provider);
              list.remove(list[index]);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: watch(_provider)[index],
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name ${watch(_provider).length + 1}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}
class _View1 extends ConsumerWidget {
  final _provider =
      Provider.autoDispose<List<TextEditingController>>((ref) => []);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          ref.read(_provider).add(TextEditingController());
        },
        icon: Icon(Icons.add),
      ),
    );

    final listView = ListView.builder(
      itemCount: ref.watch(_provider).length,
      itemBuilder: (context, index) {
        return ListTile(
          leading: IconButton(
            onPressed: () {
              final list = ref.read(_provider);
              list.remove(list[index]);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: ref.watch(_provider)[index],
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name ${ref.watch(_provider).length + 1}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}

TextField doesn’t appear!

The following code gets the current list of TextEditingController and adds a new one to it. We expect that this code notifies the state change to the consumer. However, it doesn’t work.

final button = Center(
    child: IconButton(
    onPressed: () {
        context.read(_provider).add(TextEditingController());
    },
    icon: Icon(Icons.add),
    ),
);
final button = Center(
    child: IconButton(
    onPressed: () {
        ref.read(_provider).add(TextEditingController());
    },
    icon: Icon(Icons.add),
    ),
);

Why doesn’t it work? Because it adds an item to the list that is an internal state. If the internal state changes provider doesn’t notify it. We somehow need to change the variable’s state.

Sponsored links

How to notify list state change

Define StateNotifier class

As I said, we somehow need to notify the internal state change. In this case, we want to notify the value change when a new item is added. Let’s define the add function by ourselves. We need to extend StateNotifier class for it.

class ControllerList extends StateNotifier<List<TextEditingController>> {
  final List<TextEditingController> _disposedList = [];
  ControllerList({String? text}) : super([TextEditingController(text: text)]);

  @override
  void dispose() {
    print("DISPOSE===========");
    for (final target in _disposedList) {
      target.dispose();
    }
    for (final target in state) {
      target.dispose();
    }
    super.dispose();
  }

  void add({String? text}) {
    state = [...state, TextEditingController(text: text)];
  }

  void remove(int index) {
    if (index < 0 || index >= state.length) {
      return;
    }
    final target = state[index];
    _disposedList.add(target);
    state.remove(target);
    state = [...state];
  }
}

When state variable is updated, it notifies the change to the consumer. The initial value of the state variable is defined in super constructor call. In this case, it is a list that contains a TextEditingController.

ControllerList({String? text}) : super([TextEditingController(text: text)]);

Three dots is used in the function. It expands the array.

// it's the same as [state[0], state[1], state[2]...];
state = [...state];

Let’s created add and remove functions. We want to notify the change for each function, so state is used in both of them.

void add({String? text}) {
  state = [...state, TextEditingController(text: text)];
}

void remove(int index) {
  if (index < 0 || index >= state.length) {
    return;
  }
  final target = state[index];
  _disposedList.add(target);
  state.remove(target);
  state = [...state];
}

TextEditingController.dispose() is not called in the remove function. If dispose() is called in the function, the following error occurs.

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building MouseRegion(listeners: [enter, exit], cursor: SystemMouseCursor(text), state: _MouseRegionState#34b04):
A TextEditingController was used after being disposed.

Once you have called dispose() on a TextEditingController, it can no longer be used.
The relevant error-causing widget was
TextField
lib\riverpod\dynamic_textfield_with_riverpod.dart:118

So the no longer necessary item is added to _disposedList in the remove function. They all are disposed in this function.

@override
void dispose() {
  print("DISPOSE===========");
  for (final target in _disposedList) {
    target.dispose();
  }
  for (final target in state) {
    target.dispose();
  }
  super.dispose();
}

Use the StateNotifier class

We created StateNotifier class. The next step is to use it.

final _controllerListProvider = StateNotifierProvider.autoDispose<
    ControllerList, List<TextEditingController>>((ref) => ControllerList());

With autoDispose, the data in the Provider is automatically disposed of when the Provider is no longer used. For example, when the view is switched to another view. Without this, it keeps the provider state. Therefore, when we change the view and then go back to the view it shows the same state as before but dispose function defined above in the previous section isn’t called. It causes a memory leak.

The following code uses the provider. Other than that, it’s the same structure as the previous one.

class _View2 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          // .notifier is necessary to get the value
          context.read(_controllerListProvider.notifier).add();
        },
        icon: Icon(Icons.add),
      ),
    );

    final list = watch(_controllerListProvider);
    final listView = ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        final controller = list[index];
        print("$index: ${controller.text}");
        return ListTile(
          leading: IconButton(
            onPressed: () {
              // .notifier is necessary to get the value
              context.read(_controllerListProvider.notifier).remove(index);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name $index / ${list.length}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod2")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}

class _View2 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          ref.read(_controllerListProvider.notifier).add();
        },
        icon: Icon(Icons.add),
      ),
    );

    final list = ref.watch(_controllerListProvider);
    final listView = ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        final controller = list[index];
        print("$index: ${controller.text}");
        return ListTile(
          leading: IconButton(
            onPressed: () {
              ref.read(_controllerListProvider.notifier).remove(index);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name $index / ${list.length}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod2")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}

It works as expected.

Dispose the resource immediately when it’s removed

In a previous way, the resource is disposed of when the view is switched to another view. On the other hand, the resource is immediately disposed of in this way when one of TextField is removed.

class ControllerList2
    extends StateNotifier<List<AutoDisposeProvider<TextEditingController>>> {
  ControllerList2({String? text}) : super([]);

  AutoDisposeProvider<TextEditingController> _createControllerProvider() {
    return Provider.autoDispose((ref) => TextEditingController());
  }

  void add() {
    state = [...state, _createControllerProvider()];
  }

  void remove(int index) {
    if (index < 0 || index >= state.length) {
      return;
    }
    final target = state[index];
    state.remove(target);
    state = [...state];
  }
}

The point here is to use Provider.autoDispose in the named constructor. Dispose is called when the object is removed in the remove function because it is no longer used in the class.

The class is used in the following code. The structure is the same as the previous one.

class _View3 extends ConsumerWidget {
  final _controllerList2Provider = StateNotifierProvider<ControllerList2,
          List<AutoDisposeProvider<TextEditingController>>>(
      (ref) => ControllerList2());

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          final list = context.read(_controllerList2Provider.notifier);
          list.add();
        },
        icon: Icon(Icons.add),
      ),
    );

    final list = watch(_controllerList2Provider);
    final listView = ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        return ListTile(
          leading: IconButton(
            onPressed: () {
              context.read(_controllerList2Provider.notifier).remove(index);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: watch(list[index]),
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name $index / ${list.length}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod3")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}
class _View3 extends ConsumerWidget {
  final _controllerList2Provider = StateNotifierProvider<ControllerList2,
          List<AutoDisposeProvider<TextEditingController>>>(
      (ref) => ControllerList2());

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = Center(
      child: IconButton(
        onPressed: () {
          final list = ref.read(_controllerList2Provider.notifier);
          list.add();
        },
        icon: Icon(Icons.add),
      ),
    );

    final list = ref.watch(_controllerList2Provider);
    final listView = ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        return ListTile(
          leading: IconButton(
            onPressed: () {
              ref.read(_controllerList2Provider.notifier).remove(index);
            },
            icon: Icon(Icons.remove_from_queue),
          ),
          title: TextField(
            controller: ref.watch(list[index]),
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: "name $index / ${list.length}",
            ),
          ),
        );
      },
    );

    return Scaffold(
      appBar: AppBar(title: Text("Dynamic TextField Riverpod3")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
    );
  }
}

End

Complete code is here.

https://github.com/yuto-yuto/flutter_samples/blob/main/lib/riverpod/dynamic_textfield_with_riverpod.dart

If you need to show list data with ListView, check the following article as well.

Comments

  1. Taha Aslam says:

    What changes do I need to make when implementing with the Provider?

Copied title and URL