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
- StreamBuilder (linked to bloc.name)
- Tab2
- Text
- Tab1
- Tabs
- Provider of bloc
- Page
- Button
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),
)
],
),
),
));
}
}