i try to unittest a small own widget:
testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
var text = "abc";
var label = "def";
await tester.pumpWidget(LabeledTextWidget(
text,
label: label,
));
final textFinder = find.text(text);
final labelFinder = find.text(label);
expect(textFinder , findsOneWidget);
expect(labelFinder, findsOneWidget);
});
Here is the widget code:
class LabeledTextWidget extends StatelessWidget {
final String text;
final String label;
LabeledTextWidget(this.text, {this.label});
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(label,style: Theme.of(context).textTheme.caption,),
Text(text),
],
);
}
The test always throws the exception:
...
No Directionality widget found.
RichText widgets require a Directionality widget ancestor.
The specific widget that could not find a Directionality ancestor was:
RichText
...
I can avoid this error by adding a text direction to all Text-Widgets. (textDirection: TextDirection.ltr,), but i thats a bad solution and it doesn´t work with rows.
Ok, here is the solution.
You have to wrap the Widget with this:
Directionality(
child: MediaQuery(
data: MediaQueryData(),
child: LabeledTextWidget(this.text, {this.label}),
),
textDirection: TextDirection.ltr,
);
In a real world Flutter app be preapred that the Widget under test might depend on a whole bunch of framework widgets that should preceed it and you can face numerous errors:
No Directionality widget found, No MaterialLocalizations found, No material widget, No Overlay, etc.
The best way to go is to digest the main.dart entry point and extract the bare minimum (via trial and error). Here's my pumpWidget example were I create and wrap the target OnlineDictionaries widget:
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<OnlineDictionaryManager>(
create: (context) => OnlineDictionaryManager(FakeOnlineRepo()),
)
],
child: MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('be', ''),
const Locale('ru', ''),
],
initialRoute: '/',
routes: {'/': (context) => Scaffold(body: OnlineDictionaries())},
),
),
);
Note that the OnlineDictionaries widget relies on Providers for state management, it uses localizations and the app uses Overlays/Navigator (addressed by initialRoute and routes properties).
Afterwards you can manipulate the widget (e.g. checking if load indicator is displayed and if an error message is shown if TextFormField is cleared):
await tester
.pump(Duration(milliseconds: 10)); // let progress indicator appear
expect(find.byType(LinearProgressIndicator), findsOneWidget);
await tester.pumpAndSettle();
var field = find.byType(TextFormField);
expect(field, findsOneWidget);
await tester.enterText(find.byType(TextFormField), '');
await tester.pumpAndSettle(Duration(milliseconds: 100));
final errorFinder = find.text('URL can\'t be empty');
expect(errorFinder, findsOneWidget);
Here's a complete example (also includes Mockito and mocking SharedPreferences): https://github.com/maxim-saplin/dikt/blob/master/test/ui_dictionaries_test.dart
Related
I want to have a textinputfield for the user and when he tips in something and clicks on a button the input should be shown in a list item. The user should also have the option to delete items of the list just like a in and todo app.
Here you can find my code:
Link to Code
So I decided to write this simple program just to freshen up my skills... You can directly copy paste this code and it should work just fine.
I have used the provider package here to make this a bit more professional, as you can't always rely on setState() to update your UI when tasks are added to your list. And also because you will probably be using the provider more often in the future.
I have added comments in the below code to make it easy to understand. However, do not hesitate to clear up any confusions in the comments :)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todo/list_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider( // This initiates the provider.
create: (context) => TaskProvider(), // Initiating it here makes this provider data available everywhere in the application
child: MaterialApp(
title: 'Flutter Demo',
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic List'),
),
body: const HomePageBody(), // I pass a separate widget here just to make the code a bit cleaner
floatingActionButton: FloatingActionButton(
onPressed: () => showModalBottomSheet( // This calls a bottom Modal Sheet which pops up while pressing the floating action button
context: context, builder: (context) => const BottomSheet()),// The modal sheet displays the BottomSheet() Widget which I have defined down in this code.
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
// This is where the ListView will be shown
class HomePageBody extends StatelessWidget {
const HomePageBody({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
List<String> listOfTasks = Provider.of<TaskProvider>(context).getTasks; // This is where the list is being accessed from the Provider file.
return Container(
padding: const EdgeInsets.all(20),
child: ListView.builder(
itemCount: listOfTasks.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
listOfTasks[index],
),
);
},
),
);
}
}
// This is the BottomSheet Widget where I decided to take User Input from
class BottomSheet extends StatefulWidget {
const BottomSheet({Key? key}) : super(key: key);
#override
State<BottomSheet> createState() => _BottomSheetState();
}
class _BottomSheetState extends State<BottomSheet> {
String task = ''; // This variable holds the tasks user wants to add
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: 20,
left: 20,
right: 20,
bottom: MediaQuery.of(context).viewInsets.bottom + 20, // viewInsets.bottom adds padding from the bottom to avoid keyboard overlapping textfield widget
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(// You can use TextField Widget as well
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
onChanged: (value) { // This saves the value in the TextField for every character the user types
task = value; // The value in the TextField is referred to by the 'value' variable
},
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () => saveTask(task),
child: const Text('Save Task'),
),
],
),
);
}
void saveTask(String task) {
Provider.of<TaskProvider>(context, listen: false).addTasks(task); //This is where I am calling the function to add a task to the list.
// The 'addTasks()' function is defined in the provider file which is just below
}
}
Here's the list_provider.dart file that I import in above code:
import 'package:flutter/foundation.dart';
class TaskProvider extends ChangeNotifier { // This is the class where your data exists
// and this is the only place where your data should be manipulated! I explain the reason below...
final List<String> _tasks = [];
List<String> get getTasks { // We use a getter to retrieve the list
return _tasks; // We do that in order to avoid modifications to this list from any outside sources.
}
void addTasks(task) {
_tasks.add(task); // This is simply how you add anything to a list
notifyListeners(); // This is why we use providers. This function notifies all the children widgets
// of the Widget where we initiated our provider (see the parent of MaterialApp Widget in the above code)
// This is why changes to data should be made within this class only as it extends ChangeNotifier,
// which provides us with notifyListeners() method. Which ultimately notifies the widgets that the data has been modified and its time to rebuild the widgets that rely on this data!
}
}
You can copy paste this code, just make sure to add the provider package in your pubspec.yaml file as shown below.
I'm new to programming. I have created a flutter drawer, which works fine. However, I have learned about for loops and now want to shorten my code so that I don't show 10 x ListTiles. I now have a for loop for my ListTile. I managed to get the drawer working fine, with the Text and the Icon, but cannot seem to get the onTap to work with the routes (pages). I can get the Navigator.pushNamed to return a single page in my routes, but then this same page get returned for all my items in all my ListTiles.
I have created routes in my main.dart. I have created my class and list of pages. I have created my for loop in a function outside of the Builder and parsed Buildcontext (context) into the function, which seems to be ok. My code does not throw errors, but only return one page.
I have spend four days now reading do many similar kind of solutions, but I simply can't find a workable solution for myself. I don't seem to find a proper answer to my problem as all solutions through out some kind of error, or at best, no errors, but it does not work. Here is my code.
// MAIN.DART FILE TO ILLUSTRATE PAGE ROUTES
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/page2': (context) => const InvoiceApp(),
'/page3': (context) => const Currency(),
'/page4': (context) => const Expenses(),
'/page5': (context) => const Reports(0),
'/page6': (context) => const Graphs(0),
'/page7': (context) => const Download(0),
'/page8': (context) => const Customer(),
'/page9': (context) => const People(0),
'/page10': (context) => const Resources(0),
'/page11': (context) => const Profile(),
},
home: Scaffold(
//CLASS ROUTES
class Routes {
String page;
Routes({required this.page});
}
//LIST
list<Routes> routeList = [
Routes(page: '/page2'),
Routes(page: '/page3'),
Routes(page: '/page4'),
Routes(page: '/page5'),
Routes(page: '/page6'),
Routes(page: '/page7'),
Routes(page: '/page8'),
Routes(page: '/page9'),
Routes(page: '/page10'),
Routes(page: '/page11'),
];
//MAIN CODE
class MenuBar extends StatelessWidget {
const MenuBar({Key? key}) : super(key: key);
List<ListTile> getMenuItems(BuildContext context, var index) {
List<ListTile> menuList = [];
for (var i = 0; i < menuItemList.length; i++) {
var item = menuItemList[i];
var loopCode = ListTile(
title: Text(item),
leading: Icon(
icons[i],
color: Colors.teal,
),
// onTap: () {
// Navigator.pushNamed(context, routeList[index].page);
// },
onTap: () {
Navigator.pushNamed(context, '/page3');
},
);
menuList.add(loopCode);
}
return menuList;
}
#override
Widget build(
BuildContext context,
) {
// getMenuItems(context);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const AccountHeader(),
Column(
children: getMenuItems(context, routeList),
),
],
),
);
}
List<String> menuItemList = [
'Invoices',
'Currency Converter',
'Expenses',
'Reports',
'Graphs',
'Downloads',
'Customers',
'People Management',
'Resources',
'Profile',
];
List<IconData> icons = [
Icons.receipt,
Icons.money,
Icons.money,
Icons.report,
Icons.bar_chart,
Icons.download,
Icons.people,
Icons.people,
Icons.cake,
Icons.person,
];
PLEASE NOTE:
I have 10 pages in my routes, yet my routes starts at page 2 and ends at page 11. This is not an issue as my main.dart file is my landing page for my app.
My problem is onTap. You will notice two onTap codes (one is commented out). If I use the current active one (...page3), then it works, but all 10 pages then return to page 3 once any of the ten tiles are tapped. If I use the onTap that is commented out, then the app loads, the drawer opens find, but no tile will navigate to any page. Also no errors in my code is issued.
Can you please help me. Thank you kindly.
The problem is that you're using the index that is being passed from the function instead of the index inside the loop.
replace the commented code
// onTap: () {
// Navigator.pushNamed(context, routeList[index].page);
// },
with
onTap: () {
Navigator.pushNamed(context, routeList[i].page);
},
the i is the index of the for loop.
I am having trouble with a test where I am trying to see if tapping a RadioListTile works. Simply looking the element byType with finder, doesn't find it even though the parent widget and the list tiles are displayed in the emulator. I really can't figure out what I am doing wrong.
The widget tree is as follows:
RadioQuestion
| QuestionText
| RadioChoices
| RadioListTile
class RadioChoices extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<RadioQuestionModel>(
builder: (context, model, child) => Column(
// debugPrint(model.choices) here shows data.
children: model.choices
.map((e) => RadioListTile(
title: e,
value: model.choices.indexOf(e),
groupValue: model.selectedIndex,
onChanged: (value) => model.onChanged(value),
))
.toList(),
));
}
}
testWidgets('Displays choices', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Scaffold(body: RadioQuestion(questions[0]))));
var finder = find.byType(RadioChoices);
expect(finder, findsOneWidget);
finder = find.byType(RadioListTile);
expect(finder,
findsNWidgets(questions[0].possibleChoices.length)); // This fails, 0 widgets found.
});
The following TestFailure object was thrown running a test:
Expected: exactly 4 matching nodes in the widget tree
Actual: _WidgetTypeFinder:<zero widgets with type "RadioListTile<dynamic>" (ignoring offstage
widgets)>
Which: means none were found but some were expected
Converting the List to an array of RadioListTiles fixed it:
children: <RadioListTile>[]
..addAll(model.choices.map((e) => RadioListTile(
title: e,
value: model.choices.indexOf(e),
groupValue: model.selectedIndex,
onChanged: (value) => model.onChanged(value),
)))
I have a StatefulWidget which state renders different Widget depending on loading state (Loading -> Loaded/Error):
// widget
class ListNotesScreen extends StatefulWidget {
static const route = '/listNotes';
static navigateTo(BuildContext context, [bool cleanStack = true]) =>
Navigator.pushNamedAndRemoveUntil(context, route, (_) => !cleanStack);
final String title;
final ListNotesUseCase _useCase;
final VoidCallback _addNoteCallback;
ListNotesScreen(this._useCase, this._addNoteCallback, {Key key, this.title}) : super(key: key);
#override
_ListNotesScreenState createState() => _ListNotesScreenState();
}
// state
class _ListNotesScreenState extends State<ListNotesScreen> {
ListNotesLoadState _state;
Future<ListNotesResponse> _fetchNotes() async {
return widget._useCase.listNotes();
}
#override
initState() {
super.initState();
_loadNotes();
}
_loadNotes() {
setState(() {
_state = ListNotesLoadingState();
});
_fetchNotes().then((response) {
setState(() {
_state = ListNotesLoadedState(response.notes);
});
}).catchError((error) {
setState(() {
_state = ListNotesLoadErrorState(error);
});
});
}
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('Notes list'),
actions: <Widget>[
IconButton(icon: Icon(Icons.add), onPressed: widget._addNoteCallback),
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadNotes())
],
),
body: _state.getWidget());
}
// loading states
// State:
#sealed
abstract class ListNotesLoadState {
Widget getWidget();
}
// Loading
class ListNotesLoadingState extends ListNotesLoadState {
#override
Widget getWidget() => Center(child: CircularProgressIndicator(value: null));
}
// Loaded
class ListNotesLoadedState extends ListNotesLoadState {
final List<Note> _notes;
ListNotesLoadedState(this._notes);
#override
Widget getWidget() => ListView.builder(
itemBuilder: (_, int index) => NoteItemWidget(this._notes[index]),
itemCount: this._notes.length,
padding: EdgeInsets.all(18.0));
}
Here is the test for the widget:
void main() {
testWidgets('Notes list is shown', (WidgetTester tester) async {
final title1 = 'Title1';
final title2 = 'Title2';
final body1 = 'Body1';
final body2 = 'Body2';
var notes = [
Note('1', title1, body1),
Note('2', title2, body2),
];
final listUseCase = TestListNotesInteractor(notes);
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
expect(find.text('someInvalidString'), findsNothing);
expect(find.text(title1), findsOneWidget);
expect(find.text(title2), findsOneWidget);
expect(find.text(body1), findsOneWidget);
expect(find.text(body2), findsOneWidget);
// TODO: fix the test (tested manually and it works)
});
}
So widget tester is expected to wait until the state it set to loading in initState(), then _loadNotes moves it to ListNotesLoadedState and ListNotesLoadedState.getWidget() to return ListView with expected string (NoteItemWidget root and few Text with expected string).
However the test fails. What's the reason (i was able to use test interactors in the app and visually see expected texts)? How can i analyze the actual Widgets tree on test failure?
I tend to think that WidgetTester did not wait for Future to be completed (though it's expected to be mocked and be sync behind the scenes, please correct me).
One can find the project on Github (make sure to call flutter packages pub run build_runner build to generate json de-/serialize code).
I've found the reason: MaterialApp (or probably any app) should be the root of widgets tree!
final widget = MaterialApp(home: ListNotesScreen(interactor, null)); // succeeds
instead of:
final widget = ListNotesScreen(interactor, null); // fails
Also i've removed unused title property so the test code is a bit different form what i used originally:
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
It's not mentioned in the docs (is it the reason actually?) though the test code has it. Please let me know if i miss something.
I have a ListView inside a StatelessWidget. It has items and every item contains a checkbox. When someone checks an item, I want the ListView to send this as a parameter to another page. But when I do that, it's giving me this error:
I/flutter ( 7067): The following UnsupportedError was thrown while handling a gesture:
I/flutter ( 7067): Unsupported operation: Cannot add to an unmodifiable list
I/flutter ( 7067): When the exception was thrown, this was the stack:
and this is my code
class StudentsList extends StatelessWidget {
final List<Child> mList;
StudentsList({this.mList});
#override
Widget build(BuildContext context) {
List<Child> selectedList = [];
return Container(
margin: EdgeInsets.only(top: 50, bottom: 20),
child: ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: mList == null ? 0 : mList.length,
padding: EdgeInsets.only(right: 10),
itemBuilder: (BuildContext context, int position) {
return GestureDetector(
onTap: () {
if (selectedList.isEmpty) {
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => SolokPage(
mChildList: [mList[position]],
isTeacher: true,
),
),
);
} else {
if (!selectedList.contains(mList[position])) {
selectedList.add(mList[position]);
}
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => SolokPage(
mChildList: selectedList,
isTeacher: true,
),
),
);
}
},
child: StudentItem(
student: mList[position],
),
);
},
),
);
}
}
Stateless Widget properties are meant to be immutable
class StudentsList extends StatelessWidget {
// final means, flutter will not change value in future
final List<Child> mList;
StudentsList({this.mList});
Why ?
Because Flutter expects no business logic resides in StatelessWidget.
If we need to add new Student in Student list, it is considered as business logic.
If we need to delete some Student in Student list, it is considered as business logic.
So by using stateless widget, Flutter will only focuses on How it will be displayed on Screen, what is the width, the constraints and etc.
That's why we found final syntaxes before class properties in StatelessWidget.
Similiar to our college life. Our Grades that marked in final report, will not change even after we graduate from university. As it said to be in Final Report, then it must be final.
Stateful Widget properties are mutable
Why ?
Because flutter expects business logic resides in StatefulWidget.
Changes to be made
So I suggest to change StudentsList Widget, from this :
class StudentsList extends StatelessWidget {
final List<Child> mList; // this is the issue
StudentsList({this.mList});
to this one :
class StudentsList extends StatefulWidget {
#override
_StudentsListState createState() => _StudentsListState();
}
class _StudentsListState extends State<StudentsList> {
// final List<Child> mList; // Do not mark this as final
List<Child> mList;
...
}
Working Repository
You may look working repository that is closely-related to your issue. Github
Stateless Widgets property cannot be immutable means in simple words is that it should not contain any non-final variables.
Simply convert it to Stateful widget and inside the class _StudentsListState create your variable WITHOUT the final keyword because you are modifying the value of that List.
If you want to keep stateless (IE you just need to return some data or maybe youre using hooks) you could also try toList() to create a copy, then modify, then replace the original list
I encountered this problem in a simple function, and I solved it like this.
Future<void> createProduct({required Product product, required List<File> images}) async {
for (final image in images) {
final imageId = const Uuid().v4();
final compressedimage = await ImageCompress.instance.compressFile(image);
final taskSnapShot = await StorageService.instance.uploadProductPhoto(
file: compressedimage,
productId: product.productId,
childUUID: imageId,
);
final downloadURL = await taskSnapShot.ref.getDownloadURL();
product.imagesUrl.add(downloadURL);
// sendProduct.addImages(downloadURL: downloadURL);
}
await _collection.doc().set(
product.toMap(),
);
}
turn it into this.
Future<void> createProduct({required Product product, required List<File> images})
async {
List<String> newUrls = [];
for (final image in images) {
final imageId = const Uuid().v4();
final compressedimage = await ImageCompress.instance.compressFile(image);
final taskSnapShot = await StorageService.instance.uploadProductPhoto(
file: compressedimage,
productId: product.productId,
childUUID: imageId,
);
final downloadURL = await taskSnapShot.ref.getDownloadURL();
newUrls.add(downloadURL);
// sendProduct.addImages(downloadURL: downloadURL);
}
final sendProduct = product.copyWith(imagesUrl: newUrls );
await _collection.doc().set(
sendProduct.toMap(),
);
}