SCENARIO
I'm trying to create a Flutter app that has two screens: ContactsScreen and EditContactScreen. In ContactsScreen, the user will be presented with a DropdownButton and Text. The DropdownButton holds a list of Contact objects that have been fetched through an api. Whenever a user selects a Contact from the DropdownButton, the Text object will show information regarding that particular contact. Moreover, upon Contact selection, a RaisedButton will appear, which when clicked, will direct the user to the EditContactScreen to edit the selected Contact. I'm using the BloC pattern. I created two BloCs, one for each screen: ContactsScreenBloc and EditContactScreenBloc. ContactsScreenBloc holds a Stream<Contact> and a Sink<Contact> for managing the selected Contact. Whereas EditContactScreenBloc holds streams and sinks for the Contact fields. Finally, I have a GlobalBloc that holds the list of Contacts. The GlobalBloc is an InheritedWidget that wraps up the MaterialApp. The app is oversimplified and is part of a larger one, for that reason, I can't merge ContactsScreenBloc and EditContactScreenBloc, and there should be a GlobalBloc that has the list of Contacts.
QUESTION
I'm actually fairly new to Flutter so I'm not sure if my approach is sound. If it is, then when the user navigates to the EditContactScreen and successfully updates the Contact, how can I reflect it back in the selected Contact in ContactsScreen?
CODE SNIPPITS
contact.dart
class Contact {
final String id;
final String firstName;
final String lastName;
final String phoneNumber;
Contact({this.id, this.firstName, this.lastName, this.phoneNumber});
Contact.fromJson(Map<String, dynamic> parsedJson)
: id = parsedJson['id'],
firstName = parsedJson['firstName'],
lastName = parsedJson['lastName'],
phoneNumber = parsedJson['phoneNumber'];
copyWith({String firstName, String lastName, String phoneNumber}) => Contact(
id: id,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
phoneNumber: phoneNumber ?? this.phoneNumber
);
#override
bool operator ==(other) => other.id == this.id;
#override
int get hashCode => id.hashCode;
}
global.bloc.dart
class GlobalBloc {
final _repo = Repo();
final _contacts = BehaviorSubject<List<Contact>>(seedValue: []);
Stream<List<Contact>> get contacts => _contacts.stream;
Function(List<Contact>) get updateContacts => _contacts.sink.add;
Future refreshContacts() async{
final contacts = await _repo.getContacts();
updateContacts(contacts);
}
}
contacts_screen.bloc.dart
class ContactsScreenBloc {
final _selectedContact = BehaviorSubject<Contact>(seedValue: null);
Stream<Contact> get selectedContact => _selectedContact.stream;
Function(Contact) get changeSelectedContact => _selectedContact.sink.add;
}
edit_contacts_screen.bloc.dart
class ContactsScreenBloc {
final _selectedContact = BehaviorSubject<Contact>(seedValue: null);
Stream<Contact> get selectedContact => _selectedContact.stream;
Function(Contact) get changeSelectedContact => _selectedContact.sink.add;
}
global.provider.dart
class GlobalProvider extends InheritedWidget {
final bloc = GlobalBloc();
static GlobalBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(GlobalProvider) as GlobalProvider).bloc;
bool updateShouldNotify(_) => true;
}
contacts.screen.dart
class ContactsScreen extends StatelessWidget {
final bloc = ContactsScreenBloc();
#override
Widget build(BuildContext context) {
final globalBloc = GlobalProvider.of(context);
return Column(
children: <Widget>[
StreamBuilder<List<Contact>>(
stream: globalBloc.contacts,
builder: (context, listSnapshot) {
return StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, itemSnapshot) {
return DropdownButton<Contact>(
items: listSnapshot.data
?.map(
(contact) => DropdownMenuItem<Contact>(
value: contact,
child: Text(contact.firstName + ' ' + contact.lastName),
),
)
?.toList(),
onChanged: bloc.changeSelectedContact,
hint: Text('Choose a contact.'),
value: itemSnapshot.hasData ? itemSnapshot.data : null,
);
},
);
},
), // end for DropdownButton StreamBuilder
StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, snapshot) {
return snapshot.hasData
? Row(children: <Widget>[
Text(snapshot.data.firstName + ' ' + snapshot.data.lastName + ' ' + snapshot.data.phoneNumber),
FlatButton(
child: Text('Edit Contact'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
EditContactScreen(contact: snapshot.data)));
},
),
],
)
: null;
}, // End for text description
)
],
);
}
}
edit_contact.screen.dart
class EditContactScreen extends StatelessWidget {
final bloc = EditContactScreenBloc();
final Contact contact;
EditContactScreen({this.contact});
#override
Widget build(BuildContext context) {
final globalBloc = GlobalProvider.of(context);
return Column(
children: <Widget>[
TextField(onChanged: (firstName) => bloc.updateContact(contact.copyWith(firstName: firstName))),
TextField(onChanged: (lastName) => bloc.updateContact(contact.copyWith(firstName: lastName))),
TextField(onChanged: (phoneNumber) => bloc.updateContact(contact.copyWith(firstName: phoneNumber))),
RaisedButton(child: Text('Update'), onPressed: () async {
await bloc.update();
await globalBloc.refreshContacts();
Navigator.of(context).pop();
},)
],
);
}
}
Okay, I was able to solve my issue:
In the contacts_screen.bloc.dart, I added the following method:
void updateContactInfo(List<Contact> contacts) {
final contact = _selectedContact.value;
if (contact == null) return;
final updatedContact = contacts.firstWhere((a) => a.id == contact.id);
if (updatedContact == null) return;
changeSelectedContact(updatedContact);
}
And updated the StreamBuilder<List<Contact>> for building the DropdownButton to be like this:
StreamBuilder<List<Contact>>(
stream: globalBloc.contacts,
builder: (context, listSnapshot) {
bloc.updateContactInfo(listSnapshot.data);
return StreamBuilder<Contact>(
stream: bloc.selectedContact,
builder: (context, itemSnapshot) {
return DropdownButton<Contact>(
items: listSnapshot.data
?.map(
(contact) => DropdownMenuItem<Contact>(
value: contact,
child: Text(
contact.firstName + ' ' + contact.lastName),
),
)
?.toList(),
onChanged: bloc.changeSelectedContact,
hint: Text('Choose a contact.'),
value: itemSnapshot.hasData ? itemSnapshot.data : 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 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.
Sthg makes me crazy, I try to show json products in cards and it doesn't work. Here is what I tried so far:
Product class :
class Product {
final String id;
Product({this.id});
factory Product.fromJson(Map<String, dynamic> json) {
return new Product(
id: json['id'] as String
);
}
}
JSON:
Future loadProducts() async {
final response = await http.get('https://api.stripe.com/v1/products');
return response.body;
}
The json has the following structure (data contains a list of products):
Widget:
Widget get _homeView {
return new Column(
children: <Widget>[
new FutureBuilder(
future: loadProducts(),
builder: (context, snapshot) {
List<Product> products = parseJson(snapshot.data.toString());
return !products.isEmpty
? new ProductsList(product: products)
: new CircularProgressIndicator();
}
),
...
]
);
}
List<Product> parseJson(String response) {
final parsed = json.decode(response.toString()).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => new Product.fromJson(json)).toList();
}
ProductsList class:
class ProductsList extends StatelessWidget {
final List<Product> product;
ProductsList({Key key, this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: product == null ? 0 : product.length,
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new Container(
children: <Widget>[
new Text(product[index].id),
],
)
);
}
);
}
}
Error :
Class '_InternalLinkedHashMap' has no instance
method 'cast' with matching arguments.
Edit 1 :
I tried :
Error :
This is my usual method for parsing a json list of objects (bit simpler but it works):
List<Product> parseJson(String response) {
List<Product> products = new List<Product>();
List jsonParsed = json.decode(response.toString());
for (int i = 0; i < jsonParsed.length; i++) {
products.add(new Product.fromJson(jsonParsed[i]));
}
return products;
}
I'm just parsing complex JSON and display on listView for learning purpose.
API: https://jsonplaceholder.typicode.com/users
1) Model Class
class AllUsers {
final List<User> alluser;
AllUsers({this.alluser});
factory AllUsers.formJson(List<dynamic> jsonArr){
List<User> arruser = jsonArr.map((f)=> User.formJson(f)).toList();
return AllUsers(
alluser: arruser
);
}
}
class User {
int id;
String name;
String email;
Address address;
String phone;
String website;
Company company;
User({this.id, this.name, this.email, this.address, this.phone, this.website, this.company});
factory User.formJson(Map<String, dynamic> jsonObj) {
return User(
id: jsonObj['id'],
name: jsonObj['name'],
email: jsonObj['email'],
address: Address.formJson(jsonObj['address']),
phone: jsonObj['phone'],
website: jsonObj['website'],
company: Company.formJson(jsonObj['company'])
);
}
}
class Address {
String street;
String suite;
String city;
String zipcode;
Geo geo;
Address({this.street, this.suite, this.city, this.zipcode, this.geo});
factory Address.formJson(Map<String, dynamic> jsonObj) {
return Address(
street: jsonObj['street'],
suite: jsonObj['suite'],
city: jsonObj['city'],
zipcode: jsonObj['zipcode'],
geo: Geo.formJson(jsonObj['geo'])
);
}
}
class Geo {
String lat;
String lng;
Geo({this.lat, this.lng});
factory Geo.formJson(Map<String, dynamic> jsonObj) {
return Geo(
lat: jsonObj['lat'],
lng: jsonObj['lng'],
);
}
}
class Company {
String name;
String catchPhrase;
String bs;
Company({this.name, this.catchPhrase, this.bs});
factory Company.formJson(Map<String, dynamic> jsonObj) {
return Company(
name: jsonObj['name'],
catchPhrase: jsonObj['catchPhrase'],
bs: jsonObj['bs']
);
}
}
2)ViewModel
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:parsejsonlist/All Screens/Home/Model/users.dart';
class UserViewModel {
Future<AllUsers> callWebserviceForFetchUserData() async{
var listOfUser = await http.get('https://jsonplaceholder.typicode.com/users');
List<User> decodedJSON = json.decode(listOfUser.body);
AllUsers arrayOfAlluser = AllUsers.formJson(decodedJSON);
print("arrayOfAlluser $arrayOfAlluser");
return arrayOfAlluser;
}
}
3) View Portion of the code.
import 'package:flutter/material.dart';
import 'package:parsejsonlist/All Screens/Home/Model/users.dart';
import 'package:parsejsonlist/All Screens/Home/ModelView/userviewmodel.dart';
class HomeSceen extends StatefulWidget {
#override
State<StatefulWidget> createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeSceen> {
AllUsers arryOfUser;
UserViewModel userViewmodel;
#override
initState(){
super.initState();
userViewmodel = UserViewModel();
}
callMethodFetchUserData() async {
arryOfUser = await userViewmodel.callWebserviceForFetchUserData();
User userRes = arryOfUser.alluser[0];
print("response === >> ${userRes.company.catchPhrase}");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("JSON Parsing")
),
body: Container(
child: FutureBuilder<AllUsers>(
future: callMethodFetchUserData(),
builder: (context, data){
return setupListView();
},
),
),
);
}
Widget setupListView(){
return ListView.builder(
itemCount: arryOfUser.alluser.length,
itemBuilder: (BuildContext context, int index) {
User userdata = arryOfUser.alluser[index];
setupListTile(userdata);
},
);
}
Widget setupListTile(User userdata){
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orangeAccent,
child: Text(userdata.name[0].toUpperCase(),
style: TextStyle(color: Colors.white)),
),
title: Text(userdata.name),
subtitle: Text(userdata.company.name),
);
}
}
So, My question is each time i'm getting fail with so many error like.
type 'Future' is not a subtype of type 'Future'
I know there are so many mistake but I'm new to Flutter.
Where i'm going wrong? How to solve this issue please guide me on right direction.
UPDATED
child: FutureBuilder<List<User>>(
future: userViewmodel.callWebserviceForFetchUserData(),
builder: (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Press button to start.');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result...');
case ConnectionState.done:
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return setupListView(snapshot.data);
}
return null;
},
Getting Error: "Type List<dynamic> is not a subtype of type List<User>"
Is there any mistake in model class ?
Throw away the AllUsers type, and the arryOfUser member. You don't need them, and by putting them in, you haven't matched the expectation of FutureBuilder.
class UserViewModel {
Future<List<User>> fetchUserData() async {
var response = await http.get('https://jsonplaceholder.typicode.com/users');
List<User> users = json.decode(response.body).map((u) => User.formJson(u)).toList();
print("users $users");
print("response === >> ${users[0].company.catchPhrase}");
return users;
}
}
class HomeScreenState extends State<HomeSceen> {
UserViewModel userViewmodel;
#override
initState(){
super.initState();
userViewmodel = UserViewModel();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("JSON Parsing")
),
body: Container(
child: FutureBuilder<List<User>>(
future: userViewmodel.fetchUserData(),
builder: (context, snap){
return setupListView(snap.data);
},
),
),
);
}
Widget setupListView(List<User> users){
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
setupListTile(users[index]);
},
);
}
Widget setupListTile(User userdata){
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orangeAccent,
child: Text(userdata.name[0].toUpperCase(),
style: TextStyle(color: Colors.white)),
),
title: Text(userdata.name),
subtitle: Text(userdata.company.name),
);
}
}
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.