I'm still learning bloc patter. I created two pages, ViewPage and DetailsPage using a single bloc.
This is my bloc:
getRecordEvent
deleteRecordEvent
LoadedState
LoadedErrorState
DeletedState
DeletedErrorState
The view page will only build a widget with list of records on a LoadedState. When the user taps any record, It will push the Details page and displays detailed record with a delete button. When user press the delete button, I listen to the DeletedState and call the getRecord event to populate the view page again with the updated record.
Its all working but my problem is when I encountered an error while deleting record. When the state is DeleteErrorState, my view page becomes empty since I don't call getRecord there because the error could be internet connection and two error dialog will be shown. One for the DeletedErrorState and LoadedErrorState.
I know this is the default behavior of bloc. Do I have to create a separate bloc with only deleteRecordEvent? And also if I create a new page for adding record, will this also be a separate bloc?
UPDATE:
This is a sample of ViewPage. The DetailsPage will only call the deleteRecordEvent once the button was pressed.
ViewPage.dart
void getRecord() {
BlocProvider.of<RecordBloc>(context).add(
getRecordEvent());
}
#override
Widget build(BuildContext context) {
return
Scaffold(
body: buildBody(),
),
);
}
buildBody() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: BlocConsumer<RecordBloc, RecordState>(
listener: (context, state) {
if (state is LoadedErrorState) {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
} else if (state is DeletedState) {
Navigator.pop(context);
getRecord();
} else if (state is DeletedErrorState) {
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
}
},
builder: (context, state) {
if (state is LoadedState) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
state.records.length <= 0
? noRecordWidget()
: Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: state.records.length,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(
vertical: Sizes.s8),
child: ListTile(
title: Text(state.records[index].Name),
subtitle: state.records[index].date,
onTap: () {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return BlocProvider<RecordBloc>.value(
value: BlocProvider.of<RecordBloc>(context),
child: WillPopScope(
onWillPop: () async => false,
child:
DetailsPage(record:state.records[index]),
));
});
},
),
));
}),
),
],
),
);
}
return (Container());
},
),
),
);
}
About bloc
As a general rule of thumb, you need one bloc per ui. Of course, this is not always the case, as it depends on a few factors, the most important of which is how many events are you handling in your ui. For your case, where there is a ui that holds a list of items into an item-details ui, I would create two blocs. One will only handle loading items (ItemsBloc for instance), the other will handle actions to a single item (SingleItemBloc). I might only use the delete event for now, but as the app grows, I will be adding more events. This all facilitates the Separation of Concerns concept.
Applying that to your case, the SingleItemBloc will handle deleting, modifying, subscribing, etc to a single item, while ItemsBloc will handle loading the items from the different repositories (local/remote).
Since I don't have the code for your bloc I can't offer any modifications.
Solution specific to your case
It seems that you're losing the last version of your list of items every time a new state is emitted. You should keep a local copy of the last list you acquired from your repositories. In case there is an error, you just use that list; if not just save the new list as the last list you had.
class MyBloc extends Bloc<Event, State> {
.....
List<Item> _lastAcquiredList = [];
Stream<State> mapEventToState(Event event) async* {
try {
....
if(event is GetItemsEvent) {
var newList = _getItemsFromRepository();
yield LoadedState(newList);
_lastAcquiredList = newList;
}
....
} catch(err) {
yield ErrorState(items: _lastAcquiredItems);
}
}
}
Related
I seem to have a wrong thinking when using a listview with BloC.
I have a listview populated by a BloC (dataBloc) and this list is ordered by a ViewBloc.
When I tap on the item, a SubDataBloc is updated and the result displayed in the trailing of the list tile
dataBloc/viewBloc:
ListTileA - SubdataBlocA
ListTileB - SubDataBlocB
ListTileC - SubDataBlocC
When I reorder the list - only the ListTiles are reordered, but not the SubdataBloc results
video:
https://gitlab.com/bridystone/bloc_test/-/blob/main/BloC-ListTile.mov
the whole example is here:
https://gitlab.com/bridystone/bloc_test
any idea, on how to make this happen?
ListView.builder
body: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
if (state is ViewInitial) {
return CircularProgressIndicator();
} else if (state is ViewReadyForUI) {
return ListView.builder(
itemCount: state.sortedData.length,
itemBuilder: (context, index) => BlocProvider(
create: (context) => SubDataBloc(),
child: MyTile(
dataItem: state.sortedData[index],
),
),
);
}
return Text('should not happen');
},
),
ListTile:
Widget build(BuildContext context) {
//BlocProvider.of<SubDataBloc>(context).add(SubDataRequest(dataItem.id));
return BlocBuilder<SubDataBloc, SubDataState>(
builder: (context, state) {
return ListTile(
leading: Text(dataItem.id.toString()),
title: Text(dataItem.text),
trailing: (state is SubDataReceived)
? Text('items: ${state.subdata.length}')
: (state is SubDataUpdating)
? Text('${state.percent}')
: Text('initial'),
onTap: () => BlocProvider.of<SubDataBloc>(context)
.add(SubDataRequest(dataItem.id)),
);
},
);
}
I've received a comment from Github/Bloc_library, which brought me to a solution.
I've transferred the SubdataBloc to the dataModel of the MainBloc.
So for each data, a corresponding SubBloc is stored.
I've now added the subDataBloc to the DataModel
class BlocModel {
final Model model;
SubDataBloc subDataBloc;
BlocModel(this.model, this.subDataBloc);
}
and added the BloC during generation to the Model
var modelData = List<BlocModel>.generate(
event.requestId,
(index) => BlocModel(
Model(Random().nextInt(event.requestId), "BLABLABLA $index"),
SubDataBloc())); // <-- added here
then I could just add the subbloc to the BlocBuilder with the bloc attribute
return BlocBuilder<SubDataBloc, SubDataState>(
bloc: dataItem.subDataBloc, //<-- using the data model-BloC
builder: (context, state) {
return ListTile(
leading: Text(dataItem.model.id.toString()),
title: Text(dataItem.model.text),
trailing: (state is SubDataReceived)
? Text('items: ${state.subdata.length}')
: (state is SubDataUpdating)
? Text('${state.percent}')
: Text('initial'),
onTap: () =>
dataItem.subDataBloc.add(SubDataRequest(dataItem.model.id)));
},
);
And now it is working as intended.
I am making a sms_app that can get data from the List of Objects when I press the button send the sms.Objects have a method which returns a stream. I am iterating through the data with ListViewbuilder and listening to stream via StreamViewBuilder. All the Object Streams are performing the function of sending the message. If i press the first send button ,then wait,press the second button and wait, it updates the UI accordingly.
But
UI is being updated for the last Stream. If I press the button one after another button without waiting. What am I doing wrong>.
Here is the Video
import 'package:flutter/material.dart';
import 'package:sms_maintained/sms.dart';
class SendSmsScreen extends StatelessWidget {
final SmsSender sender = SmsSender();
final SmsMessage message = SmsMessage('03009640742', '_body');
final List<SmsMessage> smsList = [
SmsMessage('03009750742', 'Random'),
SmsMessage('03008750742', 'Kaleem'),
SmsMessage('03056750742', 'Shahryar'),
SmsMessage('03127750742', 'Shahryar Zong')
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemCount: smsList.length,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: Column(
children: [
IconButton(
icon: Icon(Icons.send),
onPressed: () async {
await sender.sendSms(smsList[index]);
},
),
StreamBuilder<SmsMessageState>(
key: GlobalKey(debugLabel: 'String $index'),
// initialData: SmsMessageState.None,
builder: (context, snapshot) {
if (snapshot.data == SmsMessageState.Sent) {
return Text('Message Sent');
}
if (snapshot.data == SmsMessageState.Delivered) {
return Text('Message Delivered');
}
if (snapshot.data == SmsMessageState.Sending) {
return Text('Sending');
}
if (snapshot.data == SmsMessageState.Fail)
return Text('Sending Failed');
return Text('Error');
},
stream: smsList[index].onStateChanged,
),
],
),
);
},
));
}
}
I'm assuming the UI doesn't update properly if pressing the button too quickly.
You can disable the button (example below) until the message is delivered you just need to set your button to be a child of the StreamBuilder.
onPressed: snapshot.data == SmsMessageState.Sending ? null : () async {
await sender.sendSms(smsList[index]);
}
That's because they are all executing the same method. When you do multiple executions in quick succession, last one stops the execution of previous. You'll have to find a way to queue them as they are executed and only do the next one when when the previous one is finished.
I was now trying for days to retrieve my firestore values, but no luck so posting it here.
I have a Firestore database and some data. I want to retrieve this with the help of Flutter.
This is what I have been doing.
So I have a Flutter screen where it shows a simple 3-dot dropdown in the AppBar.
It has two options: edit and cancel.
What I want is, when I press edit, it should open a new screen and should pass the data that I retrieved from firestore.
This is where I have edit and cancel dropdown (3 dots) and calling the a function (to retrieve data and open the new screen).
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(widget.news.headline.toUpperCase()),
actions: <Widget>[
PopupMenuButton<String>(
onSelected: (value) {
_open_edit_or_delete(value); // caling the function here
},
itemBuilder: (BuildContext context) {
return {'Edit', 'Delete'}.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
),
],
),
body: _get_particular_news(widget.news),
);
}
and this is the open_edit_or_delete function it is calling. But it doesn't open up (navigate) to the screen I am calling.
open_edit_or_delete(String selectedOption) {
News news;
Visibility(
visible: false,
child: StreamBuilder(
stream: FireStoreServiceApi().getNews(),
builder: (BuildContext context, AsyncSnapshot<List<News>> snapshot) {
if (snapshot.hasError || !snapshot.hasData) {
Navigator.push(
context, MaterialPageRoute(builder: (_) => FirstScreen(news:news)));
return null;
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
news = snapshot.data[index];
},
);
}
},
));
}
And in case you need the FireStoreServiceApi().getNews(), here it is as well.
// get the news
Stream<List<News>> getNews() {
return _db.collection("news").snapshots().map(
(snapshot) => snapshot.documents
.map((doc) => News.fromMap(doc.data, doc.documentID))
.toList(),
) ;
}
Can someone please help me?
You are not passing data correctly to your fromMap method.
You can access data using doc.data['']
If you have data and documentID property in it then following will work.
News.fromMap(doc.data.data, doc.data.documentID))
I don't know your fromMap method and i also don't what your snapshot contains, if this did not work for you then add them too.
I want to show a dialog if I receive an error in a futurebuilder.
If I receiver an error, I want to show a dialog and force the user to click on the button, so that he can be redirected to another page.
The problems seems to be that it is not possible to show a dialog while widget is being built.
FutureBuilder(
future: ApiService.getPosts(),
builder: (BuildContext context, AsyncSnapshot snapShot) {
if (snapShot.connectionState == ConnectionState.done) {
if (snapShot.data.runtimeType == http.Response) {
var message =
json.decode(utf8.decode(snapShot.data.bodyBytes));
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
FlatButton(
child: Text("Ok"),
onPressed: () => null",
)
],
);
});
}
return ListView.separated(
separatorBuilder: (BuildContext context, int index) {
return Divider(
color: Colors.grey,
height: 1,
);
},
itemBuilder: (BuildContext context, int index) {
return _buildPostCard(index);
},
itemCount: snapShot.data.length,
);
return Center(
child: CircularProgressIndicator(),
);
},
)
If I return the AlertDialog alone, it works. But I need the showDialog because of the barrierDismissible property.
Does any one know if that is possible?
Also, is this a good way to handle what I want?
Thanks
UPDATE
For further reference, a friend at work had the solution.
In order to do what I was looking for, I had to decide which future I was going to pass to the futureBuilder.
Future<List<dynamic>> getPostsFuture() async {
try {
return await ApiService.getPosts();
} catch (e) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
FlatButton(
child: Text("Ok"),
onPressed: () => null",
)
],
);
});
}
}
}
Then in the futureBuilder I would just call
FutureBuilder(
future: getPostsFuture(),
Thanks
To avoid setState() or markNeedsBuild() called during build error when using showDialog wrap it into Future.delayed like this:
Future.delayed(Duration.zero, () => showDialog(...));
setState() or markNeedsBuild() called during build.
This exception is allowed because the framework builds parent widgets before its children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
So to avoid that Future Callback is used, which adds a call like this EventQueue.
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Future futureCall() async {
await Future.delayed(Duration(seconds: 2));
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureCall(),
builder: (_, dataSnapshot) {
if (dataSnapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
Future(() { // Future Callback
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Employee Data'),
content: Text('Do you want to show data?'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context).pop('No'),
child: Text('NO')),
FlatButton(
onPressed: () =>
Navigator.of(context).pop('Yes'),
child: Text('YES'))
],
));
});
return Container();
}
},
);
}
}
The builder param expects you to return a Widget. showDialog is a Future.
So you can't return that.
You show Dialog on top of other widgets, you can't return it from a build method that is expecting a widget.
What you want can be implemented the following way.
When you receive an error, show a dialog on the UI and return a Container for the builder. Modify your code to this:
if (snapShot.data.runtimeType == http.Response) {
var message =
json.decode(utf8.decode(snapShot.data.bodyBytes));
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
content: Text(message),
actions: <Widget>[
FlatButton(
child: Text("Ok"),
onPressed: () => null",
)
],
);
});
return Container();
}
I have a List Builder that creates a list based off of the documents listed in Firestore. I am trying to take the value generated from a Firestore snapshot and pass it out of the class to a variable that is updated every time the user clicks on a different entry from the List Builder.
Here is the class making the Firestore interaction and returning the ListBuilder:
class DeviceBuilderListState extends State<DeviceBuilderList> {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
#override
void initState() {
super.initState();
// Listen for our auth event (on reload or start)
// Go to our device page once logged in
_auth.onAuthStateChanged
.where((user) {
new MaterialPageRoute(builder: (context) => new DeviceScreen());
});
// Give the navigation animations, etc, some time to finish
new Future.delayed(new Duration(seconds: 1))
.then((_) => signInWithGoogle());
}
void setLoggedIn() {
_auth.onAuthStateChanged
.where((user) {
Navigator.of(context).pushNamed('/');
});
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.data != null)
return new StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(snapshot.data.uid)
.collection('devices')
.snapshots,
builder: (context, snapshot) {
if (!snapshot.hasData)
return new Container();
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new ListView.builder(
itemCount: snapshot.data.documents.length,
padding: const EdgeInsets.all(10.0),
itemBuilder: (context, index) {
DocumentSnapshot ds =
snapshot.data.documents[index];
return new Card(
child: new GestureDetector(
onTap: () {
var initialStateLink = "${ds['name']}";
Navigator.of(context).pushNamed("/widget");
},
child: new Text(
" ${ds['name']}",
style: new TextStyle(fontSize: 48.0),
),
));
}),
);
},
);
else return new Container();
}
);}
}
Then I want to send the var initialStateLink to a different function in the same dart file:
Future<String> initialStateUrl() async {
final FirebaseUser currentUser = await _auth.currentUser();
Firestore.instance.collection('users')
.document(currentUser.uid).collection('devices').document(initialStateLink).get()
.then((docSnap) {
var initialStateLink = ['initialStateLink'];
return initialStateLink.toString();
});
return initialStateUrl().toString();
}
So that it returns me the proper String. I am at a complete loss on how to do this and I was unable to find another question that answered this. Thanks for the ideas.
You can use Navigator.push(Route route) instead of Navigator.pushNamed(String routeName)
And I don't encourage you to place navigation code deeply inside the widget tree, it's hard to maintain your logic of application flow because you end up with many pieces of navigation code in many classes. My solution is to place navigation code in one place (one class). Let's call it AppRoute, it looks like:
class AppRoute {
static Function(BuildContext, String) onInitialStateLinkSelected =
(context, item) =>
Navigator.of(context).push(
new MaterialPageRoute(builder: (context) {
return new NewScreen(initialStateLink: initialStateLink);
}
));
}
and replace your code in onTap:
onTap: () {
var initialStateLink = "${ds['name']}";
AppRoute.onInitialStateLinkSelected(context, initialStateLink);
}
Now, you not only can pass data from class to another class but also can control your application flow in ease (just look at AppRoute class)
Just make initialStateLink variable Global and send it as an argument to the another class like below,
a) Specify route as follows
'/widget' : (Buildcontext context) => new Anotherscreen(initialStateLink)
b) Navigate to the Anotherscreen()
c) Then the Anotherscreen () will be like this,
class Anotherscreen () extends StatelessWidget {
var initialStateLink;
Anotherscreen (this.initialStateLink);
......
}
I ended up finding a different solution that solved the problem (kind of by skirting the actual issue).
A MaterialPageRoute allows you to build a new widget in place while sending in arguments. So this made it so I didn't have to send any data outside of the class, here is my code:
return new Card(
child: new GestureDetector(
onTap: () {
Navigator.push(context,
new MaterialPageRoute(
builder: (BuildContext context) => new WebviewScaffold(
url: ds['initialStateLink'],
appBar: new AppBar(
title: new Text("Your Device: "+'${ds['name']}'),
),
withZoom: true,
withLocalStorage: true,)
));},