I'm writing a simple reminder app that is essentially a ListView of TextFields that, when blurred or submitted, update the database. I use a bunch of GestureDetectors and FocusNodes to blur the TextField when a user taps on the checkbox or outside the TextField.
It works very well when this is the only route. However, when I push the same exact page on top of the existing one, the focus behavior becomes completely buggy and the app unusable.
Here's a video to demonstrate: https://www.youtube.com/watch?v=13E9LY8yD3A
My code is essentially this:
/// main.dart
class MyApp extends StatelessWidget {
static FocusScopeNode rootScope; // just for debug
#override
Widget build(BuildContext context) {
rootScope = FocusScope.of(context);
return MaterialApp(home: ReminderPage());
}
}
-
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
_blur(context);
},
child: ListView(
children: reminders.map((r) => ReminderCard(r)).toList(),
),
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
class ReminderCard extends StatelessWidget {
final Reminder reminder;
final TextEditingController _controller;
final _focusNode = FocusNode();
final _blurNode = FocusNode();
ReminderCard(this.reminder)
: _controller = TextEditingController(text: reminder.text) {
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
_blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: reminder.done,
onChanged: (done) {
print(MyApp.rootScope.toStringDeep()); // print Focus tree
_blur(context);
reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
onSubmitted: reminder.updateText, // update database
focusNode: _focusNode,
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
I found this question which sounds very similar, but I didn't understand how the custom transition solves anything and has anything to do with focus. And like the OP, I tried a lot of different things to mess with FocusScope, include call detach(), reparentIfNeeded(), or passing the FocusScope of the root all the way down so a new FocusScope is not created each time, but none of those gave anything close to working. And I also tried the custom transition, to no avail.
The debug output shows this on the first route (when I check boxes):
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f07c7(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f138f(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#e68b3(FOCUSED)
And this on the second route:
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#02ebf(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#917da(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
So it looks like the FocusScope of the first route becomes child 2 when we push the second route, which sounds correct to me.
What am I doing wrong?
Thanks to Lucas' comments above and this other SO question I was able to fix the problem.
First, I reduced the number of FocusNodes: just one per TextField, and one for the parent ReminderPage. The parent now has a function blur() that unfocuses all TextFields; that way, when I click the checkbox of a TextField while editing another, the one being edited is unfocused.
Second, I changed my reminder.updateText() function (not shown here) so it only updates the database when the text is different from the existing text. Otherwise, we would be rebuilding the card because of the StreamBuilder, messing up the focus of the TextField being edited.
Third, I'm now listening to the TextEditingController instead of the FocusNode to make changes to the database. But I still only update the database when the FocusNode is unfocused, otherwise the StreamBuilder would rebuild the page and mess up with focus again.
But that still doesn't explain why it works reasonably well when the ReminderPage is the homepage of the app, and not when it's pushed on top of a route. The answer comes from this other SO question which was hitting the same issue: the widget was constantly rebuilt when placed after a splash screen, but not when used as the app homepage. I still don't understand why this makes any difference, but the same fix worked for me: change it to StatefulWidget and only rebuild when something actually changed.
The final code looks like this. I highlighted the diffs with // ---> comments.
/// main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: ReminderPage());
}
}
-
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
blur(context);
},
child: ListView(
// ---> Passing the parent to each child so they can call parent.blur()
children: reminders.map((r) => ReminderCard(r, this)).toList(),
),
);
}
// ---> This will unfocus all TextFields.
void blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
// ---> Converted to a StatefulWidget! That way we can save a snapshot of reminder
// as it was when we last built the widget, and only rebuild it if it changed.
class ReminderCard extends StatefulWidget {
final Reminder reminder;
final TextEditingController _controller;
// ---> Only one focus node, for the TextField.
final _focusNode = FocusNode();
// ---> The parent.
final ReminderPage page;
ReminderCard(this.reminder, this.page)
: _controller = TextEditingController(text: reminder.text) {
// ---> Listen to text changes. But only updating the database
// if the TextField is unfocused.
_controller.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
#override
ReminderCardState createState() => ReminderCardState();
}
class ReminderCardState extends State<ReminderCard> {
Widget card;
Reminder snapshotWhenLastBuilt;
#override
Widget build(BuildContext context) {
// ---> Only rebuild if something changed, otherwise return the
// card built previously.
// The equals() function is a method of the Reminder class that just tests a
// few fields.
if (card == null || !widget.reminder.equals(snapshotWhenLastBuilt)) {
card = _buildCard(context);
snapshotWhenLastBuilt = widget.reminder;
}
return card;
}
Widget _buildCard(context) {
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
widget.page.blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: widget.reminder.done,
onChanged: (done) {
// ---> Blur all TextFields when clicking on a checkbox.
widget.page.blur(context);
widget.reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
focusNode: widget._focusNode,
controller: widget._controller,
);
}
}
Related
I am just trying out flutter and I cannot seem to get components to render conditionally based on a BehaviourStream in my Bloc.
I wish to initially show the "_buildPage()" widget (which is an auth form), then while _isLoading is true but (_loginSucceded is false) I wish to show the spinner. Lastly, when _loginSucceded is true and _isLoading is false, I wish to redirect the user.
Actual behaviour is once form is submitted loader shows as expected. Once the response is successfully received from the server however the auth for is rendered once again.
I think my logic is fine but it seems that when I set the values of the stream in the Bloc constructor something else is causing the app to rerender which results in null values in the stream.
Is there a way to ensure a stream always has base values after the constructor has run upon initialisation?
Or is there a better way to manage this scenario? I have only just started looking at Flutter from a JS background so I may well be missing something.
Bloc code:
import 'dart:async';
import 'dart:convert';
import 'package:rxdart/rxdart.dart';
import 'package:http/http.dart' as http;
import './auth_validator.dart';
class AuthBloc with AuthValidator {
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
final _isLoading = BehaviorSubject<bool>();
final _loginSucceded = BehaviorSubject<bool>();
AuthBloc() {
_isLoading.sink.add(false);
_loginSucceded.sink.add(false);
}
// stream getters
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get isLoading => _isLoading.stream;
Stream<bool> get loginSuccess => _loginSucceded.stream;
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// add data to sink onChange
Function(String) get emailChanged => _email.sink.add;
Function(String) get passwordChanged => _password.sink.add;
void submitForm() async {
try {
final Map user = {'email': _email.value, 'password': _password.value};
final jsonUser = json.encode(user);
_isLoading.sink.add(true);
// submit to server
final http.Response response = await http.post(
'http://192.168.1.213:5000/api/users/signin',
body: jsonUser,
headers: {'Content-Type': 'application/json'},
);
final Map<String, dynamic> decodedRes = await json.decode(response.body);
_isLoading.sink.add(false);
_loginSucceded.sink.add(true);
void dispose() {
_email.close();
_password.close();
_isLoading.close();
_loginSucceded.close();
}
} catch (e) {
print('error: $e');
_isLoading.sink.add(false);
}
}
}
Widget code:
import 'package:flutter/material.dart';
import '../blocs/auth_bloc.dart';
class LoginPage extends StatelessWidget {
final authBloc = AuthBloc();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authBloc.loginSuccess,
builder: (context, snapshot1) {
return StreamBuilder(
stream: authBloc.isLoading,
builder: (context, snapshot2) {
print('loginSuccess? ${snapshot1.data} isLoading? ${snapshot2.data}');
return Scaffold(
body: !snapshot1.data && snapshot2.data
? _circularSpinner()
: snapshot1.data && snapshot2.data
? Navigator.pushReplacementNamed(context, '/dashboard')
: _buildPage());
},
);
},
);
}
Widget _buildPage() {
return Container(
margin: EdgeInsets.all(20.0),
child: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
_emailField(authBloc),
_padding(),
_passwordField(authBloc),
_padding(),
_submitButton(authBloc)
],
),
),
),
);
}
Widget _circularSpinner() {
return Center(
child: CircularProgressIndicator(),
);
}
Widget _emailField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.email,
builder: (BuildContext context, snapshot) {
return TextField(
onChanged: authBloc.emailChanged,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
);
},
);
}
Widget _passwordField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.password,
builder: (BuildContext context, snapshot) {
return TextField(
onChanged: authBloc.passwordChanged,
obscureText: true,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: '8 characters or more with at least 1 number',
labelText: 'Password',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
);
},
);
}
Widget _padding() {
return Padding(
padding: EdgeInsets.only(top: 20.0),
);
}
Widget _submitButton(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: snapshot.hasError ? null : authBloc.submitForm,
);
});
}
}
main.dart
import 'package:flutter/material.dart';
import './app.dart';
import 'package:flutter/material.dart';
import './pages/auth.dart';
import './pages/dashboard.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (BuildContext context) => LoginPage(),
'/dashboard': (BuildContext context) => DashBoardPage(),
},
);
}
}
Log
Restarted application in 1,462ms.
I/flutter ( 4998): loginSuccess? false isLoading? false
I/flutter ( 4998): loginSuccess? null isLoading? null
I/flutter ( 4998): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 4998): The following assertion was thrown building StreamBuilder<bool>(dirty, state:
I/flutter ( 4998): _StreamBuilderBaseState<bool, AsyncSnapshot<bool>>#34870):
I/flutter ( 4998): Failed assertion: boolean expression must not be null
I/flutter ( 4998):
I/flutter ( 4998): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter ( 4998): more information in this error message to help you determine and fix the underlying cause.
I/flutter ( 4998): In either case, please report this assertion by filing a bug on GitHub:
I/flutter ( 4998): https://github.com/flutter/flutter/issues/new?template=BUG.md
I/flutter ( 4998):
I/flutter ( 4998): When the exception was thrown, this was the stack:
I/flutter ( 4998): #0 LoginPage.build.<anonymous closure>.<anonymous closure>
as user #user10539074 mentioned, you need to set initial data for the first run of the streambuilder - ie. in your code - if authBloc.isLoading is bool, than change:
StreamBuilder(
stream: authBloc.isLoading,
builder: (context, snapshot2) {
to:
StreamBuilder(
stream: authBloc.isLoading,
initialData: true,
builder: (context, snapshot2) {
I am trying to get an auth form to conditionally redirect in a flutter StreamBuilder widget using a ternary statement.
When the redirect condition returns true I get a red screen and the following log:
I/flutter ( 3787): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 3787): The following assertion was thrown building StreamBuilder<Map<dynamic, dynamic>>(dirty, state:
I/flutter ( 3787): _StreamBuilderBaseState<Map<dynamic, dynamic>, AsyncSnapshot<Map<dynamic, dynamic>>>#66400):
I/flutter ( 3787): setState() or markNeedsBuild() called during build.
I/flutter ( 3787): This Overlay widget cannot be marked as needing to build because the framework is already in the
I/flutter ( 3787): process of building widgets. A widget can be marked as needing to be built during the build phase
I/flutter ( 3787): only if one of its ancestors is currently building. This exception is allowed because the framework
I/flutter ( 3787): builds parent widgets before children, which means a dirty descendant will always be built.
I/flutter ( 3787): Otherwise, the framework might not visit this widget during this build phase.
I/flutter ( 3787): The widget on which setState() or markNeedsBuild() was called was:
I/flutter ( 3787): Overlay-[LabeledGlobalKey<OverlayState>#4f97a](state: OverlayState#5df28(tickers: tracking 2
I/flutter ( 3787): tickers, entries: [OverlayEntry#09e48(opaque: false; maintainState: false),
I/flutter ( 3787): OverlayEntry#61a61(opaque: false; maintainState: true), OverlayEntry#79842(opaque: false;
I/flutter ( 3787): maintainState: false), OverlayEntry#11ff2(opaque: false; maintainState: true)]))
I/flutter ( 3787): The widget which was currently being built when the offending call was made was:
I/flutter ( 3787): StreamBuilder<Map<dynamic, dynamic>>(dirty, state: _StreamBuilderBaseState<Map<dynamic, dynamic>,
I/flutter ( 3787): AsyncSnapshot<Map<dynamic, dynamic>>>#66400)
Offending widget:
import 'package:flutter/material.dart';
import '../blocs/auth_bloc.dart';
class LoginPage extends StatelessWidget {
final authBloc = AuthBloc();
Map<String, bool> initialData = {'loginSuccess': false, 'isLoading': false};
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authBloc.redirect,
initialData: initialData,
builder: (context, snapshot) {
return Scaffold(body: _render(context, snapshot));
});
}
Widget _render(BuildContext context, AsyncSnapshot snapshot) {
return !snapshot.data['loginSuccess'] && snapshot.data['isLoading']
? _circularSpinner()
: snapshot.data['loginSuccess'] && !snapshot.data['isLoading']
? _redirect(context)
: _buildPage();
}
_redirect(BuildContext context) {
return Navigator.pushReplacementNamed(context, '/dashboard');
}
Widget _buildPage() {
return Container(
margin: EdgeInsets.all(20.0),
child: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
_emailField(authBloc),
_padding(),
_passwordField(authBloc),
_padding(),
_submitButton(authBloc)
],
),
),
),
);
}
Widget _circularSpinner() {
return Center(
child: CircularProgressIndicator(),
);
}
Widget _emailField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.email,
builder: (BuildContext context, snapshot) {
return TextField(
onChanged: authBloc.emailChanged,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you#example.com',
labelText: 'Email Address',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
);
},
);
}
Widget _passwordField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.password,
builder: (BuildContext context, snapshot) {
return TextField(
onChanged: authBloc.passwordChanged,
obscureText: true,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: '8 characters or more with at least 1 number',
labelText: 'Password',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
);
},
);
}
Widget _padding() {
return Padding(
padding: EdgeInsets.only(top: 20.0),
);
}
Widget _submitButton(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.submitValid,
builder: (context, snapshot) {
return RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: snapshot.hasError ? null : authBloc.submitForm,
);
});
}
}
I have googled but cannot find anything relating to Navigator in this context.
I expect the Widget to redirect to the 'dashboard' widget. Instead I get red error screen.
I run today into the same problem. I tried the solution from #user8467470 but StreamBuilder is already registered to my Stream. So I ended up with this solution:
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<Response<ExchangeRate>>(
stream: _bloc.exchangeDataStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
switch (snapshot.data.status) {
case Status.LOADING:
return LoadingWidget(loadingMessage: snapshot.data.message);
case Status.COMPLETED:
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacementNamed(context, '/home');
});
break;
case Status.ERROR:
return Container(
child: Text(snapshot.data.message),
);
}
}
return Container();
}));
}
This way I have only one stream in my bloc.
I have found a work around but it isn't pretty. I thought I would share it here in case anyone else was having the same problem but I of course expect other more experienced Flutter devs to offer a better solution and rationale.
It seems that for whatever reason StreamBuilder does not like you Navigating away in the builder function due to its build flow. As a result I moved the navigate away outside of the StreamBuilder but inside the build method and added a listener like this:
#override
Widget build(BuildContext context) {
// Manually listening for redirect conidtions here
// once a response is received from the server
authBloc.loginSuccess.listen((data) {
if (data) {
Navigator.pushReplacementNamed(context, '/dashboard');
}
});
return StreamBuilder(
stream: authBloc.isLoading,
initialData: false,
builder: (context, snapshot) {
print(snapshot.data);
return Scaffold(
body: snapshot.data ? _circularSpinner() : _buildPage(),
);
});
}
It seems to work now but will keep my eye on best practice for this.
I'm trying to follow the Computer Vision with ML Kit - Flutter In Focus tutorial, where I followed the tutorial step-by step, and still didn't manage to make it work.
my code is as follow:
import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:async';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: FacePage(),
);
}
}
class FacePage extends StatefulWidget{
#override
createState() => _FacePageState();
}
class _FacePageState extends State<FacePage>{
File _imageFile;
List<Face> _faces;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Face Detector'),
),
body: ImageAndFaces(),
floatingActionButton: FloatingActionButton(
onPressed: _getImageAndDetectFace,
tooltip: 'Pick an Image',
child: Icon(Icons.add_a_photo),
),
);
}
void _getImageAndDetectFace() async {
final imageFile = await ImagePicker.pickImage(
source: ImageSource.gallery,
);
final image = FirebaseVisionImage.fromFile(imageFile);
final faceDetector = FirebaseVision.instance.faceDetector(
FaceDetectorOptions(
mode: FaceDetectorMode.accurate,
enableLandmarks: true,
),
);
List<Face> faces = await faceDetector.detectInImage(image);
if(mounted) {
setState(() {
_imageFile = imageFile;
_faces = faces;
});
}
}
}
class ImageAndFaces extends StatelessWidget {
ImageAndFaces({this.imageFile, this.faces});
final File imageFile;
final List<Face> faces;
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Flexible(
flex: 2 ,
child: Container(
constraints: BoxConstraints.expand(),
child: Image.file(imageFile, fit: BoxFit.cover),
),
),
Flexible(flex: 1 ,
child: ListView(
children: faces.map<Widget>((f) => FaceCoordinates(f)).toList(),
),
),
],
);
}
}
class FaceCoordinates extends StatelessWidget {
FaceCoordinates(this.face);
final Face face;
#override
Widget build(BuildContext context) {
final pos = face.boundingBox;
return ListTile(
title: Text('(${pos.top}, ${pos.left}, ${pos.bottom}, ${pos.right})'),
);
}
}
I'm getting the following exception stack:
I/flutter ( 5077): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 5077): The following assertion was thrown building ImageAndFaces(dirty):
I/flutter ( 5077): 'package:flutter/src/painting/image_provider.dart': Failed assertion: line 532 pos 14: 'file !=
I/flutter ( 5077): null': is not true.
I/flutter ( 5077):
I/flutter ( 5077): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter ( 5077): more information in this error message to help you determine and fix the underlying cause.
I/flutter ( 5077): In either case, please report this assertion by filing a bug on GitHub:
I/flutter ( 5077): https://github.com/flutter/flutter/issues/new?template=BUG.md
I/flutter ( 5077):
I/flutter ( 5077): When the exception was thrown, this was the stack:
I/flutter ( 5077): #2 new FileImage (package:flutter/src/painting/image_provider.dart:532:14)
I/flutter ( 5077): #3 new Image.file (package:flutter/src/widgets/image.dart:254:16)
I/flutter ( 5077): #4 ImageAndFaces.build (package:visionappwork/main.dart:94:28)
I/flutter ( 5077): #5 StatelessElement.build (package:flutter/src/widgets/framework.dart:3789:28)
I/flutter ( 5077): #6 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3736:15)
.........
Does anybody know what the problem is?
I tried anything I can think of, including trying to catch the 'ImageAndFaces' class constructor as it creates an instance, without any success.
I'm new to flutter and dart, so maybe it's a stupid mistake.
Thanks a lot!
The reason you're having a problem is that imageFile starts out null. Since it's being passed in to Image.file(imageFile, fit: BoxFit.cover) you're seeing the failure due to the assertion that the file passed to Image.file is not null.
You need to add some logic to check whether imageFile is null and do something different if it is.
I'm building and App where I'm using 2 StreamBuilders (one inside another).
The outer one consumes an Stream<List<User>> and render that list.
The inner one consumes Stream<User> where I can check if the user is favorite or not.
Here is the code:
users_page.dart
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<List<User>>(
stream: userBloc.outList,
initialData: [],
builder: (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
final List<User> users = snapshot.data;
return buildList(users);
})
}
Widget buildList(List<User> users) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
final User user = users[index];
return ListTile(
title: Text('${user.firstName}'),
trailing: buildFavoriteButton(user));
});
}
Widget buildFavoriteButton(User user) {
User oldUser = user;
return StreamBuilder<User>(
stream: userBloc.outFavorite,
initialData: oldUser,
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
final User newUser = snapshot.data;
if (oldUser.id == newUser.id) {
oldUser = newUser;
}
return IconButton(
icon: Icon(Icons.favorite, color: oldUser.isFavorite ? Colors.red : Colors.blueGrey),
onPressed: () {
print('onPressed: This is called once');
userBloc.inFavorite.add(newUser);
});
});
}
users_block.dart
class UserBloc {
final Repository _repository = Repository();
// More variables like the BehaviourSubject for outList and so on ...
final BehaviorSubject<User> _userFavoriteSubject = BehaviorSubject<User>();
Stream<User> _outFavorite = Stream.empty();
Stream<User> get outFavorite => _outFavorite;
Sink<User> get inFavorite => _userFavoriteSubject;
UserBloc() {
_outFavorite = _userFavoriteSubject.switchMap<User>((user) {
print('userBloc: This is called N times')
return user.isFavorite ? _repository.removeFromFavorite(user) : _repository.saveAsFavorite(user);
});
}
}
The outer stream is called once and the onPressed method is called once as well (as expected).
But the problem I'm having is when I press the Icon: userBloc prints N times (where N is the number of rows in the list), like I would pressed the Icon N times.
So the log is:
print: onPressed: This is called once
print: userBloc: This is called N times
print: userBloc: This is called N times
...
print: userBloc: This is called N times
In this case the action (pressing the icon) is executed once, but userBloc gets N inputs.
Why this is happening and how can I solve this problem?
Thanks in advance!
I made a test where I defined:
Widget buildBody() {
return Column(
children: <Widget>[
StreamBuilder<int>(
stream: userBloc.outState,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
print("Builder 1");
print("Snapshot 1: " + snapshot.data.toString());
return (IconButton(
icon: Icon(Icons.favorite, color: Colors.red),
onPressed: () {
print("onPressed 1");
userBloc.inEvents.add(1);
}));
},
),
StreamBuilder<int>(
stream: userBloc.outState,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
print("Builder 2");
print("Snapshot 2: " + snapshot.data.toString());
return (IconButton(
icon: Icon(Icons.favorite, color: Colors.red),
onPressed: () {
print("onPressed 2");
userBloc.inEvents.add(2);
}));
},
)
],
);
And the stream:
_outState = _userSubject.switchMap<int>(
(integer) {
print("Input (sink): " + integer.toString());
return doSomething(integer);
},
);
When I run this code and click the IconButton 1, this is the output:
I/flutter ( 3912): Builder 1
I/flutter ( 3912): Snapshot 1: 0
I/flutter ( 3912): Builder 2
I/flutter ( 3912): Snapshot 2: 0
I/flutter ( 3912): onPressed 1
I/flutter ( 3912): Input (sink): 1
I/flutter ( 3912): Input (sink): 1
I/flutter ( 3912): Builder 1
I/flutter ( 3912): Snapshot 1: 1
I/flutter ( 3912): Builder 2
I/flutter ( 3912): Snapshot 2: 1
As you can see the print "Input (sink): 1" is shown twice.
So for any input to the sink the code inside subject is executed n times, depending on the amount of StreamBuilders subscribed to the stream.
Is this behaviour okay, or is it a bug?
I know that the builder function should be call twice because any change in the stream is forwarded to all StreamBuilder subscribed, but the code inside the subject should be call twice too?
fifth.dart
import 'package:flutter/material.dart';
import 'package:emas_app/Dependant.dart' as Dep;
import 'package:emas_app/crm_dependent_list_model.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:emas_app/crm_dep_entitlement_model.dart';
final String url = "http://crm.emastpa.com.my/MemberInfo.json";
//Future to get list of dependent names
Future<List<DependantModel>> fetchUserInfo() async{
http.Response response = await http.get(url);
var responsejson = json.decode(response.body);
return(responsejson[0]['Dependents'] as List)
.map((user) => DependantModel.fromJson(user))
.toList();
}
class Fifth extends StatefulWidget {
#override
_FifthState createState() => _FifthState();
}
class _FifthState extends State<Fifth> {
static Future<List<DependantModel>> depState;
#override
void initState() {
depState = fetchUserInfo();
super.initState();
}
#override
Widget build(BuildContext context) {
//ListView.builder inside FutureBuilder
var futureBuilder = new FutureBuilder<List<DependantModel>>(
future: depState,
builder: (context, snapshot){
switch(snapshot.connectionState){
case ConnectionState.none:
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if(snapshot.hasError){
return new Text(snapshot.error);
}else{
List<DependantModel> user = snapshot.data;
return new ListView.builder(
itemCount: user.length,
itemBuilder: (context, index){
return new Column(
children: <Widget>[
new ListTile(
title: new Text(user[index].name,
style: TextStyle(fontSize: 20.0)),
subtitle: new Text(user[index].relationship,
style: TextStyle(fontSize: 15.0)),
trailing: new MaterialButton(color: Colors.greenAccent,
textColor: Colors.white,
child: new Text("More"),
onPressed: (){
Navigator.push(context,
new MaterialPageRoute(builder: (context) => Dep.Dependents(name: user[index].name)));
}
),
)
],
);
});
}
}
});
return new Scaffold(
body: futureBuilder,
);
}
}
I've just had the following error occurring in my flutter project.
I/flutter (12737): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (12737): The following assertion was thrown building FutureBuilder>(dirty, state:
I/flutter (12737): _FutureBuilderState>#b09b6):
I/flutter (12737): type 'NoSuchMethodError' is not a subtype of type 'String'
I/flutter (12737): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter (12737): more information in this error message to help you determine and fix the underlying cause.
I/flutter (12737): In either case, please report this assertion by filing a bug on GitHub:
what is dirty state?
As the error suggests, NoSuchMethodError, you have assigned the results of Future to a variable but the FutureBuilder expects future asynchronous computation to which this builder is currently connected and depState is not a method. You can directly set future: fetchUserInfo in FutureBuilder
Try this,
#override
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder<List<DependantModel>>(
future: fetchUserInfo, //your Future method
builder: (context, snapshot){
//Your code goes here
}
});
return new Scaffold(
body: futureBuilder,
);
}