i read flutter's doc but i couldn't test button's action ,
i tried to do it this way, the button is children of PrivacyPolicyCard, and when clicked goes to the next page
testWidgets(
"TextButton Acept",
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: PrivacyPolicyCard(),
));
final buton = find.byKey(Key("bt-acept"), skipOffstage: false);
await tester.ensureVisible(buton);
expect(find.bySemanticsLabel("Button Acept"), findsOneWidget);
await tester.tap(buton);
await tester.pumpAndSettle(Duration(seconds: 2));
expect(find.text("Next Page"), findsOneWidget);
await tester.printToConsole("test passed");
},
);
how do I find out if the button's action went to the next page?
The NavigationObserver class would help you to achieve what you want.
if you create a class
import 'package:mocktail/mocktail.dart';
class MockNavigationObserver extends Mock implements NavigationObserver {}
and pass the instance of it to your instantiation
await tester.pumpWidget(MaterialApp(
home: PrivacyPolicyCard(),
navigationObservers: [mockObserver],
));
then you'd be able to detect if the actual push has happened:
verify(mockObserver.didPush(any, any));
Hope this helps!
Related
I am running into an issue with flutter when I try to read data from local storage when the app loads.
I have an inherited widget that holds authentication information for the current user. When the app loads I want to look into local storage for session tokens. If the session tokens exist I would like to update the inherited widget with this information.
My screens are dynamic. If it knows the user is authenticated it takes them to the requested screen, otherwise it takes them to the register screen.
The issue I am running into is that I cannot update the inherited widget's state from an initState() method from a widget that depends on the inherited widget (My router widget)
How can I read from local storage when the app loads and update the inherited widget?
Error when running app:
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building _InheritedAuthContainer:
flutter: inheritFromWidgetOfExactType(_InheritedAuthContainer) or inheritFromElement() was called before
flutter: RootState.initState() completed.
flutter: When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
flutter: widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
flutter: or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
flutter: inherited widget.
flutter: Typically references to inherited widgets should occur in widget build() methods. Alternatively,
flutter: initialization based on inherited widgets can be placed in the didChangeDependencies method, which
flutter: is called after initState and whenever the dependencies change thereafter.
Router Widget (Root)
class Root extends StatefulWidget {
#override
State createState() => RootState();
}
class RootState extends State<Root> {
static Map<String, Widget> routeTable = {Constants.HOME: Home()};
bool loaded = false;
bool authenticated = false;
#override
void initState() {
super.initState();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: (routeSettings) {
WidgetBuilder screen;
if (loaded) {
if (authenticated) {
screen = (context) => SafeArea(
child: Material(
type: MaterialType.transparency,
child: routeTable[routeSettings.name]));
} else {
screen = (conext) => SafeArea(
child: Material(
type: MaterialType.transparency, child: Register()));
}
} else {
screen = (context) => new Container();
}
return new MaterialPageRoute(
builder: screen,
settings: routeSettings,
);
});
}
}
Inherited Widget method that checks for auth and updates itself which triggers a rerender of my router widget
Future<bool> isAuthenticated() async {
if (user == null) {
final storage = new FlutterSecureStorage();
List results = await Future.wait([
storage.read(key: 'idToken'),
storage.read(key: 'accessToken'),
storage.read(key: 'refreshToken'),
storage.read(key: 'firstName'),
storage.read(key: 'lastName'),
storage.read(key: 'email')
]);
if (results != null && results[0] != null && results[1] != null && results[2] != null) {
//triggers a set state on this widget
updateUserInfo(
identityToken: results[0],
accessToken: results[1],
refreshToken: results[2],
firstName: results[3],
lastName: results[4],
email: results[5]
);
}
}
return user != null && (JWT.isActive(user.identityToken) || JWT.isActive(user.refreshToken));
}
Main
void main() => runApp(
EnvironmentContainer(
baseUrl: DEV_API_BASE_URL,
child: AuthContainer(
child: Root()
)
)
);
What is a correct way of checking local storage on app load and updating the inherited widget that holds this information?
Actually you cannot access InheritedWidget from an initState method. Instead try accessing it from didChangeDependencies.
Example:
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
Another way would be to schedule the data fetch in initState with SchedulerBinding. You can find the docs here
SchedulerBinding.instance.addPostFrameCallback((_) {
// your login goes here
});
Note: remember the didChangeDependencies will be called whenever the state or dependencies of any parent InheritedWidget changes. Please look at the docs here.
Hope this helps!
While the answer by #hemanth-raj is correct, I would actually advocate a slightly different way of doing this. Instead of constructing the AuthContainer with no data, you could actually do the user session loading before you construct your widgets and pass the data in directly. This example uses the scoped_model plugin to abstract away the inherited widget boilerplate (which I highly recommend over writing inherited widgets manually!) but is otherwise pretty similar to what you've done.
Future startUp() async {
UserModel userModel = await loadUser();
runApp(
ScopedModel<UserModel>(
model: userModel,
child: ....
),
);
}
void main() {
startup();
}
This is more or less what I do in my app and I haven't had any problems with it (although you'd probably want to put in some error handling if there's any chance of loadUser failing)!
This should made your userState code much cleaner =).
And an FYI, what I've done in my UserModel is have a bool get loggedIn => ... that knows which information needs to be in there to tell whether the user is logged in or not. That way I don't need to track it separately but I still get a nice simple way to tell from outside the model.
Do like this as example given in this :
void addPostFrameCallback(FrameCallback callback) {
// Login logic code
}
Schedule a callback for the end of this frame.
Does not request a new frame.
This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
The callbacks are executed in the order in which they have been added.
Post-frame callbacks cannot be unregistered. They are called exactly once.
I have a StatefulWidget named HomePage with a class variable named _messages.
class HomePageState extends State<HomePage>{
final List<Msg> _messages = <Msg>[];
...
I also have a IconButton which will popup a dialog which has a FlatButton that when pressed, it will clear all items inside the _messages variable.
void showMyPopup() async{
return showDialog(
content: FlatButton(
onPressed: () {
setState((){
_messages = [];
...
But flutter/dart gave me an error which says HomePage doesn't have _messages variable.
I can access the _messages when I'm not on the dialog, but not within the dialog.
I would like to know what was wrong with my code. I would really appreciate any form of help.
try widget._messages and please don't make your variable final. final is final, it cannot be changed.
I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example
Brief Note:
In all of my code examples you will see things like material.Widget instead of just Widget. This is because I like to name my imports like this for example:
import 'package:flutter/material.dart' as material;
My Question:
I am trying to make a ListView show newly added Widgets at the top of the list. I have a simple stateful widget that only contains a list of models (classes that contain necessary information to construct the widgets that are going into the ListView), and should construct ListView's children based off of that list.
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
The behavior that I expect from this is that whenever the build method is called (and setState is properly used), that the ListView should be reconstructed properly and contain child widgets in the order of the list. It successfully does this if I simply add new models to cardModels sequentially:
cardModels.add(new CardModel(nextID++));
You can see widgets get added sequentially with their id's incrementing properly:
However, I want newer widgets to be inserted at the top (which would be shown with higher ids at the top). In order to accomplish this, I try inserting new models at the beginning of the list:
cardModels.insert(0, new CardModel(nextID++));
Unfortunately, instead of seeing the correct widgets, I just get widget with id 0 over and over again:
I know that the list of models is being updated correctly because I can print it out and see the ids in descending order. I am assuming that there is something about how flutter detects changes to widgets that is causing this behavior, but after a lot of reading, I still have not been able to figure it out. Any help would be much appreciated. Also, I call set state in the widget that ends up building the page (the widget containing the ListView) as one of its children:
btn = new FloatingActionButton(
onPressed: () => setState(_pageController.addCard),
tooltip: 'Upload File',
child: new Icon(Icons.file_upload),
);
_pageController is the object that modifies the list of models. If this is not enough information, let me know and I am happy to provide more code or answer any questions.
There is a simple quick fix, using the didWidgetUpdate override,
by checking if the cardModels object in the oldWidget is the same in the cardModels being passed as a parameter.
like so
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
Full example below
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
#override
void initState(){
cardModels = widget.cardModels;
super.initState();
}
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
Turns out, the Card widget I made did not need to be stateful. I changed it to stateless and it solved the problem. No idea why having them as stateful would break it though.
On FloatingActionButton change the function to insert new item and then invoke setState like:
cardModels.insert(0, new CardModel(nextId++)) ;
setState(() {
cardModels = cardModels;
});
Hope that helped!
I'm writing unit and integration tests for Flutter. Is it possible to access children/parent widgets from a given widget?
Yes, you can use find.descendant and Element.ancestorWidgetOfExactType.
Examples:
// Finds a RichText widget that a descendant (child/grand-child/etc) of a
// tab widget with text "Tab 1"
find.descendant(of: find.text('Tab 1'), matching: find.byType(RichText));
// Finds a parent widget of type MyParentWidget.
tester.element(find.byType(MyChildWidget))
.ancestorWidgetOfExactType(MyParentWidget);
// Parent with a child & grand child
class ParentWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChildWidget(
child: GrandChildWidget(),
);
}
}
// Tests
void main() {
testWidgets('parent, child & grand-child hierarchy', (WidgetTester tester) async {
Widget parentWidget = ParentWidget();
await tester.pumpWidget(parentWidget);
final childFinder = find.descendant(of: find.byWidget(parentWidget), matching: find.byType(ChildWidget));
expect(childFinder, findsOneWidget);
final grandChildFinder = find.descendant(of: find.byWidget(parentWidget), matching: find.byType(GrandChildWidget));
expect(grandChildFinder, findsOneWidget);
final parentFinder = find.ancestor(of: find.byWidget(childWidget), matching: find.byType(ParentWidget));
expect(parentFinder, findsOneWidget);
});
}
Flutter docs - descendant & ancestor
ancestorWidgetOfExactType is deprecated, prefer using findAncestorWidgetOfExactType