I currently have a simple API but my flutter widgets could not detect the changes happening in the API without restarting the whole application.
The API was made as a test API on https://www.mockable.io/. I managed to read from this API using a StreamBuilder. And still it does not change the values in the widget whenever i update their value in the API.
Default JSON
{
"id": "123",
"token": "1token",
"status": "Valid"
}
After Value Change
{
"id": "123",
"token": "1token",
"status": "Not Valid"
}
My StreamBuilder Code
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
final String url = "http://demo2722084.mockable.io/user";
class StreamPage extends StatefulWidget {
#override
StreamPageState createState() {
return new StreamPageState();
}
}
class StreamPageState extends State<StreamPage> {
StreamController _userController;
Future fetchUser() async{
final response = await http.get(url);
if(response.statusCode == 200){
return json.decode(response.body);
}else{
print("Exception caught: Failed to get data");
}
}
loadUser() async{
fetchUser().then((res) async{
_userController.add(res);
return res;
});
}
#override
void initState() {
_userController = new StreamController();
loadUser();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Stream"),
),
body: new StreamBuilder(
stream: _userController.stream,
builder: (context, snapshot){
if(snapshot.hasError){
print("Exception: ${snapshot.error}");
}
if(snapshot.hasData){
var user = snapshot.data;
var id = user["id"] ?? "";
var token = user["token"] ?? "";
var status = user["status"] ?? "";
return new Center(
child: new ListTile(
title: new Text("Token: $token"),
subtitle: new Text("Status: $status"),
leading: new Text(id),
),
);
}
if(snapshot.connectionState != ConnectionState.waiting){
return new Center(
child: new CircularProgressIndicator(),
);
}
if(!snapshot.hasData && snapshot.connectionState != ConnectionState.done){
return new Center(
child: new CircularProgressIndicator(),
);
}
},
),
);
}
}
I expected the StreamBuilder to pick up the changed status value but the app has to be restarted for the new value to be reflected in the app.
Is there an example of how I can achieve this properly since this is quite frankly my first time trying out a StreamBuilder.
Your HTTP request results in just one data point, so for that you could use a FutureBuilder. But you want to know when something changes at the server. Your HTTP connection has finished when the change is made and so has no idea that it has happened.
There are several ways to detect changes at the server, for example, polling, user-initiated refresh, websockets, etc. For demonstration purposes, instead of calling loadUser in initState you could start a 1 second periodic timer to do that, which would update the stream every second and you should see the change. (Don't forget to cancel the timer in dispose.)
That's unlikely to be viable in production, so explore websockets, etc for pushed notifications from the web server. Also explore whether Firebase Cloud Messaging is a better way to send notification of changes to your application.
#override
void initState() {
super.initState();
_userController = StreamController();
Timer.periodic(Duration(seconds: 1), (_) => loadUser());
}
you probably also need to do this only while the app is in foreground. You can use my LifecycleAwareStreamBuilder, there is an example on how to use it here
Related
I am struggling with RxDart (maybe just straight up Rx programming). I currently have a stateful widget that calls my bloc in it's didChangeDependencies(). That call goes out and gets data via http request and adds it to a stream. I'm using BehaviorSubject and this works fine. I have child widgets using StreamBuilders and they get data no problem. My issue comes in dealing with errors. If my http request fails, I hydrate the stream with addError('whatever error') but my child widget's StreamBuilder is not receiving that error. It doesn't get anything at all.
So I have a few questions.
Is that expected?
Should error handling not be done in StreamBuilder? Ideally, I want to show something in the UI if something goes wrong so not sure how else to do it.
I could make my child widget stateful and use stream.listen. I do receive the errors there but it seems like overkill to have that and the StreamBuilder.
Am I missing something fundamental here about streams and error handling?
Here is my bloc:
final _plans = BehaviorSubject<List<PlanModel>>();
Observable<List<PlanModel>> get plans => _plans.stream;
fetchPlans() async {
try {
final _plans = await _planRepository.getPlans();
_plans.add(_plans);
}
on AuthenticationException {
_plans.addError('authentication error');
}
on SocketException {
_plans.addError('no network connection');
}
catch(error) {
_plans.addError('fetch unsuccessful');
}
}
Simplified Parent Widget:
class PlanPage extends StatefulWidget {
#override
PlanPageState createState() {
return new PlanPageState();
}
}
class PlanPageState extends State<PlanPage> {
#override
void didChangeDependencies() async {
super.didChangeDependencies();
var planBloc = BaseProvider.of<PlanBloc>(context);
planBloc.fetchPlans();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( title: const Text('Your Plan') ),
body: PlanWrapper()
);
}
}
Simplified Child Widget with StreamBuilder:
class PlanWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
var planBloc = BaseProvider.of<PlanBloc>(context);
return StreamBuilder(
stream: planBloc.plans,
builder: (BuildContext context, AsyncSnapshot<List<PlanModel>> plans) {
if (plans.hasError) {
//ERROR NEVER COMES IN HERE
switch(plans.error) {
case 'authentication error':
return RyAuthErrorCard();
case 'no network connection':
return RyNetworkErrorCard();
default:
return RyGenericErrorCard(GeneralException().errorMessages()['message']);
}
}
if (plans.hasData && plans.data.isNotEmpty) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: _buildPlanTiles(context, plans.data)
);
}
return Center(child: const CircularProgressIndicator());
}
);
}
}
There was an issue about this in the RxDart GitHub (https://github.com/ReactiveX/rxdart/issues/227). BehaviorSubjects were not replaying errors to new listeners.
It was fixed in version 0.21.0. "Breaking Change: BehaviorSubject will now emit an Error, if the last event was also an Error. Before, when an Error occurred before a listen, the subscriber would not be notified of that Error."
I am creating a subscription with GraphQL, and I need to consume that subscription with Flutter, but I don't know how to do that, the thing that I need is something like a UI component that would be tied to a subscription and it will be automatically refreshed.
I will appreciate any feedback.
My GraphqlServer runs a subscription named getLogs which returns the log information in the following format.
{
"data": {
"getLogs": {
"timeStamp": "18:09:24",
"logLevel": "DEBUG",
"file": "logger.py",
"function": "init",
"line": "1",
"message": "Hello from logger.py"
}
}
}
If you are like me who wants to use only the GraphQL Client directly, then following sample could help.
import 'package:graphql/client.dart';
import 'package:graphql/internal.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class LogPuller extends StatefulWidget {
static final WebSocketLink _webSocketLink = WebSocketLink(
url: 'ws://localhost:8000/graphql/',
config: SocketClientConfig(
autoReconnect: true,
),
);
static final Link _link = _webSocketLink;
#override
_LogPullerState createState() => _LogPullerState();
}
class _LogPullerState extends State<LogPuller> {
final GraphQLClient _client = GraphQLClient(
link: LogPuller._link,
cache: InMemoryCache(),
);
// the subscription query should be of the following format. Note how the 'GetMyLogs' is used as the operation name below.
final String subscribeQuery = '''
subscription GetMyLogs{
getLogs{
timeStamp
logLevel
file
function
line
message
}
}
''';
Operation operation;
Stream<FetchResult> _logStream;
#override
void initState() {
super.initState();
// note operation name is important. If not provided the stream subscription fails after first pull.
operation = Operation(document: subscribeQuery, operationName: 'GetMyLogs');
_logStream = _client.subscribe(operation);
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _logStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Container(
child: CircularProgressIndicator(
strokeWidth: 1.0,
),
),
);
}
if (snapshot.hasData) {
return Center(
child: Text(
snapshot.data.data['getLogs']
['message'], // This will change according to you needs.
),
);
}
return Container();
},
);
}
}
As I am using a StreamBuilder to build the widget it will take care of closing the stream. If this is not the case for you, stream.listen() method will return a StreamSubscription<FetchResult> object which you can call the cancel() method which can be done inside dispose() method of a stateful widget or any such method for a standalone Dart client.
You can check the next library https://github.com/zino-app/graphql-flutter
Just find out the bug in this library, Just open the subscription.dart file by ctrl+click on Subscription. In that file, it is easy to see that socketClient variable is null. So just define it in initState() function as shown in the docs. Restart your app. And it works like a charm. So, basically, you just need to initialize that variable in the subscription.dart file.
I'm trying to publish a pub/sub message in my flutter application like this:
import 'package:flutter/material.dart';
import 'package:googleapis/pubsub//v1.dart';
import 'package:googleapis_auth/auth_io.dart';
const _SCOPES = const [PubsubApi.PubsubScope];
class Activities extends StatefulWidget {
Activities();
#override
_Activities createState() => _Activities();
}
final _credentials = new ServiceAccountCredentials.fromJson(r'''
{
"type": "service_account",
...
}
''');
class _Activities extends State<Activities> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Center(
child: Container(
child: new MaterialButton(
onPressed: () {
debugPrint("trying to publish a message...");
clientViaServiceAccount(_credentials, _SCOPES)
.then((http_client) {
var pubSubClient = new PubsubApi(http_client);
Map<String, dynamic> message = new Map<String, dynamic>();
message["data"] = Map<String, dynamic>();
message["data"]["type"] = "foo";
message["data"]["senderId"] = "bar";
pubSubClient.projects.topics
.publish(new PublishRequest.fromJson(message), "projects/myProject/topics/events")
.then((publishResponse) {
debugPrint(publishResponse.toString());
}).catchError((e,m){
debugPrint(e.toString());
});
})
.catchError((e,m) {
debugPrint(e.toString());
});
},
child: new Text("Publish message"),
),
),
)
);
}
}
But in the logs I get the following error message:
I/flutter ( 5281): DetailedApiRequestError(status: 400, message: The value for 0 is too small. You passed message_count in the request, but the minimum value is 1.)
I googled this message but did not found anything. I think my message structure is maybe wrong, but maybe it's something else ? Don't wanna lose too much time on it...
Ok, for those who wonder, here is what I have come up with:
var messages = {
'messages': [
{
'data': base64Encode(utf8.encode('{"foo": "bar"}')),
},
]
};
pubSubClient.projects.topics
.publish(new PublishRequest.fromJson(messages), "your topic")
.then((publishResponse) {
debugPrint(publishResponse.toString());
}).catchError((e,m){
debugPrint(e.toString());
});
Maybe someone should write an article on it, this was not very clear and I had to try different things and also read the source from the pubsub client library...
Use gcloud: ^0.7.3 package.
Use the below code to establish a connection.
import 'package:gcloud/pubsub.dart';
import 'package:googleapis/pubsub/v1.dart';
import 'package:googleapis_auth/auth_io.dart';
// Read the service account credentials from the file.
var jsonCredentials = new File('my-project.json').readAsStringSync();
var credentials = new auth.ServiceAccountCredentials.fromJson(jsonCredentials);
var client = await auth.clientViaServiceAccount(credentials, scopes);
var pubsub = new PubSub(client, 'my-project');
For the Publish and Subscribe refer to this section of the readme.
I've come across a problem while trying out Flutter that I can't figure out. The case I'm thinking of has a FutureBuilder widget like below:
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Example Page"),
),
body: new FutureBuilder(
future: _exampleFuture,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Center(child: new CircularProgressIndicator(),);
default:
if(snapshot.hasError) {
return new Center(child: new Text('Error: ${snapshot.error}'),);
}
else {
return new Center(child: new Text("Result: ${snapshot.data}"),);
}
}
}
)
);
}
Now let's assume the future is an http call that ends up with a 401 error, indicating that the user is unauthorized. At this point, I'd like the app to erase any token that's stored and redirect to the login page or just rebuild the app. But I can't call a method that does that in the build function, and I don't think didUpdateWidget() is guaranteed to be called, as the future might return it's value before build is called? Maybe I'm approaching this completely wrong, but is there a way to do this in Flutter?
You can check for a statusCode inside your Async method, and use setState to erase the value of the token based on the statusCode value; otherwise, if the connection is authorized, return your desired data. Now, in your FutureBuilder , check if the you snapshot is null to show a SignIn() page instead.
For example, your method that handles the http requests might look something like:
_Request() async {
var httpClinet = createHttpClient();
var response = await httpClinet.get(
url, headers: {'Authorization': "Bearer $_currentUserToken"});
if (response.statusCode == 200) {
var myRequest = JSON.decode(response.body);
var myDesiredData;
///TODO: Some data conversions and data extraction
return myDesiredData;
}
else {
setState(() {
_currentUserToken = null;
});
return null;
}
}
Then you can have a FutureBuilder like this:
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _request(),
builder: (BuildContext context, AsyncSnapshot response) {
response.hasData==false? new SignIn(): new Scaffold(
appBar: new AppBar(title: new Text("Future Builder"),),
body: new Center(
child: new Text("Build your widgets"),
),
);
},
);
}
In my app, I have a drawer with a UserAccountsDrawerHeader, which I feed its properties by simply getting the x property from FirebaseAuth.instance.currentUser.x
In the latest firebase_auth 0.2.0 version , where currentUser() is async.
I have been trying for several hours to store the information of the currently logged user and have not yet reached the correct way to do this.
I understand that I can access them by something like the following:
Future<String> _getCurrentUserName() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user.displayName;
}
...
new UserAccountsDrawerHeader(accountName: new Text(_getCurrentUserName()))
I understand that these code snippets will give type mismatch, but I am just trying to illustrate what I am trying to do.
What am I missing exactly that is preventing me from reaching a solution?
Update
class _MyTabsState extends State<MyTabs> with TickerProviderStateMixin {
TabController controller;
Pages _page;
String _currentUserName;
String _currentUserEmail;
String _currentUserPhoto;
#override
void initState() {
super.initState();
_states();
controller = new TabController(length: 5, vsync: this);
controller.addListener(_select);
_page = pages[0];
}
My method
I just coupled the auth state with my previously implemented TabBar state
_states() async{
var user = await FirebaseAuth.instance.currentUser();
var name = user.displayName;
var email = user.email;
var photoUrl = user.photoUrl;
setState(() {
this._currentUserName=name;
this._currentUserEmail=email;
this._currentUserPhoto=photoUrl;
_page = pages[controller.index];
});
}
My Drawer
drawer: new Drawer(
child: new ListView(
children: <Widget>[
new UserAccountsDrawerHeader(accountName: new Text(_currentUserName) ,
accountEmail: new Text (_currentUserEmail),
currentAccountPicture: new CircleAvatar(
backgroundImage: new NetworkImage(_currentUserPhoto),
),
Here is the exception I get from the debug console
I/flutter (14926): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (14926): The following assertion was thrown building MyTabs(dirty, state: _MyTabsState#f49aa(tickers:
I/flutter (14926): tracking 1 ticker)):
I/flutter (14926): 'package:flutter/src/widgets/text.dart': Failed assertion: line 207 pos 15: 'data != null': is not
I/flutter (14926): true.
I/flutter (14926): Either the assertion indicates an error in the framework itself, or we should provide substantially
Update 2:
This is how I modified the google sign in function from the firebase examples:
Future <FirebaseUser> _testSignInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
//checking if there is a current user
var check = await FirebaseAuth.instance.currentUser();
if (check!=null){
final FirebaseUser user = check;
return user;
}
else{
final FirebaseUser user = await _auth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
assert(user.email != null);
assert(user.displayName != null);
assert(!user.isAnonymous);
assert(await user.getToken() != null);
return user;
}
}
Update 3:
My main function
void main() {
runApp(
new MaterialApp(
home: new SignIn(),
routes: <String, WidgetBuilder>{
"/SignUp":(BuildContext context)=> new SignUp(),
"/Login": (BuildContext context)=> new SignIn(),
"/MyTabs": (BuildContext context)=> new MyTabs()},
));
}
And then my SignIn contains a google button that when pressed:
onPressed: () { _testSignInWithGoogle(). //async returns FirebaseUser
whenComplete(()=>Navigator.of(context).pushNamed("/MyTabs")
);
}
and the Drawer from update 1 is included within MyTabs build.
There are several possibilities.
First : Use a stateful widget
Override the initState method like this :
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
String _currentUserName;
#override
initState() {
super.initState();
doAsyncStuff();
}
doAsyncStuff() async {
var name = await _getCurrentUserName();
setState(() {
this._currentUserName = name;
});
}
#override
Widget build(BuildContext context) {
if (_currentUserName == null)
return new Container();
return new Text(_currentUserName);
}
}
Second : Use the FutureBuilder widget
Basically, it's a wrapper for those who don't want to use a stateful widget. It does the same in the end.
But you won't be able to reuse your future somewhere else.
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _getCurrentUserName(),
builder: (context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData)
return new Text(snapshot.data.toString());
else
return new Container();
},
);
}
}
Explanation :
Your getCurrentUserName is asynchronous.
You can't just directly mix it with other synchronous functions.
Asynchronous functions are quite useful. But if you want to use them, just remember two things :
Inside another async function, you can var x = await myFuture, which will wait until myFuture finish to get it's result.
But you can't use await inside a sync function.
Instead, you can use
myFuture.then(myFunction) or myFuture.whenComplete(myFunction). myFunction will be called when the future is finished. And they both .then and .whenComplete will pass the result of your future as parameter to your myFunction.
"How to properly implement authentification" ?
You should definitely not do it this way. You'll have tons of code duplication.
The most ideal way to organise layers such as Authentification is like this :
runApp(new Configuration.fromFile("confs.json",
child: new Authentification(
child: new MaterialApp(
home: new Column(
children: <Widget>[
new Text("Hello"),
new AuthentifiedBuilder(
inRoles: [UserRole.admin],
builder: (context, user) {
return new Text(user.name);
}
),
],
),
),
),
));
And then, when you need a configuration or the current user inside a widget, you'd do this :
#override
Widget build(BuildContext context) {
var user = Authentification.of(context).user;
var host = Configuration.of(context).host;
// do stuff with host and the user
return new Container();
}
There are so many advantages about doing this, that there's no reason not to do it.
Such as "Code once, use everywhere". Or the ability to have a generic value and override it for a specific widget.
You'll realise that a lot of Flutter widgets are following this idea.
Such as Navigator, Scaffold, Theme, ...
But "How to do this ??"
It's all thanks to the BuildContext context parameter. Which provides a few helpers to do it.
For exemple, the code of Authentification.of(context) would be the following :
class Authentification extends StatefulWidget {
final Widget child;
static AuthentificationData of(BuildContext context) {
final AuthentificationData auth = context.inheritFromWidgetOfExactType(AuthentificationData);
assert(auth != null);
return auth;
}
Authentification({this.child});
#override
AuthentificationState createState() => new AuthentificationState();
}