Flutter BLoC pattern Form validation with StreamBuilder and a TextFormField is not handling text properly

Viewed 265

I am trying to use BLoC for a Form validation using rxdart and provider.

I have trouble with the TextFormField.

Basic use case is TextFormField displays the property name from an object collected from a repo, clicking on Done button the new value is saved in the repo. A validation is performed to ensure length cannot be less than xx characters.

Structure is :

  • App
    • Button
      • Page
        • Provider of bloc
          • Tabs
            • Tab1
              • StreamBuilder (linked to bloc.name)
                • TextFormField
            • Tab2
              • Text

My problems:

  • If I use TextEditingController for TextFormField (as-in-the-code)
    • validation failure makes the text to be completely reset
    • once validation failed once impossible to enter data
  • If I don't put TextEditingController to TextFormField
    • initialValue is never displayed
    • going from Tab1 -> Tab2 -> Tab1 : the text manually entered disappeared

I don't see how to have this simple use case working properly :( Any idea of what can be the problem ?

Some code, sorry a bit long:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  rxdart: ^0.27.2
  provider: ^6.0.1

simple_bloc.dart (repo+bloc)

import 'dart:async';
import 'package:rxdart/rxdart.dart';

class Simple
{
  String name;
  Simple(this.name);
}

var globalSimple = Simple("Small");

class SimpleRepository {
  Future<Simple> get() async => Future.value(globalSimple);

  Future update(Simple simple) async {
    return Future.delayed(const Duration(seconds:2), () {
      globalSimple.name = simple.name;
    });
  }
}

class SimpleBloc {
  final SimpleRepository repo;
  SimpleBloc(this.repo);

  final _name = BehaviorSubject<String>();

  Stream<String> get name => _name.stream.transform(validateName);
  Stream<bool> get isValid => Rx.combineLatest([name],(value) => true);

  Function(String) get changeName => _name.sink.add;

  Future initializedAsync() async {
    var setting = await repo.get();
    _name.add(setting.name);
  }

  dispose() {
    _name.close();
  }

  final validateName = StreamTransformer<String, String>.fromHandlers(
      handleData: (settingsName, sink) {
    if (settingsName.length < 7) {
      sink.addError("Name must be at least 7 characters");
    } else {
      sink.add(settingsName);
    }
  });

  final validateAutomaticSend =
      StreamTransformer<bool, bool>.fromHandlers(handleData: (value, sink) {
    sink.add(value);
  });

  submit() async {
    var setting = await repo.get();
    setting.name = _name.value;
    repo.update(setting);
  }
}

main.dart

import 'package:demo/simple_bloc.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              child: const Text("Open"),
              onPressed: () { 
                Navigator.push(context,
                MaterialPageRoute(builder: (context) => const SimpleRoute()),);
              }),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Provider(
        create: (context) => SimpleBloc(SimpleRepository()),
        child: const SimpleRouteTabWidget());
  }
}

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

  @override
  Widget build(BuildContext context) {
    var bloc = Provider.of<SimpleBloc>(context);
    return FutureBuilder(
        future: bloc.initializedAsync(),
        builder: (_builder, AsyncSnapshot _snapshot) {
          if (_snapshot.connectionState == ConnectionState.done) {
            if (_snapshot.error != null) {
              return const Scaffold(
                body: Center(
                  child: Text('Error occured :/'),
                ),
              );
            } else {
              return _finalBuilder(context, bloc);
            }
          } else {
            return const Scaffold(
              body: Center(
                child: Text('Please wait...'),
              ),
            );
          }
        });
  }

  Widget _finalBuilder(BuildContext ctx, SimpleBloc bloc) {
    return DefaultTabController(
        length: 2,
        child: Scaffold(
          body: TabBarView(children: [
            Container(
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const SizedBox(
                        height: 20,
                      ),
                      StreamBuilder<String>(
                        stream: bloc.name,
                        builder: (_context, _snapshot) {
                          return TextFormField(
                            //initialValue: _snapshot.data,
                              controller: TextEditingController.fromValue(TextEditingValue(text: _snapshot.data ?? "",selection: TextSelection.collapsed(offset: (_snapshot.data ?? "").length))),
                              onChanged: bloc.changeName,
                              decoration: InputDecoration(
                                  helperText: "Name",
                                  errorText: _snapshot.error?.toString()));
                        },
                      )
                    ])),
            Container(
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                child: const Text("Something else")),
          ]),
          appBar: AppBar(
            title: const Text('Parameters'),
            actions: [
              StreamBuilder<bool>(stream: bloc.isValid,
                  builder: (context, snapshot) {
                    return IconButton(
                      icon: const Icon(Icons.done),
                      onPressed: snapshot.hasData
                          ? () {
                              bloc.submit();
                              Navigator.popUntil(context, ModalRoute.withName('/'));
                            }
                          : null,
                    );
                  })
            ],
            bottom: const TabBar(
              tabs: [
                Tab(
                  text: "General",
                  icon: Icon(Icons.directions_car),
                ),
                Tab(
                  text: "Report",
                  icon: Icon(Icons.summarize),
                )
              ],
            ),
          ),
        ));
  }
}

0 Answers
Related