I'm trying to restore my listview widget after android kills my app from memory by using RootRestorationScope:
runApp(RootRestorationScope(
child: MaterialApp(home: MyApp()),
restorationId: "root",
));
and making my own restorable widget using extends RestorableValue:
class RestorableListBloc extends RestorableValue<ListBloc> {
#override
ListBloc createDefaultValue() {
return ListBloc(
repository: Repository(),
)..add(
Fetch(),
);
}
#override
void didUpdateValue(ListBloc oldValue) {
if (oldValue.state != value.state) {
notifyListeners();
}
}
#override
ListBloc fromPrimitives(Object data) {
print('data: ' + data);
return ListBloc(repository: data);
}
#override
Object toPrimitives() {
return value.repository;
}
}
Where ListBloc is a Bloc that controls what's in my listview widget (collection of timers right now in case anyone's curious).
I extend the class with the restoration mixing and call the bloc inside of a multibloc providers widget as follows:
class _MyAppState extends State<MyApp> with RestorationMixin {
SpeedDialGenerator speedDial = SpeedDialGenerator();
RestorableListBloc _test = RestorableListBloc();
final assetsAudioPlayer = AssetsAudioPlayer();
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => DrawerCubit(value: false),
child: Scaffold(
backgroundColor: Colors.blue[50],
drawer: SafeArea(
child: AppDrawer(),
),
appBar: AppBar(
backgroundColor: Colors.blue[900],
title: Text('Myapp'),
centerTitle: true,
actions: [
IconButton(
icon: Image.asset('assets/images/myapp_white.png'),
onPressed: () {},
),
],
),
body: Stack(children: [
HourglassBackground(),
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => _test.value,
),
BlocProvider(
create: (context) => AppStateCubit(),
)
],
child: BlocBuilder<ListBloc, ListState>(
builder: (context, state) {
if (state is Failure) {
return Center(
child: Text('Oops something went wrong!'),
);
}
if (state is Loaded) {
return Stack(children: [
HomePage(state: state, displayNotification: () => {}),
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 10, 10),
child: BlocProvider(
create: (context) => TimerpopupCubit(),
child: speedDial.buildSpeedDial(context)),
),
]);
}
return Center(
child: CircularProgressIndicator(),
);
},
)),
]),
),
);
}
#override
String get restorationId => 'root';
#override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(_test, restorationId);
}
Unfortunately, everytime I run the app I get:
The following assertion was thrown building Builder:
'package:flutter/src/services/restoration.dart': Failed assertion: line 592 pos 12: 'debugIsSerializableForRestoration(value)': is not true.
There's not much documentation and I know this is a relatively new feature of flutter, but from what I gather this means that you can't currently restore a BLoC?
Is there a way around this or some other approach I should look at?
Thanks
There are some approaches such as using HydratedBloc library that has built in feature to save its state.
Or you can implement your own local database and when user re run app, the repository returns first local data and returns remote one.
Related
I am new to flutter and was trying out the implementation of Network Connectivity with the Flutter Provider. I got across this error and have tried every bit of code on the Internet from changing the context and changing the place where the Provider might lie so that the child widgets will get the context. When I am trying to get the value of res in welcome. dart I am getting the error.
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that Welcome is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider(
create: (context) => Networkprovider().networkController.stream,
initialData: Networkprovider().initRes),
],
child: MaterialApp(
theme: ThemeData(),
initialRoute: Welcome.id,
routes: {
Welcome.id: (context) => Welcome(),
NavigatorPage.id: (context) => const NavigatorPage(),
},
),
);
}
}
NetworkProvider.dart
class Networkprovider extends ChangeNotifier {
late StreamSubscription<ConnectivityResult> _subscription;
late StreamController<ConnectivityResult> _networkController;
late ConnectivityResult initRes = ConnectivityResult.none;
StreamSubscription<ConnectivityResult> get subscription => _subscription;
StreamController<ConnectivityResult> get networkController =>
_networkController;
Networkprovider() {
startup();
}
void startup() {
_networkController = StreamController<ConnectivityResult>();
networkStatusChangeListener();
}
void networkStatusChangeListener() async {
_networkController.sink.add(await Connectivity().checkConnectivity());
_subscription = Connectivity().onConnectivityChanged.listen((event) {
_networkController.sink.add(event);
});
}
void disposeStreams() {
_subscription.cancel();
_networkController.close();
}
}
Welcome.dart
class Welcome extends StatelessWidget {
static String id = "welcome_screen";
const Welcome({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var res = Provider.of<Networkprovider>(context);
return SafeArea(
child: Scaffold(
backgroundColor: MAIN_BACKGROUND_COLOR,
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: const <Widget>[
Image(image: AssetImage("assets/images/Magnet_logo.png"))
],
),
),
const Padding(
padding: EdgeInsets.all(20.0),
child:
Image(image: AssetImage("assets/images/Mannify_logo.png")),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Spinner(),
),
],
),
),
),
);
}
}
Declare ChangeNotifierProvider like this
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Networkprovider()),
],
child: <your widget>,
)
then access like this
final provider = Provider.of<Networkprovider>(context);
I have a very simply scenario. A button where when pushed would transition to the next screen. In Navigator 1.0, it is very simple to do by:
ElevatedButton(
onPressed: () {
// Navigate to next screen.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NextScreen()),
);
},
child: Text('Next Screen!'),
)
It seems that with Navigator 2.0 I would need to have a state to keep track of the current screen.
...
Navigator(
pages: [
MainScreenPageRoute()
if (state.isNextScreen) {
NextScreenPageRoute()
}
],
onPopPage: (route, result) {
// would have to keep track of this value
state.isNextScreen = false;
return route.didPop(result);
},
)
...
As for before I don't have to keep track of a state just to navigate, In Navigator 2.0 it seems that it is required. Is it really the case? If so do you guys have any suggestion of how to handle this properly?
P.S.
It also feels like now I have to keep track of the state which adds to more work compared to before.
yes in navigator 2 you change the page when you change the state.
There are some packages that help you to avoid this like qlevar_router
see this example
import 'package:flutter/material.dart';
import 'package:qlevar_router/qlevar_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
#override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: QRouteInformationParser(),
routerDelegate: QRouterDelegate([
QRoute(path: '/', builder: () => BooksListScreen(books)),
QRoute(
path:
'/books/:id([0-${books.length - 1}])', // The only available pages are the pages in the list
builder: () => BookDetailsScreen(books[QR.params['id']!.asInt!])),
]));
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
BooksListScreen(this.books);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => QR.to('/books/${books.indexOf(book)}'))
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen(this.book);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
),
),
);
}
}
don't forget to add qlevar_router: 1.4.0 in pubspec.yaml
Flutter Web(Navigator 2.0/Router API):
How to handle authenticated routes and their redirects after successful auth?
e.g.
I have these kind of routes on my system
/book/xyz (authenticated user)
/author/abc/book/xyz (authenticated user)
/authentication (non-authenticated user)
/info (non-authenticated user)
If user opens this URL directly, I wanted to ask user to login first, at that time route will be redirected to..
/authentication
Once logged in, I would like to user to navigate previously opened URL if any else home..
Seems like this kind of things may help, any thoughts - how we can achieve similar things?
https://stackoverflow.com/a/43171515/2145844
I have tried few samples for Navigation 2.0 / Router API, yes I can understand the concepts a bit..
Some of the references, I have looked at..
https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade
https://github.com/orestesgaolin/navigator_20_example
https://github.com/flutter/flutter/tree/master/dev/benchmarks/test_apps/stocks
Here is how to do it using VRouter >=1.2
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:vrouter/vrouter.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final Author author;
Book(this.title, this.author);
}
class Author {
String name;
Author(this.name);
}
class AppState extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void authenticate() {
if (isAuthenticated) return;
_isAuthenticated = true;
notifyListeners();
}
}
class BooksApp extends StatelessWidget {
final List<Book> books = [
Book('Stranger in a Strange Land', Author('Robert A. Heinlein')),
Book('Foundation', Author('Isaac Asimov')),
Book('Fahrenheit 451', Author('Ray Bradbury')),
];
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AppState(),
child: Builder(
builder: (BuildContext context) {
return VRouter(
routes: [
VWidget(path: '/login', widget: AuthenticationScreen()),
VWidget(path: '/info', widget: InfoWidget()),
VGuard(
beforeEnter: (vRedirector) =>
authenticationCheck(context, vRedirector: vRedirector),
stackedRoutes: [
VWidget(
path: '/',
widget: BooksListScreen(books: books),
stackedRoutes: [
VWidget(
path: r'book/:bookId(\d+)',
widget: Builder(builder: (BuildContext context) {
return BookDetailsScreen(
book: books[int.parse(context.vRouter.pathParameters['bookId']!)],
);
}),
),
],
),
VWidget(
path: '/authors',
widget: AuthorsListScreen(authors: books.map((e) => e.author).toList()),
stackedRoutes: [
VWidget(
path: r'/author/:authorId(\d+)',
widget: Builder(builder: (BuildContext context) {
return AuthorDetailsScreen(
author: books[int.parse(context.vRouter.pathParameters['authorId']!)]
.author,
);
}),
),
],
),
],
),
],
);
},
),
);
}
Future<void> authenticationCheck(BuildContext context, {required VRedirector vRedirector}) async {
if (!Provider.of<AppState>(context, listen: false).isAuthenticated) {
vRedirector.to('/login', queryParameters: {'redirectedFrom': '${vRedirector.toUrl}'});
}
}
}
class AuthenticationScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
Provider.of<AppState>(context, listen: false).authenticate();
context.vRouter.to(context.vRouter.queryParameters['redirectedFrom'] == null
? '/'
: context.vRouter.queryParameters['redirectedFrom']!);
},
child: Text('Click to login'),
),
);
}
}
class InfoWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text('Some info but actually there is nothing'),
);
}
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
BooksListScreen({required this.books});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author.name),
onTap: () => context.vRouter.to('/book/${books.indexOf(book)}'),
)
],
),
);
}
}
class AuthorsListScreen extends StatelessWidget {
final List<Author> authors;
AuthorsListScreen({required this.authors});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
ElevatedButton(
onPressed: () => context.vRouter.to('/'),
child: Text('Go to Books Screen'),
),
for (var author in authors)
ListTile(
title: Text(author.name),
onTap: () => context.vRouter.to('/author/${authors.indexOf(author)}'),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({required this.book});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: Theme.of(context).textTheme.headline6),
ElevatedButton(
onPressed: () {
context.vRouter.to('/author/${context.vRouter.pathParameters['bookId']}');
},
child: Text(book.author.name),
),
],
),
),
);
}
}
class AuthorDetailsScreen extends StatelessWidget {
final Author author;
AuthorDetailsScreen({required this.author});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(author.name, style: Theme.of(context).textTheme.headline6),
],
),
),
);
}
}
The trick is to use a VGuard which, before entering the stackedRoutes, checks whether or not the user is authenticated.
I used queryParameters to store where the user it redirected from, however you could use historyState if you did not want where the user is redirected from appear in the url. That being said, I still prefer queryParameters in that case since is allows link sharing.
You can use qlevar_router to do this.
from the example, in the link, you can define a Middleware with redirectGuard to check if the user can access this page or its children, otherwise just give the route to redirect to.
you can see this in the example project too.
if you give the right to access to Child 4 it will go to Child 4 otherwise you will be navigated to Child 2.
I'm using the BLOC pattern to authenticate a user in my app. I have a main BlocProvider that wraps my app. And a BlocBuilder to build according to the authentication state.
If the user is unauthenticated i have onboarding / intro screens that will navigate to the login screen.
The login screen is wrapped in another BlocProvider that contains a button that will do the login, and add a logged in event when the login is successful.
Problem is when i navigate from the onboarding screens i loose the main authenticationBloc context. What do i need to to to have access to the authentication bloc after i pushed a new screen.
void main() {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = SimpleBlocObserver();
runApp(
MyApp(),
);
}
class AuthenticationWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc()..add(AppStarted()),
child: MyApp(),
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state is Authenticated) {
_appUserProfileRepository = AppUserProfileRepository();
}
},
child: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
_authCredentialHelper = state.authCredentialHelper;
if (state is Uninitialized) {
return SplashScreen();
}
if (state is Unauthenticated) {
return OnboardingScreens(authCredentialHelper: _authCredentialHelper);
}
if (state is InvalidRegistration) {
return RegisterProfileScreen(authCredentialHelper: _authCredentialHelper);
}
if (state is Authenticated) {
xxx
}
return Scaffold(body: Center(child: LoadingIndicator()));
},
),
);
}
}
This is the onboarding screen where i loose the authenticationbloc context as soon as i navigate
class OnboardingScreens extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
OnboardingScreens({this.authCredentialHelper});
_pages(BuildContext context) {
return [
xxx
];
}
_getStartedClicked(BuildContext context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: IntroductionScreen(
pages: _pages(context),
onDone: () => _getStartedClicked(context),
showSkipButton: true,
done: xxx
),
),
);
}
}
When adding a breakpoint at 1. the context is fine with a valid value for BlocProvider.of(context)
Stepping to 2. gives me an error:
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
_getStartedClicked(BuildContext context) {
1----->Navigator.push(context, MaterialPageRoute(builder: (context) {
2----->return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
This is the LoginScreen code
class LoginScreen extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
LoginScreen({this.authCredentialHelper});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back, color: darkBlue),
onPressed: () => Navigator.of(context).pop(),
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
body: SafeArea(
child: Center(
child: BlocProvider<LoginBloc>(
create: (context) => LoginBloc(authCredentialHelper: authCredentialHelper),
child: LoginForm(authCredentialHelper: authCredentialHelper),
),
),
),
);
}
}
Getting this error:
The following assertion was thrown building _InheritedProviderScope<LoginBloc>(value: Instance of 'LoginBloc'):
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<AuthenticationBloc>().
This can happen if the context you used comes from a widget above the BlocProvider.
Change this :
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
to
Navigator.push(
context,
MaterialPageRoute(builder: (contextLoginScreen) {
return BlocProvider.value(
value: context.bloc<AuthenticationBloc>(),
child: LoginScreen(authCredentialHelper: authCredentialHelper));
}),
);
Context
I am trying to build an app, using Flutter, which requires the users to sign up and/or sign in.
Using a provider, I was able to determine at each launch of the app if the user was already signed in or not, thus showing respectively the MainScreen or the SignInScreen, as you can see from the code below.
void main() {
runApp(Stuff());
}
class Stuff extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(value: AuthService().onAuthChanged),
],
child: Consumer<FirebaseUser>(builder: (context, user, _) {
return MaterialApp(
title: 'Stuff',
home: user != null ? MainScreen() : SignInScreen(),
routes: {
'/main_screen' : (context) => MainScreen(),
'/signup_screen': (context) => SignUpScreen(),
'/signin_screen': (context) => SignInScreen(),
},
);
}),
);
}
}
where AuthService for now is just the following:
class AuthService{
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
#override
Stream<FirebaseUser> get onAuthChanged => _firebaseAuth.onAuthStateChanged;
}
If the user is not signed in, then theSignInScreen is shown, and after the sign in process the StreamProvider is notified correctly and the home property of the MaterialApp updates automatically showing the MainScreen.
Problem
When users get to the SignInScreen they have the choice to either log in or get to another the SignUpScreen to sign up (app screen), like this:
GestureDetector(
child: Text('Sign Up.'),
onTap: () {
Navigator.pushReplacementNamed(context, '/signup_screen');
},
),
If they choose to sign in, when they're done, the home is correctly updated and the MainScreen is shown as I said above. But if they choose to sign up, when they're done, nothing happens. All listeners are notified, but the StreamProvider doesn't receive this notification. The only reason I can think of for this behavior, is that the SignUpScreen is outside the scope of the Provider, but the widget tree seems to suggets that it is.
Widget Tree
It it possibile that the problem is pushReplacementNamed?
P.S: I should mention that the GestureDetector is part of the following method that I pass to the appBar property of Scaffold.
PreferredSize _buildAppBar() {
return PreferredSize(
preferredSize: Size.fromHeight(MediaQuery.of(context).size.height * 0.18),
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.18),
padding: EdgeInsets.only(left: 45.0, right: 45.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Sign In'),
SizedBox(
height: 2.5,
),
Row(
children: [
Text('Have an account already?'),
SizedBox(
width: 4.0,
),
GestureDetector(
child: Text('Sign In.'),
onTap: () {
Navigator.pushReplacementNamed(context, '/signin_screen');
},
),
],
),
],
),
),
);
}
I think the issue here is because you don't notify Stuff widget when user signs in. You can wrap it a Streambuilder in order to listen to AuthState changes like this.
class Stuff extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthService>(create: (_) => AuthService()),
],
child: Consumer<AuthService>(builder: (context, auth, _) {
return StreamBuilder<FirebaseUser>(ctx,AsyncSnapshot<FirebaseUser> snapshot)=> MaterialApp(
title: 'Stuff',
home: snapshot.data!= null && snapshot.data.user != null ? MainScreen() : SignInScreen(),
routes: {
'/main_screen' : (context) => MainScreen(),
'/signup_screen': (context) => SignUpScreen(),
'/signin_screen': (context) => SignInScreen(),
},
);
}),
);
}
}