I upgraded to flutter 2.10 for a new projet, I setup my project like i usually do (injectable, getIt... ect).
what I'm facing right now, is whenever I do a hot reload (ctrl+s), the whole application rebuilds but it displays normally for a fraction of a second then it displays a blank screen.
like so:
also when looking at the inspector after the unexpected blank screen I only find:
knowing that normally it displays the widget's tree (login screen...).
the main function:
void main() async {
setupLogging();
setPathUrlStrategy();
final LocalizationDelegate delegate = await createDelegate();
BlocOverrides.runZoned(
() => runApp(LocalizedApp(delegate, Application())),
blocObserver: ApplicationObserver(),
);
}
the Application Class:
class Application extends StatelessWidget {
Application({Key? key}) : super(key: key);
final initializer = initialize();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initializer,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Container(
child: const Center(
child: CircularProgressIndicator(),
),
);
case ConnectionState.done:
log("**************************** or here");
final delegate = getDelegate(context);
final AppRouter router = AppRouter(authGuard: getIt<AuthGuard>());
return LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MaterialApp.router(
routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate(),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
delegate,
],
supportedLocales: delegate.supportedLocales,
locale: delegate.currentLocale,
theme: ApplicationTheme.lightTheme,
// darkTheme: ApplicationTheme.darkTheme , todo
),
);
}
});
}
static Future initialize() async {
await configureDependencies();
}
}
the logs after hot reload display:
[log] **************************** or here // at the application state done
[log] ****************************** inside the login? // at the login screen build
[log] **************************** or here // back at the application state done, but no
login screen after this...
What may be the cause of thise issue? any hints please?
The issue is not related to flutter 2.10, it is because of where i'm creating the application router.
since i'm creating the application router inside the FutureBuilder, each time i hot reload, a new router gets created and weird things happen.
I fixed it by delegating the creation of my router to getIt as a singleton, and getit after the FutureBuilder is done, since it's a singleton, it won't be recreated each time.
final AppRouter router = getIt<AppRouter>();
Related
Problem:
I am having trouble setting up navigation using GetX and AutoRoute.
Code Setup:
According to the GetX documentation, if you want to use GetX navigation you have to replace MaterialApp() with GetMaterialApp(). You also set the routes.
void main() {
runApp(
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyHomePage()),
GetPage(name: '/second', page: () => Second()),
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.zoom
),
],
)
);
}
The AutoRoute example uses MaterialApp.router() to set up the routerDelegate and routeInformationParser.
final _appRouter = AppRouter()
...
Widget build(BuildContext context){
return MaterialApp.router(
routerDelegate: _appRouter.delegate(...initialConfig),
routeInformationParser: _appRouter.defaultRouteParser(),
),
}
Here is how I set up the navigation according to Getx and AutoRoute:
void main() {
configureDependencies();
runApp(Portfolio());
}
class Portfolio extends StatelessWidget {
final _appRouter = AppRouter.Router();
#override
Widget build(BuildContext context) {
return GetMaterialApp.router(
routerDelegate: _appRouter.delegate(),
routeInformationParser: _appRouter.defaultRouteParser(),
builder: (context, extendedNav) => Theme(
data: ComplexReduxTheme.complexReduxLightTheme,
child: extendedNav ?? Container(color: Colors.red),
),
);
}
}
I am using GetMaterialApp.router which returns a GetMaterialApp. Despite this, I get the error "You are trying to use contextless navigation without a GetMaterialApp or Get.key.". I have tried setting up the navigator key and setting Get.testMode = true but nothing happens(no error) when I try to navigate to another screen.
Desired Result:
I should be able to navigate to the desired screen via Get.toNamed().
Current Result:
I get the following error from GetX when trying to navigate to another screen using Get.toNamed() : "You are trying to use contextless navigation without
a GetMaterialApp or Get.key.
If you are testing your app, you can use:
[Get.testMode = true], or if you are running your app on
a physical device or emulator, you must exchange your [MaterialApp]
for a [GetMaterialApp]."
AutoRoute Version: 2.2.0
Get Version: 4.1.4
You don't need external routing plugin, GetX already did that for you, and if you want to navigate, just use Get.toNamed("/some-page") and it will show you the page you wanted. Same goes to nested route.
For Example
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.zoom,
children: [
GetPage(
name: '/child-of-third',
page: () => ChildOfThird(),
),
],
),
// You access it like this
Get.toNamed("/third");
// And this one, for the nested page
Get.toNamed("/third/child-of-third");
The reason you got the error is when you use external routing plugin in GetX, it will generate their own code, with their own context in their own ecosystem. GetX doesn't know which context does the plugin use since it was outside of its lifecycle.
I was facing the same issue when combining both getx and auto router in my case i needed nested navigation as well I created a work around like this
I created initial bindings and passed appRouter to it and saved it in getx routing controller that i was using to a method like Get.toNamed because with initial appRouter you don't need context you can navigate like this
// main app widget
class _myAppState extends State<MyApp> {
final _appRouter = AppRouter();
#override
Widget build(BuildContext context) {
return GetMaterialApp.router(
routerDelegate: _appRouter.delegate(),
routeInformationParser: _appRouter.defaultRouteParser(),
initialBinding: InitialBinding(router: _appRouter,),
);
}
}
// initial binding to store to store app router
class InitialBinding extends Bindings {
AppRouter router;
InitialBinding({required this.router,});
#override
void dependencies() {
Get.put(NavRoutesController(router: router,),permanent: true);
}
}
// router controller
class NavRoutesController extends GetxController {
AppRouter router;
NavRoutesController({required this.router,});
void toNamed(String route){
router.pushNamed(route);
}
}
//to navigate use
final router = Get.find<RouterController>();
router.toNamed("/some")
//or
Get.find<RouterController>().toNamed("/some")
// you can get base context as well from AppRouter like this
Get.find<RouterController>().router.navigatorKey.currentState.context
I added two material app to my app because my futurebuilder needed a context and my provider was not accessible to the other classes i created. is it an acceptable practice???
runApp(
MaterialApp(
title: 'register app',
home: FutureBuilder(
future: Hive.openBox('store'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError)
return Text(snapshot.error.toString());
else
return MultiProvider(providers: [
ChangeNotifierProvider.value(
value: form_entry(),
)
], child: MyApp());
} else
return Scaffold(
body: Center(
child: Text('error error ooops error'),
));
},
)),
);[enter image description here][1]
// my app class has another material app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: home_screen(),
);
}
}
The purpose of a MaterialApp widget is to provide a common theme setting based on Material design and configures the root navigator for all of its children widgets.
In order to avoid conflicting, you should only have 1 MaterialApp. In your case, you can call the openBox() method without using the FutureBuilder by calling it within the main() method:
void main() async {
// Include this line to make sure WidgetsFlutterBinding is initialized, since
// we're using main() asynchronously
WidgetsFlutterBinding.ensureInitialized();
// Open the Hive box here
var box = await Hive.openBox('store');
// Then run the app
runApp(
MaterialApp(
title: 'register app',
home: MultiProvider(providers: [
ChangeNotifierProvider.value(
value: form_entry(),
)
], child: home_screen());
)
);
Small note: When creating new class or method in Dart, best practice is to use CamelCase. So form_entry() should be named FormEntry() for Class name or formEntry() for Method name. Same goes with home_screen(). You can refer to the styling guide here
It's bad to have two MaterialApp() widgets, at least one in other.
I did that by mistake, I thought I do not have one and added an extra one. Then the app randomly crashed on hot restart, sit one whole day to debug everything and haven't found what crashed my app, then I started to refactor code and found I have two MaterialAapp, one in StatelessWidget and one in home widget that was on different file. After removing it my app stopped randomly crashing.
Never use two, atleast not one in other.
I have got a problem with the provider package.
I want to be able to clean an attribute (_user = null) of a provider ChangeNotifier class (it is a logout feature).
The problem is when I am doing that from a Widget that use info from this Provider.
My main app is like :
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
builder: (context, _) => App(),
),
);
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (_, auth, __) {
Widget displayedWidget;
switch (auth.loginState) {
case ApplicationLoginState.initializing:
displayedWidget = LoadingAppScreen();
break;
case ApplicationLoginState.loggedIn:
displayedWidget = HomeScreen();
break;
case ApplicationLoginState.loggedOut:
default:
displayedWidget = AuthenticationScreen(
signInWithEmailAndPassword: auth.signInWithEmailAndPassword,
registerAccount: auth.registerAccount,
);
}
return MaterialApp(
title: 'My App',
home: displayedWidget,
routes: {
ProfileScreen.routeName: (_) => ProfileScreen(),
},
);
});
}
}
My Provider class (simplified) :
class AuthProvider extends ChangeNotifier {
ApplicationLoginState _loginState;
ApplicationLoginState get loginState => _loginState;
bool get loggedIn => _loginState == ApplicationLoginState.loggedIn;
User _user;
User get user => _user;
void signOut() async {
// Cleaning the user which lead to the error later
_user = null;
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
My Profile screen which is accessible via named Route
class ProfileScreen extends StatelessWidget {
static const routeName = '/profile';
#override
Widget build(BuildContext context) {
final User user = Provider.of<AuthProvider>(context).user;
return Scaffold(
// drawer: AppDrawer(),
appBar: AppBar(
title: Text('Profile'),
),
body: Column(
children: [
Text(user.displayName),
FlatButton(
child: Text('logout'),
onPressed: () {
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(builder: (BuildContext context) => App()),
// ModalRoute.withName('/'),
// );
Provider.of<AuthProvider>(context, listen: false).signOut();
},
)
],
),
);
}
}
When I click the logout button from the profile screen, I don't understand why i get the error :
As I am using a Consumer<AuthProvider> at the top level of my app (this one includes my route (ProfileScreen), I thought it would redirect to the AuthenticationScreen due to the displayedWidget computed from the switch.
But it seems to rebuild the ProfileScreen first leading to the error. the change of displayedWidget do not seems to have any effect.
I'm pretty new to Provider. I don't understand what I am missing in the Provider pattern here ? Is my App / Consumer wrongly used ?
I hope you can help me understand what I've done wrong here ! Thank you.
Note : the commented Navigator.pushAndRemoveUntil redirect correctly to the login screen but I can see the error screen within a few milliseconds.
Your user is null, and you tried to get the name of him. You need to check it before using it. It will look like this:
user == null ?
Text("User Not Found!"),
Text(user.displayName),
From the provider API reference of Provider.of :
Obtains the nearest Provider up its widget tree and returns its
value.
If listen is true, later value changes will trigger a new State.build
to widgets, and State.didChangeDependencies for StatefulWidget.
So I think the line final User user = Provider.of<AuthProvider>(context).user; in your profile screen calls a rebuild when the _user variable is modified, and then the _user can be null in your ProfileScreen.
Have you tried to Navigator.pop the profile screen before clearing the _user variable?
Context:
Building my first Flutter app
It needs to be localized and I am not using intl package, but using the guidelines for LocalizationsDelegates (code below)
I want to load the localizations remotely from a server.
I want when my app starts to show a circular progress indicator right away, which stays until the copy has been loaded remotely fully and then the loading indicator to be dismissed.
I use provider for state management.
... and I struggle to do that! :)
Here is my main and starting point stripped out of unnecessary lines to the Q:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => 'MyTitle',
localizationsDelegates: [
const CopyDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('bg', ''),
],
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => Init(),
},
);
}
}
Here is the CopyDelegates class:
class CopyDelegate extends LocalizationsDelegate<Copy> {
const CopyDelegate();
//TODO change the source of supported languages
#override
bool isSupported(Locale locale) => ['en', 'bg'].contains(locale.languageCode);
#override
Future<Copy> load(Locale locale) async{
Copy copy = Copy(locale);
copy.load();
return copy;
}
#override
bool shouldReload(CopyDelegate old) => false;
}
Copy is a simple class that doesn't inherit and in the copy.load() method I have my logic that fetches the localizations remotely. It's an async function with lots of 'await' statements that gets to the Remote Config, gets the copy URL, then downloads it from Firestore and parses it as a JSON string that is loaded into a Map. Copy then provides methods to retrieve the loaded strings.
Finally, here is the 'Init' widget stripped of other stuff:
class Init extends StatelessWidget {
#override
Widget build(BuildContext context) {
//TODO add logic to transition away from spinner when copy has loaded
return CircularProgressIndicator();
}
}
The main issue I have is:
How do I notify 'Init' widget once my copy state has changed, i.e. when 'copy.load()' has finished all its async calls, so that I can transition away from the loading spinner?
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);
});
}