Flutter: Mapbox onMapCreated not called in widget test - flutter

I'm using the Mapbox map and when I run the app in an emulator, it works fine. However, in my widget test, the onMapCreated method is never called, which makes it impossible to test the behavior of the app.
My MapWidget (condensed):
class MapWidget extends StatefulWidget {
final LatLng initialCameraPositionCoordinates;
final double initialZoomLevel;
const MapWidget({
Key key,
#required this.initialCameraPositionCoordinates,
#required this.initialZoomLevel,
}) : super(key: key);
#override
State createState() => MapWidgetState();
}
class MapWidgetState extends State<MapWidget> {
#override
Widget build(BuildContext context) {
return BlocConsumer<Cubit, State>(
builder: (context, state) {
return MouseRegion(
cursor: MouseCursor.defer,
child: MapboxMap(
onMapCreated: _onMapCreated,
),
);
},
);
}
void _onMapCreated(MapboxMapController controller) async {
print("This is never called in the test");
}
And the corresponding widget test looks like this:
void main() {
testWidgets("Mapbox", (WidgetTester tester) async {
await tester.pumpWidget(
BlocProvider(
create: (_) => _cubit,
child: MaterialApp(
home: MapWidget(
initialCameraPositionCoordinates:
_initialCameraPositionCorrdinates,
initialZoomLevel: _initialZoomLevel,
),
),
),
);
await tester.pump(Duration(seconds: 10));
});
}
No exception is thrown, meaning it renders fine. However, the print statement in the onMapCreated is never printed.
Running the app in an emulator, the print statement is called.
Does anyone have an idea what's going on here?

I ran into the same issue, and worked around it by manually invoking the callbacks. Something like this in your widget test:
var mapboxMap = tester.firstWidget(find.byType(MapboxMap)) as MapboxMap;
mapboxMap.onMapCreated!(<mocked controller>);
mapboxMap.onStyleLoadedCallback!();
Where "<mocked controller" is an instance of your MapboxMapController mock.
I imagine the underlying issue has to do with the native map view not actually being loaded during widget tests, and therefore, no callbacks are invoked.

Related

How can I execute a FutureBuilder future on an Autorouter tabs more than once?

I am currently trying to execute a FutureBuilder future function in an Autorouter - the library (https://pub.dev/packages/auto_route#tab-navigation) - and it works perfectly. However, as I am using a FutureBuilder in the tabs, the future is only executed once - the first time I access the tab - and isn't re-executed again when I leave the tab and come back to it. I would like to be able to execute the future function every time I access the tab since the future is reading data from the database.
I have tried the following:
making the widget stateful and executing setState function to force a rebuild
using the overridden function didChangeDependencies
override the deactivate function of the widget
None of the above seem to work.
And after going through the documentation of the Autoroute library, I haven't come across any explanation on how to force a rebuild of the current tab.
I welcome any suggestions.
Thank you
NB: I'm using Flutter to make a mobile application, the solution doesn't necessarily have to work on a web application.
Tab View
class MyTabView extends StatelessWidget {
MyTabView({Key? key}) : super(key: key);
final tabRoutes = [
TabRoute1(),
TabRoute2(),
];
#override
Widget build(BuildContext context) {
return AutoTabsScaffold(
routes: tabRoutes,
bottomNavigationBuilder: (_, tabRouter) {
return BottomNavigationBar(
currentIndex: tabRouter.activeIndex,
onTap: tabRouter.setActiveIndex,
items: [
BottomNavigationBarItem(
icon: BaseIcon(
svgFileName: 'calendar.svg',
),
label: LocaleKeys.careProfessionalLabelProfile.tr(),
),
BottomNavigationBarItem(
icon: BaseIcon(
svgFileName: 'wallet.svg',
),
label: LocaleKeys.careProfessionalLabelChat.tr(),
),
],
);
},
);
}
}
Tab with child that contains FutureBuilder
class TabRoute2 extends StatefulWidget {
const TabRoute2({Key? key}) : super(key: key);
#override
State<TabRoute2> createState() => _TabRoute2State();
}
class _TabRoute2State extends State<TabRoute2> {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
// ---- END SPACER
Expanded(
child: ShowFutureData(),
),
],
);
}
}
ShowFutureData
class ShowFutureData extends StatefulWidget {
const ShowFutureData({
super.key,
});
#override
State<ShowFutureData> createState() =>
_ShowFutureDataState();
}
class _ShowFutureDataState extends State<ShowFutureData> {
late FutureDataObjectProvider futureObjectProvider;
#override
initState() {
super.initState();
futureObjectProvider = context.read<FutureDataObjectProvider>();
}
#override
Widget build(BuildContext context) {
retrieved = futureObjectProvider.retrieveAllData();
return FutureBuilder(
future: retrieved, // only executed when the tab is first accessed
initialData: const [],
builder: (context, snapshot) {
// do something with the data
},
);
}
}
You can reassign the future to recall the future.
FutureBuilder(
future: myFuture,
Then reassign it again
myFuture = getData();

Using the auto_route and flutter_bloc libraries to navigate page not working

I trying to using the auto_route and flutter_bloc libraries to navigate page, but BlocListener is not triggered.
I'm using print(SplashRoute == NavigationState.initial().routeType); to check the trigger condition with BlocListener, it's return true.
However, the BlocListener still not triggered.
How do I fix my code problem :(? This is the sample code of my app. Thanks.
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const AppWidget());
}
class AppWidget extends StatelessWidget {
const AppWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final rootRouter = RootRouter();
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => NavigationCubit()..nav(SplashRoute),
),
// ... Other blocProvider
],
child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: state.themeData,
routerDelegate: rootRouter.delegate(),
routeInformationParser: rootRouter.defaultRouteParser(),
);
},
));
}
}
class SplashPage extends StatelessWidget {
const SplashPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print(SplashRoute == NavigationState.initial().routeType); // <------ return ture
return MultiBlocListener(
listeners: [
BlocListener<NavigationCubit, NavigationState>( // <------ Not working here
listenWhen: (p, c) => c.routeType is SplashRoute,
listener: (context, state) {
LoggerService.simple.i('NavigationCubit page listening!!');
context.read<NavigationCubit>().nav(HomeRoute);
context.pushRoute(const HomeRoute());
},
// ... Other blocListener
),
],
child: const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
navigation_state.dart
part of 'navigation_cubit.dart';
#freezed
abstract class NavigationState with _$NavigationState {
const factory NavigationState({
required Type routeType,
}) = _NavigationState;
factory NavigationState.initial() => const NavigationState(
routeType: SplashRoute,
);
}
navigation_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../presentation/routes/router.gr.dart';
part 'navigation_cubit.freezed.dart';
part 'navigation_state.dart';
class NavigationCubit extends Cubit<NavigationState> {
NavigationCubit() : super(NavigationState.initial());
void nav(Type routeType) {
emit(
state.copyWith(
routeType: routeType,
),
);
}
#override
Future<void> close() async {
return super.close();
}
}
There are two things I can interpret from your code. Either way it will not work as you had hoped.
My understanding from your question is that you are trying to navigate using the BlocListener on initial state, which doesn't work.
The reason is that BlocListener is not triggered on initial state as it is not a state change, but rather something that is defined by the bloc.
The second thing I see is that you call the nav method when providing the bloc, which is a good thing: NavigationCubit()..nav(SplashRoute). However, it will set the same value for the parameter routeType, which will not trigger a state change as it is the same value. Meaning that the BlocListener will not be triggered.
Set routeType to something else initially, perhaps set it to null, so that your bloc can identify a state change, then your BlocListener will be triggered.
EDIT:
Also, c.routeType is SplashRoute doesn't seem right. try changing to c.routeType == SplashRoute in your listenWhen property, otherwise your function in the listener property will not trigger

Waiting asynchronously for Navigator.push() - linter warning appears: use_build_context_synchronously

In Flutter, all Navigator functions that push a new element onto the navigation stack return a Future as it's possible for the caller to wait for the execution and handle the result.
I make heavy use of it e. g. when redirecting the user (via push()) to a new page. As the user finishes the interaction with that page I sometimes want the original page to also pop():
onTap: () async {
await Navigator.of(context).pushNamed(
RoomAddPage.routeName,
arguments: room,
);
Navigator.of(context).pop();
},
A common example is the usage of a bottom sheet with a button with a sensitive action (like deleting an entity). When a user clicks the button, another bottom sheet is opened that asks for the confirmation. When the user confirms, the confirm dialog is to be dismissed, as well as the first bottom sheet that opened the confirm bottom sheet.
So basically the onTap property of the DELETE button inside the bottom sheet looks like this:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (deleteConfirmed) {
Navigator.of(context).pop();
}
},
Everything is fine with this approach. The only problem I have is that the linter raises a warning: use_build_context_synchronously because I use the same BuildContext after the completion of an async function.
Is it safe for me to ignore / suspend this warning? But how would I wait for a push action on the navigation stack with a follow-up code where I use the same BuildContext? Is there a proper alternative? There has to be a possibility to do that, right?
PS: I can not and I do not want to check for the mounted property as I am not using StatefulWidget.
Short answer:
It's NOT SAFE to always ignore this warning, even in a Stateless Widget.
A workaround in this case is to use the context before the async call. For example, find the Navigator and store it as a variable. This way you are passing the Navigator around, not passing the BuildContext around, like so:
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop(); // use the Navigator, not the BuildContext
},
Long answer:
This warning essentially reminds you that, after an async call, the BuildContext might not be valid anymore. There are several reasons for the BuildContext to become invalid, for example, having the original widget destroyed during the waiting, could be one of the (leading) reasons. This is why it's a good idea to check if your stateful widget is still mounted.
However, we cannot check mounted on stateless widgets, but it absolutely does not mean they cannot become unmounted during the wait. If conditions are met, they can become unmounted too! For example, if their parent widget is stateful, and if their parent triggered a rebuild during the wait, and if somehow a stateless widget's parameter is changed, or if its key is different, it will be destroyed and recreated. This will make the old BuildContext invalid, and will result in a crash if you try to use the old context.
To demonstrate the danger, I created a small project. In the TestPage (Stateful Widget), I'm refreshing it every 500 ms, so the build function is called frequently. Then I made 2 buttons, both open a dialog then try to pop the current page (like you described in the question). One of them stores the Navigator before opening the dialog, the other one dangerously uses the BuildContext after the async call (like you described in the question). After clicking a button, if you sit and wait on the alert dialog for a few seconds, then exit it (by clicking anywhere outside the dialog), the safer button works as expected and pops the current page, while the other button does not.
The error it prints out is:
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: Looking up a
deactivated widget's ancestor is unsafe. At this point the state of
the widget's element tree is no longer stable. To safely refer to a
widget's ancestor in its dispose() method, save a reference to the
ancestor by calling dependOnInheritedWidgetOfExactType() in the
widget's didChangeDependencies() method.
#0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4032:9)
#1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4046:6)
#2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4093:12)
#3 Navigator.of (package:flutter/src/widgets/navigator.dart:2736:40)
#4 MyDangerousButton.build. (package:helloworld/main.dart:114:19)
Full source code demonstrating the problem:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Open Test Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => TestPage()),
);
},
),
),
);
}
}
class TestPage extends StatefulWidget {
#override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late final Timer timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
setState(() {});
});
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final time = DateTime.now().millisecondsSinceEpoch;
return Scaffold(
appBar: AppBar(title: Text('Test Page')),
body: Center(
child: Column(
children: [
Text('Current Time: $time'),
MySafeButton(key: UniqueKey()),
MyDangerousButton(key: UniqueKey()),
],
),
),
);
}
}
class MySafeButton extends StatelessWidget {
const MySafeButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Safely'),
onPressed: () async {
final navigator = Navigator.of(context);
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop();
},
);
}
}
class MyDangerousButton extends StatelessWidget {
const MyDangerousButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Dangerously'),
onPressed: () async {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
Navigator.of(context).pop();
},
);
}
}
Flutter ≥ 3.7 answer:
You can now use mounted on a StatelessWidget. This solution will not show linter warning:
onTap: () async {
bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */);
if (mounted && deleteConfirmed) {
Navigator.of(context).pop();
}
},
Alternatively, you can use context.mounted if outside of the widget.

open camera on flutter dart

hello i`m trying to open camera on flutter app but i'm getting I/flutter (12909): NoSuchMethodError: The getter 'name' was called on null.
I/flutter (12909): Receiver: null
I/flutter (12909): Tried calling: name
here is my code
`import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();
// 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;
// Attempt to take a picture and get the file `image`
// where it was saved.
final image = await _controller.takePicture();
// If the picture was taken, display it on a new screen.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DisplayPictureScreen(
// Pass the automatically generated path to
// the DisplayPictureScreen widget.
imagePath: image?.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)),
);
}
}
From https://flutter.dev/docs/cookbook/plugins/picture-using-camera#3-create-and-initialize-the-cameracontroller`

how to open camera on flutter

can anyone help me to open camera on flutter app but i'm getting I/flutter (12909): NoSuchMethodError: The getter 'name' was called on null. I/flutter (12909): Receiver: null I/flutter (12909): Tried calling: name
i dont know why
here is my code
`import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();
// 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;
// Attempt to take a picture and get the file `image`
// where it was saved.
final image = await _controller.takePicture();
// If the picture was taken, display it on a new screen.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DisplayPictureScreen(
// Pass the automatically generated path to
// the DisplayPictureScreen widget.
imagePath: image?.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)),
);
}
}
From https://flutter.dev/docs/cookbook/plugins/picture-using-camera#3-create-and-initialize-the-cameracontroller
I assume you have already added the camera package from pub site in your pubspec.yaml file and sync / get the dependencies.
In my case, I tested on my Android Emulator as well as Android Physical device too. The problem which I faced was from the app/build.gradle file where the camera plugin required the minSdkVersion 21 but By default it was minSdkVersion 16
Upgrading to the SdkVersion solves the first issue. Make sure to upgrade the minSdkVersion from 16 to 21 and should look like minSdkVersion 21
NOTE: Allow the required Runtime Permission required by the camera plugin.
I hope this solution will help you to solve your issue.