Preserve state and prevent initState of been called more than once - flutter

I have 3 page (all statefull widgets) :
Home page
Weather page
Setting page
The things is when i'm going from home page to weather page with a "Navigator.pushNamed" and going from the weather page to home page with a "Navigator.pop", the next time i'm trying to go to the weather page from the home page, initState method is called again...
How i can manage to make it call only the first time and not been called every time i push into the weather page ?
Here my app.dart code :
import 'package:exomind/src/core/views/home_view.dart';
import 'package:exomind/src/features/weather/presentation/views/weather_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../injection_container.dart';
import 'core/styles/colors.dart';
import 'features/settings/presentation/bloc/settings_bloc.dart';
import 'features/settings/presentation/views/settings_view.dart';
import 'features/weather/presentation/bloc/weather_bloc.dart';
/// The Widget that configures your application.
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
// Glue the SettingsController to the MaterialApp.
//
// The AnimatedBuilder Widget listens to the SettingsController for changes.
// Whenever the user updates their settings, the MaterialApp is rebuilt.
return MultiBlocProvider(
providers: [
BlocProvider<WeatherBloc>(
create: (_) => serviceLocator<WeatherBloc>()),
BlocProvider<SettingsBloc>(
create: (_) => serviceLocator<SettingsBloc>()
..add(
const SettingsLoaded(),
)),
],
child:
BlocBuilder<SettingsBloc, SettingsState>(builder: (context, state) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// Providing a restorationScopeId allows the Navigator built by the
// MaterialApp to restore the navigation stack when a user leaves and
// returns to the app after it has been killed while running in the
// background.
restorationScopeId: 'app',
// Provide the generated AppLocalizations to the MaterialApp. This
// allows descendant Widgets to display the correct translations
// depending on the user's locale.
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en', ''), // English, no country code
],
// Use AppLocalizations to configure the correct application title
// depending on the user's locale.
//
// The appTitle is defined in .arb files found in the localization
// directory.
onGenerateTitle: (BuildContext context) =>
AppLocalizations.of(context)!.appTitle,
// Define a light and dark color theme. Then, read the user's
// preferred ThemeMode (light, dark, or system default) from the
// SettingsController to display the correct theme.
theme:
ThemeData(fontFamily: 'Circular', primaryColor: kPrimaryColor),
darkTheme: ThemeData.dark(),
themeMode: state.themeMode,
// Define a function to handle named routes in order to support
// Flutter web url navigation and deep linking.
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute<void>(
settings: routeSettings,
builder: (BuildContext context) {
switch (routeSettings.name) {
case SettingsView.routeName:
return const SettingsView();
case WeatherView.routeName:
return const WeatherView();
case HomeView.routeName:
return const HomeView();
default:
return const HomeView();
}
},
);
},
);
}));
}
}
Here my home_view.dart code :
import 'package:flutter/material.dart';
import '../../features/weather/presentation/views/weather_view.dart';
class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);
static const routeName = '/home';
#override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView>
with SingleTickerProviderStateMixin {
late AnimationController rotationController;
#override
void initState() {
rotationController =
AnimationController(duration: const Duration(seconds: 1), vsync: this)
..repeat();
super.initState();
}
#override
void dispose() {
rotationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final double height = MediaQuery.of(context).size.height;
final double width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: (height / 2),
child: RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(rotationController),
child: IconButton(
icon: const Icon(Icons.wb_sunny),
color: Colors.yellow,
iconSize: (width * 0.2),
onPressed: () {
Navigator.of(context).pushNamed(WeatherView.routeName);
},
),
),
)
],
),
);
}
}
Here my weather_view.dart code :
import 'dart:async';
import 'package:exomind/src/features/weather/presentation/bloc/weather_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/percent_indicator.dart';
class WeatherView extends StatefulWidget {
const WeatherView({Key? key}) : super(key: key);
static const routeName = '/weather';
#override
State<WeatherView> createState() => _WeatherViewState();
}
class _WeatherViewState extends State<WeatherView>
with SingleTickerProviderStateMixin {
#override
void initState() {
print("initcalled")
super.initState();
}
#override
void dispose() {
rotationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return Scaffold();
}
}
Any help and explanation would be appreciate :)

I can't think of a "clean" way of not executing the initState in _WeatherViewState. Are you trying to avoid the same city added to the WeatherBloc more than once? If so, I'd check for the existence of 'city' in the WeatherBloc before adding.

In your onGenerateRoute you call the WeatherView constructor each time:
case WeatherView.routeName:
return const WeatherView();
This in turn will call initState. What you need to do is create the WeatherView page widget once and use it in the onGenerateRoute:
final _weatherView = const WeatherView();
In your onGenerateRoute:
case WeatherView.routeName:
return _weatherView;

As #RoslanAmir said there is no way to prevent initstate of been called each time we push into a statefulwidget.
So to prevent my event of being added into my bloc each time we push into the stateful widget i add a bool variable to each state to know if the event should be added or not again.
For those who want a precise answer don't hesitate.

Just add a parameter to the Weather page: a boolean that specifies if the rebuild is true or false. (If true, it will call the initState())
This code works fine.
class WeatherView extends StatefulWidget {
final bool rebuild;
static const routeName = '/weather';
WeatherView({
Key? key,
required this.rebuild,
}) : super(key: key);
#override
State<WeatherView> createState() => _WeatherViewState();
}
and the WeatherViewState's initState() will be:
#override
void initState() {
if (widget.rebuild) {
print("initcalled");
super.initState();
} else {
print("Not called");
}
}
So, in your app.dart you should now route to the page by doing
case WeatherView.routeName:
return const WeatherView(rebuild: true); //Choose if rebuild or not by true and false

Related

Flutter - Accessing a Provider from a higher point in the Widget tree

I've been working with Flutter recently, and I saw that there was many ways to deal with state management.
Following the recommendations there, I've been using Provider to deal with the state of my app.
I can update a part of my state from one of the widgets in my UI. To do that, I can call a method of the provider that's above the current widget in the context. No problems with this.
But I want the update of my state to be made from an overlay.
The issue is: When I'm inserting an OverlayEntry with Overlay.of(context)?.insert(), it inserts the overlayEntry to the closest Overlay, which is in general the root of the app, which is above the ChangeProvider. As a result, I get an exception saying I can't find the Provider from the OverlayEntry.
Here is a replication code I've been writting:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => NumberModel(), // All widgets that will be lower in the widget tree will have access to NumberModel
child: NumberDisplayer()
),
);
}
}
// Simple ChangeNotifier. We have a number that we can increment.
class NumberModel extends ChangeNotifier {
int _number = 10;
int get number => _number;
void add_one() {
_number = number + 1;
notifyListeners();
}
}
// This class displays a number, and a button.
class NumberDisplayer extends StatelessWidget {
NumberDisplayer({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var overlayEntry = OverlayEntry(builder: (context) =>
Positioned(
top: 100,
left: 50,
child: FloatingActionButton(onPressed: (){
// Throws "Error: Could not find the correct Provider<NumberModel> above this _OverlayEntryWidget Widget"
Provider.of<NumberModel>(context, listen: false).add_one();
})));
return Consumer<NumberModel>(
builder: (context, numberModel, child) {
return Column(
children: [
Text('Number: ${numberModel.number}'),
FloatingActionButton(onPressed: () {
Overlay.of(context)?.insert(overlayEntry);
})
],
);
},
);
}
}
I would like to find a way to update the information in my provider from the overlay, but I'm not sure how to approach this problem.
Thanks for your help everyone !

Flutter Bloc State Only Updates Once

The problem is that I would like to show a loading indicator when the user tries to fetch some data from an api. But when the user presses the button, loading indicator shows once. But I would like to show the loading indicator every time when the user tries to fetch. It works but as I say It works once. Could anyone have any idea what can cause this problem? Here's the minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeCubit()),
],
child: const MaterialApp(
title: 'Flutter Bloc Demo',
home: HomeView(),
),
);
}
}
class HomeView extends BaseView<HomeCubit, HomeState> {
const HomeView({Key? key}) : super(key: key);
#override
Widget builder(HomeCubit cubit, HomeState state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.count.toString()),
ElevatedButton(
onPressed: cubit.increment,
child: const Text('Increase'),
),
],
),
);
}
}
class HomeState extends BaseState {
final int count;
HomeState({required this.count});
HomeState copyWith({
int? count,
}) {
return HomeState(
count: count ?? this.count,
);
}
}
class HomeCubit extends BaseCubit<HomeState> {
HomeCubit() : super(HomeState(count: 0));
void increment() {
flow(() async {
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
});
}
}
#immutable
abstract class BaseView<C extends StateStreamable<S>, S extends BaseState>
extends StatelessWidget {
const BaseView({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return BaseCubit(context.read<S>());
},
child: Scaffold(
body: BlocBuilder<C, S>(
builder: (context, state) {
final cubit = context.read<C>();
if (state.loadingState == LoadingState.loading) {
return loadingWidget;
}
return builder.call(cubit, state);
},
),
),
);
}
Widget builder(C cubit, S state);
Widget get loadingWidget => const Center(
child: CircularProgressIndicator(),
);
}
enum LoadingState { initial, loading, loaded }
class BaseState {
LoadingState loadingState;
BaseState({
this.loadingState = LoadingState.initial,
});
}
class BaseCubit<S extends BaseState> extends Cubit<S> {
BaseCubit(S state) : super(state);
Future<void> flow(Future<void> Function() function) async {
state.loadingState = LoadingState.loading;
emit(state);
await function();
state.loadingState = LoadingState.loaded;
emit(state);
}
}
Is it overengineering? I don't think you are duplicating much code if you just use BlocBuilder instead of some base class.
If bloc already exist you should provide it by BlocProvider.value instead of BlocProvider(create: read())
You should use context.watch instead of context.read to get a new value every time the state changes. context.read receives state only once.
It's overengineering, please take a look at https://bloclibrary.dev/#/coreconcepts. There are enough tutorials to catch the basic idea.
Then try to use bloc + freezed. Here is an example https://dev.to/ptrbrynt/why-bloc-freezed-is-a-match-made-in-heaven-29ai

Count page transitions in Flutter using iframes [flutter web]

I would like to include another website in my own website.
For that I would like to register a callback to track site-tranitions (i.e. the user clicks on a link on the embedded site and is redirected to a different url / sub-url (?).) I currently use IFrameElement to embed a site, this would in theory allow to register event listeners, but I cannot find any documentation about that.
My main goal is to count the number of page transitions. This is my current code:
import 'package:flutter/material.dart';
import 'package:wikipoker/widgets/my_iframe.dart';
import 'package:wikipoker/widgets/player_tab.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wikipedia Poker',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Game of Wikipedia Poker'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
buildIFrame(constraints.maxHeight, constraints.maxWidth),
],
);
},
),
);
}
String _youtube = 'https://www.youtube.com/embed/RQzhAQlg2JQ';
String _wiki = 'https://de.wikipedia.org/wiki/Hunde';
Widget buildIFrame(double height, double width) {
return Column(
children: [
IFrameWidget(
_wiki,
height,
width * (4 / 5),
),
],
);
}
}
import 'dart:html';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
class IFrameWidget extends StatefulWidget {
final String _url;
double _height = 500;
double _width = 500;
IFrameWidget(this._url, this._height, this._width);
#override
State<StatefulWidget> createState() => _IFrameWidgetState();
}
class _IFrameWidgetState extends State<IFrameWidget> {
Widget _iframeWidget;
#override
void initState() {
super.initState();
final IFrameElement _iframeElement = IFrameElement();
// _iframeElement.height = '500';
// _iframeElement.width = '500';
// FIXME This does not load.
// _iframeElement.addEventListener('onLoad', (event) {
// setState(() {
// _iframeWidget = Text("Lol");
// });
// });
_iframeElement.src = widget._url;
_iframeElement.style.border = 'none';
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'iframeElement',
(int viewId) => _iframeElement,
);
_iframeWidget = HtmlElementView(
key: UniqueKey(),
viewType: 'iframeElement',
);
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: widget._height,
width: widget._width,
child: _iframeWidget,
);
}
}
The IFrameElement has some fields and methods, which look like they could be useful.
addEventListener expects a type of event, but there is no overview about what that might be.
The documentation is very incomplete for this and I have no idea which event I would like to register.
My hope is, that I can use events from the native html iframe for that.
Documentation for IFrames: https://api.flutter.dev/flutter/dart-html/IFrameElement-class.html
Old question, but I hope the answer will help someone looking for a solution:
here is described very well
Note: need to restart the IDE (at least mine refused to work without restart)

Flutter provider not updating state

I'm still relatively new to flutter and even newer to Provider so I may be entirely off with this but from what I've read it looks correct.
General idea is there's a header widget with a button that will either open an endrawer or bring the user to a login page depending on the state of the app.
Login works and the states all are working correctly but only on the login widget. When the user is routed back to the main screen - the state is still in its default state even though the state gets set on a successful login.
The widget tree is like so:
Main
|_ HomeScreen
| |_ AppHeader
|_ Login
main.dart
Widget build(BuildContext context) {
return MultiProvider (
providers: [
ChangeNotifierProvider (create: (_) => LoginState(),)
],
child: MaterialApp(
title: kAppTitle,
theme: alcDefaultLightTheme(),
home: HomeScreen(title: "kAppTitle"),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
const AlcLocalizationsDelegate(),
],
supportedLocales: [
const Locale(kEn),
const Locale(kFr),
],
initialRoute: HomeScreen.id,
routes: {
LoadingScreen.id: (context) => LoadingScreen(),
HomeScreen.id: (context) => HomeScreen(title: kAppTitle),
}),
);
}
home_screen.dart
class HomeScreen extends StatefulWidget {
static const String id = 'home_screen';
HomeScreen({Key key, this.title}) : super(key: key);
final String title;
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
AccountDrawerOpen() {
_scaffoldKey.currentState.openEndDrawer();
FirebaseAnalytics().logEvent(
name: 'account_drawer_open',
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
...display code here
body: AppHeader()
);}
}
And this is where I need to access the state to determine if the player is logged in or not
app_header.dart
import 'package:provider/provider.dart';
class AppHeader extends StatelessWidget {
#override
Widget build(BuildContext context) {
LoginState testLoginState = Provider.of<LoginState>(context);
return Column(
children: <Widget>[
FlatButton(
child: Text('Check state'),
onPressed: () {
print("APP HEADER | STATE IS NOW ${testLoginState.status}");
},
)
],
);
}
}
Lastly, here's my LoginState.dart
enum Status {
Authenticated,
Authenticating,
Unauthenticated,
InvalidLogin
}
class LoginState with ChangeNotifier {
Status _status = Status.Unauthenticated;
Status get status => _status;
Future signIn(String email, String password) async {
try {
_status = Status.Authenticating;
notifyListeners();
... goes to the DB, some logic happens and returns true
_status = Status.Authenticated;
notifyListeners();
print("FROM LOGIN STATE: $_status");
} catch (e) {
print('Oops');
_status = Status.InvalidLogin;
notifyListeners();
}
}
Any help is appreciated, thanks for your help.
Figured it out. In my Login widget - I had a ChangeNotifierProvider which changes the context. So in this case - this changed the context to the lowest possible widget - the login widget.

Passing data from one screen to other screen in Flutter is null

I have two screen, from one i want to pass a title string to another screen. This title can be sign in or sign up, pre decided in first screen. What I have tried:
Container(
child: RaisedGradientButton(
onPressed: () {
print('Login clicked');
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
MyApp(
formMode: FormMode.LOGIN,
screenTitle: "Login",
)
),
);
},
textButton: "Login",
height: 55,
width : 200.0,
buttonTitleColor: Colors.white,
buttonBackgroundColor: Colors.red,
)
),
Below is second screen with initialization steps:
enum FormMode { LOGIN, SIGNUP }
void main() {
runApp(
MaterialApp(
home: StatelessLanding(),
),
);
}
class MyApp extends StatelessWidget{
// In the constructor, require a Todo
final FormMode formMode;
final String screenTitle;
MyApp({Key key, #required this.formMode, #required this.screenTitle})
: super(key: key);
#override
Widget build(BuildContext context){
return MyAppStateFul();
}
}
class _MyAppStateFulState extends State<MyAppStateFul> {
FormMode formMode;
String screenTitle;
_MyAppStateFulState(FormMode formMode, String screenTitle) {
this.formMode = formMode;
this.screenTitle = screenTitle;
}
}
This is the place where I am using screen title:
#override
Widget build(BuildContext context) {
var screenTitle = "Login";
print('screen title is $widget.screenTitle');
print('screen title is $this.screenTitle');
print('screen title is $screenTitle');
}
Can experts please help me.
Thanks
The code is a bit hard to follow however it looks like you forgot to pass the screenTitle from the MyApp class to the MyAppStateful widget.
In the code you listed above you have the following stateless widget:
class MyApp extends StatelessWidget{
// In the constructor, require a Todo
final FormMode formMode;
final String screenTitle;
MyApp({Key key, #required this.formMode, #required this.screenTitle}) : super(key: key);
#override
Widget build(BuildContext context) {
return MyAppStateFul();
}
}
To me it seems you'll have to pass the screenTitle to the MyAppStateFul constructor in order to make it available in your stateful widget, like so:
#override
Widget build(BuildContext context) {
return MyAppStateFul(screenTitle);
}
Of course this also requires you to change the MyAppStateFulconstructor to accept the screenTitle parameter if it doesn't already.
I think this should fix it for you.