I am developing a simple todo app using flutter with BloC pattern.
It has a ui to display TodoDetails.
When a user click a button, it show a new SimpleDialog.
I want to show some Tag list in the SimpleDialog like:
class AddEditTodoPage extends StatefulWidget {
final TodoRepository todoRepository;
final TagRepository tagRepository;
final Todo todo;
final SaveTodoBloc bloc;
AddEditTodoPage({this.todoRepository, this.tagRepository, this.todo})
: bloc = SaveTodoBloc(
todoRepository: todoRepository,
tagRepository: tagRepository,
todo: todo);
#override
State<StatefulWidget> createState() => _AddEditTodoPageState(todo: todo);
}
class _AddEditTodoPageState extends State<AddEditTodoPage> {
final Todo todo;
_AddEditTodoPageState({this.todo});
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder<Tag>(
stream: widget.bloc.tag,
builder: (context, snapshot) {
final tag = snapshot.data;
return OutlineButton(
onPressed: () async {
final selectedTag = await showDialog<TagSelection>(
context: context,
builder: (context) => _showTagSelectDialog(context),
);
},
);
}},
);
}
_showTagSelectDialog(BuildContext context) => SimpleDialog(
title: Text("Select a Tag or create a new one"),
children: <Widget>[
StreamBuilder<List<Tag>>(
stream: widget.bloc.tags,
builder: (context, snapshot) {
final tagList = snapshot.data;
if (tagList == null || tagList.isEmpty) {
// This is always 'null'!!!
return SizedBox();
} else {
return ListView(
children: tagList.map(_buildTagName).toList(),
);
}
}),
],
);
Widget _buildTagName(Tag tag) => Text(tag.name);
}
So my bloc is getting the TagList like:
class SaveTodoBloc {
final TodoRepository todoRepository;
final TagRepository tagRepository;
final Todo todo;
SaveTodoBloc({this.todoRepository, this.tagRepository, this.todo}) {
if (tagRepository != null) {
_getTags();
}
}
final _getTagsSubject = PublishSubject<List<Tag>>();
Stream<List<Tag>> get tags => _getTagsSubject.stream;
Future<Null> _getTags() async {
await tagRepository.getAll().then((list) {
_getTagsSubject.add(list);
print("[SaveTodoBloc][JOS] _getTags - $list"); // It resturns correct list of Tags.
});
}
}
When I check the log, the bloc logic returns correct list of Tags.
But when I show the Dialog, it doesn't have list of tags.
The list is null.
Related
How to display one by one data using this DB function?
Future<List<Data>> display() async {
//final Database db = await database;
var db = await db1;
final List<Map<String, dynamic>> maps = await db.query('syncTable');
return List.generate(maps.length, (i) {
return Data(
syn_TableName: maps[i]['syn_TableName'],
syn_ChangeSequence: maps[i]['syn_ChangeSequence'],
);
});
}
You can use the FutureBuilder to consume your display() method. Then inside the FutureBuilder you can use AsyncSnapshot.data to get your List of Dataelements.
In the next step you use can call List.map() to map your Data to widgets. In this example I use the ListTile to display:
snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
Here a minimal working example which you can try out:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder<List<Data>>(
initialData: [],
future: display(),
builder: (context, snapshot) {
return ListView(
children: snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
);
}),
),
);
}
Future<List<Data>> display() async {
return List.generate(15, (i) {
return Data(
syn_TableName: 'syn_TableName $i',
syn_ChangeSequence: 'syn_ChangeSequence $i',
);
});
}
}
class Data {
final String syn_TableName;
final String syn_ChangeSequence;
Data({this.syn_ChangeSequence, this.syn_TableName});
}
I am trying to close a Dialog dynamically.
What I am actually trying to do is to change the content of the dialog depending on the information I have at the moment.
Starts with loading info and no button and after a few seconds could be an error with the OK button to close the Dialog Box.
class Dialogs{
loginLoading(BuildContext context, String type, String description){
var descriptionBody;
if(type == "error"){
descriptionBody = CircleAvatar(
radius: 100.0,
maxRadius: 100.0,
child: new Icon(Icons.warning),
backgroundColor: Colors.redAccent,
);
} else {
descriptionBody = new Center(
child: new CircularProgressIndicator(),
);
}
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return AlertDialog(
title: descriptionBody,
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Center(child: Text(description))
],
),
),
);
}
);
}
}
So after creating the instance os the dialog and opening it
Dialogs _dialog = new Dialogs();
_dialog.loginLoading(context, "loading", "loading...");
// Close the dialog code here
don't know how to do it
// Call again the AlertDialog with different content.
https://docs.flutter.io/flutter/material/showDialog.html
The dialog route created by this method is pushed to the root navigator. If the application has multiple Navigator objects, it may be necessary to call Navigator.of(context, rootNavigator: true).pop(result) to close the dialog rather than just Navigator.pop(context, result).
So any one of the below should work for you
Navigator.of(context, rootNavigator: true).pop(result)
Navigator.pop(context, result)
You don't need to close and reopen the dialog. Instead let flutter handle the dialog update. The framework is optimised for just that.
Here is a working example app that you can use as a starting point (just add your own Dialogs class):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
home: Login(
child: Home(),
),
);
}
}
class Home extends StatefulWidget {
final Dialogs dialog = Dialogs();
#override
State<StatefulWidget> createState() => HomeState();
}
class HomeState extends State<Home> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 50)).then((_) {
widget.dialog.loginLoading(
context,
LoginStateProvider.of(context).type,
LoginStateProvider.of(context).description,
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Updating Dialog'),
),
body: Container(),
);
}
}
class Login extends StatefulWidget {
final Widget child;
Login({#required this.child});
#override
State<StatefulWidget> createState() => LoginState();
}
class LoginState extends State<Login> {
String type = 'wait';
String description = 'foo';
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 2000)).then((_) {
setState(() {
type = 'error';
description = 'bar';
});
});
}
#override
Widget build(BuildContext context) {
return LoginStateProvider(widget.child, type, description);
}
}
class LoginStateProvider extends InheritedWidget {
final String type;
final String description;
LoginStateProvider(Widget child, this.type, this.description)
: super(child: child);
#override
bool updateShouldNotify(LoginStateProvider old) {
return type != old.type || description != old.description;
}
static LoginStateProvider of(BuildContext context) =>
context.inheritFromWidgetOfExactType(LoginStateProvider);
}
How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}
Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}
Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}
Let's say I have something like this:
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh,
...
)
}
)
In the _handleRefresh method, I want to programmatically trigger the re-run of the FutureBuilder.
Is there such a thing?
The use case:
When a user pulls down the refreshIndicator, then the _handleRefresh simply makes the FutureBuilder rerun itself.
Edit:
Full code snippet end to end, without the refreshing part. I've switched to using the StreamBuilder, how will the refreshIndicator part fit in all of it?
class DealList extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _DealList();
}
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
// prevents refreshing of tab when switch to
// Why? https://stackoverflow.com/q/51224420/1757321
bool get wantKeepAlive => true;
final RestDatasource api = new RestDatasource();
String token;
StreamController _dealsController;
#override
void initState() {
super.initState();
_dealsController = new StreamController();
_loadingDeals();
}
_loadingDeals() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
this.token = prefs.getString('token');
final res =
this.api.checkInterests(this.token).then((interestResponse) async {
_dealsController.add(interestResponse);
return interestResponse;
});
return res;
}
_handleRefresh(data) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
await this.api.checkInterests(token).then((interestResponse) {
_dealsController.add(interestResponse);
});
return null;
}
#override
Widget build(BuildContext context) {
super.build(context); // <-- this is with the wantKeepAlive thing
return StreamBuilder(
stream: _dealsController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
...
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No deals');
}
if (snapshot.hasData) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DealsDetailPage(
dealDetail: deal,
),
),
);
},
title: Text(deal['name']),
subtitle: Text(deal['expires']),
);
},
),
}
},
);
}
}
Why not using a StreamBuilder and a Stream instead of a FutureBuilder?
Something like that...
class _YourWidgetState extends State<YourWidget> {
StreamController<String> _refreshController;
...
initState() {
super...
_refreshController = new StreamController<String>();
_loadingDeals();
}
_loadingDeals() {
_refreshController.add("");
}
_handleRefresh(data) {
if (x) _refreshController.add("");
}
...
build(context) {
...
return StreamBuilder(
stream: _refreshController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh(snapshot.data),
...
)
}
);
}
}
I created a Gist with the Flutter main example using the StreamBuilder, check it out
Using StreamBuilder is a solution, however, to trigger the FutureBuilder programmatically, just call setState, it'll rebuild the Widget.
return RefreshIndicator(
onRefresh: () {
setState(() {});
},
...
)
I prefer FutureBuilder over StreamBuilder since I am using Firestore for my project and you get billed by reads so my solution was this
_future??= getMyFuture();
shouldReload(){
setState(()=>_future = null)
}
FutureBuilder(
future: _future,
builder: (context, snapshot){
return Container();
},
)
and any user activity that needs you to get new data simply call shouldReload()
I have one question regarding how to reload a list after refresh indicator is called in Flutter, using Streams and RxDart.
Here is what I have , my model class:
class HomeState {
List<Event> result;
final bool hasError;
final bool isLoading;
HomeState({
this.result,
this.hasError = false,
this.isLoading = false,
});
factory HomeState.initial() =>
new HomeState(result: new List<Event>());
factory HomeState.loading() => new HomeState(isLoading: true);
factory HomeState.error() => new HomeState(hasError: true);
}
class HomeBloc {
Stream<HomeState> state;
final EventRepository repository;
HomeBloc(this.repository) {
state = new Observable.just(new HomeState.initial());
}
void loadEvents(){
state = new Observable.fromFuture(repository.getEventList(1)).map<HomeState>((List<Event> list){
return new HomeState(
result: list,
isLoading: false
);
}).onErrorReturn(new HomeState.error())
.startWith(new HomeState.loading());
}
}
My widget:
class HomePageRx extends StatefulWidget {
#override
_HomePageRxState createState() => _HomePageRxState();
}
class _HomePageRxState extends State<HomePageRx> {
HomeBloc bloc;
_HomePageRxState() {
bloc = new HomeBloc(new EventRest());
bloc.loadEvents();
}
Future<Null> _onRefresh() async {
bloc.loadEvents();
return null;
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: bloc.state,
builder: (BuildContext context, AsyncSnapshot<HomeState> snapshot) {
var state = snapshot.data;
return new Scaffold(
body: new RefreshIndicator(
onRefresh: _onRefresh,
child: new LayoutBuilder(builder:
(BuildContext context, BoxConstraints boxConstraints) {
if (state.isLoading) {
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.deepOrangeAccent,
strokeWidth: 5.0,
),
);
} else {
if (state.result.length > 0) {
return new ListView.builder(
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return new Text(snapshot.data.result[index].title);
});
} else {
return new Center(
child: new Text("Empty data"),
);
}
}
}),
),
);
});
}
}
The problem is when I do the pull refresh from list, the UI doesn't refresh (the server is called, the animation of the refreshindicator also), I know that the issue is related to the stream but I don't know how to solve it.
Expected result : Display the CircularProgressIndicator until the data is loaded
Any help? Thanks
You are not supposed to change the instance of state.
You should instead submit a new value to the observable. So that StreamBuilder, which is listening to state will be notified of a new value.
Which means you can't just have an Observable instance internally, as Observable doesn't have any method for adding pushing new values. So you'll need a Subject.
Basically this changes your Bloc to the following :
class HomeBloc {
final Stream<HomeState> state;
final EventRepository repository;
final Subject<HomeState> _stateSubject;
factory HomeBloc(EventRepository respository) {
final subject = new BehaviorSubject(seedValue: new HomeState.initial());
return new HomeBloc._(
repository: respository,
stateSubject: subject,
state: subject.asBroadcastStream());
}
HomeBloc._({this.state, Subject<HomeState> stateSubject, this.repository})
: _stateSubject = stateSubject;
Future<void> loadEvents() async {
_stateSubject.add(new HomeState.loading());
try {
final list = await repository.getEventList(1);
_stateSubject.add(new HomeState(result: list, isLoading: false));
} catch (err) {
_stateSubject.addError(err);
}
}
}
Also, notice how loadEvent use addError with the exception. Instead of pushing a HomeState with a hasError: true.