How to wait for camera to initialize? - flutter

I kept getting an error from the camera.dart that "name" was being called on a null object.
After some time, I realized that the problem was the that the build method is called before the async code in my initstate finished (I'm actually slightly proud that I understood the problem at least :))
I tried many different ways to initialize my camera properly, but I could not.
This is the last iteration of my code.
What's the idiomatic way of handling this future?
class _PicturePreviewState extends State<PicturePreview> {
List<CameraDescription> cameras;
CameraDescription camera;
CameraController cameraController;
Future<void> initializeController;
Future<void> getCameras() async {
try {
cameras = await availableCameras();
} catch(e) {print(e);}
camera = cameras.last;
print(camera);
}
#override
void initState() {
super.initState();
// getCameras();
availableCameras().then((availableCameras) {
cameras = availableCameras;
camera = cameras.first;
cameraController = CameraController(
camera,
ResolutionPreset.low,
);
initializeController = cameraController.initialize();
print(cameraController.value.isInitialized);
});
// cameraController = CameraController(
// camera,
// ResolutionPreset.low,
// );
// initializeController = cameraController.initialize();
// print(cameraController.value.isInitialized);
}
#override
void dispose() {
cameraController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<void>(
future: initializeController,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(cameraController);
}
else {
// Otherwise, display a loading indicator.
print(snapshot.connectionState);
return Center(child: CircularProgressIndicator());
}
},
),
I have been relying on this page to use the camera package, but I could not use it verbatim because I can't keep passing down the camera object down my widget tree.

I fixed it.
I put the initializing of the camera object in the in the parent of the widget.
class _TakeReceiptPictureState extends State<TakeReceiptPicture> {
List<CameraDescription> cameras;
CameraDescription camera;
#override
void initState() {
super.initState();
availableCameras().then((availableCameras) {
cameras = availableCameras;
camera = cameras.first;
});
}
Then made the widget that takes the picture have a parameter of type CameraDescription.
class PicturePreview extends StatefulWidget {
final CameraDescription camera;
const PicturePreview(this.camera, {Key key}) : super(key: key);
#override
_PicturePreviewState createState() => _PicturePreviewState();
}
Then passed the camera initialized in the parent to picture widget
onTap: () {
Navigator.of(context).push(
PageTransition(
type: PageTransitionType.transferRight,
child: PicturePreview(camera)),
);
}),
by the time the child widget's build method runs, the camera object is already initialized and ready to go.
Now the state of the child have only two variables, the camera controller and the initialize controller future.
CameraController cameraController;
Future<void> initializeController;
#override
void initState() {
super.initState();
cameraController = CameraController(
widget.camera,
ResolutionPreset.low,
);
initializeController = cameraController.initialize();
}
TLDR: let the initialization of the camera object be the responsibility of the parent of the widget.

Related

I want to call images and audio lists in order using AudioPlayer()

I'm going to make an app that plays a list of images and audio in order. After audio playback, the following images and audio are played.
I update image to setState() when the playback is completed. And each time use setState(), I used play() function inside the build() method to recall play().
However, the method in the build is called twice, so the audio file is also played twice.
How do I run this method only once when I build it?
class PlayingScreen extends StatefulWidget {
final PostModel postModel;
PlayingScreen({
Key? key,
required this.postModel,
}) : super(key: key);
#override
State<PlayingScreen> createState() => _PlayingScreenState();
}
class _PlayingScreenState extends State<PlayingScreen> {
final _audioPlayer = AudioPlayer();
List<String> _audios = []; // audio file path list
int _playIndex = 0;
#override
void initState() {
_getAudios();
super.initState();
}
#override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_play(_playIndex);
return Scaffold(
appBar: AppBar(
title: Text(widget.postModel.title),
),
body: InkWell(
onTap: () {
Get.back();
},
// image widget
),
),
);
}
Future _play(int index) async {
await _audioPlayer.play(DeviceFileSource(_audios[index]));
logger.d("current index: $_playIndex / ${_audios.length - 1}");
//complete event
_audioPlayer.onPlayerComplete.listen((event) {
logger.d("complet, next");
setState(() {
if (_playIndex >= _imageSets.length - 1) {
_playIndex = 0;
logger.d("return first index");
} else {
_playIndex++;
}
});
});
}
}
// ...
int _playIndex = 0;
bool played = false
// ...
// build
if(! played) {
_play(_playIndex);
played = true;
}
Add the variable "played" at the class level
Add a check in the build method
Calling a future inside the build method will always rebuild multiple times not only twice.
I would probably use it in a FutureBuilder() like so:
FutureBuilder(
future:_play(_playIndex),
builder:(context, snapshot){
...
}
)

How to initialize CameraController with SharedPreferences?

First I have initialized my camera controller (camera: ^0.9.4+11) like this and it works:
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
const TakePictureScreen({required Key key, required this.camera})
: super(key: key);
#override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
late CameraController _controller;
late Future<void> _initializeControllerFuture;
#override
void initState() {
super.initState();
_controller = CameraController(
widget.camera,
ResolutionPreset.max, // TODO: this should come from SharedPreferences
);
_initializeControllerFuture = _controller.initialize();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
return (snapshot.connectionState == ConnectionState.done)
? CameraPreview(_controller)
: Text("");
}
),
);
}
}
But now I want to load the ResolutionPreset dynamically from SharedPreferences (shared_preferences: ^2.0.13).
What's a good way to do this?
I failed when trying it like this (adding some variables and changing initState method):
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
late Future<int> _resolutionIndex;
List<ResolutionPreset> resolutions = ResolutionPreset.values.toList(growable: false);
static const String sharedPrefResolution = "resolution";
#override
void initState() {
super.initState();
_resolutionIndex = _prefs.then((SharedPreferences prefs) {
int resolutionIndex = prefs.getInt(sharedPrefResolution) ?? (resolutions.length - 1);
_controller = CameraController(
widget.camera,
resolutions[resolutionIndex],
);
_initializeControllerFuture = _controller.initialize();
return resolutionIndex;
});
}
Getting the error: LateInitializationError: Field '_initializeControllerFuture#19039262' has not been initialized.
initState can't be an async method, and getting a value from SharedPreferences is an async function. You can't use await (or then) in initState, I mean you can use but the code execution will not wait for this to complete. So what happens here is that your build method will run earlier than the future getting the value from SharedPreferences completes. And as I presume your _initializeControllerFuture is marked as late, so when your build tries to use it, it is still null, and that will get you this error.
The common way to solve this issue is to use a FutureBuilder. Get the values from SharedPreferences with FutureBuilder, display a progress indicator while it is being loaded (it will be quick so if you think you can skip this part), and then when you get the value from it, build your widget using the value coming from SharedPreferences, and initialize CameraController only after this.

Async Data Initialization in initState

I'm calling an async method getMyLocation() to get my current location in my initState(). The method can take a while...
I wanted to understand the behavior of initState() in these cases. Does the method still execute in the background as build() renders or does initState() timeout since it needs to complete before build() renders?
In my build() I have a statement checking if my latitude is null, in which case I return a Loading() widget. Sometimes Screen() renders and sometimes Loading() goes on indefinitely. I am assuming sometimes the getMyLocation() successfully executes during initState() and sometimes it timesout?
#override
void initState() {
super.initState();
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = userData.getUser();
userData.getMyLocation();
}
getMyLocation() async {
_myUser.longitude = await getCurrentLongitude();
_myUser.latitute = await getCurrentLatitude();
notifyListeners();
}
Widget build(BuildContext context) {
final userData = Provider.of<MyUser>(context);
final myUser = userData.getUser();
myUser.latitude == null?
return Loading()
: return Screen()
Great question. First of all, initState() runs synchronously, it prepares various things needed for build() method to run properly. If you are executing some async function here, it will just return a Future because you can't await it in the initState(). In your case you probably need a FutureBuilder. The "proper way" of dealing with futures would be something like:
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<void> getMyLocation() async {
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = await userData.getUser();
// if getUser() is async then we have to await
myUser.longitude = await getCurrentLongitude();
myUser.latitute = await getCurrentLatitude();
// notifyListeners();
// You probably do not need this, should be done in provider methods instead
}
Widget build(BuildContext context) {
return FutureBuilder(
future: getMyLocation(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return SomeErrorWidget();
}
if (snapshot.hasData) {
return Screen(snapshot.data);
}
return SomeLoadingWidget();
});
}

Flutter LateError on controller has not been initialized

When I try to call my widget it's showing an error on the controller that _controller is not initialized I try to set it in initstate.
class CameraApp extends StatefulWidget {
final dynamic loadingWidget;
CameraApp(this.loadingWidget);
_CameraAppState createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> with WidgetsBindingObserver {
late List<CameraDescription> _cameras;
late CameraController _controller;
int _selected = 0;
#override
void initState() {
CameraController _controller;
super.initState();
setupCamera();
WidgetsBinding.instance!.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance!.addObserver(this);
_controller.dispose();
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (_controller == null || !_controller.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
_controller.dispose();
} else if (state == AppLifecycleState.resumed) {
setupCamera();
}
}
#override
Widget build(BuildContext context) {
if (_controller == null) {
if (widget.loadingWidget != null) {
return widget.loadingWidget;
} else {
return Container(
color: Colors.black,
);
}
} else {
return CameraPreview(_controller);
}
}
Future<void> setupCamera() async {
await [
Permission.camera,
].request();
_cameras = await availableCameras();
var controller = await selectCamera();
setState(() => _controller = controller);
}
selectCamera() async {
var controller =
CameraController(_cameras[_selected], ResolutionPreset.max);
await controller.initialize();
return controller;
}
toggleCamera() async {
int newSelected = (_selected + 1) % _cameras.length;
_selected = newSelected;
var controller = await selectCamera();
setState(() => _controller = controller);
}
}
I am showing this camera on some widgets but don't figure out how to solve this issue. Maybe because of late it's causing an issue. Showing every time when its load i also try to add contoller.initialize(); in initstate but not working
LateError means a variable declared using the late keyword has not been initialized by the time you try to use it, as a general rule, I try to never use the late keyword unless there is no better way to achieve what I want because it tends to cause hard to find errors.
So you have two late variables, _controller and _cameras.
both initialize on the setupCamera method, which is asynchronous and gets called on initState, but the problem I believe is that initState does not wait for them to finish initializing before running build, where you try to read _controller and, because you have yet to assign it, you get a LateError.
If my assertion is correct, it should be a relatively simple fix:
from:
late List<CameraDescription> _cameras;
late CameraController _controller;
to:
List<CameraDescription> _cameras = []; // could also be null I guess.
CameraController _controller = null;
You already have null checks everywhere in case _controller is null, I believe you should take advantage of that so that if build runs before _controller has a value assigned, you get the loading widget.
CameraController _controller = null;
It can't take null value.

Result of consuming a Future with future.then() seems to only live inside the .then function()

I am building an app as a project for university and one of the requirements is to play videos within the app.
I have links to exercise videos (bicep curls and so on) stored in a column of a table in SQLite.
I am using Moor in order to interact with the database.
I have the following screen where I am trying to have the video referred in the link from the database play:
class ExerciseVideoTab extends StatefulWidget {
final int exerciseId;
ExerciseVideoTab(this.exerciseId);
#override
_ExerciseVideoTabState createState() => _ExerciseVideoTabState();
}
class _ExerciseVideoTabState extends State<ExerciseVideoTab> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
String _exerciseVideoLink;
#override
void initState() {
super.initState();
locator<MoorDB>().getExerciseById(widget.exerciseId).then((value) =>
_exerciseVideoLink = value.exerciseVideoLink);
_controller = VideoPlayerController.network(_exerciseVideoLink.toString());
_initializeVideoPlayerFuture = _controller.initialize();
print(_exerciseVideoLink); // prints null for some reason
}
#override
void dispose() {
// Ensure disposing of the VideoPlayerController to free up resources.
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the VideoPlayerController has finished initialization, use
// the data it provides to limit the aspect ratio of the video.
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
// Use the VideoPlayer widget to display the video.
child: VideoPlayer(_controller),
);
} else {
// If the VideoPlayerController is still initializing, show a
// loading spinner.
return Center(child: CircularProgressIndicator());
}
}
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Wrap the play or pause in a call to `setState`. This ensures the
// correct icon is shown.
setState(() {
// If the video is playing, pause it.
if (_controller.value.isPlaying) {
_controller.pause();
} else {
// If the video is paused, play it.
_controller.play();
}
});
},
// Display the correct icon depending on the state of the player.
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}
I am trying to consume the Future<Exercise> that is returned by the getExerciseById(int id) method and assign the exerciseVideoLink column value to the local _exerciseVideoLink and then use that String to initialize the VideoPlayerController with the link contained within.
The implementation of getExerciseById(int id) is the following:
Future<Exercise> getExerciseById(int id) {
return (select(exercises)..where((exercise) => exercise.exerciseId.equals(id))).getSingle();
}
My problem right now is that after consuming the Future<Exercise> and assigning its exerciseVideoLink attribute to the local String variable, the variable becomes null as soon as the .then((value) => ... function is over and thus, the initialization of the VideoPlayerController fails because the URI is null.
Why is that? How can I make it so that I can consume the Future<Exercise> and use its exerciseVideoLink in order to pass it to the VideoPlayerController?
Your _controller depends on the result of getExerciseById() so you need to wait for that Future to complete before you can assign it. You may find async/await syntax a bit easier to read when working with lots of nested Futures.
An example implementation could be:
#override
void initState() {
super.initState();
_init(); // split out a separate method, initState cannot be async
}
Future<void> _init() async {
final exercise = await locator<MoorDB>().getExerciseById(widget.exerciseId);
_controller = VideoPlayerController.network(exercise.exerciseVideoLink.toString());
_initializeVideoPlayerFuture = _controller.initialize();
}