Flutter riverpod: too many rebuilds for StreamProvider/FutureProvider combination - flutter

Using flutter_riverpod, given the following providers:
final s = StreamProvider.autoDispose<bool>((ref) async* {
yield true;
});
final f = FutureProvider.autoDispose<bool>((ref) async {
final source = await ref.watch(s.last);
print('Stream value received: $source');
return source;
});
When f is referenced from a Widget, it will print the message twice, i.e. it will rebuild twice, even though the referenced StreamProvider's stream only emits a single value.
Stream value received: true
Stream value received: true
Why is that? How can I make sure that my FutureProvider will only run once for each emitted stream value?
Here is the full minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final s = StreamProvider.autoDispose<bool>((ref) async* {
yield true;
});
final f = FutureProvider.autoDispose<bool>((ref) async {
final source = await ref.watch(s.last);
print('Stream value received: $source');
return source;
});
void main() {
runApp(ProviderScope(child: (MyApp())));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
watch(f);
return const Text('hi');
}
}

Related

Flutter login control in splash screen

I want to make a small login application. When entering the application, I want to inquire whether the user has a token code or not on the splash screen. How can do this? thank you for help.
main.dart file
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashScreen(),
);
}
}
My splash screen.
I want to know if the user has a token or not
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
loginControl();
}
// ignore: missing_return
Future<bool> loginControl() async {
bool status = AuthController.isLoginUser() as bool;
print(status);
if (status) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => HomeScreen()));
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => LoginScreen()));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('welcome my app'),
),
);
}
}
my auth controller like this;
class AuthController {
static Future<bool> isLoginUser() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
String token = sharedPreferences.getString("token");
if (token == null) {
return false;
} else {
return true;
}
}
}
Your isLoginUser is actually returning a Future<bool> means that it returns a Future that will later resolve to a bool value.
So, when you use it like this in your loginControl,
bool status = AuthController.isLoginUser() as bool;
AuthController.isLoginUser() return Future<bool> and it can't be directly converted to a bool using as bool.
Instead you should await that Future to resolve, like this.
bool status = await AuthController.isLoginUser(); // This will work.
Now, your code will pause at this line, until it gets a return value from isLoginUser and then resume to next line with status being an actual bool value. i.e., true or false.

What is the correct way to execute initial network request in Flutter?

I have the following code, to get initial data for the screen and this SchedulerBinding seems to be a hack, but if I remove it, request data is lost.
I think it happens due to the fact widgets(streamBuilders etc.) are not yet built.
Any ideas how can I fix this?
Full screen code: https://gist.github.com/Turbozanik/7bdfc69b36fea3dd38b94d8c4fcdcc84
Full bloc code: https://gist.github.com/Turbozanik/266d3517a297b1d08e7a3d7ff6ff245f
SchedulerBining is not a hack,according to docs addPostFrame call callback only once and if you remove it your stream will never get the data
but you can call your stream loading in iniState
void initState(){
super.initState();
_mblock.loadSpotMock();
}
You can load your data asynchronously in the initState method, meanwhile you can show a loader or message. Once your data has loaded, call setState to redraw the widget.
Here is an example of this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
createState() => new MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
String _data;
Future<String> loadData() async {
// Simulate a delay loading the data
await Future<void>.delayed(const Duration(seconds: 3));
// Return the data
return "This is your data!";
}
#override
initState() {
super.initState();
// Call loadData asynchronously
loadData().then((s) {
// Data has loaded, rebuild the widget
setState(() {
_data = s;
});
});
}
#override
Widget build(BuildContext context) {
if (null == _data) {
return Text("Loading...");
}
return Text(_data);
}
}
You can test it in https://dartpad.dartlang.org
It works like this:
initState will call loadData asynchronously, then the build method will draw the widget.
when loadData returns, the call to setState will redraw the widget.
Using StreamBuilder
The following example uses a StreamBuilder to show the data, once it's loaded:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
createState() => new MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
// Create a stream and execute it
final Stream<String> _myStream = (() async* {
// Simulate a delay loading the data
await Future<void>.delayed(const Duration(seconds: 3));
// Return the data
yield "This is your data!";
})();
#override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: _myStream,
builder: (BuildContext context, s) {
String result;
if (s.hasError) {
result = "Error";
}
else {
if (s.connectionState == ConnectionState.done) {
result = s.data;
}
else {
result = "Loading...";
}
}
return Text(result);
}
);
}
}
Hope this helps :)

StreamProvider with RiverPod not working (try to migrate from Provider)

I'm trying to understand RiverPod by migrating my simple FireStore auth Provider example to RiverPod.
This is my AuthenticationService:
import 'package:firebase_auth/firebase_auth.dart';
class AuthenticationService {
final FirebaseAuth _firebaseAuth;
AuthenticationService(this._firebaseAuth);
// with StreamProvider we listen to these changes
Stream<User> get authStateChanges => _firebaseAuth.authStateChanges();
Future<String> signIn({String email, String password}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
return 'Signed in';
} on FirebaseAuthException catch (e) {
return e.message;
}
}
Future<String> signUp({String email, String password}) async {
try {
await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password);
return 'Signed up ';
} on FirebaseAuthException catch (e) {
return e.message;
}
}
Future<void> signOut() async {
await _firebaseAuth.signOut();
}
}
In main.dart I made 2 providers so I can use the service and listen to the property inside of the AuthenticationService
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
// initalize Firebase and before that we need to initialize the widgets.
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Normal provider to serve the AuthenticationService in the widgettree
// so the login form can use this provider to use .singIn()
Provider<AuthenticationService>(
create: (_) => AuthenticationService(FirebaseAuth.instance),
),
// also a StreamProvider that serves the AuthenticationSerivce authStateChanges
// this stream is updated by the FirebaseAuth package when users signin or out
// this provider use context.read<AuthenticationService> to find the
// provider dived here above
StreamProvider(
create: (context) =>
context.read<AuthenticationService>().authStateChanges,
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AuthenticationWrapper(),
),
);
}
}
class AuthenticationWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final firebaseUser = context.watch<User>();
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
Here the SingIn page:
import 'package:flutter/material.dart';
import 'package:meditatie_app/authentication_service.dart';
import 'package:provider/provider.dart';
class SignInPage extends StatelessWidget {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
...
RaisedButton(
onPressed: () {
// Sign in code
context.read<AuthenticationService>().signIn(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
},
...
This works fine with normal Provider, but I can't get it to work with RiverPod
What I did was:
These providers I made global in providers.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/all.dart';
import 'authentication_service.dart';
final authenticationSeriviceProvider =
Provider((ref) => AuthenticationService(FirebaseAuth.instance));
final authStateChangeProvider = StreamProvider.autoDispose<User>((ref) {
return ref
.watch(authenticationSeriviceProvider)
.authStateChanges;
});
Is this correct? The authStateChangeProvider is using the authenticationSeriviceProvider
When is use it like:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:meditatie_app/home_page.dart';
import 'package:meditatie_app/signin_page.dart';
import 'package:flutter_riverpod/all.dart';
import 'providers.dart';
Future<void> main() async {
// initialize Firebase and before that we need to initialize the widgets.
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
// riverpod needs at toplevel a Provider container
// for storing state of different providers.
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: AuthenticationWrapper(),
);
}
}
// Riverpods ConsumerWidget
// which can consume a provider
// rebuild if the value of the provider changes
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final firebaseUser = watch(authStateChangeProvider);
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
My 'firebaseUser' is not a User anymore, but an AsyncValue
When I change it to:
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final User firebaseUser = watch(authStateChangeProvider).data?.value;
if (firebaseUser != null) {
return HomePage();
}
return SignInPage();
}
}
It is working, but what am I doing wrong that I now work with AsyncValue
Expanding the previous answer AsyncValue<T> is a sealed class, think of it as StreamBuilder in Flutter having AsyncSnapshot<T> which wraps the value returned from the stream and gives you options to check if its connecting, waiting, withError or withData. In your case
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(authStateChangeProvider).when(
data: (user) => user == null ? SignInPage() : HomePage(),
loading: () => CircularProgressIndicator(),
error: (err, stack) => SignInPage(),
);
}
}
should handle all the options, now when loading it will show a progress indicator, if there is an error (connection, bad result, etc) it will display the SignInPage, and finally when there is a value you still will need to check if the value returned from the Stream is null (As far as I understand Firebase returns null when there is no user signed in, it doesn't mean the stream is empty) and display the right widget if its null or not.
Just like Provider, after retrieving the user you still have to do the logic with that
See the documentation.
You should use AsyncValue's exposed states to decide what to render. Your code could look something like the following:
class AuthenticationWrapper extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(authStateChangeProvider).when(
data: (user) => user == null ? SignInPage() : HomePage(),
loading: () => CircularProgressIndicator(),
error: (err, stack) => SignInPage(),
);
}
}
So adjust your return logic to what you'd like for the data, loading, and error states, but this should give you a general idea on how to use AsyncValue.
Another way I found was to use it the way this tutorial did, but with the new riverpod changes:
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_shopping_list/repositories/auth_repository.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final authControllerProvider = StateNotifierProvider<AuthController, User?>(
(ref) => AuthController(ref.read)..appStarted(),
);
class AuthController extends StateNotifier<User?> {
final Reader _read;
StreamSubscription<User?>? _authStateChangesSubscription;
AuthController(this._read) : super(null) {
_authStateChangesSubscription?.cancel();
_authStateChangesSubscription = _read(authRepositoryProvider)
.authStateChanges
.listen((user) => state = user);
}
#override
void dispose() {
_authStateChangesSubscription?.cancel();
super.dispose();
}
void appStarted() async {
final user = _read(authRepositoryProvider).getCurrentUser();
if (user == null) {
await _read(authRepositoryProvider).signInAnonymously();
}
}
}
And then I used it like this:
#override
Widget build(BuildContext context, WidgetRef ref) {
User? user = ref.watch<User?>(authControllerProvider);
return user != null
? MaterialApp(
title: 'My App',
builder: (context, child) => _Unfocus(child: child!),
home: MainNavigation(),
debugShowCheckedModeBanner: false,
)
: const MaterialApp(
title: 'My App,
home: LoginPage(),
debugShowCheckedModeBanner: false,
);
}

Using FutureBuilder in main.dart

Below code always show OnboardingScreen a little time (maybe miliseconds), after that display MyHomePage. I am sure that you all understand what i try to do. I am using FutureBuilder to check getString method has data. Whats my fault ? Or any other best way for this ?
saveString() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('firstOpen', '1');
}
getString() method always return string.
getString() async {
final prefs = await SharedPreferences.getInstance();
String txt = prefs.getString('firstOpen');
return txt;
}
main.dart
home: new FutureBuilder(
future: getString(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
})
Usually I'm using another route, rather than FutureBuilder. Because futurebuilder every hot reload will reset the futureBuilder.
There always will be some delay before the data loads, so you need to show something before the data will load.
Snapshot.hasData is showing only the return data of the resolved future.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.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: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
const isOnboardingFinished = 'isOnboardingFinished';
class _SplashScreenState extends State<SplashScreen> {
Timer timer;
bool isLoading = true;
#override
void initState() {
_checkIfFirstOpen();
super.initState();
}
Future<void> _checkIfFirstOpen() async {
final prefs = await SharedPreferences.getInstance();
var hasOpened = prefs.getBool(isOnboardingFinished) ?? false;
if (hasOpened) {
_changePage();
} else {
setState(() {
isLoading = false;
});
}
}
_changePage() {
Navigator.of(context).pushReplacement(
// this is route builder without any animation
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading ? Container() : OnBoarding();
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(child: Text('homePage'));
}
}
class OnBoarding extends StatelessWidget {
Future<void> handleClose(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(isOnboardingFinished, true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
onPressed: () => handleClose(context),
child: Text('finish on bording and never show again'),
),
),
);
}
}
From the FutureBuilder class documentation:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
So you need to create a new Stateful widget to store this Future's as a State. With this state you can check which page to show. As suggested, you can start the future in the initState method:
class FirstPage extends StatefulWidget {
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final Future<String> storedFuture;
#override
void initState() {
super.initState();
storedFuture = getString();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: storedFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
});
}
}
So in your home property you can call it FirstPage:
home: FirstPage(),
Your mistake was calling getString() from within the build method, which would restart the async call everytime the screen gets rebuilt.

How to declare async function as a variable in flutter(dart)?

In flutter, we can declare a function as variable and call it like this
MyWidget((){print('HI');});
class MyWidget extends StatelessWidget{
final Function sayHi;
MyWidget(this.sayHi);
#override
Widget build(BuildContext context) {
sayHi();
return ...
}
}
But what if sayHi() is a async function? How to declare a async function as variable? There seems no class like AsyncFunction. So how to achive that?
Async functions are normal functions with some sugar on top. Here, the function variable type just needs to specify that it returns a Future:
class Example {
Future<void> Function() asyncFuncVar;
Future<void> asyncFunc() async => print('Do async stuff...');
Example() {
asyncFuncVar = asyncFunc;
asyncFuncVar().then((_) => print('Hello'));
}
}
void main() => Example();
Hope this helps.
Like this
class MyApp extends StatelessWidget {
void asyncFunc() async {
await Future.delayed(Duration(seconds: 5));
print('printed after 5 seconds');
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(asyncFunc),
),
),
);
}
}
class MyWidget extends StatelessWidget {
final void Function() asyncFunc;
const MyWidget(this.asyncFunc);
#override
Widget build(BuildContext context) {
return MaterialButton(
onPressed: asyncFunc,
);
}
}