Dart - Re-using Flutter widgets sometime contains wrong data - flutter

I have a Flutter web application that displays multiple user profiles on a card within a Row. The cards can each flip over to reveal more information via this library:
https://pub.dev/packages/flip_card
The application uses WebSockets and receives a JSON list of user details which maps to a User dart class, and as soon as new list arrives on a socket, we create a widget and add it to a widgetList and wrap it in a setState():
webSocket.onMessage.listen((e) {
final List receivedJsonUserList = json.decode(e.data);
final List<User> userListFromSocket =
receivedJsonUserList.map((item) => User.fromJson(item)).toList();
userListFromSocket.forEach((newUser) {
setState(() {
widgets[newUser.user.id] = UserDetails(user: newUser);
widgetList = widgets.entries.map((entry) => entry.value).toList();
});
});
}
}
});
The widget is drawn like this:
#override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInformation) => Scaffold(
drawer: sizingInformation.deviceScreenType == DeviceScreenType.mobile
? NavigationDrawer()
: null,
backgroundColor: Colors.white,
body: Scrollbar(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgetList),
],
),
),
),
),
);
}
The code works 90% of the time, but occasionally the wrong data is on the back of a card. So User 1 will have User 2's data on the back, etc.
Am I doing this correctly? Is there an obvious issue with this implementation? I tried to create a seperate widget for each user and it seems to resolve the issue but re-using widgets surely has to be possible.

Related

Does watch(provider) update children of parent widget?

I'm trying to update both Pages with one UserInteraction, therefore trying to access the same Stream in both Pages, with the Riverpod library.
Now to explain it further. When I pass the Stream to the CustomerPage I'm able to get the data (the String Anton). and when I click on the Button that triggers the change in FireStore, the String gets updated to "Marco" in the ParentWidget, when I go back to it. But it doesn't change in the CustomerPage unless I reopen the Page via the RaisedButton in the ParentWidget.
But I want it to update after I click the Button on the CustomerPage.
I hope this makes it clearer.
class ParentWidget extends ConsumerWidget{
Widget build(BuildContext context, ScopedReader watch){
Stream<DocumentSnapshot> doc = watch(streamProvider.stream);
return Container(
child: Column(
children: [
Text(doc.name), //Lets say the name is Anton,
RaisedButton(
child: Text(" road to CustomerPage"),
onPressed:(){
Navigator.of(context).pushNamed(RouteGenerator.customerPage, arguments: doc);
},), //RaisedButton
],), //Column
); //Container
}
}
class CustomerPage extends StatelessWidget{
Stream<DocumentSnapshot> docStream
CustomerPage({this.docStream});
Widget build(BuildContext context){
return Column(
children: [
Text(docStream.name) //Here is also Anton
RaisedButton(
child: Text("Change Name"),
onPressed: () {
context.read(streamProvider).changeName("Marco");
},), //RaisedButton
]
); //Column
}
}
On how I've understood so far is that, riverpod allows you to fetch the state of a provider, which basically is a value(?), that's why it's sufficient to just watch it in any Widget you want to access it's data from. There is no need anymore (just speaking for my case), to let Widgets pass the around in the App.
Down below is the solution which i believe to be right.
It also doesn't matter on how many times I call the provider. It's always going to be same Instance. For my case it means, that doc and doc2 are the same.
I hope this makes it clearer.
class ParentWidget extends ConsumerWidget{
Widget build(BuildContext context, ScopedReader watch){
Stream<DocumentSnapshot> doc = watch(streamProvider.stream);
return Container(
child: Column(
children: [
Text(doc.name), //Lets say the name is Anton,
RaisedButton(
child: Text(" road to CustomerPage"),
onPressed:(){
Navigator.of(context).pushNamed(RouteGenerator.customerPage);
},), //RaisedButton
],), //Column
); //Container
}
}
class CustomerPage extends ConsumerWidget{
Widget build(BuildContext context, ScopedReader watch){
Stream<DocumentSnapshot> doc2 = watch(streamProvider.stream);
return Column(
children: [
Text(doc2.name) //Here is also Anton
RaisedButton(
child: Text("Change Name"),
onPressed: () {
context.read(streamProvider).changeName("Marco");
},), //RaisedButton
]
); //Column
}
}

Showing one card in flutter when no other cards are displaying

I have a group of cards that are connected to my cloud firestore which shows when the icon related to each card is chosen. However, I want a card that tells the user to choose an icon when none of the other cards are showing. This is my code:
class Tips extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream:
FirebaseFirestore.instance.collection('moodIcons').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text('Loading data... Please Wait');
var currentCards = 0;
return Container(
child: new ListView(
children: snapshot.data.documents.map<Widget>((DocumentSnapshot document) {
if (document.data()['display'] == true) {
//code to display each card when specific icon is chosen
} else {
if (currentCards < 1) {
currentCards++;
return new Card(
child: new Column(
children: <Widget>[
new Container(
width: 400.0,
height: 45.0,
child: new Text(
"Choose an icon to show a tip!",
),
],
));
Currently, the card shows all the time but I want it to disappear when an icon is clicked. When an icon is clicked, the value of display becomes true.
I tried to use a counter so that if the value of display is true then it adds one to the counter then if the counter is more than 0, the card in the else statement wont show. However, this didn't work as I couldn't return the value of the counter from within the if statement.
Any help would be appreciated!
You could conditionally add an item at the beginning of your ListView if no document is set to display: true:
class Tips extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: FirebaseFirestore.instance.collection('moodIcons').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text('Loading data... Please Wait');
return Container(
child: new ListView(children: [
if (!snapshot.data.documents
.any((doc) => doc.data()['display'] == true))
Card(child: Text("Choose an icon to show a tip!")),
...snapshot.data.documents
.where((doc) => doc.data()['display'] == true)
.map((doc) => MyWidget(doc))
.toList(),
]),
);
},
),
);
}
}
If you want this to be done by StatelessWidget you need some state management like Bloc or Riverpod, your own etc.
a good answer here
You need to abstract your stream and create an event/action when you click on the icon.
That event is to be handlined via the above mentioned approaches. where a new card will be pushed to the stream.
Or you can make your widget Stateful, create a list of Card Widgets (to provide to your list builder) and use setState to add a element to a Card to the list.

Flutter : using changeNotifier and provider when the context is not available

I'm trying to use the simple state management described in the Flutter docs, using a ChangeNotifier, a Consumer, and a ChangeNotifierProvider.
My problem is that I can't get a hold a on valid context to update my model (details below...). I get an error:
Error: Error: Could not find the correct Provider above this CreateOrganizationDialog Widget
This likely happens because you used a BuildContext that does not include the provider of your choice. There are a few common scenarios:
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that CreateOrganizationDialog is under your MultiProvider/Provider.
This usually happen when you are creating a provider and trying to read it immediately.
Here are extracts of my code:
class OrganizationModel extends ChangeNotifier {
final List<Organization> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Organization> get items => UnmodifiableListView(_items);
void addList(List<Organization> items) {
_items.addAll(items);
notifyListeners();
}
}
This is my model.
class OrganizationBodyLayout extends StatelessWidget {
Future<void> _showCreateOrganizationDialog() async {
return showDialog<void>(
context: navigatorKey.currentState.overlay.context,
barrierDismissible: false,
child: CreateOrganizationDialog());
}
_onCreateOrganizationPressed() {
_showCreateOrganizationDialog();
}
_onDeleteOrganizationPressed() {
//TODO something
}
_onEditOrganizationPressed() {
//TODO something
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(mainAxisSize: MainAxisSize.max, children: [
ButtonBar(
alignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
onPressed: _onCreateOrganizationPressed,
child: Text("New Organization"),
),
],
),
Expanded(
child: Container(
color: Colors.pink,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: ChangeNotifierProvider(
create: (context) => OrganizationModel(),
child: OrganizationListView(),
)),
Expanded(child: Container(color: Colors.brown))
]))),
]));
}
}
A stateless widget that contains a ChangeNotifierProvider just on top of the list widget using the model.
On a button click, a modal dialog is shown, then data is fetched from the network. I should then update my model calling the addList operation.
Below is the code for the stateful dialog box.
class CreateOrganizationDialog extends StatefulWidget {
#override
_CreateOrganizationDialogState createState() =>
_CreateOrganizationDialogState();
}
class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
TextEditingController _nametextController;
TextEditingController _descriptionTextController;
#override
initState() {
_nametextController = new TextEditingController();
_descriptionTextController = new TextEditingController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 200,
height: 220,
child: Column(
children: [
Text('New organization',
style: Theme.of(context).textTheme.headline6),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
decoration: new InputDecoration(hintText: "Organization name"),
controller: _nametextController,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
decoration:
new InputDecoration(hintText: "Organization description"),
controller: _descriptionTextController,
),
),
ButtonBar(
children: [
FlatButton(
child: new Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: new Text("Create"),
onPressed: () {
setState(() {
Future<Organization> organization =
backendCreateOrganization(_nametextController.text,
_descriptionTextController.text);
organization.then((value) {
Future<List<Organization>> organizations =
backendReloadOrganizations();
organizations.then((value) {
var model = context.read<OrganizationModel>();
// var model = navigatorKey.currentState.overlay.context.read<OrganizationModel>();
//model.addList(value);
});
});
});
Navigator.of(context).pop();
//context is the one for the create dialog here
},
)
],
)
],
),
));
}
}
My problem happens at the line
var model = context.read<OrganizationModel>();
Thinking of it, the context available here is the modal dialog box context - so it's kind of logical that the Provider is not found in the widget tree.
However, I can't see how to retrieve the proper context (which would be the one for the result list view, where the Provider is located) in order to get the model and then update it.
Any idea is welcome :-)
Solved (kind of).
The only way I've found to solve this is by making my model a global variable:
var globalModel = OrganizationModel();
And referencing this global model in all widgets that consume it. I can't find a way to find the context of a stateless widget from within a callback in another stateful widget.
It works, but it's ugly. Still open to elegant solutions here :-)
Get_it seems to be elegant way of sharing models across the application. Please check the documentation for the different use cases they provide.
You could do something like the following
GetIt getIt = GetIt.instance;
getIt.registerSingleton<AppModel>(AppModelImplementation());
getIt.registerLazySingleton<RESTAPI>(() =>RestAPIImplementation());
And in other parts of your code, you could do something like
var myAppModel = getIt.get<AppModel>();

ListView lagging only when widgets in the list are Video_Player widgets

I'm trying to achieve a scrollable List with videos. I'm using the video_player widget and wrapping the player in a Card with a simple button.
Now i noticed that whenever I use ListView.builder with videos in the list, it is extremely lagging, especially when scrolling back up. i'm posting a GiF bellow if you would like to see the behaviour.
I have this problem ONLY when I have Videos in the list.
If I replace the videos with a simple Image widget, the scrolling is smooth and runs as intended. (Also provided a GiF below)
When I scroll through the list I get this message in my Console:
flutter: Another exception was thrown: setState() or markNeedsBuild() called during build.
And I think (but not sure) that this is the cause of the problem, maybe the way I implemented the video_player plugin (?)
class VideoPlayPause extends StatefulWidget {
VideoPlayPause(this.controller);
final VideoPlayerController controller;
#override
_VideoPlayPauseState createState() => _VideoPlayPauseState();
}
class _VideoPlayPauseState extends State<VideoPlayPause> {
//This Part here
_VideoPlayPauseState() {
listener = () {
setState(() {});
};
}
Maybe its setting state every time I scroll ?
I tried flutter run --release but saw no difference at all.
I'm running the app on a physical Iphone X.
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MVP_Test1'),
),
body: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: videoUrl.length,
itemBuilder: (context, i) {
return Card(
child: Column(
children: <Widget>[
Column(
children: <Widget>[
const ListTile(
leading: Icon(Icons.live_tv),
title: Text("Nature"),
),
Stack(
alignment: FractionalOffset.bottomRight +
const FractionalOffset(-0.1, -0.1),
children: <Widget>[
// If I replace this with Image.asset("..."), the scrolling is very smooth.
AssetPlayerLifeCycle(
videoUrl[i],
(BuildContext context,
VideoPlayerController controller) =>
AspectRatioVideo(controller)),
]),
],
),
ButtonTheme.bar(
child: ButtonBar(
children: <Widget>[
FlatButton(
child: const Text('ADD VIDEO'),
onPressed: () {
/* ... */
},
),
],
),
),
],
),
);
},
),
);
}
Result when I run Flutter Analyze
flutter analyze
Analyzing mvp_1...
No issues found! (ran in 1.9s)
You can see how when scrolling back up it looks like the app is skipping frames or something. Here's a video:
https://giphy.com/gifs/u48BNQ13r15Zay5SnN
Here's a video with photos instead of videos:
https://giphy.com/gifs/236RKyA8y1pfecmR1d

Navigator gets the wrong context

I have a flutter application which shows a camera, then scans a qr-code using git://github.com/arashbi/flutter_qrcode_reader and on the call back, I want to navigate to another screen to show the information I get from some query. The problem is, it seems Navigator get a wrong context, so instead of full page navigation, I see the second page pushed inside my first page as a widget.
The build of first page is as follow :
#override
Widget build(BuildContext context) {
Widget main =
new Scaffold(
appBar: _buildAppBar(),
body: Column(
children: <Widget>[
_eventButton ?? new Text(""),
new Center(
child: new Text("Hi"))
],
),
floatingActionButton: new FloatingActionButton(
onPressed:
() {
new QRCodeReader()
.setAutoFocusIntervalInMs(200)
.setForceAutoFocus(true)
.setTorchEnabled(true)
.setHandlePermissions(true)
.setExecuteAfterPermissionGranted(true)
.scan().then((barcode) => _showTicketPage(barcode));
}
,
child: new Icon(Icons.check),
),
);
if (!_loading) {
return main;
} else {
return new Stack(
children: [main,
new Container(
decoration: new BoxDecoration(
color: Color.fromRGBO(220, 220, 220,
0.3) // Specifies the background color and the opacity
),
child: new Center(child: new CircularProgressIndicator(),))
]
);
}
}
The 'Hi' Widget is for debugging, and the navigate method is
void _showTicketPage(var barcode) {
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) => TicketScreen(barcode)));
}
After getting the barcode, the second screen - TicketScreen - is pushed under 'Hi' Widget. I didn't even know this was possible to do.
I tried to get the context of the screen, save it to a field var, but that didn't help either.
How can I fix it? I ran out of ideas.