UPDATE
This is the StreamBuilder code:
I am trying currently to update with a timer that runs a Stream.fromFuture which updates the data, but with the flicker and scroll weirdness.
new StreamBuilder(
initialData: myInitialData,
stream: msgstream,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
myInitialData = snapshot.data;
return new RefreshIndicator(
child: new ListView.builder(
itemBuilder: (context, index) {
Stream<List<Map>> msgstream2;
Future<List<Map>> _responseDate = ChatDB.instance.getMessagesByDate(snapshot.data[index]['msgkey'], snapshot.data[index]['msgdate']);
msgstream2 = new Stream.fromFuture(_responseDate);
return new StreamBuilder(
initialData: myInitialData2,
stream: msgstream2,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
List messList;
var mybytes;
File myimageview;
Image newimageview;
String imgStr;
String vidStr;
String vidimgstr;
myInitialData2 = snapshot.data;
List<dynamic> json = snapshot.data;
List messagelist = [];
json.forEach((element) {
DateTime submitdate =
DateTime.parse(element['submitdate']).toLocal();
String myvideo = (element['chatvideo']);
String myimage = element['chatimage'];
String myvideoimage = element['chatvideoimage'];
File imgfile;
File vidfile;
File vidimgfile;
bool vidInit = false;
Future<Null> _launched;
String localAssetPath;
String localVideoPath;
String mymessage = element['message'].replaceAll("[\u2018\u2019]", "'");
//print('MYDATE: '+submitdate.toString());
_checkFile(File file) async {
var checkfile = await file.exists();
print('VIDEXISTS: '+checkfile.toString());
}
Future<Null> _launchVideo(String url, bool isLocal) async {
if (await canLaunchVideo(url, isLocal)) {
await launchVideo(url, isLocal);
} else {
throw 'Could not launch $url';
}
}
void _launchLocal() =>
setState(() => _launched = _launchVideo(localVideoPath, true)
);
Widget _showVideo() {
return new Flexible(
child: new Card(
child: new Column(
children: <Widget>[
new ListTile(subtitle: new Text('Video'), title: new Text(element['referralname']),),
new GestureDetector(
onTap: _launchLocal,
child: new Image.file(
vidimgfile,
width: 150.0,
),
),
],
),
)
);
}
if (myimage != "") {
imgStr = element['chatimage'];
imgfile = new File(imgStr);
}
if (myvideo != "") {
vidStr = element['chatvideo'];
vidimgstr = element['chatvideoimage'];
vidimgfile = new File(vidimgstr);
localVideoPath = '$vidStr';
}
_showLgPic() {
Route route = new MaterialPageRoute(
settings: new RouteSettings(name: "/ShowPic"),
builder: (BuildContext context) => new ShowPic(
image: imgfile,
),
);
Navigator.of(context).push(route);
}
Widget _showGraphic() {
Widget mywidget;
if (myimage != "") {
mywidget = new GestureDetector(
child: new Image.file(
imgfile,
width: 300.0,
),
onTap: _showLgPic,
);
} else if (myvideo != "") {
mywidget = _showVideo();
} else {
mywidget = new Container();
}
return mywidget;
}
messagelist.add(
new Container(
//width: 300.0,
padding: new EdgeInsets.all(10.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
padding: new EdgeInsets.only(bottom: 5.0),
child: new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new CircleAvatar(
child: new Text(
element['sendname'][0],
style: new TextStyle(fontSize: 15.0),
),
radius: 12.0,
),
new Text(' '),
new Text(
element['sendname'],
style: new TextStyle(
fontSize: 15.0,
fontWeight: FontWeight.bold),
),
new Text(' '),
new Text(
new DateFormat.Hm().format(submitdate),
style: new TextStyle(
color: Colors.grey, fontSize: 12.0),
),
],
),
),
new Row(
children: <Widget>[
new Text(' '),
new Flexible(
child: new Text(mymessage),
)
],
),
new Container(
width: 150.0,
child: new Row(
children: <Widget>[
new Text(' '),
_showGraphic()
],
)),
],
),
),
);
});
return new Column(children: messagelist);
}
}
);
/*return new MyChatWidget(
datediv: snapshot.data[index]['msgdate'],
msgkey: snapshot.data[index]['msgkey'],
);*/
},
//itemBuilder: _itemBuilder,
controller: _scrollController,
reverse: true,
itemCount: snapshot.data.length,
),
onRefresh: _onRefresh
);
}
}
}),
I started with a Future> from a local sqlite DB. I take that future and use the data to to get another Future> from the DB. I use Listview.builder to build the widget, etc... All works great, but need to refresh the data in realtime as messages come in and get updated in the DB. I converted the furtures to streams and use a timer to go get new data, but of course my screen flickers and eventhough the data refreshes its ugly.
So I am trying to figure out a better way to get the data to update without the flicker and not affect the user if they are scrolling on the page looking at messages.
I currently do the 2 futures, because one is used to build Date Dividers between messages for each Date. I have been looking at have a StreamController and subscribing to it, but not clear how to update the data in the controller as the the data needs to be full when they come to the page and then added to as the new messages sync.
So I have been looking at something like this that I found:
class Server {
StreamController<List<Map>> _controller = new StreamController.broadcast();
void addMessage(int message) {
var newmsg = await database.rawQuery('select c.*, date(submitdate, "localtime") as msgtime from v_groupchats g join chats c on c.id = g.id where (oid = $oid or prid = $prid) and c.msgkey not in (select msgkey from chatArchive) order by submitdate desc');
_controller.add(message);
}
Stream get messages => _controller.stream;
}
This is not complete, just hoping it helps someone with some ideas for me.
Thanks in advance for any help.
This flickering is most likely the result of your:
case ConnectionState.waiting:
return new Text('');
Because every time you fetch data, your stream will enter a brief moment of ConnectionState.waiting before it is ConnectionState.done. And what you did was tell the UI to display essentially nothing Text('') every time while it fetches data. Even if it only takes like 50ms, it's noticeable to the human eye...
So if you don't care while your stream is fetching data, then remove that check. Otherwise, you could change your layout into a Stack and overlay a loading animation somewhere, or just something to indicate that it's currently fetching data.
(note that I was able to see this flickering when I tried to test your setup with a simulated Future.delayed(const Duration(milliseconds: 50), () => data), and we can perceive quicker still)
Related
I am currently working on a side project to learn about Rx and BLoC pattern.
I would like to manage the form state without using any setState().
I already have a BLoC that manage my 'events' which are stored in a SQLite db and added after validating this form.
Do I need to create a need BLoC specifically for this UI part, and how ? Is it OK to keep a code like that ? Should I change my actual BLoC ?
You can find my current code here :
class _EventsAddEditScreenState extends State<EventsAddEditScreen> {
bool hasDescription = false;
bool hasLocation = false;
bool hasChecklist = false;
DateTime eventDate;
TextEditingController eventNameController = new TextEditingController();
TextEditingController descriptionController = new TextEditingController();
#override
Widget build(BuildContext context) {
final eventBloc = BlocProvider.of<EventsBloc>(context);
return BlocBuilder(
bloc: eventBloc,
builder: (BuildContext context, EventsState state) {
return Scaffold(
body: Stack(
children: <Widget>[
Column(children: <Widget>[
Expanded(
child: ListView(
shrinkWrap: true,
children: <Widget>[
_buildEventImage(context),
hasDescription ? _buildDescriptionSection(context) : _buildAddSection('description'),
_buildAddSection('location'),
_buildAddSection('checklist'),
//_buildDescriptionSection(context),
],
))
]),
new Positioned(
//Place it at the top, and not use the entire screen
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.check), onPressed: () async{
if(this._checkAllField()){
String description = hasDescription ? this.descriptionController.text : null;
await eventBloc.dispatch(AddEvent(Event(this.eventNameController.text, this.eventDate,"balbla", description: description)));
print('Saving ${this.eventDate} ${eventNameController.text}');
}
},)
],
backgroundColor: Colors.transparent, //No more green
elevation: 0.0, //Shadow gone
),
),
],
),
);
},
);
}
Widget _buildAddSection(String sectionName) {
TextStyle textStyle = TextStyle(
color: Colors.black87, fontSize: 18.0, fontWeight: FontWeight.w700);
return Container(
alignment: Alignment.topLeft,
padding:
EdgeInsets.only(top: 20.0, left: 40.0, right: 40.0, bottom: 20.0),
child: FlatButton(
onPressed: () {
switch(sectionName){
case('description'):{
this.setState((){hasDescription = true;});
}
break;
case('checklist'):{
this.setState((){hasChecklist = true;});
}
break;
case('location'):{
this.setState((){hasLocation=true;});
}
break;
default:{
}
break;
}
},
padding: EdgeInsets.only(top: 0.0, left: 0.0),
child: Text(
'+ Add $sectionName',
style: textStyle,
),
),
);
}
Let's solve this step by step.
Your first question:
Do I need to create a need BLoC specifically for this UI part?
Well this relative of your needs and your app. You can have a BLoC for each screen if needed but you can have too a single BLoC for 2 or 3 widgets, there is no rule about it. If you think that in this case is a good approach implement another BLoC for your screen because the code will be more readable, organized and scaleable you can do this or if you think that is better make only one bloc with all inside you're free to this too.
Your second question: and how ?
Well in your code I only see setState calls in _buildAddSection so let's change this writing a new BLoc class and handle state changes with RxDart streams.
class LittleBloc {
// Note that all stream already start with an initial value. In this case, false.
final BehaviorSubject<bool> _descriptionSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasDescription => _descriptionSubject.stream;
final BehaviorSubject<bool> _checklistSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasChecklist => _checklistSubject.stream;
final BehaviorSubject<bool> _locationSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasLocation => _locationSubject.stream;
void changeDescription(final bool status) => _descriptionSubject.sink.add(status);
void changeChecklist(final bool status) => _checklistSubject.sink.add(status);
void changeLocation(final bool status) => _locationSubject.sink.add(status);
dispose(){
_descriptionSubject?.close();
_locationSubject?.close();
_checklistSubject?.close();
}
}
Now I will use this BLoc in your widget. I will put the entire build method code below with the changes. Basically we'll use StreamBuilder to build widgets in widget tree.
final LittleBloc bloc = LittleBloc(); // Our instance of bloc
#override
Widget build(BuildContext context) {
final eventBloc = BlocProvider.of<EventsBloc>(context);
return BlocBuilder(
bloc: eventBloc,
builder: (BuildContext context, EventsState state) {
return Scaffold(
body: Stack(
children: <Widget>[
Column(children: <Widget>[
Expanded(
child: ListView(
shrinkWrap: true,
children: <Widget>[
_buildEventImage(context),
StreamBuilder<bool>(
stream: bloc.hasDescription,
builder: (context, snapshot){
hasDescription = snapshot.data; // if you want hold the value
if (snapshot.data)
return _buildDescriptionSection(context);//we got description true
return buildAddSection('description'); // we have description false
}
),
_buildAddSection('location'),
_buildAddSection('checklist'),
//_buildDescriptionSection(context),
],
),
),
]
),
new Positioned(
//Place it at the top, and not use the entire screen
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.check),
onPressed: () async{
if(this._checkAllField()){
String description = hasDescription ? this.descriptionController.text : null;
await eventBloc.dispatch(AddEvent(Event(this.eventNameController.text, this.eventDate,"balbla", description: description)));
print('Saving ${this.eventDate} ${eventNameController.text}');
}
},
),
],
backgroundColor: Colors.transparent, //No more green
elevation: 0.0, //Shadow gone
),
),
],
),
);
},
);
}
And no more setState calls in your _buildAddSection. Just need change a switch statement. The changes...calls will update the streams in BLoc class and this will make a rebuild of the widget that is listening the stream.
switch(sectionName){
case('description'):
bloc.changeDescription(true);
break;
case('checklist'):
bloc.changeChecklist(true);
break;
case('location'):
bloc.changeLocation(true);
break;
default:
// you better do something here!
break;
}
And don't forgot to call bloc.dispose() inside inside WidgetState dispose method.
am new to flutter ... and am calling an api in my app and i want to display a loading indicator while the api function is finished and every thing is retrieved from the api .. how to achieve this?
class _CompaniesPageState extends State<CompaniesPage> {
getcompanies() async {
var url = 'my link';
http
.post(url, body: json.encode({ 'token': globals.token }))
.then((response) {
// print("Response body: ${response.body}");
Map data = json.decode(response.body);
final companies =
(data['Companies'] as List).map((i) => new Company.fromJson(i));
setState(() {
for (final company in companies) {
if (!company.category.contains("الراعي") &&
!company.category.contains("الشريك") &&
!company.category.contains("الداعم")) {
if (company.logo != "") {
names.add(company.name);
logos.add(company.logo);
}
}
}
});
});
}
#override
Widget build(BuildContext context) {
getcompanies();
if (globals.isguest) {
return new Directionality(
.....
);
}
}
List<Widget> createCompaniesChildren() {
List<Widget> children = List<Widget>();
for (int i = 0; i < names.length; i++) {
children.add(
new Padding(
padding: new EdgeInsets.only(right: 15.0, left: 15.0),
child: new Image.network(
logos[i],
width: 346.0,
height: 180.0,
),
),
);
children.add(
new Padding(
padding: new EdgeInsets.only(right: 15.0, left: 15.0),
child: new Center(
child: new Text(
names[i],
style: new TextStyle(color: Colors.black, fontSize: 17.0),
textAlign: TextAlign.center,
),
),
),
);
children.add(new Divider());
}
return children;
}
how can i display a loading indicator while the list is being populating and then dismiss it once it's finished??
Tried this:
body: new Padding(
padding: new EdgeInsets.only(top: 15.0),
child: new Center(
child: new FutureBuilder(
future:
getcompanies(), // your async method that returns a future
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// if data is loaded
if (snapshot.data != null) {
return new ListView.builder(
padding: new EdgeInsets.all(8.0),
itemExtent: 20.0,
itemBuilder: (BuildContext context, int i) {
return new Center(
child: new Text(
snapshot.data[i].name,
style: new TextStyle(
color: Colors.black, fontSize: 17.0),
textAlign: TextAlign.center,
),
);
},
);
} else {
// if data not loaded yet
return new CircularProgressIndicator();
}
}
},
),
),
),
but got this error: I/flutter (31276): Another exception was thrown: A build function returned null.
You could use the FutureBuilder class for that purpose.
Also instead of manually creating the companies list you could use a ListView.
Something like this:
new FutureBuilder(
future: getcompanies(), // your async method that returns a future
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// if data is loaded
return new ListView.builder(
padding: new EdgeInsets.all(8.0),
itemExtent: 20.0,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int i) {
return new Center(
child: new Text(
snapshot.data[i].name,
style: new TextStyle(color: Colors.black, fontSize: 17.0),
textAlign: TextAlign.center,
),
);
},
).build(context);
} else {
// if data not loaded yet
return new CircularProgressIndicator();
}
},
)
Although you have already solved or got the answer. I will show you 2 Ways.
1. Using any builder like FutureBuilder, StreamBuilder....
FutureBuilder(
future: getcompanies(), // your async method that returns a future
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Center(
child: CircularProgressIndicator()
),
);
case ConnectionState.none:
return Center(child: Text("Unable to connect right now"));
case ConnectionState.done:
if (snapshot.hasError) {
print("Error: ${snapshot.error}");
}
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int i) {
return new Center(
child: new Text(
snapshot.data[i].name,
style: new TextStyle(color: Colors.black, fontSize: 17.0),
textAlign: TextAlign.center,
),
);
},
);
}
},
),
2. Without using builder
Put this code before API call, Like
AlertDialog alert = AlertDialog(
content: Row(children: [
CircularProgressIndicator(
backgroundColor: Colors.red,
),
Container(margin: EdgeInsets.only(left: 10), child: Text("Loading...")),
]),
);
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return alert;
},
);
api.getcompanies();
And after getting API response
var response = api.getcompanies();
Navigator.pop(context);
Note : You can put the code into a method and then call it here
I'm new to Flutter,
I want to destruct cards created initially and construct them again as per data provided in API call.
Basically when I tap on button in UI, it should call APIs and based on data from API call, if it is different from the data I already have, I want to destruct cards and construct them again.
How I can achieve this?
The cards will auto update their content when you make the call again, it is like refreshing your data.
I have made a simple example with a single card that shows data from this JSON Where I am calling the API first time in initState and then repeating the call each time I press on the FAB.
I am adding the index variable just to show you the updates (updating my single card with the next item in the list)
Also it is worth noting that I am handling the null or empty values poorly for the sake of time.
Also forget about the UI overflow ¯_(ツ)_/¯
class CardListExample extends StatefulWidget {
#override
_CardListExampleState createState() => new _CardListExampleState();
}
class _CardListExampleState extends State<CardListExample> {
Map cardList = {};
int index = 0;
#override
void initState() {
_getRequests();
super.initState();
}
_getRequests() async {
String url = "https://jsonplaceholder.typicode.com/users";
var httpClinet = createHttpClient();
var response = await httpClinet.get(
url,
);
var data = JSON.decode(response.body);
//print (data);
setState(() {
this.cardList = data[index];
this.index++;
});
print(cardList);
print(cardList["name"]);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton:
new FloatingActionButton(onPressed: () => _getRequests()),
appBar: new AppBar(
title: new Text("Card List Example"),
),
body: this.cardList != {}
? new ListView(children: <Widget>[
new Card(
child: new Column(
children: <Widget>[
new Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Text(
cardList["name"] ?? '',
style: Theme.of(context).textTheme.display1,
),
new Text(
this.cardList['email'] ?? '',
maxLines: 50,
),
],
),
new Text(cardList["website"] ?? '')
],
),
),
])
: new Center(child: new CircularProgressIndicator()),
);
}
}
Yes, Answer from Aziza works.
Though I used the code as below :
void main() =>
runApp(new MaterialApp(
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/about':
return new FromRightToLeft(
builder: (_) => new _aboutPage.About(),
settings: settings,
);
}
},
home : new HomePage(),
theme: new ThemeData(
fontFamily: 'Poppins',
primarySwatch: Colors.blue,
),
));
class HomePage extends StatefulWidget{
#override
HomePageState createState() => new HomePageState();
}
class HomePageState extends State<HomePage>{
List data;
Future<String> getData() async{
var response = await http.get(
Uri.encodeFull(<SOMEURL>),
headers: {
"Accept" : "application/json"
}
);
this.setState((){
data = JSON.decode(response.body);
});
return "Success";
}
#override
void initState() {
// TODO: implement initState
super.initState();
this.getData();
}
#override
Widget build(BuildContext context){
return new Scaffold(
appBar : new AppBar(
title : new Text("ABC API"),
actions: <Widget>[
new IconButton( // action button
icon: new Icon(Icons.cached),
onPressed: () => getData(),
)],
),
drawer: new Drawer(
child: new ListView(
children: <Widget> [
new Container(
height: 120.0,
child: new DrawerHeader(
padding: new EdgeInsets.all(0.0),
decoration: new BoxDecoration(
color: new Color(0xFFECEFF1),
),
child: new Center(
child: new FlutterLogo(
colors: Colors.blueGrey,
size: 54.0,
),
),
),
),
new ListTile(
leading: new Icon(Icons.chat),
title: new Text('Support'),
onTap: () {
Navigator.pop(context);
Navigator.of(context).pushNamed('/support');
}
),
new ListTile(
leading: new Icon(Icons.info),
title: new Text('About'),
onTap: () {
Navigator.pop(context);
Navigator.of(context).pushNamed('/about');
}
),
new Divider(),
new ListTile(
leading: new Icon(Icons.exit_to_app),
title: new Text('Sign Out'),
onTap: () {
Navigator.pop(context);
}
),
],
)
),
body: this.data != null ?
new ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index){
return new Container(
padding: new EdgeInsets.fromLTRB(8.0,5.0,8.0,0.0),
child: new Card(
child: new Padding(
padding: new EdgeInsets.fromLTRB(10.0,12.0,8.0,0.0),
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new ListTile(
enabled: data[index]['active'] == '1' ? true : false,
title: new Text(data[index]['header'],
style:Theme.of(context).textTheme.headline,
),
subtitle: new Text("\n" + data[index]['description']),
),
new ButtonTheme.bar(
child: new ButtonBar(
children: <Widget>[
new FlatButton(
child: new Text(data[index]['action1']),
onPressed: data[index]['active'] == '1' ? _launchURL :null,
),
],
),
),
],
),
),
),
);
},
)
:new Center(child: new CircularProgressIndicator()),
);
}
}
_launchURL() async {
const url = 'http://archive.org';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
class FromRightToLeft<T> extends MaterialPageRoute<T> {
FromRightToLeft({ WidgetBuilder builder, RouteSettings settings })
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
return new SlideTransition(
child: new Container(
decoration: new BoxDecoration(
boxShadow: [
new BoxShadow(
color: Colors.black26,
blurRadius: 25.0,
)
]
),
child: child,
),
position: new Tween(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
)
.animate(
new CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
)
),
);
}
#override Duration get transitionDuration => const Duration(milliseconds: 400);
}
The above code includes Navigation drawer, page navigation animation and also answer to the above question.
I want to use a LietView.build populated by Widgets that obtain the data from an async method, before the widget is built. Here is my function that collects the data from a website:
fetchBasicData(String URL) async {
final response = await http.get(URL);
var document = parse(response.body);
var result = new List<dom.Node>();
result = document.getElementsByClassName('datas-nev');
var dogName = result[0].nodes[1].toString();
result = document.getElementsByClassName('datas-tipus');
var dogBreed = result[0].nodes[1].toString();
result = document.getElementsByClassName('datas-nem');
var dogGender = result[0].nodes[1].toString();
result = document.getElementsByClassName('datas-szin');
var dogColor = result[0].nodes[1].toString();
result = document.getElementsByClassName('datas-kor');
var dogAge = result[0].nodes[1].toString();
result = document.getElementsByClassName('pirobox_gall');
String imageLink;
imageLink = urlPrefix + result[0].nodes[0].attributes.values.first;
return new Dog.basic(
URL,
dogName,
dogBreed,
dogGender,
dogColor,
dogAge,
imageLink);
}
The function is executed and gathers the data, but the widget building fails with type '_Future' is not a subtype of type 'Widget' of 'child' where
Here is the function that is supposed to build the widget:
buildBasicWidget(String URL) async {
Dog myDog = await fetchBasicData(URL);
return new SizedBox(
width: 500.0,
height: 400.0,
child: new Card(
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
//Header image row
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Expanded(
child: new FutureBuilder(
future: fetchPortrait(myDog.imageLink),
builder: (context, snapshot) {
if (snapshot.hasData) {
return new Image.network(
snapshot.data,
fit: BoxFit.scaleDown,
);
} else {
if (snapshot.hasError) {
return new Text('Hiba');
}
}
return new Center(
child: new CircularProgressIndicator(),
);
}))
],
), //Header image row
new Row(
children: <Widget>[
new Expanded(
child: new Text(
dogName,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
color: Colors.black),
))
],
),
new Row(
children: <Widget>[
new Expanded(
child: new Text(myDog.dogColor +
'színű, ' +
myDog.dogBreed +
' ' +
myDog.dogGender))
],
),
new Row(
children: <Widget>[
new Expanded(child: new Text('Kora: kb. ' + myDog.dogAge))
],
)
],
),
),
);
}
I tried making this function async as well, and making it wait for the fetchBasicDetails() to finish, so the data is present when it would use it.
I even tried using dynamic fetchBasicData(String URL) async {...} but that didn't help either.
Using Future<Dog> instead of dynamic also causes errors.
How could I make the buildBasicWidget use the fetchBasicData result? Or should I just handle the widget building in the fetchBasicData as a simple workaround?
You need to use a FutureBuilder and add your async function in the future argument otherwise the build method gets called before the data are obtained.
Alternatively do your async request inside initState.
I have a FutureBuilder that gets DISTINCT dates from a local sqlite DB, then I take each date and get the messages for those dates to put them in the widget, this works fine, until you want to listen realtime to a stream or poll for new messages which rebuilds the widgets and flickers the page and then scrolls to the beginning each time. I am hoping to find a way to take all the data into some object or other widget and then group by date and order, etc.. This way I can listen to a stream for updated messages, etc..
Any help would be great, here is my code if it helps anyone see what I do, this is after I converted to Streambuilder, but same result.
new StreamBuilder(
initialData: myInitialData,
stream: msgstream,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
myInitialData = snapshot.data;
return new RefreshIndicator(
child: new ListView.builder(
itemBuilder: (context, index) {
return new MyChatWidget(
datediv: snapshot.data[index]['msgdate'],
msgkey: snapshot.data[index]['msgkey'],
);
},
//itemBuilder: _itemBuilder,
controller: _scrollController,
reverse: true,
itemCount: snapshot.data.length,
),
onRefresh: _onRefresh
);
}
}
}),
This is the Widget that the StreamBuilder calls:
class MyChatWidget extends StatefulWidget {
MyChatWidget({Key key, this.datediv, this.msgkey}) : super(key: key);
final String datediv;
final String msgkey;
#override
_MyChatWidgetState createState() => new _MyChatWidgetState();
}
class _MyChatWidgetState extends State<MyChatWidget> {
List<Widget> messagelist;
int messagecount = 0;
var jsonCodec = const JsonCodec();
var mydate = '';
var _urlMessages = '';
PageStorageKey _key;
VideoPlayerController vcontroller;
//Future<http.Response> _responseFuture;
Future<List<Map>> _responseFuture;
List messList;
var mybytes;
File myimageview;
Image newimageview;
String imgStr;
String vidStr;
String vidimgstr;
bool submitting = false;
List<Map> myInitialData;
Stream<List<Map>> msgstream;
#override
void initState() {
super.initState();
if (new DateFormat.yMd().format(DateTime.parse(widget.datediv)) ==
new DateFormat.yMd().format(new DateTime.now())) {
mydate = 'Today';
} else {
mydate = new DateFormat.yMMMEd().format(DateTime.parse(widget.datediv));
}
DateChatMessage dcm =
new DateChatMessage(widget.msgkey, widget.datediv.toString());
var json = jsonCodec.encode(dcm);
_urlMessages =
'http://loop-dev.clinicalsoftworks.com/chat/messages/getbydate';
//_responseFuture = http.post(_urlMessages, body: json, headers: getAuthHeader());
_responseFuture =
ChatDB.instance.getMessagesByDate(widget.msgkey, widget.datediv);
msgstream = new Stream.fromFuture(_responseFuture);
//controller = new TabController(length: 4, vsync: this);
//_getMessages();
}
/*#override
void dispose() {
super.dispose();
if (vcontroller != null) {
vcontroller.dispose();
}
}*/
#override
Widget build(BuildContext context) {
_key = new PageStorageKey('${widget.datediv.toString()}');
return new Column(
children: <Widget>[
new Container(
child: new Text(
mydate,
textAlign: TextAlign.left,
style: new TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.centerLeft,
padding: new EdgeInsets.only(left: 10.0),
),
new Container(
child: new Divider(
height: 5.0,
color: Colors.grey,
),
padding: new EdgeInsets.only(left: 10.0, right: 10.0),
),
/**/
new StreamBuilder(
initialData: myInitialData,
stream: msgstream,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
myInitialData = snapshot.data;
List<dynamic> json = snapshot.data;
messagelist = [];
json.forEach((element) {
DateTime submitdate =
DateTime.parse(element['submitdate']).toLocal();
String myvideo = (element['chatvideo']);
String myimage = element['chatimage'];
String myvideoimage = element['chatvideoimage'];
File imgfile;
File vidfile;
File vidimgfile;
bool vidInit = false;
Future<Null> _launched;
String localAssetPath;
String localVideoPath;
String mymessage = element['message'].replaceAll("[\u2018\u2019]", "'");
//print('MYDATE: '+submitdate.toString());
_checkFile(File file) async {
var checkfile = await file.exists();
print('VIDEXISTS: '+checkfile.toString());
}
Future<Null> _launchVideo(String url, bool isLocal) async {
if (await canLaunchVideo(url, isLocal)) {
await launchVideo(url, isLocal);
} else {
throw 'Could not launch $url';
}
}
void _launchLocal() =>
setState(() => _launched = _launchVideo(localVideoPath, true)
);
Widget _showVideo() {
/*return new Flexible(
child: new vplayer.VideoCard(
controller: vcontroller,
title: element['referralname'],
subtitle: 'video',
),
);*/
return new Flexible(
child: new Card(
child: new Column(
children: <Widget>[
new ListTile(subtitle: new Text('Video'), title: new Text(element['referralname']),),
new GestureDetector(
onTap: _launchLocal,
child: new Image.file(
vidimgfile,
width: 150.0,
),
),
],
),
)
);
}
_initVideo() {
setState(() {vidInit = true;});
}
_onError() {
print('VIDEO INIT ERROR');
}
if (myimage != "") {
imgStr = element['chatimage'];
imgfile = new File(imgStr);
}
if (myvideo != "") {
vidStr = element['chatvideo'];
vidimgstr = element['chatvideoimage'];
vidimgfile = new File(vidimgstr);
//vidfile = new File(vidStr);
//_checkFile(vidfile);
//print('vidfile: '+vidfile.path);
localVideoPath = '$vidStr';
//print('LOCALVIDEO: '+localVideoPath);
//vcontroller = new VideoPlayerController('file://$vidStr')..initialize();
}
_showLgPic() {
Route route = new MaterialPageRoute(
settings: new RouteSettings(name: "/ShowPic"),
builder: (BuildContext context) => new ShowPic(
image: imgfile,
),
);
Navigator.of(context).push(route);
}
Widget _showGraphic() {
Widget mywidget;
if (myimage != "") {
mywidget = new GestureDetector(
child: new Image.file(
imgfile,
width: 300.0,
),
onTap: _showLgPic,
);
} else if (myvideo != "") {
mywidget = _showVideo();
} else {
mywidget = new Container();
}
return mywidget;
}
messagelist.add(
new Container(
//width: 300.0,
padding: new EdgeInsets.all(10.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
padding: new EdgeInsets.only(bottom: 5.0),
child: new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new CircleAvatar(
child: new Text(
element['sendname'][0],
style: new TextStyle(fontSize: 15.0),
),
radius: 12.0,
),
new Text(' '),
new Text(
element['sendname'],
style: new TextStyle(
fontSize: 15.0,
fontWeight: FontWeight.bold),
),
new Text(' '),
new Text(
new DateFormat.Hm().format(submitdate),
style: new TextStyle(
color: Colors.grey, fontSize: 12.0),
),
],
),
),
new Row(
children: <Widget>[
new Text(' '),
new Flexible(
child: new Text(mymessage),
)
],
),
new Container(
width: 150.0,
child: new Row(
children: <Widget>[
new Text(' '),
_showGraphic()
/*myimage != ""
? new GestureDetector(
child: new Image.file(
imgfile,
width: 300.0,
),
onTap: _showLgPic,
)
: myvideo != "" ? _showVideo() : new Container(),*/
],
)),
],
),
),
);
});
return new Column(children: messagelist);
}
},
)
],
);
}
}
Thanks for any assistance
which rebuilds the widgets and flickers the page and then scrolls to the beginning each time
To solve problem with scrolling try ScrollController. Create your own, keep it between updates and inject into List you created.
To solve flickering you could use Key for List widgets. Key should be unique identifier of message, e.g. msgkey
This example how to keep scrolloffset works for me
class SomeWidget extends StatefulWidget {
#override
_SomeWidgetState createState() => new _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
ScrollController _scrollController;
int _count;
#override
void initState() {
super.initState();
_count = 10;
_scrollController = new ScrollController();
}
void _add() {
setState(() => _count += 5);
}
#override
Widget build(BuildContext context) {
final _titles = new List<String>.generate(_count, (i) => 'Title ${i}');
return new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
actions: <Widget>[
new IconButton(icon: new Icon(Icons.add), onPressed: _add)
],
),
body: new ListView.builder(
controller: _scrollController,
itemCount: _titles.length,
itemBuilder: (context, index) => new ListTile(
title: new Text(_titles[index]),
),
),
);
}
}