Hello
I'm trying to make my first social app with Flutter and I'm stuck.
I would like to get my messages (in a conversasion between tow users) from my api.
Not a probleme when I use Future and Future Builder, but I would like the message list to update when a new message is send !
I found we can achieve it with stream, but every time I try to convert my Future In Stream, it still work, but just as if it was a Future (it never upadate on new message).
here I a simplified part of my code :
class Test extends StatelessWidget {
final Conv conv;
final User otherUser;
const Test({Key key, this.conv, this.otherUser}) : super(key: key);
Stream<List<Message>> messageFlow(String convId) {
return Stream.fromFuture(getMessages(convId));
}
Future<List<Message>> getMessages(String convId) async {
var data = await http
.post(MyApiUrl, headers: <String, String>{}, body: <String, String>{
"someParam": "param",
"id": convId,
});
var jsonData = json.decode(data.body);
List<Message> messages = [];
for (var m in jsonData) {
Message message = Message.fromJson(m);
messages.add(message);
}
return messages;
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: messageFlow(conv.id),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Container(
child: Center(
child: Text('Loading'),
),
);
}
return ListView.builder(
reverse: true,
controller: _messagesListController,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Message message = snapshot.data[index];
var isMe = message.owner == otherUser.id ? false : true;
return _buildMessage(message, isMe);
});
});
}
}
it would be so nice if you could help me !
I'm not able to replicate your sample code, but here how I understood your question.
Let's first define the difference about Future and Streams:
From this SO post
A Future is like the token with a number on it that they give you when
you order takeout; you made the request, but the result is not yet
ready but you have a placeholder. And when the result is ready, you
get a callback (the digital board above the takeout counter shows your
number or they shout it out) - you can now go in and grab your food
(the result) to take out.
A Stream is like that belt carrying little sushi bowls. By sitting
down at that table, you've "subscribed" to the stream. You don't know
when the next sushi boat will arrive - but when the chef (message
source) places it in the stream (belt), then the subscribers will
receive it. The important thing to note is that they arrive
asynchronously (you have no idea when the next boat/message will come)
but they will arrive in sequence (i.e., if the chef puts three types
of sushi on the belt, in some order -- you will see them come by you
in that same order)
Now here is an example of how you can create your own stream from scratch:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 1st approach
final StreamController _streamController = StreamController();
addData()async{
for(int i = 1; i<= 10; i++) {
await Future.delayed(Duration(seconds: 1));
_streamController.sink.add(i);
}
}
// 2nd approach
// This approach will prevent some approach of memory leaks
Stream<int> numberStream() async*{
for(int i = 1; i<= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
_streamController.close();
}
#override
void initState() {
// TODO: implement initState
super.initState();
addData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Stream"),
),
body: Center(
child: StreamBuilder(
stream: numberStream().map((number) => "number $number"),
builder: (context, snapshot){
if(snapshot.hasError)
return Text("hey there is some error");
else if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
return Text("${snapshot.data}", style: Theme.of(context).textTheme.display1,);
},
)
),
);
}
}
You can also check this SO post for some references.
Here, I tweaked the sample in the SO post above to create a mini simple chat server to show how the messages updates.
import 'dart:async';
import 'package:flutter/material.dart';
class Server {
StreamController<String> _controller = new StreamController.broadcast();
void simulateMessage(String message) {
_controller.add(message);
}
Stream get messages => _controller.stream;
}
final server = new Server();
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<String> _messages = <String>[];
StreamSubscription<String> _subscription;
#override
void initState() {
_subscription = server.messages.listen((message) async => setState(() {
_messages.add(message);
}));
super.initState();
}
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.display2;
return new Scaffold(
appBar: new AppBar(
title: new Text('Sample App'),
),
body: new ListView(
children: _messages.map((String message) {
return new Card(
child: new Container(
height: 100.0,
child: new Center(
child: new Text(message, style: textStyle),
),
),
);
}).toList(),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
new FloatingActionButton(
child: new Icon(Icons.account_circle_outlined),
onPressed: () {
// simulate a message arriving
server.simulateMessage('Hello World');
},
),
SizedBox(
height: 20.0,
),
new FloatingActionButton(
child: new Icon(Icons.account_circle_rounded),
onPressed: () {
// simulate a message arriving
server.simulateMessage('Hi Flutter');
},
),
],
),
);
}
}
class SampleApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomeScreen(),
);
}
}
void main() {
runApp(new SampleApp());
}
And here are some tutorials for better references:
https://www.youtube.com/watch?v=nQBpOIHE4eE
https://www.youtube.com/watch?v=OTS-ap9_aXc
this works for me
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as HTTP;
class PeriodicRequester extends StatelessWidget {
Stream<http.Response> getRandomNumberFact() async* {
yield* Stream.periodic(Duration(seconds: 5), (_) {
return http.get("http://numbersapi.com/random/");
}).asyncMap((event) async => await event);
}
#override
Widget build(BuildContext context) {
return StreamBuilder<http.Response>(
stream: getRandomNumberFact(),
builder: (context, snapshot) => snapshot.hasData
? Center(child: Text(snapshot.data.body))
: CircularProgressIndicator(),
);
}
}
Related
I'm trying to create a chart on a Flutter web app by accessing data from Cloud Firestore.
However, it can't seem to extract and map the details from Firestore.
Firebase console
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
home.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:inglesy/items.dart';
import 'package:charts_flutter/flutter.dart' as charts;
class Home extends StatefulWidget {
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
List<charts.Series<Item, String>>? _seriesBarData; //Try (dynamic,String)
List<Item>? myData;
_generateData(myData) {
print("_generateData worked");
_seriesBarData?.add(
charts.Series(
domainFn: (Item item, _) => item.itemstring.toString(),
measureFn: (Item item, _) => item.itemvotes,
id: 'Items',
data: myData,
),
);
}
#override
Widget build(BuildContext context) {
print("returning AppBar/scaffold now");
return Scaffold(
appBar: AppBar(
title: const Text("This is a title."),
foregroundColor: Colors.pink,
),
body: _buildBody(context),
);
}
Widget _buildBody(context) {
print("Doing _buildBody now");
final Stream<QuerySnapshot> _userStream =
FirebaseFirestore.instance.collection("poll").snapshots();
return StreamBuilder<QuerySnapshot>(
stream: _userStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
} else {
List<Item> item = snapshot.data!.docs
.map((DocumentSnapshot document) =>
Item.fromMap(document.data() as Map<String, dynamic>))
.toList();
return _buildChart(context, item);
}
},
);
}
Widget _buildChart(BuildContext context, List<Item> item) {
myData = item;
_generateData(myData);
return Padding(
padding: EdgeInsets.all(8.0),
child: Container(
child: Center(
child: Column(
children: [
Text("This is a text"),
SizedBox(height: 10.0),
Expanded(
child: charts.BarChart(
_seriesBarData!,
animate: true,
animationDuration: const Duration(seconds: 2),
),
)
],
),
),
),
);
}
}
items.dart
class Item {
final String? itemstring;
final int? itemvotes;
Item({this.itemstring, this.itemvotes});
Item.fromMap(Map<String, dynamic> map)
: assert(map['itemstring'] != null),
assert(map['itemvotes'] != null),
itemstring = map['itemstring'],
itemvotes = map['itemvotes'];
#override
String toString() {
return "Item string: $itemstring | Item votes: $itemvotes";
}
}
It shows this error
PS, I've already done the necessary set-up i.e. I've already installed Firebase CLI and have it generated firbase_options.dart
PPS, I have also already set up Firebase (anonymous) authentication and it works with no errors. But for now, I'm not using it and I'm automatically running home.dart to focus on the Firebase database aspect.
Why don’t you try making the Item.fromMap method a regular factory method like:
factory Item.fromMap(Map<String, dynamic> map) {
return Item(
itemstring = map['itemstring'] ?? '',
itemvotes = map['itemvotes'] ?? ''
);
}
On Flutter Web, the userChanges stream from a FirebaseAuth instance never emits the signed in user after a page reload. Instead, it only emits null. With the example below, the app gets stuck on the loading page.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/loading',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/error':
return MaterialPageRoute(builder: (_) => ErrorScreen());
case '/loading':
return MaterialPageRoute(builder: (_) => LoadingScreen());
case '/signin':
return MaterialPageRoute(builder: (_) => SignInScreen());
case '/welcome':
return MaterialPageRoute(builder: (_) => WelcomeScreen());
default:
return MaterialPageRoute(
builder: (_) => Center(child: Text('Unknown route')));
}
},
);
}
}
class ErrorScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Text('An error occurred.'),
),
);
}
}
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
#override
void initState() {
init();
super.initState();
}
init() async {
try {
await Firebase.initializeApp();
if (kDebugMode) {
await FirebaseAuth.instance.useEmulator('http://localhost:1001');
}
final preferences = await SharedPreferences.getInstance();
bool signedIn = preferences.getBool('IS_SIGNED_IN') ?? false;
String landingPath = '/signin';
if (signedIn) {
landingPath = '/welcome';
// Wait for the userChanges to emit a non-null element.
await FirebaseAuth.instance
.userChanges()
.firstWhere((user) => user != null);
}
Navigator.of(context).pushReplacementNamed(landingPath);
} catch (error) {
print(error);
Navigator.of(context).pushReplacementNamed('/error');
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Text('Loading...'),
),
);
}
}
class SignInScreen extends StatelessWidget {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email address'),
),
TextField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
),
ElevatedButton(
onPressed: () => _submit(context),
child: Text('SIGN IN'),
),
],
),
),
);
}
void _submit(BuildContext context) async {
final email = _emailController.text;
final password = _passwordController.text;
FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password)
.then((credential) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool('IS_SIGNED_IN', true);
Navigator.of(context).pushReplacementNamed('/welcome');
});
}
}
class WelcomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('Welcome!'),
ElevatedButton(
onPressed: () => _signOut(context),
child: Text('SIGN OUT'),
)
],
),
),
);
}
void _signOut(BuildContext context) {
FirebaseAuth.instance.signOut().then((_) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool('IS_SIGNED_IN', false);
Navigator.of(context).pushReplacementNamed('/signin');
});
}
}
If it helps, I'm using version 8.3.0 of the Firebase Javascript libraries in my index.html file.
Am I missing something here?
It turns out the code above is fine. The issue is the emulator. Commenting out these lines of code makes the app behave as expected:
if (kDebugMode) {
FirebaseAuth.instance.useEmulator('http://localhost:1001');
}
I've filed a bug report on FlutterFire's repo about this emulator issue.
I'm trying to play a video from a URL of a Firestore Document. To play a video in Flutter, I have to instantiate its Url in the init() method. I set a default URL to a butterfly video, and the value was supposed to be replaced by the URL obtained from Firestore. (So that it is easy for me to see if the code works). However, the code does not work properly. I got an error that says "NoSuchMethodError: The getter 'value' was called on null".
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Create the initialization Future outside of build
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
// Check for error
if (snapshot.hasError) {
print(snapshot.error);
return Center(
child: Container(
child: Text(
"Something went wrong",
textDirection: TextDirection.ltr,
),
),
);
}
//Once complete, show your application
if (snapshot.connectionState == ConnectionState.done) {
return MaterialApp(
title: 'Flutter Demo',
home: VideoPlayerScreen(),
);
}
return CircularProgressIndicator();
});
}
}
class VideoPlayerScreen extends StatefulWidget {
#override
_VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
FirebaseFirestore firestore = FirebaseFirestore.instance;
String videoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4';
#override
void initState() {
firestore.collection("videos").get().then((QuerySnapshot querySnapshot) => {
querySnapshot.docs.forEach((doc) {
// _controller.dispose();
videoUrl = doc["videoUrl"];
_controller = VideoPlayerController.network(videoUrl);
_initializeVideoPlayerFuture = _controller.initialize();
print(videoUrl);
})
});
// _controller = VideoPlayerController.network(videoUrl);
// _initializeVideoPlayerFuture = _controller.initialize();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Video Player"),
),
body: FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Column(
children: [
AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
),
],
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (_controller.value.isPlaying) {
_controller.pause();
} else {
_controller.play();
}
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}
Try the following:
#override
void initState() {
super.initState();
firestore.collection("videos").get().then((QuerySnapshot querySnapshot) => {
querySnapshot.docs.forEach((doc) {
videoUrl = doc["videoUrl"];
_controller = VideoPlayerController.network(videoUrl);
_initializeVideoPlayerFuture = _controller.initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
});
});
}
Since initialize() is asynchronous, then you can use the method then which will get called when the future completes. Inside the callback, you can call setState() which will trigger a rebuild and notify the framework that the internal state of the widgets has changed .
https://pub.dev/packages/video_player
Code:
final ref = Firestore.instance.document('some/path');
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Stream _stream;
#override
void initState() {
super.initState();
_stream = ref.snapshots().map((snapshot) {
// <--------------------------------------------------------------- line 1
return snapshot.data;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: _stream,
builder: (_, snapshot) {
if (snapshot.hasData) {
return RaisedButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => Page2())),
child: Text("Navigate"),
);
}
return CircularProgressIndicator();
},
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
onPressed: () => ref.updateData({'key': "value"}), // <------ line 2
child: Text('Update data'),
),
);
}
}
This code is reproducible, no compile time error. Let me give you an overview.
There are 2 screen Screen1 and Screen2. On first screen, I have set up a StreamBuilder which listens for value provided using _stream. So far so good.
But when I navigate to Screen2 (see line 2), and update the server from it, the Screen1 snapshot gets built again (see line 1). How can I cancel that Stream so that I don't have to be worry about when Screen1 isn't in view?
i'm new to flutter
i try to do an http call and then populate a list. The problem is that the http call works fine but the UI doesn't refresh.
i think that the problem is that the framework create ui with the list before the http call is finish. Than when the http call is finished i'm unable to update the ui.
i've search but i didn't find anything.
EDIT--FOUND SOLUTION IN Listview.Builder code here
http call
static Future<Map> getData() async {
try{
http.Response res = await http.get("http://....");
Map data = JSON.decode(res.body);
print(res.body);
return data;
}
catch(exception){
//todo
}
}
main
class _MyHomePageState extends State<MyHomePage> {
List<Widget> _items = new List<GithubCardItem>();
#override
void initState() {
super.initState();
print("start download");
_downloadData();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text(widget.title)),
body: new ListView.builder(
itemBuilder: (BuildContext context, int index) => _items[index],
itemExtent: 128.0,
itemCount: _items.length,
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add),
onPressed: (){
_downloadData();
},
)
);
}
GithubCard _extractDataFromJson(Map githubCard){
GithubCard card = new GithubCard(githubCard["name"], githubCard["avatar_url"], githubCard["description"], githubCard["stargazers_count"], githubCard["open_issued_count"]);
return card;
}
void _downloadData(){
MyHttpCall.getData().then((jsonData) {
//check if i have card to display
if(jsonData["items"] != null){
for(var githubCard in jsonData["items"]){
setState(() {
GithubCard card = _extractDataFromJson(githubCard);
this._items.add(new GithubCardItem(card));
});
print("adding elements");
}
}
});
}
}
the GithubCardItem is a simple stateless widget that return a text.
There are different ways to use ListView as well as get data from network
here is the sample code for your problem
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List list = new List();
void fetchData() {
getData().then((res) {
setState(() {
list.addAll(res);
});
});
}
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new ListView.builder(
itemCount: list.length,
itemBuilder: ((BuildContext _context, int position) {
return new ListTile(
title: new Text( list[position]['login'].toString()),
subtitle: new Text(list[position]['url']),
leading: new Image.network(list[position]['avatar_url']),
);
}),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: fetchData,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
Future<List> getData() async {
var url = "https://api.github.com/users";
List data = new List();
var httpClient = new HttpClient();
var request = await httpClient.getUrl(Uri.parse(url));
var response = await request.close();
if (response.statusCode == HttpStatus.OK) {
var jsonString = await response.transform(utf8.decoder).join();
data = json.decode(jsonString);
return data;
}else{
return data;
}
}
}
Try this:
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {----------- }
class MyHomePage extends StatefulWidget {-----------}
class _MyHomePageState extends State<MyHomePage> {
List data;
Future<String> getData() async {
var response = await http.get(
Uri.encodeFull("https://api.github.com/users"),
headers: {"Accept": "application/json"});
this.setState(() {
data = json.decode(response.body);
});
return "Success!";
}
#override
void initState() {
this.getData();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Listviews"),
),
body: new ListView.builder(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int position) {
return new ListTile(
title: new Text(data[position]['login'].toString()),
subtitle: new Text(data[position]['url']),
leading: new Image.network(data[position]['avatar_url']),
);
},
),
);
}
}
Made bit more simple and it's working. Check the sample here