A RouteState was used after being disposed Error - flutter

Let me explain my Flutter structure first. I have a flutter main application and another application added as a package that has a different routing method and navigation. app behavior is when I click on a card on the main app it will get me to the package app, but when I go back to the home interface which is the main app. I'm getting the following error.
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
A RouteState was used after being disposed.
What I have tried so far
I have tried to observe the navigation stack using route_observer_mixin but, it didn't work because I have two different navigations in the main app and the package.
if I try to remove the RouteState.dispose() in the package the error is gone, but that is a bad practice, right? because the memory leak could happen.
I'll put the related code section below for your reference.
Code section from main project main.dart file
class GalleryApp extends StatefulWidget {
// GalleryApp({super.key});
GalleryApp({
super.key,
this.initialRoute,
this.isTestMode = false,
});
late final String? initialRoute;
late final bool isTestMode;
final _auth = CampusAppsPortalAuth();
#override
State<GalleryApp> createState() => _GalleryAppState();
}
RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class _GalleryAppState extends State<GalleryApp> {
late final String loginRoute = '/signin';
get isTestMode => false;
#override
Widget build(BuildContext context) {
return ModelBinding(
initialModel: GalleryOptions(
themeMode: ThemeMode.system,
textScaleFactor: systemTextScaleFactorOption,
customTextDirection: CustomTextDirection.localeBased,
locale: null,
timeDilation: timeDilation,
platform: defaultTargetPlatform,
isTestMode: isTestMode,
),
child: Builder(
builder: (context) {
final options = GalleryOptions.of(context);
return MaterialApp(
restorationScopeId: 'rootGallery',
title: 'Flutter Gallery',
debugShowCheckedModeBanner: false,
navigatorObservers: [routeObserver],
themeMode: options.themeMode,
theme: GalleryThemeData.lightThemeData.copyWith(
platform: options.platform,
),
darkTheme: GalleryThemeData.darkThemeData.copyWith(
platform: options.platform,
),
localizationsDelegates: const [
...GalleryLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate()
],
initialRoute: loginRoute,
supportedLocales: GalleryLocalizations.supportedLocales,
locale: options.locale,
localeListResolutionCallback: (locales, supportedLocales) {
deviceLocale = locales?.first;
return basicLocaleListResolution(locales, supportedLocales);
},
onGenerateRoute: (settings) {
return RouteConfiguration.onGenerateRoute(settings);
},
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) =>
Scaffold(body: Center(child: Text('Not Found'))),
);
});
},
),
);
}
}
class RootPage extends StatelessWidget {
const RootPage({
super.key,
});
#override
Widget build(BuildContext context) {
return const ApplyTextOptions(
child: SplashPage(
child: Backdrop(),
),
);
}
}
Code section from main project Backdrop
class Backdrop extends StatefulWidget {
const Backdrop({super.key, this.settingsPage, this.homePage, this.loginPage});
final Widget? settingsPage;
final Widget? homePage;
final Widget? loginPage;
#override
State<Backdrop> createState() => _BackdropState();
}
RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class _BackdropState extends State<Backdrop>
with TickerProviderStateMixin, RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute<dynamic>);
}
late AnimationController _settingsPanelController;
late AnimationController _iconController;
late FocusNode _settingsPageFocusNode;
late ValueNotifier<bool> _isSettingsOpenNotifier;
late Widget _settingsPage;
late Widget _homePage;
late Widget _unknownPage;
#override
void initState() {
super.initState();
_settingsPanelController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_iconController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_settingsPageFocusNode = FocusNode();
_isSettingsOpenNotifier = ValueNotifier(false);
_settingsPage = widget.settingsPage ??
SettingsPage(
animationController: _settingsPanelController,
);
_homePage = widget.homePage ?? const HomePage();
_unknownPage = widget.homePage ?? const HomePage();
}
#override
void dispose() {
_settingsPanelController.dispose();
_iconController.dispose();
_settingsPageFocusNode.dispose();
_isSettingsOpenNotifier.dispose();
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPush() {
final route = ModalRoute.of(context)!.settings.name;
print('didPush route: $route');
}
#override
void didPopNext() {
final route = ModalRoute.of(context)!.settings.name;
print('didPopNext route: $route');
}
#override
void didPushNext() {
final route = ModalRoute.of(context)!.settings.name;
print('didPushNext route: $route');
}
#override
void didPop() {
final route = ModalRoute.of(context)!.settings.name;
print('didPop route: $route');
}
void _toggleSettings() {
// Animate the settings panel to open or close.
if (_isSettingsOpenNotifier.value) {
_settingsPanelController.reverse();
_iconController.reverse();
} else {
_settingsPanelController.forward();
_iconController.forward();
}
_isSettingsOpenNotifier.value = !_isSettingsOpenNotifier.value;
}
Animation<RelativeRect> _slideDownSettingsPageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0),
end: const RelativeRect.fromLTRB(0, 0, 0, 0),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Animation<RelativeRect> _slideDownHomePageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: const RelativeRect.fromLTRB(0, 0, 0, 0),
end: RelativeRect.fromLTRB(
0,
constraints.biggest.height - galleryHeaderHeight,
0,
-galleryHeaderHeight,
),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final isDesktop = isDisplayDesktop(context);
bool signedIn = campusAppsPortalInstance.getSignedIn();
log('signedIn: $signedIn! ');
print('signedIn: $signedIn!');
log('is decktop $isDesktop');
final Widget settingsPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: !isSettingsOpen,
child: isSettingsOpen
? RawKeyboardListener(
includeSemantics: false,
focusNode: _settingsPageFocusNode,
onKey: (event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
_toggleSettings();
}
},
child: FocusScope(child: _settingsPage),
)
: ExcludeFocus(child: _settingsPage),
);
},
);
final Widget homePage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _homePage),
);
},
);
final Widget unknownPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _unknownPage),
);
},
);
final Widget loginPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(
child: LoginPage(
// onSignIn: (credentials) async {
// var signedIn = await authState.signIn(
// credentials.username, credentials.password);
// if (signedIn) {
// await routeState.go('/gallery');
// }
// },
),
),
);
},
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: GalleryOptions.of(context).resolvedSystemUiOverlayStyle(),
child: Stack(
children: [
if (!isDesktop) ...[
// Slides the settings page up and down from the top of the
// screen.
PositionedTransition(
rect: _slideDownSettingsPageAnimation(constraints),
child: settingsPage,
),
// Slides the home page up and down below the bottom of the
// screen.
PositionedTransition(
rect: _slideDownHomePageAnimation(constraints),
child: homePage,
),
PositionedTransition(
rect: _slideDownHomePageAnimation(constraints),
child: loginPage,
),
],
if (isDesktop && signedIn) ...[
Semantics(sortKey: const OrdinalSortKey(2), child: homePage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
if (isSettingsOpen) {
return ExcludeSemantics(
child: Listener(
onPointerDown: (_) => _toggleSettings(),
child: const ModalBarrier(dismissible: false),
),
);
} else {
return Container();
}
},
),
Semantics(
sortKey: const OrdinalSortKey(3),
child: ScaleTransition(
alignment: Directionality.of(context) == TextDirection.ltr
? Alignment.topRight
: Alignment.topLeft,
scale: CurvedAnimation(
parent: _settingsPanelController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryContainer,
child: Container(
constraints: const BoxConstraints(
maxHeight: 560,
maxWidth: desktopSettingsWidth,
minWidth: desktopSettingsWidth,
),
child: settingsPage,
),
),
),
),
),
],
if (isDesktop && !signedIn) ...[
Semantics(sortKey: const OrdinalSortKey(2), child: loginPage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
if (isSettingsOpen) {
return ExcludeSemantics(
child: Listener(
onPointerDown: (_) => _toggleSettings(),
child: const ModalBarrier(dismissible: false),
),
);
} else {
return Container();
}
},
),
Semantics(
sortKey: const OrdinalSortKey(3),
child: ScaleTransition(
alignment: Directionality.of(context) == TextDirection.ltr
? Alignment.topRight
: Alignment.topLeft,
scale: CurvedAnimation(
parent: _settingsPanelController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryContainer,
child: Container(
constraints: const BoxConstraints(
maxHeight: 560,
maxWidth: desktopSettingsWidth,
minWidth: desktopSettingsWidth,
),
child: settingsPage,
),
),
),
),
),
],
_SettingsIcon(
animationController: _iconController,
toggleSettings: _toggleSettings,
isSettingsOpenNotifier: _isSettingsOpenNotifier,
),
_LogoutIcon(
animationController: _iconController,
toggleSettings: _toggleSettings,
isSettingsOpenNotifier: _isSettingsOpenNotifier,
),
],
),
);
}
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: _buildStack,
);
}
}
Code from main project route file
class Path {
const Path(this.pattern, this.builder, {this.openInSecondScreen = false});
/// A RegEx string for route matching.
final String pattern;
/// The builder for the associated pattern route. The first argument is the
/// [BuildContext] and the second argument a RegEx match if that is included
/// in the pattern.
///
/// ```dart
/// Path(
/// 'r'^/demo/([\w-]+)$',
/// (context, matches) => Page(argument: match),
/// )
/// ```
final PathWidgetBuilder builder;
/// If the route should open on the second screen on foldables.
final bool openInSecondScreen;
}
class RouteConfiguration {
/// List of [Path] to for route matching. When a named route is pushed with
/// [Navigator.pushNamed], the route name is matched with the [Path.pattern]
/// in the list below. As soon as there is a match, the associated builder
/// will be returned. This means that the paths higher up in the list will
/// take priority.
static List<Path> paths = [
Path(
r'^' + DemoPage.baseRoute + r'/([\w-]+)$',
(context, match) => DemoPage(slug: match),
openInSecondScreen: false,
),
Path(
r'^' + rally_routes.homeRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(rally.loadLibrary,
() => rally.RallyApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + shrine_routes.homeRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(shrine.loadLibrary,
() => shrine.ShrineApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + shrine_routes.attendanceRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(
attendance.loadLibrary,
() => attendance
.CampusAttendanceManagementSystem()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + crane_routes.defaultRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(crane.loadLibrary,
() => crane.CraneApp(), // ignore: prefer_const_constructors
placeholder: const DeferredLoadingPlaceholder(name: 'Crane')),
),
openInSecondScreen: true,
),
Path(
r'^' + fortnightly_routes.defaultRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(
fortnightly.loadLibrary,
// ignore: prefer_const_constructors
() => fortnightly.FortnightlyApp()),
),
openInSecondScreen: true,
),
Path(
r'^' + reply_routes.homeRoute,
// ignore: prefer_const_constructors
(context, match) =>
const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true),
openInSecondScreen: true,
),
Path(
r'^' + starter_app_routes.defaultRoute,
(context, match) => const StudyWrapper(
study: starter_app.StarterApp(),
),
openInSecondScreen: true,
),
Path(
r'^/',
(context, match) => const RootPage(),
openInSecondScreen: false,
),
Path(
r'^' + starter_app_routes.loginRoute,
(context, match) => const LoginPage(),
openInSecondScreen: false,
),
];
/// The route generator callback used when the app is navigated to a named
/// route. Set it on the [MaterialApp.onGenerateRoute] or
/// [WidgetsApp.onGenerateRoute] to make use of the [paths] for route
/// matching.
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
for (final path in paths) {
final regExpPattern = RegExp(path.pattern);
if (regExpPattern.hasMatch(settings.name!)) {
final firstMatch = regExpPattern.firstMatch(settings.name!)!;
final match = (firstMatch.groupCount == 1) ? firstMatch.group(1) : null;
if (kIsWeb) {
return NoAnimationMaterialPageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
}
if (path.openInSecondScreen) {
return TwoPanePageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
} else {
return MaterialPageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
}
}
}
return null;
}
}
Code from package app.dart
class CampusAttendanceManagementSystem extends StatefulWidget {
const CampusAttendanceManagementSystem({super.key});
#override
State<CampusAttendanceManagementSystem> createState() =>
_CampusAttendanceManagementSystemState();
}
class _CampusAttendanceManagementSystemState
extends State<CampusAttendanceManagementSystem> {
final _auth = SMSAuth();
final _navigatorKey = GlobalKey<NavigatorState>();
late final RouteState _routeState;
late final SimpleRouterDelegate _routerDelegate;
late final TemplateRouteParser _routeParser;
#override
void initState() {
/// Configure the parser with all of the app's allowed path templates.
_routeParser = TemplateRouteParser(
allowedPaths: [
'/signin',
'/avinya_types/new',
'/avinya_types/all',
'/avinya_types/popular',
'/avinya_type/:id',
'/avinya_type/new',
'/avinya_type/edit',
'/activities/new',
'/activities/all',
'/activities/popular',
'/activity/:id',
'/activity/new',
'/activity/edit',
'/attendance_marker',
'/#access_token',
],
guard: _guard,
initialRoute: '/signin',
);
_routeState = RouteState(_routeParser);
_routerDelegate = SimpleRouterDelegate(
routeState: _routeState,
navigatorKey: _navigatorKey,
builder: (context) => SMSNavigator(
navigatorKey: _navigatorKey,
),
);
// Listen for when the user logs out and display the signin screen.
_auth.addListener(_handleAuthStateChanged);
super.initState();
}
#override
Widget build(BuildContext context) => RouteStateScope(
notifier: _routeState,
child: SMSAuthScope(
notifier: _auth,
child: MaterialApp.router(
routerDelegate: _routerDelegate,
routeInformationParser: _routeParser,
// Revert back to pre-Flutter-2.5 transition behavior:
// https://github.com/flutter/flutter/issues/82053
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
},
),
),
),
),
);
Future<ParsedRoute> _guard(ParsedRoute from) async {
final signedIn = await _auth.getSignedIn();
// String? jwt_sub = campusAttendanceSystemInstance.getJWTSub();
final signInRoute = ParsedRoute('/signin', '/signin', {}, {});
final avinyaTypesRoute =
ParsedRoute('/avinya_types', '/avinya_types', {}, {});
final activitiesRoute = ParsedRoute('/activities', '/activities', {}, {});
final attendanceMarkerRoute =
ParsedRoute('/attendance_marker', '/attendance_marker', {}, {});
// // Go to /apply if the user is not signed in
log("_guard signed in $signedIn");
// log("_guard JWT sub ${jwt_sub}");
log("_guard from ${from.toString()}\n");
if (signedIn && from == avinyaTypesRoute) {
return avinyaTypesRoute;
} else if (signedIn && from == activitiesRoute) {
return activitiesRoute;
} else if (signedIn && from == attendanceMarkerRoute) {
return attendanceMarkerRoute;
}
// Go to /application if the user is signed in and tries to go to /signin.
else if (signedIn && from == signInRoute) {
return ParsedRoute('/avinya_types', '/avinya_types', {}, {});
}
log("_guard signed in2 $signedIn");
// else if (signedIn && jwt_sub != null) {
// return avinyaTypesRoute;
// }
return from;
}
void _handleAuthStateChanged() async {
bool signedIn = await _auth.getSignedIn();
log("_handleAuthStateChanged signed in $signedIn");
if (!signedIn) {
_routeState.go('/signin');
}
}
#override
void dispose() {
_auth.removeListener(_handleAuthStateChanged);
_routeState.dispose();
_routerDelegate.dispose();
super.dispose();
}
}
Please make any suggestions to fix this issue. Thanks in advance

Related

How to make BLoC instance persistent?

I'm developing an app using movies database API. I want to read a list of the movies and then choose one and display the details to user.
MovieDetailsBloc code:
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_recruitment_task/movie_details/models/movie_details.dart';
import 'package:flutter_recruitment_task/services/api_service.dart';
import 'package:stream_transform/stream_transform.dart';
part 'movie_details_event.dart';
part 'movie_details_state.dart';
const throttleDuration = Duration(milliseconds: 100);
EventTransformer<E> throttleDroppable<E>(Duration duration) {
return (events, mapper) {
return droppable<E>().call(events.throttle(duration), mapper);
};
}
class MovieDetailsBloc extends Bloc<MovieDetailsEvent, MovieDetailsState> {
MovieDetailsBloc({required this.apiService}) : super(MovieDetailsState()) {
on<MovieSelected>(
_onMovieSelected,
transformer: throttleDroppable(throttleDuration),
);
}
final ApiService apiService;
Future<void> _onMovieSelected(MovieSelected event, Emitter<MovieDetailsState> emit) async {
try{
final movie = await _fetchMovieDetails(event.query);
return emit(state.copyWith(
status: MovieDetailsStatus.success,
movie: movie,
));
} catch (_) {
emit(state.copyWith(status: MovieDetailsStatus.failure));
}
}
Future<MovieDetails> _fetchMovieDetails(String id) async {
final movie = apiService.fetchMovieDetails(id);
return movie;
}
String shouldIWatchIt() {
final movie = state.movie;
if(movie.revenue - movie.budget > 1000000 && DateTime.now().weekday == DateTime.sunday){
return 'Yes';
} else {
return 'No';
}
}
}
MovieListPage code:
import 'package:flutter/material.dart';
import 'package:flutter_recruitment_task/movie_details/bloc/movie_details_bloc.dart';
import 'package:flutter_recruitment_task/movies/bloc/movies_bloc.dart';
import 'package:flutter_recruitment_task/movies/models/movie.dart';
import 'package:flutter_recruitment_task/pages/movie_details/movie_details_page.dart';
import 'package:flutter_recruitment_task/pages/movie_list/movie_card.dart';
import 'package:flutter_recruitment_task/pages/movie_list/search_box.dart';
import 'package:flutter_recruitment_task/services/api_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MovieListPage extends StatefulWidget {
#override
_MovieListPage createState() => _MovieListPage();
}
class _MovieListPage extends State<MovieListPage> {
final ApiService apiService = ApiService();
late final MoviesBloc moviesBloc;
late final MovieDetailsBloc movieDetailsBloc;
void _onSearchBoxSubmitted(String text) {
moviesBloc.add(MoviesFetched(query: text));
}
#override
void initState() {
moviesBloc = MoviesBloc(apiService: apiService);
movieDetailsBloc = MovieDetailsBloc(apiService: apiService);
super.initState();
}
#override
void dispose() {
movieDetailsBloc.close();
moviesBloc.close();
super.dispose();
}
#override
Widget build(BuildContext context) => BlocProvider(
create: (_) => movieDetailsBloc,
child: Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.movie_creation_outlined),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MovieDetailsPage(
movieDetailsBloc: movieDetailsBloc,
)
)
);
},
),
],
title: Text('Movie Browser'),
),
body: BlocProvider(
create: (_) => moviesBloc,
child: Column(
children: <Widget>[
SearchBox(onSubmitted: _onSearchBoxSubmitted),
Expanded(child: _buildContent()),
],
),
),
),
);
Widget _buildContent() => BlocBuilder<MoviesBloc, MoviesState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
switch (state.status) {
case MoviesStatus.failure:
return const Center(child: Text('failed to fetch movies'));
case MoviesStatus.success:
if (state.movies.isEmpty) {
return const Center(child: Text('no movies'));
}
return _buildMoviesList(state.movies);
case MoviesStatus.initial:
return const Center(child: CircularProgressIndicator());
}
});
Widget _buildMoviesList(List<Movie> movies) => ListView.separated(
separatorBuilder: (context, index) => Container(
height: 1.0,
color: Colors.grey.shade300,
),
itemBuilder: (context, index) => BlocBuilder<MovieDetailsBloc, MovieDetailsState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
return MovieCard(
title: movies[index].title,
rating: '${(movies[index].voteAverage * 10).toInt()}%',
color: state.movie.title == movies[index].title ?
Colors.amberAccent : Colors.white,
onTap: () {
movieDetailsBloc.add(MovieSelected(query: movies[index].id.toString()));
},
);
}
),
itemCount: movies.length,
);
}
MovieDetailsPage code:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_recruitment_task/movie_details/bloc/movie_details_bloc.dart';
import 'package:flutter_recruitment_task/movie_details/models/movie_detail_placeholder.dart';
class MovieDetailsPage extends StatefulWidget {
final MovieDetailsBloc? movieDetailsBloc;
const MovieDetailsPage({super.key, this.movieDetailsBloc});
#override
_MovieDetailsPageState createState() => _MovieDetailsPageState();
}
class _MovieDetailsPageState extends State<MovieDetailsPage> {
var _details = [];
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) => BlocProvider(
create: (_) => widget.movieDetailsBloc!,
child: BlocBuilder<MovieDetailsBloc, MovieDetailsState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
_details = [
MovieDetailPlaceholder(title: 'Budget', content: '\$ ${state.movie.budget}'),
MovieDetailPlaceholder(title: 'Revenue', content: '\$ ${state.movie.revenue}'),
MovieDetailPlaceholder(title: 'Should I watch it today?', content: widget.movieDetailsBloc!.shouldIWatchIt()),
];
return Scaffold(
appBar: AppBar(
title: Text(state.movie.title),
),
body: ListView.separated(
separatorBuilder: (context, index) => Container(
height: 1.0,
color: Colors.grey.shade300,
),
itemBuilder: (context, index) => Container(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
_details[index].title,
style: Theme.of(context).textTheme.headline5,
),
SizedBox(height: 8.0),
Text(
_details[index].content,
style: Theme.of(context).textTheme.subtitle1,
),
],
),
),
itemCount: _details.length,
),
);
}
),
);
}
I'm passing an instance of BLoC from MovieListPage to MovieDetailsPage because it holds the details of the movie to display. It works fine for one time, however when I go back to the MovieListPage I'm not able to choose a new movie to display the details. How I can make an instance of BLoC persist through navigation?
Create your bloc's intance after your bloc ends like
class MovieDetailsBloc extends Bloc<MovieDetailsEvent, MovieDetailsState> {
MovieDetailsBloc({required this.apiService}) : super(MovieDetailsState()) {
on<MovieSelected>(
_onMovieSelected,
transformer: throttleDroppable(throttleDuration),
);
}
final ApiService apiService;
Future<void> _onMovieSelected(MovieSelected event, Emitter<MovieDetailsState> emit) async {
try{
final movie = await _fetchMovieDetails(event.query);
return emit(state.copyWith(
status: MovieDetailsStatus.success,
movie: movie,
));
} catch (_) {
emit(state.copyWith(status: MovieDetailsStatus.failure));
}
}
Future<MovieDetails> _fetchMovieDetails(String id) async {
final movie = apiService.fetchMovieDetails(id);
return movie;
}
String shouldIWatchIt() {
final movie = state.movie;
if(movie.revenue - movie.budget > 1000000 && DateTime.now().weekday == DateTime.sunday){
return 'Yes';
} else {
return 'No';
}
}
}
MovieDetailsBloc movieDetailsBloc = MovieDetailsBloc();
and you can access it anywhere in your code with movieDetailsBloc.add or whatever you want.

Can not navigate with Go Router when implement Navigation Widget in navigatorBuilder function

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

How to show Icon when I started dragging a Widget in Flutter

I want to know how can I show at the bottom of the screen a delete icon when I start dragging a Container in LongPressDraggable widget
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _onTap(context),
child: LongPressDraggable(
data: index,
maxSimultaneousDrags: 1,
onDragUpdate: (details) => print('update'),
onDragStarted: () => _buildDragTarget(),
onDragEnd: (_) => print('end'),
feedback: Material(
child: Container(
height: Sizes.height / 4.5,
width: Sizes.height / 4.5,
child: _DraggableContent(
index: index,
place: place,
),
),
),
childWhenDragging: Container(color: Colors.transparent),
child: _DraggableContent(
index: index,
place: place,
),
));
}
Widget _buildDragTarget() {
return DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return Icon(Icons.delete);
},
onAcceptWithDetails: (DragTargetDetails<int> dragTargetDetails) {
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
);
}
At the moment, when I start dragging the item, anything happens and I don't know how to continue
As far as I understood you need to show the delete icon on bottom when you are dragging an object.
You can wrap the delete icon with Visibility widget and set the visible parameter true or false based on the drag activity.
It will go something like this:
Widget _buildDragTarget() {
return DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return Visibility(
visible: isDragEnable,
child: Icon(Icons.delete));
},
onAcceptWithDetails: (DragTargetDetails<int> dragTargetDetails) {
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
);
}
You have an example with Dragable widget here : https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/
On the DROPPING AN ITEM part https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/#:~:text=the%20tomato%20image.-,Dropping%20an%20item,-At%20this%20point
You need to use the onAccept event on your DragTarget widget :
onAccept: (data) {
setState(() {
showSnackBarGlobal(context, 'Dropped successfully!');
_isDropped = true;
});
},
And on Drag starting, you can show up your delete icon by using this event (https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/#:~:text=Listening%20to%20drag%20events) :
onDragStarted: () {
showSnackBarGlobal(context, 'Drag started');
},
I hope it will help you
I found the solution. It works creating a BlocProvider and working with the state.
class _Trash extends StatefulWidget {
const _Trash({
Key key,
}) : super(key: key);
#override
__TrashState createState() => __TrashState();
}
class __TrashState extends State<_Trash> with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
_listenerManager(bool isDragging) {
if (isDragging) {
setState(() => animation =
CurvedAnimation(parent: controller, curve: Curves.elasticOut));
controller.forward();
} else {
setState(() => animation = CurvedAnimation(
parent:
CurvedAnimation(parent: controller, curve: Interval(0.75, 1.0)),
curve: Curves.ease));
controller.reverse();
}
}
_deleteWidget(data) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.delete(data));
}
#override
void initState() {
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = CurvedAnimation(parent: controller, curve: Curves.elasticOut);
super.initState();
}
#override
Widget build(BuildContext context) {
return Positioned(
bottom: 0,
left: Sizes.width / 2 - Sizes.height / 15 / 2,
child: ScaleTransition(
scale: animation,
child: BlocListener<BoardBloc, BoardState>(
listenWhen: (previous, current) =>
previous.isDragging != current.isDragging,
listener: (context, state) => _listenerManager(state.isDragging),
child: Container(
height: Sizes.height / 15,
width: Sizes.height / 15,
color: Colors.amber,
child: DragTarget<int>(
builder: (BuildContext context, List<int> data,
List<dynamic> rejects) {
return Icon(Icons.delete);
},
onAcceptWithDetails:
(DragTargetDetails<int> dragTargetDetails) {
_deleteWidget(dragTargetDetails.data);
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
),
),
)));
}
}
class _PlaceTile extends StatelessWidget {
final int index;
final Place place;
const _PlaceTile({#required this.place, #required this.index});
_onTap(BuildContext context) => Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
PlaceViewer(index: index, animation: animation, place: place)));
_startDragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.startedDragging());
}
_stopDragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.stopDragging());
}
_dragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.dragging());
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _onTap(context),
child: LongPressDraggable(
data: index,
maxSimultaneousDrags: 1,
onDragUpdate: (details) => _dragging(context),
onDragStarted: () => _startDragging(context),
onDragEnd: (_) => _stopDragging(context),
feedback: Material(
child: Container(
height: Sizes.height / 4.5,
width: Sizes.height / 4.5,
child: _DraggableContent(
index: index,
place: place,
),
),
),
childWhenDragging: Container(color: Colors.transparent),
child: _DraggableContent(
index: index,
place: place,
),
));
}
}

How to set a loading indicator while FutureProvider is not done

I'm using FutureProvider to fetch data from a local db with SQflite, and then render a graph in the Consumer child. However, when loading the app, during a brief period an error is shown :
The following StateError was thrown building Consumer<List<Map<String, dynamic>>>(dirty,
dependencies: [_InheritedProviderScope<List<Map<String, dynamic>>>]):
Bad state: No element
After the graph is rendered fine.
How can I catch this loading state so the error disappears and I can show a CircularProgressIndicator() ?
Parent
FutureProvider<List<Map<String, dynamic>>>(
create: (context) {
return RecordsDatabase.instance.getRecords();
},
catchError: (context, error) {
print("error: ${error.toString()}");
return [];
},
initialData: [],
child: HomeCustom(),
)
Child
#override
Widget build(BuildContext context) {
return Consumer<List<Map<String, dynamic>>>(
builder: (context, records, child) {
GraphState graph =GraphState(records: records, context: context);
return ChangeNotifierProvider<GraphState>(
create: (_) => graph,
child: Scaffold(
backgroundColor: Colors.black,
body: Stack(children: [
Center(
child: graph.records.isEmpty
? Text(
'No Records',
style: TextStyle(color: Colors.white, fontSize: 24),
)
: MyGraph()),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 30, bottom: 50),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _setVisible,
),
),
)
]),
),
);
});
}
}
In the Consumer, check the records value first then return the appropriate widget.
Sample...
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureProvider<List<Map<String, dynamic>>?>(
create: (_) => _getRecords(),
initialData: null,
catchError: (_, __) => <Map<String, dynamic>>[
{'error': 'Something went wrong'}
],
child: HomePage(),
),
);
}
Future<List<Map<String, dynamic>>> _getRecords() async {
final bool isError = false; // set to "true" to check error case
await Future<void>.delayed(const Duration(seconds: 5));
if (isError) {
throw Exception();
}
return <Map<String, dynamic>>[
<String, int>{'item': 1},
<String, String>{'itemTxt': 'one'},
];
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer<List<Map<String, dynamic>>?>(
builder: (_, List<Map<String, dynamic>>? records, __) {
if (records == null) {
return const CircularProgressIndicator();
} else if (records.isNotEmpty &&
records.first.containsKey('error')) {
return Text(records.first['error'] as String);
}
return Text(records.toString());
},
),
),
);
}
}

Pagination for Firestore animated list flutter

I'm making a chat app that messages should be shown on screen with nice animation and my backend is Firestore, so I decided to use this (https://pub.dev/packages/firestore_ui) plugin for animating messages.
Now I want to implement pagination to prevent expensive works and bills.
Is there any way?
How should I implement it?
main problem is making a firestore animated list with pagination,
It's easy to make simple ListView with pagination.
as you can see in below code, this plugin uses Query of snapShots to show incoming messages (documents) with animation:
FirestoreAnimatedList(
query: query,
itemBuilder: (
BuildContext context,
DocumentSnapshot snapshot,
Animation<double> animation,
int index,
) => FadeTransition(
opacity: animation,
child: MessageListTile(
index: index,
document: snapshot,
onTap: _removeMessage,
),
),
);
if we want to use AnimatedList widget instead, we will have problem because we should track realtime messages(documents) that are adding to our collection.
I put together an example for you: https://gist.github.com/slightfoot/d936391bfb77a5301335c12e3e8861de
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:provider/provider.dart';
///
/// Firestore Chat List Example - by Simon Lightfoot
///
/// Setup instructions:
///
/// 1. Create project on console.firebase.google.com.
/// 2. Add firebase_auth package to your pubspec.yaml.
/// 3. Add cloud_firestore package to your pubspec.yaml.
/// 4. Follow the steps to add firebase to your application on Android/iOS.
/// 5. Go to the authentication section of the firebase console and enable
/// anonymous auth.
///
/// Now run the example on two or more devices and start chatting.
///
///
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final user = await FirebaseAuth.instance.currentUser();
runApp(ExampleChatApp(user: user));
}
class ExampleChatApp extends StatefulWidget {
const ExampleChatApp({
Key key,
this.user,
}) : super(key: key);
final FirebaseUser user;
static Future<FirebaseUser> signIn(BuildContext context, String displayName) {
final state = context.findAncestorStateOfType<_ExampleChatAppState>();
return state.signIn(displayName);
}
static Future<void> postMessage(ChatMessage message) async {
await Firestore.instance
.collection('messages')
.document()
.setData(message.toJson());
}
static Future<void> signOut(BuildContext context) {
final state = context.findAncestorStateOfType<_ExampleChatAppState>();
return state.signOut();
}
#override
_ExampleChatAppState createState() => _ExampleChatAppState();
}
class _ExampleChatAppState extends State<ExampleChatApp> {
StreamSubscription<FirebaseUser> _userSub;
FirebaseUser _user;
Future<FirebaseUser> signIn(String displayName) async {
final result = await FirebaseAuth.instance.signInAnonymously();
await result.user.updateProfile(
UserUpdateInfo()..displayName = displayName,
);
final user = await FirebaseAuth.instance.currentUser();
setState(() => _user = user);
return user;
}
Future<void> signOut() {
return FirebaseAuth.instance.signOut();
}
#override
void initState() {
super.initState();
_user = widget.user;
_userSub = FirebaseAuth.instance.onAuthStateChanged.listen((user) {
print('changed ${user?.uid} -> ${user?.displayName}');
setState(() => _user = user);
});
}
#override
void dispose() {
_userSub.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Provider<FirebaseUser>.value(
value: _user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Firestore Chat List',
home: _user == null ? LoginScreen() : ChatScreen(),
),
);
}
}
class LoginScreen extends StatefulWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return LoginScreen();
},
);
}
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
TextEditingController _displayName;
bool _loading = false;
#override
void initState() {
super.initState();
_displayName = TextEditingController();
}
#override
void dispose() {
_displayName.dispose();
super.dispose();
}
Future<void> _onSubmitPressed() async {
setState(() => _loading = true);
try {
final user = await ExampleChatApp.signIn(context, _displayName.text);
if (mounted) {
await ExampleChatApp.postMessage(
ChatMessage.notice(user, 'has entered the chat'));
Navigator.of(context).pushReplacement(ChatScreen.route());
}
} finally {
if (mounted) {
setState(() => _loading = false);
}
}
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Firestore Chat List'),
),
body: SizedBox.expand(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Login',
style: theme.textTheme.headline4,
textAlign: TextAlign.center,
),
SizedBox(height: 32.0),
if (_loading)
CircularProgressIndicator()
else ...[
TextField(
controller: _displayName,
decoration: InputDecoration(
hintText: 'Display Name',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _onSubmitPressed(),
textInputAction: TextInputAction.go,
),
SizedBox(height: 12.0),
RaisedButton(
onPressed: () => _onSubmitPressed(),
child: Text('ENTER CHAT'),
),
],
],
),
),
),
);
}
}
class ChatScreen extends StatelessWidget {
static Route<dynamic> route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return ChatScreen();
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Firestore Chat List'),
actions: [
IconButton(
onPressed: () async {
final user = Provider.of<FirebaseUser>(context, listen: false);
ExampleChatApp.postMessage(
ChatMessage.notice(user, 'has left the chat.'));
Navigator.of(context).pushReplacement(LoginScreen.route());
await ExampleChatApp.signOut(context);
},
icon: Icon(Icons.exit_to_app),
),
],
),
body: Column(
children: [
Expanded(
child: FirestoreChatList(
listenBuilder: () {
return Firestore.instance
.collection('messages')
.orderBy('posted', descending: true);
},
pagedBuilder: () {
return Firestore.instance
.collection('messages')
.orderBy('posted', descending: true)
.limit(15);
},
itemBuilder: (BuildContext context, int index,
DocumentSnapshot document, Animation<double> animation) {
final message = ChatMessage.fromDoc(document);
return SizeTransition(
key: Key('message-${document.documentID}'),
axis: Axis.vertical,
axisAlignment: -1.0,
sizeFactor: animation,
child: Builder(
builder: (BuildContext context) {
switch (message.type) {
case ChatMessageType.notice:
return ChatMessageNotice(message: message);
case ChatMessageType.text:
return ChatMessageBubble(message: message);
}
throw StateError('Bad message type');
},
),
);
},
),
),
SendMessagePanel(),
],
),
);
}
}
class ChatMessageNotice extends StatelessWidget {
const ChatMessageNotice({
Key key,
#required this.message,
}) : super(key: key);
final ChatMessage message;
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(24.0),
alignment: Alignment.center,
child: Text(
'${message.displayName} ${message.message}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
),
),
);
}
}
class ChatMessageBubble extends StatelessWidget {
const ChatMessageBubble({
Key key,
#required this.message,
}) : super(key: key);
final ChatMessage message;
MaterialColor _calculateUserColor(String uid) {
final hash = uid.codeUnits.fold(0, (prev, el) => prev + el);
return Colors.primaries[hash % Colors.primaries.length];
}
#override
Widget build(BuildContext context) {
final isMine = message.isMine(context);
return Container(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
width: double.infinity,
child: Column(
crossAxisAlignment:
isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
FractionallySizedBox(
widthFactor: 0.6,
child: Container(
decoration: BoxDecoration(
color: _calculateUserColor(message.uid).shade200,
borderRadius: isMine
? const BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
bottomLeft: Radius.circular(24.0),
)
: const BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
bottomRight: Radius.circular(24.0),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.displayName?.isNotEmpty ?? false) ...[
const SizedBox(width: 8.0),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _calculateUserColor(message.uid),
),
padding: EdgeInsets.all(8.0),
child: Text(
message.displayName.substring(0, 1),
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
),
),
),
],
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(message.message),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
message.infoText(context),
style: TextStyle(
fontSize: 12.0,
color: Colors.grey.shade600,
),
),
),
],
),
);
}
}
class SendMessagePanel extends StatefulWidget {
#override
_SendMessagePanelState createState() => _SendMessagePanelState();
}
class _SendMessagePanelState extends State<SendMessagePanel> {
final _controller = TextEditingController();
FirebaseUser _user;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_user = Provider.of<FirebaseUser>(context);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onSubmitPressed() {
if (_controller.text.isEmpty) {
return;
}
ExampleChatApp.postMessage(ChatMessage.text(_user, _controller.text));
_controller.clear();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey.shade200,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: Offset(0.0, -3.0),
blurRadius: 4.0,
spreadRadius: 3.0,
)
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 160.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.grey.shade300,
isDense: true,
),
onSubmitted: (_) => _onSubmitPressed(),
maxLines: null,
textInputAction: TextInputAction.send,
),
),
),
IconButton(
onPressed: () => _onSubmitPressed(),
icon: Icon(Icons.send),
),
],
),
);
}
}
enum ChatMessageType {
notice,
text,
}
class ChatMessage {
const ChatMessage._({
this.type,
this.posted,
this.message = '',
this.uid,
this.displayName,
this.photoUrl,
}) : assert(type != null && posted != null);
final ChatMessageType type;
final DateTime posted;
final String message;
final String uid;
final String displayName;
final String photoUrl;
String infoText(BuildContext context) {
final timeOfDay = TimeOfDay.fromDateTime(posted);
final localizations = MaterialLocalizations.of(context);
final date = localizations.formatShortDate(posted);
final time = localizations.formatTimeOfDay(timeOfDay);
return '$date at $time from $displayName';
}
bool isMine(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
return uid == user?.uid;
}
factory ChatMessage.notice(FirebaseUser user, String message) {
return ChatMessage._(
type: ChatMessageType.notice,
posted: DateTime.now().toUtc(),
message: message,
uid: user.uid,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
}
factory ChatMessage.text(FirebaseUser user, String message) {
return ChatMessage._(
type: ChatMessageType.text,
posted: DateTime.now().toUtc(),
message: message,
uid: user.uid,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
}
factory ChatMessage.fromDoc(DocumentSnapshot doc) {
return ChatMessage._(
type: ChatMessageType.values[doc['type'] as int],
posted: (doc['posted'] as Timestamp).toDate(),
message: doc['message'] as String,
uid: doc['user']['uid'] as String,
displayName: doc['user']['displayName'] as String,
photoUrl: doc['user']['photoUrl'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'type': type.index,
'posted': Timestamp.fromDate(posted),
'message': message,
'user': {
'uid': uid,
'displayName': displayName,
'photoUrl': photoUrl,
},
};
}
}
// ---- CHAT LIST IMPLEMENTATION ----
typedef Query FirestoreChatListQueryBuilder();
typedef Widget FirestoreChatListItemBuilder(
BuildContext context,
int index,
DocumentSnapshot document,
Animation<double> animation,
);
typedef Widget FirestoreChatListLoaderBuilder(
BuildContext context,
int index,
Animation<double> animation,
);
class FirestoreChatList extends StatefulWidget {
const FirestoreChatList({
Key key,
this.controller,
#required this.listenBuilder,
#required this.pagedBuilder,
#required this.itemBuilder,
this.loaderBuilder = defaultLoaderBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = true,
this.primary,
this.physics,
this.shrinkWrap = false,
this.initialAnimate = false,
this.padding,
this.duration = const Duration(milliseconds: 300),
}) : super(key: key);
final FirestoreChatListQueryBuilder listenBuilder;
final FirestoreChatListQueryBuilder pagedBuilder;
final FirestoreChatListItemBuilder itemBuilder;
final FirestoreChatListLoaderBuilder loaderBuilder;
final ScrollController controller;
final Axis scrollDirection;
final bool reverse;
final bool primary;
final ScrollPhysics physics;
final bool shrinkWrap;
final bool initialAnimate;
final EdgeInsetsGeometry padding;
final Duration duration;
static Widget defaultLoaderBuilder(
BuildContext context, int index, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: Container(
padding: EdgeInsets.all(32.0),
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
#override
_FirestoreChatListState createState() => _FirestoreChatListState();
}
class _FirestoreChatListState extends State<FirestoreChatList> {
final _animatedListKey = GlobalKey<AnimatedListState>();
final _dataListen = List<DocumentSnapshot>();
final _dataPaged = List<DocumentSnapshot>();
Future _pageRequest;
StreamSubscription<QuerySnapshot> _listenSub;
ScrollController _controller;
ScrollController get controller =>
widget.controller ?? (_controller ??= ScrollController());
#override
void initState() {
super.initState();
controller.addListener(_onScrollChanged);
_requestNextPage();
}
#override
void dispose() {
controller.removeListener(_onScrollChanged);
_controller?.dispose();
_listenSub?.cancel();
super.dispose();
}
void _onScrollChanged() {
if (!controller.hasClients) {
return;
}
final position = controller.position;
if ((position.pixels >=
(position.maxScrollExtent - position.viewportDimension)) &&
position.userScrollDirection == ScrollDirection.reverse) {
_requestNextPage();
}
}
void _requestNextPage() {
_pageRequest ??= () async {
final loaderIndex = _addLoader();
// await Future.delayed(const Duration(seconds: 3));
var pagedQuery = widget.pagedBuilder();
if (_dataPaged.isNotEmpty) {
pagedQuery = pagedQuery.startAfterDocument(_dataPaged.last);
}
final snapshot = await pagedQuery.getDocuments();
if (!mounted) {
return;
}
final insertIndex = _dataListen.length + _dataPaged.length;
_dataPaged.addAll(snapshot.documents);
_removeLoader(loaderIndex);
for (int i = 0; i < snapshot.documents.length; i++) {
_animateAdded(insertIndex + i);
}
if (_listenSub == null) {
var listenQuery = widget.listenBuilder();
if (_dataPaged.isNotEmpty) {
listenQuery = listenQuery.endBeforeDocument(_dataPaged.first);
}
_listenSub = listenQuery.snapshots().listen(_onListenChanged);
}
_pageRequest = null;
}();
}
void _onListenChanged(QuerySnapshot snapshot) {
for (final change in snapshot.documentChanges) {
switch (change.type) {
case DocumentChangeType.added:
_dataListen.insert(change.newIndex, change.document);
_animateAdded(change.newIndex);
break;
case DocumentChangeType.modified:
if (change.oldIndex == change.newIndex) {
_dataListen.removeAt(change.oldIndex);
_dataListen.insert(change.newIndex, change.document);
setState(() {});
} else {
final oldDoc = _dataListen.removeAt(change.oldIndex);
_animateRemoved(change.oldIndex, oldDoc);
_dataListen.insert(change.newIndex, change.document);
_animateAdded(change.newIndex);
}
break;
case DocumentChangeType.removed:
final oldDoc = _dataListen.removeAt(change.oldIndex);
_animateRemoved(change.oldIndex, oldDoc);
break;
}
}
}
int _addLoader() {
final index = _dataListen.length + _dataPaged.length;
_animatedListKey?.currentState
?.insertItem(index, duration: widget.duration);
return index;
}
void _removeLoader(int index) {
_animatedListKey?.currentState?.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.loaderBuilder(context, index, animation);
},
duration: widget.duration,
);
}
void _animateAdded(int index) {
final animatedListState = _animatedListKey.currentState;
if (animatedListState != null) {
animatedListState.insertItem(index, duration: widget.duration);
} else {
setState(() {});
}
}
void _animateRemoved(int index, DocumentSnapshot old) {
final animatedListState = _animatedListKey.currentState;
if (animatedListState != null) {
animatedListState.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return widget.itemBuilder(context, index, old, animation);
},
duration: widget.duration,
);
} else {
setState(() {});
}
}
#override
Widget build(BuildContext context) {
if (_dataListen.length == 0 &&
_dataPaged.length == 0 &&
!widget.initialAnimate) {
return SizedBox();
}
return AnimatedList(
key: _animatedListKey,
controller: controller,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
primary: widget.primary,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
padding: widget.padding ?? MediaQuery.of(context).padding,
initialItemCount: _dataListen.length + _dataPaged.length,
itemBuilder: (
BuildContext context,
int index,
Animation<double> animation,
) {
if (index < _dataListen.length) {
return widget.itemBuilder(
context,
index,
_dataListen[index],
animation,
);
} else {
final pagedIndex = index - _dataListen.length;
if (pagedIndex < _dataPaged.length) {
return widget.itemBuilder(
context, index, _dataPaged[pagedIndex], animation);
} else {
return widget.loaderBuilder(
context,
pagedIndex,
AlwaysStoppedAnimation<double>(1.0),
);
}
}
},
);
}
}
you can check this github project by simplesoft-duongdt3;
TLDR this is how to go about it
StreamController<List<DocumentSnapshot>> _streamController =
StreamController<List<DocumentSnapshot>>();
List<DocumentSnapshot> _products = [];
bool _isRequesting = false;
bool _isFinish = false;
void onChangeData(List<DocumentChange> documentChanges) {
var isChange = false;
documentChanges.forEach((productChange) {
print(
"productChange ${productChange.type.toString()} ${productChange.newIndex} ${productChange.oldIndex} ${productChange.document}");
if (productChange.type == DocumentChangeType.removed) {
_products.removeWhere((product) {
return productChange.document.documentID == product.documentID;
});
isChange = true;
} else {
if (productChange.type == DocumentChangeType.modified) {
int indexWhere = _products.indexWhere((product) {
return productChange.document.documentID == product.documentID;
});
if (indexWhere >= 0) {
_products[indexWhere] = productChange.document;
}
isChange = true;
}
}
});
if(isChange) {
_streamController.add(_products);
}
}
#override
void initState() {
Firestore.instance
.collection('products')
.snapshots()
.listen((data) => onChangeData(data.documentChanges));
requestNextPage();
super.initState();
}
#override
void dispose() {
_streamController.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.maxScrollExtent == scrollInfo.metrics.pixels) {
requestNextPage();
}
return true;
},
child: StreamBuilder<List<DocumentSnapshot>>(
stream: _streamController.stream,
builder: (BuildContext context,
AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Text('Loading...');
default:
log("Items: " + snapshot.data.length.toString());
return //your grid here
ListView.separated(
separatorBuilder: (context, index) => Divider(
color: Colors.black,
),
itemCount: snapshot.data.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: new ListTile(
title: new Text(snapshot.data[index]['name']),
subtitle: new Text(snapshot.data[index]['description']),
),
),
);
}
},
));
}
void requestNextPage() async {
if (!_isRequesting && !_isFinish) {
QuerySnapshot querySnapshot;
_isRequesting = true;
if (_products.isEmpty) {
querySnapshot = await Firestore.instance
.collection('products')
.orderBy('index')
.limit(5)
.getDocuments();
} else {
querySnapshot = await Firestore.instance
.collection('products')
.orderBy('index')
.startAfterDocument(_products[_products.length - 1])
.limit(5)
.getDocuments();
}
if (querySnapshot != null) {
int oldSize = _products.length;
_products.addAll(querySnapshot.documents);
int newSize = _products.length;
if (oldSize != newSize) {
_streamController.add(_products);
} else {
_isFinish = true;
}
}
_isRequesting = false;
}
}