From 92dc398ef8385c725316dfd513a32ea45265d7f1 Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Wed, 27 Feb 2019 15:29:24 +0100 Subject: [PATCH 1/5] Beginning of camera recipe --- src/docs/cookbook/persistence/key-value.md | 4 +- src/docs/cookbook/plugins/index.md | 5 + .../cookbook/plugins/picture-using-camera.md | 205 ++++++++++++++++++ .../testing/integration/introduction.md | 4 +- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 src/docs/cookbook/plugins/index.md create mode 100644 src/docs/cookbook/plugins/picture-using-camera.md diff --git a/src/docs/cookbook/persistence/key-value.md b/src/docs/cookbook/persistence/key-value.md index a3e9ecb2c1..dde094bf26 100644 --- a/src/docs/cookbook/persistence/key-value.md +++ b/src/docs/cookbook/persistence/key-value.md @@ -4,8 +4,8 @@ prev: title: Reading and Writing Files path: /docs/cookbook/persistence/reading-writing-files next: - title: An introduction to integration testing - path: /docs/cookbook/testing/integration/introduction + title: Take a picture using the Camera + path: /docs/cookbook/plugins/picture-with-camera --- If you have a relatively small collection of key-values that you'd like diff --git a/src/docs/cookbook/plugins/index.md b/src/docs/cookbook/plugins/index.md new file mode 100644 index 0000000000..8ec66e28d2 --- /dev/null +++ b/src/docs/cookbook/plugins/index.md @@ -0,0 +1,5 @@ +--- +title: Plugins +--- + +{% include cookbook_group_index.md %} diff --git a/src/docs/cookbook/plugins/picture-using-camera.md b/src/docs/cookbook/plugins/picture-using-camera.md new file mode 100644 index 0000000000..efc4fd523d --- /dev/null +++ b/src/docs/cookbook/plugins/picture-using-camera.md @@ -0,0 +1,205 @@ +--- +title: Take a picture using the Camera +prev: + title: Storing key-value data on disk + path: /docs/cookbook/persistence/key-value +next: + title: An introduction to integration testing + path: /docs/cookbook/testing/integration/introduction +--- + + + + +If you have a relatively small collection of key-values that you'd like +to save, you can use the +[shared_preferences]({{site.pub}}/packages/shared_preferences) plugin. + +Normally you would have to write native platform integrations for storing +data on both platforms. Fortunately, the +[shared_preferences]({{site.pub-pkg}}/shared_preferences) +plugin can be used to persist key-value data on disk. The shared preferences +plugin wraps `NSUserDefaults` on iOS and `SharedPreferences` on Android, +providing a persistent store for simple data. + +## Directions + + 1. Add the dependency + 2. Save Data + 3. Read Data + 4. Remove Data + +## 1. Add the dependency + +Before starting, you need to add the +[shared_preferences]({{site.pub-pkg}}/shared_preferences) +plugin to the `pubspec.yaml` file: + +```yaml +dependencies: + flutter: + sdk: flutter + shared_preferences: "" +``` + +## 2. Save data + +To persist data, use the setter methods provided by the +`SharedPreferences` class. Setter methods are available for various primitive +types, such as `setInt`, `setBool`, and `setString`. + +Setter methods do two things: First, synchronously update the key-value pair +in-memory. Then, persist the data to disk. + + +```dart +// obtain shared preferences +final prefs = await SharedPreferences.getInstance(); + +// set value +prefs.setInt('counter', counter); +``` + +## 3. Read data + +To read data, use the appropriate getter method provided by the +`SharedPreferences` class. For each setter there is a corresponding getter. +For example, you can use the `getInt`, `getBool`, and `getString` methods. + + +```dart +final prefs = await SharedPreferences.getInstance(); + +// Try reading data from the counter key. If it does not exist, return 0. +final counter = prefs.getInt('counter') ?? 0; +``` + +## 4. Remove data + +To delete data, use the `remove` method. + + +```dart +final prefs = await SharedPreferences.getInstance(); + +prefs.remove('counter'); +``` + +## Supported types + +While it is easy and convenient to use key-value storage, it has limitations: + +* Only primitive types can be used: `int`, `double`, `bool`, `string` and + `stringList` +* It's not designed to store a lot of data. + +For more information about Shared Preferences on Android, see +[Shared preferences +documentation]({{site.android-dev}}/guide/topics/data/data-storage#pref) +on the Android developers website. + +## Testing support + +It can be a good idea to test code that persists data using +`shared_preferences`. To do so, you'll need to mock out the +`MethodChannel` used by the `shared_preferences` library. + +You can populate `SharedPreferences` with initial values in your tests +by running the following code in a `setupAll` method in your test files: + + +```dart +const MethodChannel('plugins.flutter.io/shared_preferences') + .setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'getAll') { + return {}; // set initial values here if desired + } + return null; + }); +``` + +## Example + +```dart +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + // This widget is the root of the application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Shared preferences demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Shared preferences demo'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + @override + void initState() { + super.initState(); + _loadCounter(); + } + + //Loading counter value on start + _loadCounter() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + _counter = (prefs.getInt('counter') ?? 0); + }); + } + + //Incrementing counter after click + _incrementCounter() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + _counter = (prefs.getInt('counter') ?? 0) + 1; + prefs.setInt('counter', _counter); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.display1, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} +``` diff --git a/src/docs/cookbook/testing/integration/introduction.md b/src/docs/cookbook/testing/integration/introduction.md index dfc8af8ad4..cf699c9aea 100644 --- a/src/docs/cookbook/testing/integration/introduction.md +++ b/src/docs/cookbook/testing/integration/introduction.md @@ -2,8 +2,8 @@ title: An introduction to integration testing short-title: Introduction prev: - title: Storing key-value data on disk - path: /docs/cookbook/persistence/key-value + title: Take a picture using the Camera + path: /docs/cookbook/plugins/picture-with-camera next: title: Performance profiling path: /docs/cookbook/testing/integration/profiling From 765a5eda1a677914315cf3767e92e4b94f00cfd7 Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Mon, 4 Mar 2019 14:43:44 +0100 Subject: [PATCH 2/5] Adds recipe describing how to use the Camera plugin to take pictures --- .../cookbook/plugins/picture-using-camera.md | 408 ++++++++++++------ 1 file changed, 285 insertions(+), 123 deletions(-) diff --git a/src/docs/cookbook/plugins/picture-using-camera.md b/src/docs/cookbook/plugins/picture-using-camera.md index efc4fd523d..849f09c3e0 100644 --- a/src/docs/cookbook/plugins/picture-using-camera.md +++ b/src/docs/cookbook/plugins/picture-using-camera.md @@ -8,197 +8,359 @@ next: path: /docs/cookbook/testing/integration/introduction --- +Many apps require working with the device's cameras to take photos and videos. +Flutter provides the [`camera`](https://pub.dartlang.org/packages/camera) plugin +for this purpose. The `camera` plugin provides tools to get a list of the +available cameras, display a preview coming from a specific camera, and take +photos or videos. - - -If you have a relatively small collection of key-values that you'd like -to save, you can use the -[shared_preferences]({{site.pub}}/packages/shared_preferences) plugin. - -Normally you would have to write native platform integrations for storing -data on both platforms. Fortunately, the -[shared_preferences]({{site.pub-pkg}}/shared_preferences) -plugin can be used to persist key-value data on disk. The shared preferences -plugin wraps `NSUserDefaults` on iOS and `SharedPreferences` on Android, -providing a persistent store for simple data. +This recipe demonstrates how to use the `camera` plugin to display a preview, +take a photo, and display it. ## Directions - 1. Add the dependency - 2. Save Data - 3. Read Data - 4. Remove Data + 1. Add the required dependencies + 2. Get a list of the available cameras + 3. Create and initialize the `CameraController` + 4. Use a `CameraPreview` to display the camera's feed + 5. Take a picture with the `CameraController` + 6. Display the picture with an `Image` Widget + +## 1. Add the required dependencies -## 1. Add the dependency +To complete this recipe, you need to add three dependencies to your app: -Before starting, you need to add the -[shared_preferences]({{site.pub-pkg}}/shared_preferences) -plugin to the `pubspec.yaml` file: + - [`camera`](https://pub.dartlang.org/packages/camera) - Provides tools to work with the cameras on device + - [`path_provider`](https://pub.dartlang.org/packages/path_provider) - Finds the correct paths to store images + - [`path`](https://pub.dartlang.org/packages/path) - Creates paths that work on any platform ```yaml dependencies: flutter: sdk: flutter - shared_preferences: "" + camera: + path_provider: + path: ``` -## 2. Save data +## 2. Get a list of the available cameras -To persist data, use the setter methods provided by the -`SharedPreferences` class. Setter methods are available for various primitive -types, such as `setInt`, `setBool`, and `setString`. - -Setter methods do two things: First, synchronously update the key-value pair -in-memory. Then, persist the data to disk. +Next, you can get a list of available cameras using the `camera` plugin. ```dart -// obtain shared preferences -final prefs = await SharedPreferences.getInstance(); +// Obtain a list of the available cameras on the device. +final cameras = await availableCameras(); -// set value -prefs.setInt('counter', counter); +// Get a specific camera from the list of available cameras +final firstCamera = cameras.first; ``` -## 3. Read data +## 3. Create and initialize the `CameraController` + +Once you have a camera to work with, you need to create and initialize a +`CameraController`. This process establishes a connection to the device's camera +that allows you to control the camera and display a preview of the camera's +feed. -To read data, use the appropriate getter method provided by the -`SharedPreferences` class. For each setter there is a corresponding getter. -For example, you can use the `getInt`, `getBool`, and `getString` methods. +To achieve this, please: + 1. Create a `StatefulWidget` with a companion `State` class + 2. Add a variable to the `State` class to store the `CameraController` + 3. Add a variable to the `State` class to store the `Future` returned from + `CameraController.initialize` + 4. Create and initialize the controller in the `initState` method + 5. Dispose of the controller in the `dispose` method + ```dart -final prefs = await SharedPreferences.getInstance(); +// A screen that takes in a list of Cameras and the Directory to store images. +class TakePictureScreen extends StatefulWidget { + final CameraDescription camera; -// Try reading data from the counter key. If it does not exist, return 0. -final counter = prefs.getInt('counter') ?? 0; -``` + const TakePictureScreen({ + Key key, + @required this.camera, + }) : super(key: key); -## 4. Remove data + @override + TakePictureScreenState createState() => TakePictureScreenState(); +} -To delete data, use the `remove` method. +class TakePictureScreenState extends State { + // Add two variables to the state class to store the CameraController and + // the Future + CameraController _controller; + Future _initializeControllerFuture; - -```dart -final prefs = await SharedPreferences.getInstance(); + @override + void initState() { + super.initState(); + // In order to display the current output from the Camera, you need to + // 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, you need to initialize the controller. This will return a Future + _initializeControllerFuture = _controller.initialize(); + } -prefs.remove('counter'); + @override + void dispose() { + // Make sure to dispose of the controller when the Widget is disposed + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Fill this out in the next steps + } +} ``` -## Supported types +{{site.alert.warning}} +If you do not initialize the `CameraController`, you will *not* be able to work with +the camera. +{{site.alert.end}} + +## 4. Use a `CameraPreview` to display the camera's feed -While it is easy and convenient to use key-value storage, it has limitations: +Next, you can use the `CameraPreview` Widget from the `camera` package to +display a preview of the camera's feed. -* Only primitive types can be used: `int`, `double`, `bool`, `string` and - `stringList` -* It's not designed to store a lot of data. +Remember: You must wait until the controller has finished initializing before +working with the camera. Therefore, you must wait for the +`_initializeControllerFuture` created in the previous step to complete before +showing a `CameraPreview`. + +You can use a +[`FutureBuilder`](https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html) +for exactly this purpose. + + +```dart +// You must 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 +FutureBuilder( + 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()); + } + }, +) +``` -For more information about Shared Preferences on Android, see -[Shared preferences -documentation]({{site.android-dev}}/guide/topics/data/data-storage#pref) -on the Android developers website. +## 5. Take a picture with the `CameraController` -## Testing support +You can also use the `CameraController` to take pictures using the +[`takePicture`](https://pub.dartlang.org/documentation/camera/latest/camera/CameraController/takePicture.html) +method. In this example, create a `FloatingActionButton` that takes a picture +using the `CameraController` when a user taps on the button. -It can be a good idea to test code that persists data using -`shared_preferences`. To do so, you'll need to mock out the -`MethodChannel` used by the `shared_preferences` library. +Saving a picture requires 3 steps: -You can populate `SharedPreferences` with initial values in your tests -by running the following code in a `setupAll` method in your test files: + 1. Ensure the camera is initialized + 2. Construct a path that defines where the picture should be saved + 3. Use the controller to take a picture and save the result to the path + +It is good practice to wrap these operations in a `try / catch` block in order +to handle any errors that might occur. ```dart -const MethodChannel('plugins.flutter.io/shared_preferences') - .setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'getAll') { - return {}; // set initial values here if desired +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 the camera is initialized + await _initializeControllerFuture; + + // Construct the path where the image should be saved using the path + // package. + final path = join( + // In this example, 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); + } catch (e) { + // If an error occurs, log the error to the console. + print(e); } - return null; - }); + }, +) ``` +## 6. Display the picture with an `Image` Widget + +If you take the picture successfully, you can then display the saved picture +using an `Image` widget. In this case, the picture will be stored as a file on +the device. -## Example +Therefore, you must provide a `File` to the `Image.file` constructor. You +can create an instance of the `File` class by passing in the path you created in +the previous step. ```dart -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +Image.file(File('path/to/my/picture.png')) +``` -void main() => runApp(MyApp()); +## Complete Example -class MyApp extends StatelessWidget { - // This widget is the root of the application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Shared preferences demo', - theme: ThemeData( - primarySwatch: Colors.blue, +```dart +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 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, ), - home: MyHomePage(title: 'Shared preferences demo'), - ); - } + ), + ); } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - final String title; +// 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 - _MyHomePageState createState() => _MyHomePageState(); + TakePictureScreenState createState() => TakePictureScreenState(); } -class _MyHomePageState extends State { - int _counter = 0; +class TakePictureScreenState extends State { + CameraController _controller; + Future _initializeControllerFuture; @override void initState() { super.initState(); - _loadCounter(); - } + // In order to display the current output from the Camera, you need to + // create a CameraController. + _controller = CameraController( + // Get a specific camera from the list of available cameras + widget.camera, + // Define the resolution to use + ResolutionPreset.medium, + ); - //Loading counter value on start - _loadCounter() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - setState(() { - _counter = (prefs.getInt('counter') ?? 0); - }); + // Next, you need to initialize the controller. This will return a Future! + _initializeControllerFuture = _controller.initialize(); } - //Incrementing counter after click - _incrementCounter() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - setState(() { - _counter = (prefs.getInt('counter') ?? 0) + 1; - prefs.setInt('counter', _counter); - }); + @override + void dispose() { + // Make sure to dispose of the controller when the Widget is disposed + _controller.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.display1, - ), - ], - ), + appBar: AppBar(title: Text('Take a picture!')), + // You must 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( + 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( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + 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 the camera is initialized + await _initializeControllerFuture; + + // Construct the path where the image should be saved using the path + // package. + final path = join( + // In this example, 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)), ); } } From abb3289c868466774d7239242329f81425120895 Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Mon, 4 Mar 2019 19:18:50 +0100 Subject: [PATCH 3/5] Fix broken build --- example/pubspec.yaml | 1 + src/docs/cookbook/plugins/picture-using-camera.md | 1 + 2 files changed, 2 insertions(+) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 97949a26aa..c0d14092e5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,3 +15,4 @@ dev_dependencies: css_colors: 1.0.2 web_socket_channel: 1.0.9 sqflite: ^1.1.0 + camera: ^0.4.0+3 diff --git a/src/docs/cookbook/plugins/picture-using-camera.md b/src/docs/cookbook/plugins/picture-using-camera.md index 849f09c3e0..dc274358e8 100644 --- a/src/docs/cookbook/plugins/picture-using-camera.md +++ b/src/docs/cookbook/plugins/picture-using-camera.md @@ -217,6 +217,7 @@ Therefore, you must provide a `File` to the `Image.file` constructor. You can create an instance of the `File` class by passing in the path you created in the previous step. + ```dart Image.file(File('path/to/my/picture.png')) ``` From 85abc2f90c7a3064705993feb6073b50ab0d5fd5 Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Tue, 5 Mar 2019 10:52:25 +0100 Subject: [PATCH 4/5] Broken build fix #2 --- src/docs/cookbook/persistence/key-value.md | 2 +- src/docs/cookbook/testing/integration/introduction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/cookbook/persistence/key-value.md b/src/docs/cookbook/persistence/key-value.md index dde094bf26..d3cbd96f0b 100644 --- a/src/docs/cookbook/persistence/key-value.md +++ b/src/docs/cookbook/persistence/key-value.md @@ -5,7 +5,7 @@ prev: path: /docs/cookbook/persistence/reading-writing-files next: title: Take a picture using the Camera - path: /docs/cookbook/plugins/picture-with-camera + path: /docs/cookbook/plugins/picture-using-camera --- If you have a relatively small collection of key-values that you'd like diff --git a/src/docs/cookbook/testing/integration/introduction.md b/src/docs/cookbook/testing/integration/introduction.md index cf699c9aea..686a2ea499 100644 --- a/src/docs/cookbook/testing/integration/introduction.md +++ b/src/docs/cookbook/testing/integration/introduction.md @@ -3,7 +3,7 @@ title: An introduction to integration testing short-title: Introduction prev: title: Take a picture using the Camera - path: /docs/cookbook/plugins/picture-with-camera + path: /docs/cookbook/plugins/picture-using-camera next: title: Performance profiling path: /docs/cookbook/testing/integration/profiling From bd13f24a8212d1d817c2ff96bb67fee819d8b397 Mon Sep 17 00:00:00 2001 From: Brian Egan Date: Mon, 4 Mar 2019 19:05:55 +0100 Subject: [PATCH 5/5] Merge video_player recipe onto camera recipe --- example/pubspec.yaml | 1 + src/docs/cookbook/persistence/key-value.md | 4 +- .../cookbook/plugins/picture-using-camera.md | 4 +- src/docs/cookbook/plugins/play-video.md | 330 ++++++++++++++++++ 4 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 src/docs/cookbook/plugins/play-video.md diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c0d14092e5..18df322a63 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,3 +16,4 @@ dev_dependencies: web_socket_channel: 1.0.9 sqflite: ^1.1.0 camera: ^0.4.0+3 + video_player: ^0.10.0+2 diff --git a/src/docs/cookbook/persistence/key-value.md b/src/docs/cookbook/persistence/key-value.md index d3cbd96f0b..20a1195789 100644 --- a/src/docs/cookbook/persistence/key-value.md +++ b/src/docs/cookbook/persistence/key-value.md @@ -4,8 +4,8 @@ prev: title: Reading and Writing Files path: /docs/cookbook/persistence/reading-writing-files next: - title: Take a picture using the Camera - path: /docs/cookbook/plugins/picture-using-camera + title: Play and pause a video + path: /docs/cookbook/plugins/play-video --- If you have a relatively small collection of key-values that you'd like diff --git a/src/docs/cookbook/plugins/picture-using-camera.md b/src/docs/cookbook/plugins/picture-using-camera.md index dc274358e8..965465567e 100644 --- a/src/docs/cookbook/plugins/picture-using-camera.md +++ b/src/docs/cookbook/plugins/picture-using-camera.md @@ -1,8 +1,8 @@ --- title: Take a picture using the Camera prev: - title: Storing key-value data on disk - path: /docs/cookbook/persistence/key-value + title: Play and pause a video + path: /docs/cookbook/plugins/play-video next: title: An introduction to integration testing path: /docs/cookbook/testing/integration/introduction diff --git a/src/docs/cookbook/plugins/play-video.md b/src/docs/cookbook/plugins/play-video.md new file mode 100644 index 0000000000..839b43f431 --- /dev/null +++ b/src/docs/cookbook/plugins/play-video.md @@ -0,0 +1,330 @@ +--- +title: Play and pause a video +prev: + title: Storing key-value data on disk + path: /docs/cookbook/persistence/key-value +next: + title: Take a picture using the Camera + path: /docs/cookbook/plugins/picture-using-camera +--- + +Playing videos is a common task in app development, and Flutter apps are no +exception. In order to play videos, the Flutter team provides the +[`video_player`](https://pub.dartlang.org/packages/video_player) plugin. You can +use the `video_player` plugin to play videos stored on the file system, as an +asset, or from the internet. + +On iOS, the `video_player` plugin makes use of +[`AVPlayer`](https://developer.apple.com/documentation/avfoundation/avplayer) to +handle playback. On Android, it uses +[`ExoPlayer`](https://google.github.io/ExoPlayer/). + +This recipe demonstrates how to use the `video_player` package to stream a +video from the internet with basic play and pause controls. + +## Directions + + 1. Add the `video_player` dependency + 2. Add permissions to your app + 3. Create and initialize a `VideoPlayerController` + 4. Display the video player + 5. Play and pause the video + +## 1. Add the `video_player` dependency + +This recipe depends on one Flutter plugin: `video_player`. First, add this +dependency to your `pubspec.yaml`. + +```yaml +dependencies: + flutter: + sdk: flutter + video_player: +``` + +## 2. Add permissions to your app + +Next, you need to ensure your app has the correct permissions to stream videos +from the internet. To do so, update your `android` and `ios` configurations. + +### Android + +Add the following permission to the `AndroidManifest.xml` just after the +`` definition. The `AndroidManifest.xml` can be found at `/android/app/src/main/AndroidManifest.xml` + + +```xml + + + + + + + +``` + +### iOS + +For iOS, you need to add the following to your `Info.plist` file found at +`/ios/Runner/Info.plist`. + + +```xml +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +``` + +{{site.alert.warning}} +The `video_player` plugin does not work on iOS simulators. You must test videos +on real iOS devices. +{{site.alert.end}} + +## 3. Create and initialize a `VideoPlayerController` + +Now that you have the `video_player` plugin installed with the correct +permissions, you need to create a `VideoPlayerController`. The +`VideoPlayerController` class allows you to connect to different types of +videos and control playback. + +Before you can play videos, you must also `initialize` the controller. This will +establish the connection to the video and prepare the controller for playback. + +To create an initialize the `VideoPlayerController`, please: + + 1. Create a `StatefulWidget` with a companion `State` class + 2. Add a variable to the `State` class to store the `VideoPlayerController` + 3. Add a variable to the `State` class to store the `Future` returned from + `VideoPlayerController.initialize` + 4. Create and initialize the controller in the `initState` method + 5. Dispose of the controller in the `dispose` method + + +```dart +class VideoPlayerScreen extends StatefulWidget { + VideoPlayerScreen({Key key}) : super(key: key); + + @override + _VideoPlayerScreenState createState() => _VideoPlayerScreenState(); +} + +class _VideoPlayerScreenState extends State { + VideoPlayerController _controller; + Future _initializeVideoPlayerFuture; + + @override + void initState() { + // Create an store the VideoPlayerController. The VideoPlayerController + // offers several different constructors to play videos from assets, files, + // or the internet. + _controller = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4', + ); + + _initializeVideoPlayerFuture = _controller.initialize(); + + super.initState(); + } + + @override + void dispose() { + // Ensure you dispose the VideoPlayerController to free up resources + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Show the video in the next step + } +} +``` + +## 4. Display the video player + +Now, it's time to display the video. The `video_player` plugin provides the +[`VideoPlayer`](https://pub.dartlang.org/documentation/video_player/latest/video_player/VideoPlayer-class.html) +Widget to display the video initialized by the `VideoPlayerController`. By +default, the `VideoPlayer` Widget will take up as much space as possible. This +often isn't ideal for videos because they are meant to be displayed in a +specific aspect ratio, such as 16x9 or 4x3. + +Therefore, you can wrap the `VideoPlayer` widget in an +[`AspectRatio`](https://docs.flutter.io/flutter/widgets/AspectRatio-class.html) +widget to ensure the video is the correct proportions. + +Furthermore, you must display the `VideoPlayer` widget after the +`_initializeVideoPlayerFuture` completes. You can use a `FutureBuilder` to +display a loading spinner until finishes initializing. Note: initializing the +controller does not begin playback. + + +```dart +// Use a FutureBuilder to display a loading spinner while you wait for the +// VideoPlayerController to finish initializing. +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 VideoPlayer + 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()); + } + }, +) +``` + +## 5. Play and pause the video + +By default, the video will be displayed in a paused state. To start playback, +call the +[`play`](https://pub.dartlang.org/documentation/video_player/latest/video_player/VideoPlayerController/play.html) +method provided by the `VideoPlayerController`. To pause playback, call the +[`pause`](https://pub.dartlang.org/documentation/video_player/latest/video_player/VideoPlayerController/pause.html) +method. + +For this example, add a `FloatingActionButton` to your app that displays a play +or pause icon depending on the situation. When the user taps the button, play +the video if it's currently paused, or pause the video if it's playing. + + +```dart +FloatingActionButton( + onPressed: () { + // Wrap the play or pause in a call to `setState`. This will ensure + // 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, + ), +) +``` + +## Complete Example + +```dart +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +void main() => runApp(VideoPlayerApp()); + +class VideoPlayerApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Video Player Demo', + home: VideoPlayerScreen(), + ); + } +} + +class VideoPlayerScreen extends StatefulWidget { + VideoPlayerScreen({Key key}) : super(key: key); + + @override + _VideoPlayerScreenState createState() => _VideoPlayerScreenState(); +} + +class _VideoPlayerScreenState extends State { + VideoPlayerController _controller; + Future _initializeVideoPlayerFuture; + + @override + void initState() { + // Create and store the VideoPlayerController. The VideoPlayerController + // offers several different constructors to play videos from assets, files, + // or the internet. + _controller = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4', + ); + + // Initialize the controller and store the Future for later use + _initializeVideoPlayerFuture = _controller.initialize(); + + // Use the controller to loop the video + _controller.setLooping(true); + + super.initState(); + } + + @override + void dispose() { + // Ensure you dispose the VideoPlayerController to free up resources + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Butterfly Video'), + ), + // Use a FutureBuilder to display a loading spinner while you wait for the + // VideoPlayerController to finish initializing. + body: 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 will ensure + // 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, + ), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} +```