Where do I run initialisation code when starting a flutter app? - flutter

Where do I run initialisation code when starting a flutter app?
void main() {
return runApp(MaterialApp(
title: "My Flutter App",
theme: new ThemeData(
primaryColor: globals.AFI_COLOUR_PINK,
backgroundColor: Colors.white),
home: RouteSplash(),
));
}
If I want to run some initialisation code to, say fetch shared preferences, or (in my case) initialise a package (and I need to pass in the the BuildContext of the MaterialApp widget), what is the correct way to do this?
Should I wrap the MaterialApp in a FutureBuilder? Or is there a more 'correct' way?
------- EDIT ---------------------------------------------------
I have now placed the initialisation code in RouteSplash() widget. But since I required the BuildContext of the app root for the initialisation, I called the initialisation in the Widget build override and passed in context.ancestorInheritedElementForWidgetOfExactType(MaterialApp). As I don't need to wait for initialisation to complete before showing the splash screen, I haven't used a Future

One simple way of doing this will be calling the RouteSplash as your splash screen and inside it perform the initialization code as shown.
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
bool shouldProceed = false;
_fetchPrefs() async {
await Future.delayed(Duration(seconds: 1));// dummy code showing the wait period while getting the preferences
setState(() {
shouldProceed = true;//got the prefs; set to some value if needed
});
}
#override
void initState() {
super.initState();
_fetchPrefs();//running initialisation code; getting prefs etc.
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: shouldProceed
? RaisedButton(
onPressed: () {
//move to next screen and pass the prefs if you want
},
child: Text("Continue"),
)
: CircularProgressIndicator(),//show splash screen here instead of progress indicator
),
);
}
}
and inside the main()
void main() {
runApp(MaterialApp(
home: RouteSplash(),
));
}
Note: It is just one way of doing it. You could use a FutureBuilder if you want.

To run code at startup, put it in your main.dart. Personally, that's the way I do it, to initialise lists etc.

Related

Flutter: Dipose HTTP request on close controller

Original Answer
I'm using the Getx State Management on Flutter.
Simplifying as much as possible:
I build a GetxController to control my Page, and in this controller i have a StatefulWidget instance that evoque http requests.
class MyController extends GetxController {
Player player;
}
class Player extends StatefulWidget {
PlayerState state;
#override
PlayerState createState() {
state = PlayerState();
return state;
}
}
class PlayerState extends State<Player> {
void methodName async() {
futureRequest().then((data) {
// when the error ocurrs
setState(() {});
});
}
}
The problem occurs when the user closes the mobile page, triggering the controller's close method, before the end of the request.
That way, when setState is triggered, there is no more page instance and the error occurs.
I believe that the solution would be to interrupt all requests related to this GetxController and "delete" this instance of StatefulWidget at the moment the controller close method was called.
I don't know if this would be right, and if it's how to do it ..
==================================================================
Updated Answer
The main problem was that the async request in getDetails() method, return a response even after the controller is disposed, even using GetBuilder, and this response carried a url from a video that is started by the videoPlayerController (a video_player plugin instance).
So, the user is in another screen but keep listen to the video that is playing on background.
As a workaround and thinking in apply good practices to the code, i make a refactor to use only stateless widgets, following the GetX rules. I solved the problem, but i had to convert the Future's to Stream's
The binding is being created with Get.lazyPut() to perform dependencies injection:
class Binding implements Bindings {
Get.lazyPut<PlayerController>(() {
return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
});
}
This binding is linked to the page router, based on GetX documentation.
class AppPages {
static final routes = [
GetPage(name: Routes.MyRoute, page: () => MyPage(), binding: MyBinding()),
];
}
To prevent the controller to make actions even before it is disposed, i have to created a Stream and cancel it on controller dispose.
class MyController extends GetxController {
MyController({#required this.repository}) : assert(repository != null);
StreamSubscription<bool> stream;
// Instance of plugin video_player
VideoPlayerController videoPlayerController;
#override
void onClose() {
if (streamGetVideo != null) streamGetVideo.cancel();
super.onClose();
if (videoPlayerController != null) videoPlayerController?.dispose();
}
// This is the method called by the user on screen
void loadVideo() {
stream = getDetails().asStream().listen((bool response) {
// This code is canceled on onClose() method by the stream
if (response) update();
});
}
Future<bool> getDetails() async {
return await repository.getDetails().then((data) async {
videoPlayerController = VideoPlayerController.network(data);
initFuture = videoPlayerController.initialize();
await initFuture.whenComplete(() { return true; });
});
}
}
I think that Flutter/GetX should have a better way to do this, without these workarounds that i made. If anyone has a better approach or a hint, i'm open to suggestions.
One solution could be to wrap your setState with
if(mounted){
setState(() {});
}
GetBuilder + update()
In GetX using a GetBuilder with update() takes care of that lifecycle checking / handling so you don't have to do it.
Below is an example of a screen/route being closed prior to an HTTP call finishing & calling setState(), without an exception thrown.
(On the 2nd screen, click the Go Back! button fast to simulate an already disposed StatefulWidget.)
Below, an update() call is used to update the screen, instead of setState(), but they are the same in a GetBuilder. GetBuilder is (extends) a StatefulWidget.
GetBuilder adds listeners to the Controller you pass it, either through init: constructor arg or via the GetBuilder<Type> parameter if the Controller was initialized elsewhere/earlier.
That listener will be disposed if the StatefulWidget (i.e. GetBuilder) is disposed.
(See GetBuilder's dispose() function for some wizardry. While adding a listener, the returned value from adding that listener, is a function to dispose/unsubscribe from that listen. Pretty clever.)
So the GetBuilder/StatefulWidget will never have its update() / setState() called if that widget has been disposed because the listener for those calls has been disposed. So a slow returning HTTP call won't attempt to update/setState a widget that no longer exists in the widget tree.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HttpX extends GetxController {
String slowValue = 'loading...';
#override
void onInit() {
slowCall();
}
/// Simulate a slow, long running HTTP call
Future<void> slowCall() async {
slowValue = 'Slow call STARTED!';
print(slowValue);
update(); // update the screen to show started message
await Future.delayed(Duration(seconds: 5), () {
slowValue = 'Slow call FINISHED!';
print(slowValue);
update(); // won't call setState() if GetBuilder is disposed
});
}
}
class GetXDisposePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('awaiting http call to finish'),
RaisedButton(
child: Text('Go Call Page'),
onPressed: () => Get.to(SlowCallPage()),
// using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
)
],
),
),
);
}
}
class SlowCallPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose - Go Back!'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<HttpX>(
init: HttpX(), // fake slow http call starts on init
builder: (hx) => Text(hx.slowValue),
),
RaisedButton(
child: Text('Go Back!'),
onPressed: () => Get.back(),
),
],
),
),
);
}
}

How to reload the page whenever the page is on screen - flutter

Is there any callbacks available in flutter for every time the page is visible on screen? in ios there are some delegate methods like viewWillAppear, viewDidAppear, viewDidload.
I would like to call a API call whenever the particular page is on-screen.
Note: I am not asking the app states like foreground, backround, pause, resume.
Thank You!
Specifically to your question:
Use initState but note that you cannot use async call in initState because it calls before initializing the widget as the name means. If you want to do something after UI is created didChangeDependencies is great. But never use build() without using FutureBuilder or StreamBuilder
Simple example to demostrate:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(home: ExampleScreen()));
}
class ExampleScreen extends StatefulWidget {
ExampleScreen({Key key}) : super(key: key);
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
List data = [];
bool isLoading = true;
void fetchData() async {
final res = await http.get("https://jsonplaceholder.typicode.com/users");
data = json.decode(res.body);
setState(() => isLoading = false);
}
// this method invokes only when new route push to navigator
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: isLoading
? CircularProgressIndicator()
: Text(data?.toString() ?? ""),
),
);
}
}
Some lifecycle method of StatefulWidget's State class:
initState():
Describes the part of the user interface represented by this widget.
The framework calls this method in a number of different situations:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
The framework replaces the subtree below this widget with the widget
returned by this method, either by updating the existing subtree or by
removing the subtree and inflating a new subtree, depending on whether
the widget returned by this method can update the root of the existing
subtree, as determined by calling Widget.canUpdate.
Read more
didChangeDependencies():
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an
InheritedWidget that later changed, the framework would call this
method to notify this object about the change.
This method is also called immediately after initState. It is safe to
call BuildContext.dependOnInheritedWidgetOfExactType from this method.
Read more
build() (Stateless Widget)
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the
tree in a given BuildContext and when the dependencies of this widget
change (e.g., an InheritedWidget referenced by this widget changes).
Read more
didUpdateWidget(Widget oldWidget):
Called whenever the widget configuration changes.
If the parent widget rebuilds and request that this location in the
tree update to display a new widget with the same runtimeType and
Widget.key, the framework will update the widget property of this
State object to refer to the new widget and then call this method with
the previous widget as an argument.
Read more
Some widgets are stateless and some are stateful. If it's a stateless widget, then only values can change but UI changes won't render.
Same way for the stateful widget, it will change for both as value as well as UI.
Now, will look into methods.
initState(): This is the first method called when the widget is created but after constructor call.
#override
void initState() {
// TODO: implement initState
super.initState();
}
didChangeDependecies() - Called when a dependency of this State object changes.Gets called immediately after initState method.
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
didUpdateWidget() - It gets called whenever widget configurations gets changed. Framework always calls build after didUpdateWidget
#override
void didUpdateWidget (
covariant Scaffold oldWidget
)
setState() - Whenever internal state of State object wants to change, need to call it inside setState method.
setState(() {});
dispose() - Called when this object is removed from the tree permanently.
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
You don't need StatefulWidget for calling the api everytime the screen is shown.
In the following example code, press the floating action button to navigate to api calling screen, go back using back arrow, press the floating action button again to navigate to api page.
Everytime you visit this page api will be called automatically.
import 'dart:async';
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ApiCaller())),
),
);
}
}
class ApiCaller extends StatelessWidget {
static int counter = 0;
Future<String> apiCallLogic() async {
print("Api Called ${++counter} time(s)");
await Future.delayed(Duration(seconds: 2));
return Future.value("Hello World");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Api Call Count: $counter'),
),
body: FutureBuilder(
future: apiCallLogic(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator();
if (snapshot.hasData)
return Text('${snapshot.data}');
else
return const Text('Some error happened');
},
),
);
}
}
This is the simple code with zero boiler-plate.
The simplest way is to use need_resume
1.Add this to your package's pubspec.yaml file:
dependencies:
need_resume: ^1.0.4
2.create your state class for the stateful widget using type ResumableState instead of State
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends ResumableState<HomeScreen> {
#override
void onReady() {
// Implement your code inside here
print('HomeScreen is ready!');
}
#override
void onResume() {
// Implement your code inside here
print('HomeScreen is resumed!');
}
#override
void onPause() {
// Implement your code inside here
print('HomeScreen is paused!');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Go to Another Screen'),
onPressed: () {
print("hi");
},
),
),
);
}
}
If you want to make an API call, then you must be (or really should be) using a StatefulWidget.
Walk through it, let's say your stateful widget receives some id that it needs to make an API call.
Every time your widget receives a new id (including the first time) then you need to make a new API call with that id.
So use didUpdateWidget to check to see if the id changed and, if it did (like it does when the widget appears because the old id will be null) then make a new API call (set the appropriate loading and error states, too!)
class MyWidget extends StatefulWidget {
Suggestions({Key key, this.someId}) : super(key: key);
String someId
#override
State<StatefulWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
dynamic data;
Error err;
bool loading;
#override
Widget build(BuildContext context) {
if(loading) return Loader();
if(err) return SomeErrorMessage(err);
return SomeOtherStateLessWidget(data);
}
#override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// id changed in the widget, I need to make a new API call
if(oldWidget.id != widget.id) update();
}
update() async {
// set loading and reset error
setState(() => {
loading = true,
err = null
});
try {
// make the call
someData = await apiCall(widget.id);
// set the state
setState(() => data = someData)
} catch(e) {
// oops an error happened
setState(() => err = e)
}
// now we're not loading anymore
setState(() => loading = false);
}
}
I'm brand new to Flutter (literally, just started playing with it this weekend), but it essentially duplicates React paradigms, if that helps you at all.
Personal preference, I vastly prefer this method rather than use FutureBuilder (right now, like I said, I'm brand new). The logic is just easier to reason about (for me).

How to check first time app launch in Flutter

I am a beginner in a flutter, I have created my application but I want to check if the user opens the application for the first time after installing, I have seen this article but did not know how that?
This is the splash screen code, the code move the user directly to the Main screen after 3 sec, But I want to check if user first time opens the app and move the user to Welcome screen or if user not the first time and move the user to the Main screen.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:book_pen/main.dart';
import 'package:book_pen/Welcome.dart';
void main() {
runApp(new MaterialApp(
home: new SplashScreen(),
routes: <String, WidgetBuilder>{
'/HomePage': (BuildContext context) => new HomePage(),
'/WelcomePage': (BuildContext context) => new WelcomePage()
},
));
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => new _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
startTime() async {
var _duration = new Duration(seconds: 3);
return new Timer(_duration, navigationPageHome);
}
void navigationPageHome() {
Navigator.of(context).pushReplacementNamed('/HomePage');
}
void navigationPageWel() {
Navigator.of(context).pushReplacementNamed('/WelcomePage');
}
#override
void initState() {
super.initState();
startTime();
}
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: <Widget>[
Center(
child: new Image.asset(
'assets/images/SplashBack.jpg',
width: size.width,
height: size.height,
fit: BoxFit.fill,
),
),
Center(
child: new Image.asset(
'assets/images/BigPurppleSecSh.png',
height: 150,
width: 300,
)),
],
),
);
}
}
#Abdullrahman, please use shared_preferences as suggested by others. Here is how you can do that,
Depend on shared_preferences package in pubspec.yaml and run Packages get:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^0.5.4+6
Import the package:
import 'package:shared_preferences/shared_preferences.dart';
Implement it:
class _SplashScreenState extends State<SplashScreen> {
startTime() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool firstTime = prefs.getBool('first_time');
var _duration = new Duration(seconds: 3);
if (firstTime != null && !firstTime) {// Not first time
return new Timer(_duration, navigationPageHome);
} else {// First time
prefs.setBool('first_time', false);
return new Timer(_duration, navigationPageWel);
}
}
void navigationPageHome() {
Navigator.of(context).pushReplacementNamed('/HomePage');
}
void navigationPageWel() {
Navigator.of(context).pushReplacementNamed('/WelcomePage');
}
........
Note: SharedPreferences data will be removed if user clears the cache. SharePreferences is a local option. If you want to prevent that, you can use firestore to save bool value but firestore would probably be an overkill for a simple task like this.
Hope this helps.
You can use https://pub.dev/packages/shared_preferences add a value the first time a user enters
It is even simpler with is_first_run package. You simply do:
bool firstRun = await IsFirstRun.isFirstRun();
It returns true if the app is launched for the first time.
You may set up a boolean during first time app is launched or installed. Once the app is launched or installed first time, set it to true. The default value should be false.
After setting it to true, you must save this in the shared_preference in local storage.
After that each time on you relaunch the app, read the shared_preference value. The value should be always true unless you change it.
watch the video here

Build function in StatelessWidget keeps refiring

Consider the following StatelessWidget:
class SwitchScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final testService = Provider.of<TestService>(context); // Line 1
Future( () {
Navigator.of(context).push( // Segment 2
MaterialPageRoute(builder: (context) => TestScreen()) // Segment 2
); // Segment 2
});
return Scaffold(body: Center( child: Text("lol") ) );
}
}
The widget is directly below the root in the widget tree and wrapped by a ChangeNotifierProvider:
void main() => runApp(new Main());
class Main extends StatefulWidget {
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SampleProgram',
home: ChangeNotifierProvider<TestService>(
builder: (_) { return TestService(); } ,
child: SwitchScreen(),
),
);
}
}
The service associated with the provider, TestService, is currently empty. TestScreen is simply another StatelessWidget which includes AppBar wrapped inside a Scaffold.
I would expect the program to finish rendering the SwitchScreen, navigate to TestScreen to fulfill the future, and finally render the AppBar inside TestScreen. However, every time it enters the TestScreen view, something appears to trigger a rebuild of SwitchScreen. The app then bounces back to SwitchScreen, moves to TestScreen to fulfill the future, and repeats this process. By using debug Print statements I'm sure that the build method of SwitchScreen is called immediately after TestScreen finishes rendering.
The interesting thing is that if I comment out Line 1, the build method won't be re-triggered. Similarly if I replace the entirety of Segment 2 with anything else, say a print statement, the build method won't keep firing either. I suspected that Navigator is resulting in some value change in TestService, forcing SwitchScreen to rebuild, so I overrode the notifyListeners method in TestService since this method is the only way SwitchScreen can be affected by TestService.
class TestService with ChangeNotifier {
#override
void notifyListeners() {
print("Triggering SwitchScreen's build method");
}
}
But no string is printed out. Right now I'm very curious about what's causing the rebuilding and what roles do the Provider and the Navigator play in this. Any help would be really appreciated.
Instead of calling
final testService = Provider.of<TestService>(context);
Use
final testService = Provider.of<TestService>(context, listen: false);
Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called.

How to mock navigation arguments for testing flutter screen widgets?

I would like to write a mockito test for a screen widget in flutter. The problem is, that this widget uses a variable from the navigation argument and I'm not sure how to mock this variable.
This is the example screen:
class TestScreen extends StatefulWidget {
static final routeName = Strings.contact;
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
Contact _contact;
#override
Widget build(BuildContext context) {
_contact = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(title: Text(Strings.contact)),
body: Text(_contact.name),
);
}
}
With this command I open the screen
Navigator.pushNamed(context, TestScreen.routeName, arguments: contact);
Normally I would mock some components, but I'm not sure how to mock the screen arguments. I hope it works something like this. However, I do not know what I can exactly mock.
when(screenArgument.fetchData(any))
.thenAnswer((_) async => expectedContact);
This is the current test, which of course is not working since _contact is null:
void main() {
testWidgets('contact fields should be filled with data from argument', (WidgetTester tester) async {
// GIVEN
final testScreen = TestApp(widget: TestScreen());
// WHEN
await tester.pumpWidget(testScreen);
// THEN
expect(find.text("test"), findsOneWidget);
});
}
An ugly way would be to use constructor parameters for the screen only for testing, but I want to avoid that.
Maybe someone of you knows how to best test such screen widgets.
The way that I've found is the same approach how flutter guys are testing it:
https://github.com/flutter/flutter/blob/d03aecab58f5f8b57a8cae4cf2fecba931f60673/packages/flutter/test/widgets/navigator_test.dart#L715
Basically they create a MaterialApp, put a button that after pressing will navigate to the tested page.
My modified solution:
Future<void> pumpArgumentWidget(
WidgetTester tester, {
#required Object args,
#required Widget child,
}) async {
final key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
home: FlatButton(
onPressed: () => key.currentState.push(
MaterialPageRoute<void>(
settings: RouteSettings(arguments: args),
builder: (_) => child,
),
),
child: const SizedBox(),
),
),
);
await tester.tap(find.byType(FlatButton));
await tester.pumpAndSettle(); // Might need to be removed when testing infinite animations
}
This approach works ok-ish, had some issues with testing progress indicators as it was not able to find those even when debugDumpApp displayed them.
If you are using a Dependency Injector such as I am, you may need to avoid pass contextual arguments to the constructor if your view is not built at the time the view class is instantiated. Otherwise, just use the view constructor as someone suggested.
So if you can't use constructor as I can't, you can solve this using Navigator directly in your tests. Navigator is a widget, so just use it to return your screen. Btw, it has no problem with Progress Indicator as pointed above.
import 'package:commons/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MyCustomArgumentsMock extends Mock implements MyCustomArguments {}
void main() {
testWidgets('indicator is shown when screen is opened', (tester) async {
final MyCustomArguments mock = MyCustomArgumentsMock();
await tester.pumpWidget(MaterialApp(
home: Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute<Widget>(
builder: (_) => TestScreen(),
settings: RouteSettings(arguments: mock),
);
},
),
));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}