Flutter: changing the URL strategy results in the app reloading - flutter

I'm using navigator 2.0 and all is well with the default URL strategy ('#'). I have a singleton service that loads data and this singleton is called only once no matter the routes.
However, when I change the URL strategy either by including url_strategy: ^0.2.0 as a dependency or following the steps at https://flutter.dev/docs/development/ui/navigation/url-strategies, the app reloads, and my singleton instantiates all over (and the expensive load operations happen again).
Has anyone else experienced this or know a solution?
As an example, below loads with initial route of / and displays "the number is 0 (zero)." and the URL is updated to /0. Pressing the increment button shows everything in sync (URL and screen). Manually entering a new route works fine and the StateService global variable is instantiated just one (have a print line of "*** in StateService() constructor ***" to show this). But if you uncomment out "setPathUrlStrategy()" in function main() and then try again, you'll see that manually entering a new route causes the StateService to be instantiated all over again. This will obviously kill performance for apps doing work upon startup.
Any ideas?
import 'package:flutter/material.dart';
import 'package:url_strategy/url_strategy.dart';
void main() {
// setPathUrlStrategy(); // causes app to reload when URL manually changed; leaving out doesn't
runApp(MyApp());
}
// below sb a Singleton; will just instatiate a global variable once to simulate...
class StateService {
int number = 0;
List<String> words = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"];
StateService() {
print("*** in StateService() constructor.... ***");
}
void increaseNumber() {
number++;
}
String getWord(int number) {
if (number < 0) return "unknown";
if (number > words.length - 1) return "unknown";
return words[number];
}
}
// display the number and its word (if 0-11); has button to increment value
class DisplayNumberScreen extends StatelessWidget {
final VoidCallback increase;
final int number;
DisplayNumberScreen({Key? key, required this.number, required this.increase}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text("The number is: $number (${globalState.getWord(number)})")),
floatingActionButton: FloatingActionButton(
onPressed: increase,
tooltip: "Add 1",
child: Icon(Icons.add),
),
);
}
}
class NavigationState {
final int value;
NavigationState({required this.value});
}
class UrlHandlerRouterDelegate extends RouterDelegate<NavigationState> with ChangeNotifier, PopNavigatorRouterDelegateMixin {
#override
GlobalKey<NavigatorState> get navigatorKey => urlHandlerRouterDelegateNavigatorKey;
void _increaseNumberHandler() {
globalState.increaseNumber();
notifyListeners();
}
// App state to Navigation state, triggered by notifyListeners()
#override
NavigationState get currentConfiguration => NavigationState(value: globalState.number);
#override
Widget build(BuildContext context) {
return Navigator(
pages: [
MaterialPage(child: DisplayNumberScreen(number: globalState.number, increase: _increaseNumberHandler)),
],
onPopPage: (_, __) {
return false;
},
);
}
// Navigation state to app state
#override
Future<void> setNewRoutePath(NavigationState navigationState) async {
globalState.number = navigationState.value;
}
}
class UrlHandlerInformationParser extends RouteInformationParser<NavigationState> {
// URL to navigation state; note initial route of '/' will result in 0 as will any route that's not a number
#override
Future<NavigationState> parseRouteInformation(RouteInformation routeInformation) async =>
NavigationState(value: int.tryParse(routeInformation.location!.substring(1)) ?? 0); // substring(1) to remove the leading '/'
// Navigation state to URL
#override
RouteInformation restoreRouteInformation(NavigationState navigationState) => RouteInformation(location: '/${navigationState.value}');
}
// Assign global variables
final GlobalKey<NavigatorState> urlHandlerRouterDelegateNavigatorKey = GlobalKey<NavigatorState>();
final globalState = StateService();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: UrlHandlerRouterDelegate(),
routeInformationParser: UrlHandlerInformationParser(),
);
}
}

Related

Flutter awesome notifications how to fix StateError (Bad state: Stream has already been listened to.)

I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}

flutter bloc library percentage progress bar

How can a bloc show percentage progress bar
//For example, there is a regular bloc
#override
Stream<JobState> mapEventToState(JobEvent event) async* {
if (event is HardJobEvent) {
yield* _mapHardJobToState();
}
}
Stream<UpdateState> _mapHardJobToState() async* {
try {
//It is necessary to display a progress bar for this method.
await doSomeHardJob();
} catch (e) {
print(e);
}
}
doSomeHardJob() async* {
for( var i = 1 ; i < 1000; i++ ) {
//This yield does not work. Doesn't display any errors
//State not transfer
yield HardJob(nowCounter: i);
}
}
I use cubit instead of bloc. but the technique should be similar.
I have a broadcast stream controller in the payload generating function. In the event dispatcher (this should be your bloc) I listen to it and emit loading states with a double value. the bloc builder in the widgets can react to it. check put my little implementation:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Model
class Data {
final DateTime date;
Data(this.date);
}
/// Repo
class DataRepository {
/// here comes the trick:
final StreamController<double> progress = StreamController.broadcast(); // make it broadcast to allow multiple subcribtions
Future<List<Data>> generateData() async {
/// here comes your time taking work
progress.sink.add(0.0); // set progress to 0
List<Data> payload =
[]; // this will be the data you want to transport in the loadED event
await Future.forEach(List.generate(10, (i) => i), (int i) async {
/// here comes the progess; dont send it too late otherwise
/// the loadED state will be followed by a loadING state and
/// you will see a never ending spinner
progress.sink.add(i / 10);
/// this would be like eg a http call
payload.add(Data(DateTime.now()));
await Future.delayed(
const Duration(seconds: 1)); // simulate the time consuming action
});
return payload;
}
}
/// State
#immutable
abstract class DataState {}
class DataInitial extends DataState {
DataInitial();
}
class DataLoading extends DataState {
/// this state will emit the actual progress value to the spinner
final double progress;
DataLoading(this.progress);
/// boilerplate code to tell state events apart from each other even though they are of the same type
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoading && other.progress == progress;
}
#override
int get hashCode => progress.hashCode;
}
class DataLoaded extends DataState {
/// this state will transport the payload data
final List<Data> data;
DataLoaded(this.data);
/// same as aboth
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoaded && other.data == data;
}
#override
int get hashCode => data.hashCode;
}
/// Cubit
class DataCubit extends Cubit<DataState> {
/// Cubit works like a simple Bloc
final DataRepository dataRepository;
/// the repo will do the actual work
DataCubit({required this.dataRepository}) : super(DataInitial());
Future<void> generateData() async {
/// this will bring the progress value to the loading spinner widget.
/// each time a new progress is made a new DataLoadING state will be emitted
dataRepository.progress.stream
.listen((progress) => emit(DataLoading(progress)));
/// this await is sincere; it will take aaages; really
final payload = await dataRepository.generateData();
/// finally the payload will be sent to the widgets
emit(DataLoaded(payload));
}
}
late DataRepository dataRepository;
void main() {
/// init the repo that will do the heavy lifting like eg a db or http request
dataRepository = DataRepository();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
/// makes the cubit (like a baby bloc) available to all child widgets of the app
return BlocProvider<DataCubit>(
create: (context) => DataCubit(dataRepository: dataRepository),
child: const MaterialApp(
title: 'Flutter Progress Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Progress Example'),
),
body: Center(
child: BlocBuilder<DataCubit, DataState>(
builder: (context, state) {
if (state is DataLoading) {
return CircularProgressIndicator.adaptive(
value: state.progress,
);
} else if (state is DataLoaded) {
List<Data> longAnticipatedData = state.data;
return ListView.builder(
itemCount: longAnticipatedData.length,
itemBuilder: (context, i) => ListTile(
title:
Text(longAnticipatedData[i].date.toIso8601String()),
));
} else {
/// initial state
return const Center(
child: Text('press the FAB'),
);
}
},
),
),
floatingActionButton: FloatingActionButton(
/// generate data; will take a good while
onPressed: () => context.read<DataCubit>().generateData(),
child: const Icon(Icons.start),
),
);
}
}

Flutter pass data to new screen with onTap

My application has a bottom navigation bar, with 2 pages in the menu.
On page 1, I can fill out a form and it calculates me values ​​that it displays to me by pushing in a 1.1 page.
On this page I have a button that allows me to redirect me to page 2 as if I clicked menu 2 of the navigation bar.
This works. My problem is how to send the data from my page 1.1 to this page 2.
The goal being that my page 2 is a form which is empty if I call it by the navigation bar but which is filled automatically if I pass by the page 1.1 in focus of the calculated values.
Here an exemple of the redirection that I do:
Here is my code :
my_app.dart :
final ThemeData _AppTheme = AppTheme().data;
final navBarGlobalKey = GlobalKey(); // => This is my key for redirect page
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp>{
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App',
home: MyBottomNavigationBar(),
theme: _AppTheme,
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: Router.generateRoute,
initialRoute: HOME_ROUTE,
);
}
}
My bottom_navigation_bar.dart :
class MyBottomNavigationBar extends StatefulWidget
{
MyBottomNavigationBar({Key key}) : super(key: key);
#override
_MyBottomNavigationBarState createState() => _MyBottomNavigationBarState();
}
class _MyBottomNavigationBarState extends State<MyBottomNavigationBar>
{
int _pageIndex = 0;
final List<Widget> _pagesOption = [
page1.1(), // => Here I load direclty my page 1.1 with data for the exemple
page2(),
];
void onTappedBar(int index)
{
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child : Scaffold(
body : _pagesOption.elementAt(_pageIndex),
bottomNavigationBar: BottomNavigationBar(
key: navBarGlobalKey,
currentIndex: _pageIndex,
onTap: onTappedBar,
type: BottomNavigationBarType.fixed,
items : [
BottomNavigationBarItem(
icon : Icon(Icons.home),
title : Text('Home')
),
BottomNavigationBarItem(
icon : Icon(Icons.settings),
title : Text('Setting')
),
]
),
)
);
}
}
And here my widget submit button of page 1.1 :
Widget _getSubmitButton(){
return RaisedButton(
child: Text(
'Send'
),
onPressed: () {
final BottomNavigationBar navigationBar = navBarGlobalKey.currentWidget;
navigationBar.onTap(1); // => How to send data that I have in my page ???
},
);
}
For this, you can use Shared Preferences, the main idea is that:
Store the value of the calculated value in SharedPref from Page 1 when you're passing to Page 1.1
Let you checks for the value by default in Page 2's initState(), any changes in the Shared Preferences will be fetched in the Page 2 itself, using SharedPref get method.
WHY?
This is probably a cleaner way to achieve what you want, since in the BottomNavigationBar will not help you do this, but a Shared Preferences value will always give you that data which you can use it any time
Let's see how you can achieve this:
PAGE ONE
// Set the data of the form here
class _PageOneState extends State<PageOne>
{
void onSubmit() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
//make sure you store the calculated value, if that is String
// use setString() or if it is an int, setInt()
// and then pass it to the SharedPref
// key is a string name, which is used to access
// key and store, so choose the name wisely
await prefs.setInt("key", your_calculated_value);
}
}
PAGE TWO
class _PageTwoState extends State<PageTwo>
{
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
// This will be responsible for getting the result from SharedPref
int calculated_value;
#override
void initState(){
super.initState();
// get your list here
calculated_value = _prefs.then((SharedPreferences prefs){
// here if no data is then _values will have 0
// which you can use it to check and populate data
return (prefs.getInt("key") ?? 0);
});
}
}
This is the most reasonable way of doing the thing which you want. In this manner, whenever, PageTwo will trace any values, it will reflect, else, your choice for 0 check result. Let me know, if you have any doubts in that.
In your FirstActivity
onPressed: () {
navigatePush(SecondActivity(text: "Data"));
}
In your SecondActivity
class SecondActivity extends StatefulWidget {
String text;
SecondActivity({this.text});
}
You can pass the the values as arguments when you push to your new screen. This could get messy if you're building a larger project.
A cleaner implementation would be to use a Provider. Set up the data you want in a model mixed in with ChangeNotifier and use Provider.of<*name of your class*>(context) where ever you need to use it.

How to handle navigation using stream from inheritedWidget?

I'm using an inherited Widget to access a Bloc with some long running task (e.g. search).
I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page.
Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().
Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.
I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState().
Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?
Code examples
My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.
#override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("counter: $_counter -Did change dependencies called");
// This works the first time, after that going back and forth to the second screen is opened several times
BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
void _handleRouting(bool isFinished) async {
if (isFinished && !_onSecondPage) {
_onSecondPage = true;
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
_onSecondPage = false;
} else {
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
}
}
#override
void dispose() {
debugPrint("counter: $_counter - disposing my homepage State");
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: BlocProvider.of(context).bloc.counter.stream,
initialData: 0,
builder: (context, snapshot) {
_counter = snapshot.data;
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
A simple Bloc faking some long running work
///Long Work Bloc
class LongWorkBloc {
final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
final BehaviorSubject<bool> finished = BehaviorSubject<bool>();
int _counter = 0;
final BehaviorSubject<int> counter = BehaviorSubject<int>();
LongWorkBloc() {
startLongWork.stream.listen((bool start) {
if (start) {
debugPrint("Start long running work");
Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
_counter++;
counter.sink.add(_counter);
finished.sink.add(true);
finished.sink.add(false);
});
}
});
}
dispose() {
startLongWork?.close();
finished?.close();
counter?.close();
}
}
Better working code
If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.
Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()
Is this ok to do so? Or what would be a flutter best practice in this case?
#override
void initState() {
super.initState();
//this works, but I don't know if this is good practice or has any side effects?
BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
if (p != null) {
p.bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
}
Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose
If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.
like So
void initState() {
super.initState();
_counterBloc = BlocProvider.of(context);
_subscription = _counterBloc.stateStream.listen((state) {
if (state.total > 20) {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return TestPush();
}));
}
});
}
Here is an example of a nice BlocProvider that should work in any case
import 'package:flutter/widgets.dart';
import 'bloc_base.dart';
class BlocProvider<T extends BlocBase> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
#override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
final T bloc;
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
... and finally the BLoC
import 'dart:async';
import 'bloc_base.dart';
abstract class CounterEventBase {
final int amount;
CounterEventBase({this.amount = 1});
}
class CounterIncrementEvent extends CounterEventBase {
CounterIncrementEvent({amount = 1}) : super(amount: amount);
}
class CounterDecrementEvent extends CounterEventBase {
CounterDecrementEvent({amount = 1}) : super(amount: amount);
}
class CounterState {
final int total;
CounterState(this.total);
}
class CounterBloc extends BlocBase {
CounterState _state = CounterState(0);
// Input Streams/Sinks
final _eventInController = StreamController<CounterEventBase>();
Sink<CounterEventBase> get events => _eventInController;
Stream<CounterEventBase> get _eventStream => _eventInController.stream;
// Output Streams/Sinks
final _stateOutController = StreamController<CounterState>.broadcast();
Sink<CounterState> get _states => _stateOutController;
Stream<CounterState> get stateStream => _stateOutController.stream;
// Subscriptions
final List<StreamSubscription> _subscriptions = [];
CounterBloc() {
_subscriptions.add(_eventStream.listen(_handleEvent));
}
_handleEvent(CounterEventBase event) async {
if (event is CounterIncrementEvent) {
_state = (CounterState(_state.total + event.amount));
} else if (event is CounterDecrementEvent) {
_state = (CounterState(_state.total - event.amount));
}
_states.add(_state);
}
#override
void dispose() {
_eventInController.close();
_stateOutController.close();
_subscriptions.forEach((StreamSubscription sub) => sub.cancel());
}
}

how to implement in app time out session in flutter

I want to log a user out after a specific amount time the user has not interacted with the app.
I've wrapped the whole child widget in GestureDetector().
Please suggest if this is the best optimised way of doing this.
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: () {
// duration reset's to a specific time
startTimeout([int milliseconds]) { return new Timer(duration, handleTimeout); }
},
child: new HomeWidget(),);
}
void handleTimeOut {
// Log User Out
}
}
You should cancel previous timers before initializing a new one
static Timer _sessionTimer;
#override
Widget build(BuildContext context) {
...
onTap: () {
_sessionTimer?.cancel();
// duration reset's to a specific time
_sessionTimer = new Timer(duration, handleTimeout);
},
If you need something for the web target then better setup a key-up and a mouse-click listener on your index.html's 'body' as follows.
...
<body id = 'myapp-main-content'>
...
Then implement the listeners, here is an example borrowed from Task Tracker (https://github.com/botorabi/TaskTracker/tree/master/src/flutter-app/TaskTracker/lib).
import 'dart:async';
import 'dart:html';
import 'package:TaskTracker/service/authstatus.dart';
import 'package:flutter/material.dart';
import 'config.dart';
import 'navigation.links.dart';
import 'service/service.login.dart';
/// Logout user after long inactivity period.
class SessionTimeoutHandler {
static const MAIN_CONTAINER_ID = 'myapp-main-content';
final GlobalKey<NavigatorState> _navigator;
Timer _sessionTimer;
int _timeoutInSeconds;
static DateTime _timeLeft;
SessionTimeoutHandler(this._navigator, this._timeoutInSeconds);
void installLogoutHandler() {
var body = document.getElementById(MAIN_CONTAINER_ID);
body.addEventListener("click", (event) => resetLogoutTimer());
body.addEventListener("keyup", (event) => resetLogoutTimer());
resetLogoutTimer();
}
/// Return the time left to logout in seconds.
/// If user is not authenticated then 0 is returned.
static int timeLeftInSeconds() {
if ((_timeLeft == null) || !Config.authStatus.authenticated) {
return 0;
}
return ((DateTime.now().millisecondsSinceEpoch - _timeLeft.millisecondsSinceEpoch) / 1000).floor();
}
void resetLogoutTimer() {
_timeLeft = DateTime.now();
_sessionTimer?.cancel();
_sessionTimer = Timer(Duration(seconds: _timeoutInSeconds), _logout);
}
void _logout() {
if (Config.authStatus.authenticated) {
ServiceLogin().logoutUser().then((result) {
Config.authStatus = AuthStatus();
_navigator.currentState.pushNamedAndRemoveUntil(
NavigationLinks.NAV_HOME, (Route<dynamic> route) => false);
});
}
}
}
Then use the SessionTimeoutHandler above in your main widget setup (see initState below).
class AppTaskTracker extends StatefulWidget {
#override
_AppTaskTrackerState createState() => _AppTaskTrackerState();
}
class _AppTaskTrackerState extends State<AppTaskTracker> {
final GlobalKey<NavigatorState> _navigator = GlobalKey<NavigatorState>();
#override
void initState() {
super.initState();
SessionTimeoutHandler(_navigator, Config.LOGOUT_TIMEOUT).installLogoutHandler();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
...
Take into account that SessionTimeoutHandler gets the navigator in order to redirect to home after automatic logout.