Widget testing DropdownButton finds duplicate DropdownMenuItems - flutter

I'm trying to write widget tests for a DropdownButton in my app. I noticed that after tapping the button to open it the call to find.byType(DropdownMenuItem) is returning double the expected number of DropdownMenuItems.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
enum MyColor {
blue,
green,
red,
yellow,
black,
pink
}
Future<void> main() async {
// runApp(MyApp());
// tests
group('dropdown tests', () {
testWidgets('how many elements should be found?', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
expect(find.byType(DropdownButton<MyColor>), findsOneWidget);
await tester.tap(find.byType(DropdownButton<MyColor>));
await tester.pumpAndSettle();
// fails
// expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length));
// passes
expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length * 2));
});
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
MyColor selected = MyColor.blue;
#override
Widget build(BuildContext context) {
return DropdownButton<MyColor>(
value: selected,
items: MyColor.values.map((col) {
return DropdownMenuItem<MyColor>(
child: Text(col.name),
value: col,
);
}).toList(),
onChanged: (value) {
if (value == null) {
return;
}
print('${value.name} selected');
setState(() {
selected = value;
});
}
);
}
}
Dartpad: https://dartpad.dev/?id=ce3eadff6bd98e6005817c70883451a0
I suspect that this has something to do with how Flutter renders the scene. I looked into the widget tests for the dropdown in the Flutter repo but I don't see any difference between my setup and theirs, but I also don't see any calls to find.byType(DropdownMenuItem). Does anyone know why this happens? Or is there an error in my code?

When an DropdownButton is rendered initially all items are rendered with IndexedStack and based on the selected value we see one visible item at the top
At that stage find.byType(DropdownMenuItem<MyColor>) will find 6
items
Once you tap on DropdownButton a _DropdownRoute route is pushed with all the items
At that stage find.byType(DropdownMenuItem<MyColor>) will find 12
items (the first 6 items are from IndexedStack and the second 6
items are from the new route)
So the number of items should be double at this stage as documented in the flutter tests as well
// Each item appears twice, once in the menu and once
// in the
dropdown button's IndexedStack.
https://github.com/flutter/flutter/blob/504e66920005937b6ffbc3ccd6b59d594b0e98c4/packages/flutter/test/material/dropdown_test.dart#L2230
Once you tap on of the DropdownMenuItem items the number of found widgets will go back to 6

Related

How to set ThemeMode in splash screen using value stored in sqflite FLUTTER

I have a Flutter Application where an sqflite database stored the user preference of ThemeMode (viz Dark, Light and System). I have created a splash screen using flutter_native_splash which supports dark mode too.
The Problem is this that I want the splash screen to follow the users stored value for theme mode. Currently, the code I am using is as follows:
class MyRoot extends StatefulWidget {
// const MyRoot({Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
ThemeMode? tmSaved;
#override
void initState() {
Future.delayed(Duration.zero, () async => await loadData());
super.initState();
}
#override
Widget build(BuildContext context) {
//to prevent auto rotation of the app
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return ValueListenableBuilder<ThemeMode>(
valueListenable: MyRoot.themeNotifier,
builder: (_, ThemeMode currentMode, __) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
title: 'My Application',
theme: themeLight, //dart file for theme
darkTheme: themeDark, //dart file for theme
themeMode: tmSaved ?? currentMode,
initialRoute: // my initial root
routes: {
// my routes
.
.
.
// my routes
},
);
},
);
},
);
}
Future<void> loadData() async {
if (databaseHelper != null) {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
if (themeMode != null) {
MyRoot.themeNotifier.value = themeMode;
return;
}
}
MyRoot.themeNotifier.value = ThemeMode.system;
}
}
Currently, this shows a light theme splash screen loading, then converts it into dark with a visible flicker.
ValueListenableBuilder<ThemeMode>(... is to enable real time theme change from settings page in my app which working as intended (taken from A Goodman's article: "Flutter: 2 Ways to Make a Dark/Light Mode Toggle".
main.dart has the below code:
void main() {
runApp(MyRoot());
}
Have you tried loading the setting from sqflite in main() before runApp? If you can manage to do so, you should be able to pass the setting as argument to MyRoot and then the widgets would be loaded from the start with the correct theme. I'm speaking in theory, I can't test what I'm suggesting right now.
Something like:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
runApp(MyRoot(themeMode));
}
[...]
class MyRoot extends StatefulWidget {
ThemeMode? themeMode;
const MyRoot(this.themeMode, {Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
EDIT
Regarding the nullable value you mentioned in comments, you can change the main like this:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
themeMode ??= ThemeMode.system;
runApp(MyRoot(themeMode!));
}
which makes themeMode non-nullable, and so you can change MyRoot in this way:
class MyRoot extends StatefulWidget {
ThemeMode themeMode;
const MyRoot(required this.themeMode, {Key? key}) : super(key: key);
[...]
}
Regarding the functionality of ValueNotifier, I simply thought of widget.themeMode as the initial value of your tmSaved property in your state, not as a value to be reused in the state logic. Something like this:
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
late ThemeMode tmSaved;
#override
void initState() {
tmSaved = widget.themeMode;
super.initState();
}
[...]
}
so that your widgets would already have the saved value at the first build.
PS the code in this edit, as well as in the original part, isn't meant to be working by simply pasting it. Some things might need adjustments, like adding final to themeMode in MyRoot.
Make your splashscreen. A main widget which get data from sqlflite
And make splashscreen widget go to the your home widget with remove it using navigation pop-up
for example :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ToDo',
color: // color of background
theme: // theme light ,
darkTheme: // darktheme
themeMode: // choose default theme light - dark - system
home: Splashscreen(),// here create an your own widget of splash screen contains futurebuilder to fecth data and return the mainWidget ( home screen for example)
);
}
}
class Splashscreen extends StatelessWidget {
Future<bool> getData()async{
// get info
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getData(),
builder: (context,snapshot){
// if you want test snapshot
//like this
if(snapshot.hasData) {
return Home();
} else {
return Container(color: /* background color as same as theme's color */);
}
}
);
}
}

Flutter - rebuild GestureDetector widget without user interaction

I am trying to display 3 images one after another by using GestureDetector and its onTap method but one image should be displayed without tapping. Supposing I have three images 1.png, 2.png, 3.png I would like to display the first image 1.png at the first place. Clicking on it will result in displaying the second image 2.png.
The second image 2.png should now be replaced after x seconds by displaying the image 3.png.
I am using a stateful widget that stores which image is currently shown. For the first image change the onTap will execute setState( () {screen = "2.png"} and the widget is rebuilt. However, I am pretty stuck now when the third image should be displayed after x seconds delay because there should not be any user interaction and I am not sure where to change it. I tried a lot of stuff especially editting the state from outside the widget but I did not succeed til now.
This is the code I am using:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Navigation Basics',
home: MyScreen(),
));
}
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
String currentScreen = 'assets/images/1.png';
#override
Widget build(BuildContext context) {
return GestureDetector(
child: FittedBox(
child: Container(
child: Image.asset(currentScreen),
),
fit: BoxFit.fill,
),
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
}
},
);
}
}
Displaying the second image worked without any issues but I have no idea what/where to code in order to display the third image.
Thanks for any help!
I'm assuming you don't want the second and third picture to respond to the gesture detection. Try this, please.
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
Future.delayed(const Duration(milliseconds: 3000), () {
setState(() {
currentScreen = 'assets/images/3.png';
});
});
} else {
return;
}
},

I was told that 25th line of code contains an issue where setState() with random color being used. Help identify an issue

Original task sounded like:
The application should: display the text "Hey there" in the middle of
the screen and after tapping anywhere on the screen a background color
should be changed to a random color. You can also add any other
feature to the app - that adds bonus points Please do not use any
external libraries for color generation
My solution (GitHub):
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: RandomBackgroundColorWidget(),
);
}
}
class RandomBackgroundColorWidget extends StatefulWidget {
#override
_RandomBackgroundColorWidget createState() => _RandomBackgroundColorWidget();
}
class _RandomBackgroundColorWidget extends State<RandomBackgroundColorWidget> {
int _colorIndex = 0xFF42A5F5;
void _randomColorIndexGenerator() {
final _rng = new Random();
setState(() => {_colorIndex = (_rng.nextInt(0xFFFFFF) + 0xFF000000)});
}
#override
Widget build(BuildContext context) {
return Stack(children: [
Material(
color: Color(_colorIndex),
child: Center(
child: Text("Hey there"),
),
),
GestureDetector(
onTap: () => _randomColorIndexGenerator(),
),
]);
}
}
While reviewing my test task interviewer said that 25th line of code contains an issue.
setState(() => {_colorIndex = (_rng.nextInt(0xFFFFFF) + 0xFF000000)});
And he commented:
"It is working in a way that is not intended by you."
Help to identify an issue in 25th line of code.
You are accidentally combining the two ways to declare a function in Dart: the arrow operator => and curly braces {}.
Line 25 should be:
setState(() => _colorIndex = _rng.nextInt(0xFFFFFF) + 0xFF000000);
with no extra curly braces.
The issue is a syntax error. When using setState() =>, you dont need the {}
setState(() {_colorIndex = (_rng.nextInt(0xFFFFFF) + 0xFF000000)});
or
setState(() => _colorIndex = (_rng.nextInt(0xFFFFFF) + 0xFF000000));
I couldn't find the error you mention, however I recommend that you always use init state when assigning default values.
Here other way
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: RandomBackgroundColorWidget(),
);
}
}
class RandomBackgroundColorWidget extends StatefulWidget {
#override
_RandomBackgroundColorWidget createState() => _RandomBackgroundColorWidget();
}
class _RandomBackgroundColorWidget extends State<RandomBackgroundColorWidget> {
Color _color;
#override
void initState() {
_color = Colors.white;
super.initState();
}
void getrandomColor() {
setState(() {
_color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
});
}
#override
Widget build(BuildContext context) {
return Stack(children: [
Material(
color: _color,
child: Center(
child: Text("Hey there"),
),
),
GestureDetector(
onTap: getrandomColor,
),
]);
}
}
Issue is in nextInt function.
Flutter accepts color codes for generation up to 0xFFFFFFFF, where first pair of numbers is needed for opacity level and other pairs needed for RGB levels
nextInt function generates random number in range up to, but not including, passed number. For example, nextInt(3) will generate randomly 0,1 or 2, but not 3.
So original app was generating all random colors (from 0x0 to 0xFFFFFE) except last one - 0xFFFFFF
Therefore, 25th line should look like this in order to generate every possible color.
setState(() => _colorIndex = (_rng.nextInt(0x1000000) + 0xFF000000));

Flutter pass data to new screen with onTap

My application has a bottom navigation bar, with 2 pages in the menu.
On page 1, I can fill out a form and it calculates me values ​​that it displays to me by pushing in a 1.1 page.
On this page I have a button that allows me to redirect me to page 2 as if I clicked menu 2 of the navigation bar.
This works. My problem is how to send the data from my page 1.1 to this page 2.
The goal being that my page 2 is a form which is empty if I call it by the navigation bar but which is filled automatically if I pass by the page 1.1 in focus of the calculated values.
Here an exemple of the redirection that I do:
Here is my code :
my_app.dart :
final ThemeData _AppTheme = AppTheme().data;
final navBarGlobalKey = GlobalKey(); // => This is my key for redirect page
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp>{
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App',
home: MyBottomNavigationBar(),
theme: _AppTheme,
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: Router.generateRoute,
initialRoute: HOME_ROUTE,
);
}
}
My bottom_navigation_bar.dart :
class MyBottomNavigationBar extends StatefulWidget
{
MyBottomNavigationBar({Key key}) : super(key: key);
#override
_MyBottomNavigationBarState createState() => _MyBottomNavigationBarState();
}
class _MyBottomNavigationBarState extends State<MyBottomNavigationBar>
{
int _pageIndex = 0;
final List<Widget> _pagesOption = [
page1.1(), // => Here I load direclty my page 1.1 with data for the exemple
page2(),
];
void onTappedBar(int index)
{
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child : Scaffold(
body : _pagesOption.elementAt(_pageIndex),
bottomNavigationBar: BottomNavigationBar(
key: navBarGlobalKey,
currentIndex: _pageIndex,
onTap: onTappedBar,
type: BottomNavigationBarType.fixed,
items : [
BottomNavigationBarItem(
icon : Icon(Icons.home),
title : Text('Home')
),
BottomNavigationBarItem(
icon : Icon(Icons.settings),
title : Text('Setting')
),
]
),
)
);
}
}
And here my widget submit button of page 1.1 :
Widget _getSubmitButton(){
return RaisedButton(
child: Text(
'Send'
),
onPressed: () {
final BottomNavigationBar navigationBar = navBarGlobalKey.currentWidget;
navigationBar.onTap(1); // => How to send data that I have in my page ???
},
);
}
For this, you can use Shared Preferences, the main idea is that:
Store the value of the calculated value in SharedPref from Page 1 when you're passing to Page 1.1
Let you checks for the value by default in Page 2's initState(), any changes in the Shared Preferences will be fetched in the Page 2 itself, using SharedPref get method.
WHY?
This is probably a cleaner way to achieve what you want, since in the BottomNavigationBar will not help you do this, but a Shared Preferences value will always give you that data which you can use it any time
Let's see how you can achieve this:
PAGE ONE
// Set the data of the form here
class _PageOneState extends State<PageOne>
{
void onSubmit() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
//make sure you store the calculated value, if that is String
// use setString() or if it is an int, setInt()
// and then pass it to the SharedPref
// key is a string name, which is used to access
// key and store, so choose the name wisely
await prefs.setInt("key", your_calculated_value);
}
}
PAGE TWO
class _PageTwoState extends State<PageTwo>
{
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
// This will be responsible for getting the result from SharedPref
int calculated_value;
#override
void initState(){
super.initState();
// get your list here
calculated_value = _prefs.then((SharedPreferences prefs){
// here if no data is then _values will have 0
// which you can use it to check and populate data
return (prefs.getInt("key") ?? 0);
});
}
}
This is the most reasonable way of doing the thing which you want. In this manner, whenever, PageTwo will trace any values, it will reflect, else, your choice for 0 check result. Let me know, if you have any doubts in that.
In your FirstActivity
onPressed: () {
navigatePush(SecondActivity(text: "Data"));
}
In your SecondActivity
class SecondActivity extends StatefulWidget {
String text;
SecondActivity({this.text});
}
You can pass the the values as arguments when you push to your new screen. This could get messy if you're building a larger project.
A cleaner implementation would be to use a Provider. Set up the data you want in a model mixed in with ChangeNotifier and use Provider.of<*name of your class*>(context) where ever you need to use it.

Flutter Camera Plugin taking dark image bug

I am getting dark images from flutter Camera Plugin.
Camera Preview is showing correctly but after taking the picture it becomes too dark.
I searched and what i found that it's about the FPS and exposure of the camera.
How can I solve this problem?
I need to show camera preview and take pictures in my app.
Please don't tell me to use image_picker package.
Device : Redmi note 4
Android OS : 7.0
Here is the Image
dark image
Here is the code
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';
Future<void> main() async {
// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();
// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;
runApp(
MaterialApp(
theme: ThemeData.dark(),
home: TakePictureScreen(
// Pass the appropriate camera to the TakePictureScreen widget.
camera: firstCamera,
),
),
);
}
// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
const TakePictureScreen({
Key key,
#required this.camera,
}) : super(key: key);
#override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
CameraController _controller;
Future<void> _initializeControllerFuture;
#override
void initState() {
super.initState();
// To display the current output from the Camera,
// create a CameraController.
_controller = CameraController(
// Get a specific camera from the list of available cameras.
widget.camera,
// Define the resolution to use.
ResolutionPreset.medium,
);
// Next, initialize the controller. This returns a Future.
_initializeControllerFuture = _controller.initialize();
}
#override
void dispose() {
// Dispose of the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Take a picture')),
// Wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner
// until the controller has finished initializing.
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(_controller);
} else {
// Otherwise, display a loading indicator.
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.camera_alt),
// Provide an onPressed callback.
onPressed: () async {
// Take the Picture in a try / catch block. If anything goes wrong,
// catch the error.
try {
// Ensure that the camera is initialized.
await _initializeControllerFuture;
// Construct the path where the image should be saved using the
// pattern package.
final path = join(
// Store the picture in the temp directory.
// Find the temp directory using the `path_provider` plugin.
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
// Attempt to take a picture and log where it's been saved.
await _controller.takePicture(path);
// If the picture was taken, display it on a new screen.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DisplayPictureScreen(imagePath: path),
),
);
} catch (e) {
// If an error occurs, log the error to the console.
print(e);
}
},
),
);
}
}
// A widget that displays the picture taken by the user.
class DisplayPictureScreen extends StatelessWidget {
final String imagePath;
const DisplayPictureScreen({Key key, this.imagePath}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Display the Picture')),
// The image is stored as a file on the device. Use the `Image.file`
// constructor with the given path to display the image.
body: Image.file(File(imagePath)),
);
}
}
Just put delay before take picture.
Future.delayed(const Duration(milliseconds: 500), () {
_controller.takePicture(path);
});
I think it's not about a delay, images are dark if exposure is not handled.
Also exposure requires focus pre captures to work and are not handled in official plugin now.
You can use this plugin : CamerAwesome
Official plugin has been quite abandonned. This plugin includes flash, zoom, auto focus, exposure... and no initialisation required.
It uses value notifier to change data directly in preview like this :
// init Notifiers
ValueNotifier<CameraFlashes> _switchFlash = ValueNotifier(CameraFlashes.NONE);
ValueNotifier<Sensors> _sensor = ValueNotifier(Sensors.BACK);
ValueNotifier<Size> _photoSize = ValueNotifier(null);
#override
Widget build(BuildContext context) {
return CameraAwesome(
onPermissionsResult: (bool result) { }
selectDefaultSize: (List<Size> availableSizes) => Size(1920, 1080),
onCameraStarted: () { },
onOrientationChanged: (CameraOrientations newOrientation) { },
zoom: 0.64,
sensor: _sensor,
photoSize: _photoSize,
switchFlashMode: _switchFlash,
orientation: DeviceOrientation.portraitUp,
fitted: true,
);
};
A hack that works for me, with camera plugin: take the picture twice. The first one buys time for the second one to have the proper exposure and focus.
final image = await controller.takePicture(); // is not used
final image2 = await controller.takePicture();