The pagebuilder in GoRoute has the the only place I can grab the state.params, I want to update my StateNotifier when the route changes if it is different.
final LibraryRoutes = Provider<RouteBase>((ref) {
return ShellRoute(
navigatorKey: _LibraryKey,
builder: (context, state, child) {
return LibraryHomePage(
child: child,
);
},
routes: [
//dashboard
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
final String passedValue = state.params['LibraryKey']!;
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
//this line error's out because its during lifecycle method
ref.read(AsyncLibraryProvidor.notifier).updateLibrary(newLibrary);
final AsyncLibraryNotifier = ref.watch(AsyncLibraryProvidor);
return AsyncLibraryNotifier.when(data: (data) {
return NoTransitionPage(
child: Text("dashboard"),
);
}, error: (_, __) {
return NoTransitionPage(
child: const Text("An error occurred"),
);
}, loading: () {
return NoTransitionPage(
child: CircularProgressIndicator(),
);
});
}),
]);
});
I've managed to put the update in a future to get around the problem is there a more elegant solution as the library is used in many different places.
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
if (ref.read(LibraryProvider) == null) {
final String passedValue = state.params['LibraryKey']!;
try {
//can't update during widget tree delay by 150 micoseconds so we can update after future
Future.delayed(
const Duration(microseconds: 150),
() {
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
ref.read(LibraryProvider.notifier).updateLibrary(newLibrary);
},
);
} on Exception catch (ex) {
print(ex.toString());
}
}
return NoTransitionPage(
child: Text("dashboard"),
);
}),
Right now It's not possible to use await within the page Builder,
Write Some Logic in your page side to prefetch the values before the page renders.
Related
Currently I have a StreamBuilder switching between a HomePage and LandingPage depending on the current auth state. The issue I have encountered is that I cannot pop the stack to the original /landing directory on a state change. This means that when a user logs in, the AuthPage remains on the top of the stack and the StreamBuilder builds the HomePage underneath.
AuthGate
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
#override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
return snapshot.hasData ? const HomePage() : const LandingPage();
},
);
}
}
LandingPage
This pushes the AuthPage to the stack.
class LandingPage extends StatelessWidget {
const LandingPage({super.key});
...
Row(
children: <Widget>[
FloatingActionButton.extended(
heroTag: UniqueKey(),
onPressed: () {
context.push('/auth');
},
label: const Text('Get started'),
),
FloatingActionButton.extended(
heroTag: UniqueKey(),
onPressed: () {
context.push('/auth');
},
label: const Text('Log in'),
),
],
)
...
}
}
Stack before auth change
Stack after auth change
Note how the AuthPage remains on the top of the stack but the Widget under StreamBuilder changes to the HomePage
(This is my first Stack question so please feel free to ask me to amend any information etc.)
If you are using GoRouter, then what you want to achieve should be done through GoRouter similarly to this:
GoRouter(
refreshListenable:
GoRouterRefreshListenable(FirebaseAuth.instance.authStateChanges()),
initialLocation: '/auth',
routes: <GoRoute>[
GoRoute(
path: '/landing',
name: 'landing',
builder: (context, state) {
return LandingPage()
},
routes: [
GoRoute(
path: 'auth',
name: 'auth',
builder: (context, state) => const AuthPage(),
),
],
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
],
errorBuilder: (context, state) {
return const Scaffold(
body: Text('404'),
);
},
redirect: (context, state) async {
final userRepo = injector.get<UserRepository>();
final user = FirebaseAuth.instance;
const authPaths = [
'/landing',
'/landing/auth',
];
bool isAuthPage = authPaths.contains(state.subloc);
if(user != null) {
if (isAuthPage) {
return '/home';
}
}
if(!isAuthPage) {
return '/auth';
}
return null;
},
);
class GoRouterRefreshListenable extends ChangeNotifier {
GoRouterRefreshListenable(Stream stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen(
(_) {
notifyListeners();
},
);
}
late final StreamSubscription _subscription;
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
Please also read documentation on of GoRouter.
Description
I am trying to Navigate between screens with a Navigation Menu using go_router plugin. When I click the item in the menu, nothing happens but if I change the URL the screen does change.
Video shows the problem
Expect
Every time I navigate back and forth, both URL and screen change
My Code
app_router.dart
class AppRouter {
AppRouter(this._appBloc);
final AppBloc _appBloc;
GoRouter get router => GoRouter(
routes: pages.values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(_appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: _redirect,
);
String? _redirect(GoRouterState state) {
final loggedIn = _appBloc.state.status == AppStatus.authenticated;
final name = state.subloc;
final loggingIn = name == '/login' || name == '/';
if (!loggedIn) return loggingIn ? null : '/login';
if (loggingIn) return '/app';
return null;
}
static Map<String, GoRoute> pages = {
route_names.onboard: GoRoute(
name: route_names.onboard,
path: routes[route_names.onboard]!,
pageBuilder: (context, state) => OnboardPage.page(key: state.pageKey),
routes: [
GoRoute(
path: route_names.login.subRoutePath,
name: route_names.login,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
GoRoute(
path: route_names.signUp.subRoutePath,
name: route_names.signUp,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
],
),
'app': GoRoute(
path: '/app',
// All /app pages get the main scaffold
builder: (context, state) {
return Text("App Main");
},
routes: [
ExplorePage.route,
PlanPage.route,
AccountPage.route,
]),
};
Widget _buildRouterView(BuildContext context, GoRouterState state, Widget child) {
return Builder(
builder: (context) => BlocBuilder<AppBloc, AppState>(builder: (context, appState) {
if (appState.status == AppStatus.unauthenticated) {
return child;
}
return HomePageSkeleton(
child: child,
);
}),
);
}
}
app.dart
class AppView extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
AppView({super.key, required AppBloc appBloc}) {
_appBloc = appBloc;
_appRouter = AppRouter(_appBloc);
}
late final AppBloc _appBloc;
late final AppRouter _appRouter;
#override
Widget build(BuildContext context) {
return BlocListener<AppBloc, AppState>(
listener: (context, state) {
if (state == const AppState.unauthenticated()) {
_appRouter.router.goNamed(route_names.login);
}
},
child: MaterialApp.router(
supportedLocales: AppLocalizations.supportedLocales,
routeInformationParser: _appRouter.router.routeInformationParser,
routeInformationProvider: _appRouter.router.routeInformationProvider,
routerDelegate: _appRouter.router.routerDelegate,
),
);
}
}
HomePageSkeleton.class
// inside build method
class HomePageSkeleton extends StatelessWidget with NavigationMixin {
const HomePageSkeleton({Key? key,required this.child}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Responsive.isMobile(context) ? AppBottomNavigation(index: 0) : const SizedBox(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isTablet(context) || Responsive.isLaptop(context))
// It takes 1/6 part of the screen
Expanded(
child: SideMenu(index: 0, onSelected: (index) => onTabSelected(index, context)),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: child),
],
),
),
);
}
}
//onTapSelected method
void onTabSelected(int index, BuildContext context) {
switch (index) {
case 0:
// context.goNamed(route_names.explore);
context.go('/app/explore');
break;
case 1:
// context.goNamed(route_names.plan);
context.go('/app/plan');
break;
case 2:
// context.goNamed(route_names.account);
context.go('/app/account');
break;
default:
throw Exception('Unknown view');
}
}
I change my class AppRouter into:
class AppRouter {
AppRouter(AppBloc appBloc)
: _router = GoRouter(
routes: getPages().values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: (GoRouterState state) {
_redirect(state, appBloc);
},
),
_appBloc = appBloc;
final AppBloc _appBloc;
final GoRouter _router;
GoRouter get router {
return _router;
}
... and it worked
In my Flutter app the user can create new tasks and see them in the homepage, but right now I am fetching all the tasks from Firebase at once, and I wish I could do that using infinite scroll. I googled how to do this, but I really couldn't figure it out.
In my API project I have the following:
async getTasksByFilter(filters: Array<IFilter>): Promise<Array<ITask>> {
let tasksUser: Array<ITask> = [];
let collectionQuery: Query<DocumentData> = this.db.collection(
this.taskCollection,
);
let query = collectionQuery;
return new Promise((resolve, reject) => {
filters.forEach(entry => {
switch (entry.searchType) {
case 'where':
query = query.where(
entry.field,
entry.condition as WhereFilterOp,
entry.value,
);
break;
default:
break;
}
});
query
.orderBy('createdAt', 'asc')
.get()
.then(query => {
if (query.docs.length > 0) {
query.docs.forEach(doc => {
let task: ITask = this.transformDate(doc.data());
tasksUser.push(task);
});
}
return resolve(tasksUser);
})
.catch(error => {
return reject(error);
});
});
}
In my app I use this function to fetch the tasks
Future<List<Task>> getUserTasks(String _extension, Filter filters) async {
final Response response =
await client.post(Uri.parse("$BASE_URL$_extension"),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(filters.toJson()),
);
Iterable l = jsonDecode(response.body);
List<Task> tasks = List<Task>.from(l.map((model) => Task.fromJson(model)));
return tasks;
}
So when the tasks page is opened, the cubit changes its state to InitTaskListState, start to fetch all the tasks data and show a loading spinner for the user. When its done the state changes to LoadedTaskListState and the task list is displayed.
This is my code for it:
BlocConsumer<TaskListCubit, TaskListState>(
listenWhen: (previous, current) =>
current is FatalErrorTaskListState,
listener: (context, state) {
if (state is FatalErrorTaskListState) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(state.title ?? 'Error'),
content: Text(state.message ?? 'Error'),
actions: <Widget>[
TextButton(
child: const Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
},
builder: (context, state) {
if (state is InitTaskListState ||
state is LoadingTaskListState) {
return const ProgressView(message: 'Loading tasks');
}
if (state is LoadedTaskListState) {
final tasks = state.tasks;
return taskList(context, cubit, state, tasks);
}
return const Text('Unknown error');
},
)
TabBarView taskList(BuildContext context, TaskListCubit cubit,
LoadedTaskListState state, List<Task> tasks) {
return TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: List<Widget>.generate(
cubit.tabNames.length,
(int index) {
if (index == state.tabIndex) {
return Center(
child: tasks.isEmpty
? setEmptyListText(state.tabName)
: Column(
children: [
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final task = tasks[index];
return TaskCard(
task,
state.tabName,
state.tabType,
cubit,
onClick: () {
push(
context,
TaskDetailsContainer(task, state.tabType),
);
},
);
},
itemCount: tasks.length,
),
),
],
),
);
} else {
return const Text('');
}
},
),
);
}
Does someone can explain me how to implement the infinite scroll in my project?
I think this will give you a pretty good idea of how to do it:
https://medium.com/flutter-community/flutter-infinite-list-tutorial-with-flutter-bloc-2fc7a272ec67
What you need to do is that each time reach the bottom of the list you will increment the limit of your current query, which can be easily done with firebase.
In your api call you can do like this:
firestore.collection("products").limit(_myLimit).get();
Make sure to update your limit as you reach the bottom of the screen and change states accordingly. Your bloc could keep track of the current limit and then you just increase it as you scroll down.
At the moment I get a white background with a spinning CircularProgressIndicator when I swipe to a new route. The new route has a Future that fetches data from a HTTP Post. Instead I'd like the background of the original page to remain behind the spinner until the future completes and the transition happens. So how do I make the spinners' background transparent instead of white? Here's the code to the future which I assume is where the spinner gets triggered;
FutureBuilder<List<ReplyContent>> replyStage({String replyid, String replytitle}) {
return new FutureBuilder<List<ReplyContent>>(
future: downloadReplyJSON(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ReplyContent> replycrafts = snapshot.data;
return StageBuilderVR(replycrafts, replytitle);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return CircularProgressIndicator();
},
);
}
And here's the code which swipes to the future;
onSwipeUp: () {
Navigator.of(context).push(_createRoute());
}
And the code for the PageRouteBuilder:
Route _createRoute() {
return PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) => ReplyHome(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(0.0, 1.0);
var end = Offset.zero;
var curve = Curves.ease;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
You can use showDialog to open a dialog which will open a transparent background with the AlertDialog, You can return your own stateful widget. Instead of streamBuilder just use future Builder.
try following code:
void uploadAndShowProgress() async {
await showDialog(
context: context,
builder: (context) {
return StreamBuilder(
stream: uploadFile(),
builder: (context, snapshot) {
if (snapshot.hasData) {
StorageTaskEvent event = snapshot.data;
final _snapshot = event.snapshot;
_progress = _snapshot.bytesTransferred / _snapshot.totalByteCount;
} else {
//* pop when there's no data or error...
Navigator.pop(context);
}
if (snapshot.hasError) {
SnackbarWidget.showSnackbar(
content: Text(snapshot.error.toString()), context: context);
}
return AlertDialogWidget(progress: _progress);
},
);
},
);
}
this function pushes route with transparent background
onPressed: () {
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (_, __, ___) {
return MyTransperentRoute();
},
),
);
}
so in your CircularProgressIndicator page you can change background color of the root Widget like color: Colors.transperent or a plain Container without any color set will achieve the effect you need
class MyTrnsperentPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: const Center(
child: CircularProgressIndicator(),
),
);
}
}
see the working code on dartpad here
Stack(
children: <Widget>[
Opacity(
opacity: _isLoading ? 1.0 : 0,
child: CircularProgressIndicator(),
),
],
);
In the end, I created a dummy Page that graphically mirrored the previous page with all the graphic elements of the previous page and none of the code. I put that in place of the CircularProgressIndicator. I don't know if it's ideal, but it seems to work well.
FutureBuilder<List<ReplyContent>> replyStage({String replyid, String replytitle}) {
return new FutureBuilder<List<ReplyContent>>(
future: downloadReplyJSON(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ReplyContent> replycrafts = snapshot.data;
return StageBuilderVR(replycrafts, replytitle);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return DummyPage();
},
);
}
I was wondering if someone could show me how to implement the Flutter StreamProvider "catchError" property?
Example code below to add to:
StreamProvider<LocationModelNormal>.value(
initialData: LocationModelNormal.initialData(),
stream: locationStreamInstance.specificLocation(_secondWonder),
catchError: ?????????
),
class LocationModelNormal {
final String name;
LocationModelNormal({
this.name
});
factory LocationModelNormal.fromMap(Map<String, dynamic> data) {
return LocationModelNormal(
name: data['name'] ?? '',
);
}
factory LocationModelNormal.initialData() {
return LocationModelNormal(
name: '',
);
}
}
You'll want to model your data using sealed classes:
abstract class Data {}
class Content implements Data {
Content(this.data);
final List<String> data;
}
class Error implements Data {
Error(this.msg);
final String msg;
}
class Loading implements Data {
const Loading();
}
Then used like so in the provider:
StreamProvider<Data>(
builder: (_) async* {
yield Content(['hello', 'world']);
},
initialData: const Loading(),
catchError: (_, err) => Error(err.toString()),
child: Container(),
);
And consumed as such:
Consumer<Data>(
builder: (_, data, __) {
if (data is Loading) {
return const CircularProgressIndicator();
} else if (data is Error) {
return Center(child: Text(data.msg));
} else if (data is Content) {
return ListView.builder(
itemCount: data.data.length,
itemBuilder: (_, index) => Text(data.data[index]),
);
}
throw FallThroughError();
},
);
Easy fix for now.
#override
Widget build(BuildContext context) {
return StreamProvider<UserModel?>.value(
value: AuthenticationService().user,
initialData: null,
catchError: (_, err) => null,
child: const MaterialApp(
home: AuthWrapper(),
),
);
}
}
Remi of course has the most thorough and proper method, since in the case of an error, you need to provide a value in its place or make it nullable. His solution is the most complete.
However, if you have other things already established, and need a down and dirty solution: Below I make it nullable with the ? and return a null value in the case of an error. The return is not technically necessary.
StreamProvider<LocationModelNormal?>.value(
initialData: LocationModelNormal.initialData(), //or null maybe better
stream: locationStreamInstance.specificLocation(_secondWonder),
catchError: (context, e) {
print('error in LocationModelNormal: ${e.toString()}');
//or pop a dialogue...whatever.
return null;
},
),
You can also do this
StreamProvider<DocumentSnapshot>.value(
value: api.myDetails(mail),
child: Builder(
builder: (context){
var snapshot = Provider.of<DocumentSnapshot>(context);
if(snapshot == null){
return customF.loadingWidget();
}else{
return Stack(
children: <Widget>[
getMainListViewUI(),
getAppBarUI(),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
)
],
);
}
}
),
),