序言
众所周知,Flutter是一个UI库,当涉及到硬件时不可避免的需要和原生交互,如果每一个功能都需要自己编写交互逻辑那也太反锁了,pub.dev是flutter的插件平台,几乎所有的常用插件都可以找到。
下面给大家推荐几个插件
GPS
一个 Flutter 地理定位插件,它能便捷地访问特定平台的定位服务(在 Android 上是 FusedLocationProviderClient,若该服务不可用则使用 LocationManager;在 iOS 上是 CLLocationManager)。

下面的代码展示了一个获取设备当前位置的示例,包括检查位置服务是否已启用以及检查 / 请求访问设备位置的权限:
kotlin
import 'package:geolocator/geolocator.dart';
/// Determine the current position of the device.
///
/// When the location services are not enabled or permissions
/// are denied the `Future` will return an error.
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
// Test if location services are enabled.
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Location services are not enabled don't continue
// accessing the position and request users of the
// App to enable the location services.
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// Permissions are denied, next time you could try
// requesting permissions again (this is also where
// Android's shouldShowRequestPermissionRationale
// returned true. According to Android guidelines
// your App should show an explanatory UI now.
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
// Permissions are denied forever, handle appropriately.
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
}
// When we reach here, permissions are granted and we can
// continue accessing the position of the device.
return await Geolocator.getCurrentPosition();
}
更多API可查阅插件说明文档
camera
一个 Flutter 插件,用于从图片库中选取图片以及用相机拍摄新照片。
下面是此插件的使用案例
kotlin
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:video_player/video_player.dart';
class ImagePickPage extends StatefulWidget {
const ImagePickPage({super.key, this.title});
final String? title;
@override
State<ImagePickPage> createState() => _ImagePickPageState();
}
class _ImagePickPageState extends State<ImagePickPage> {
List<XFile>? _mediaFileList;
void _setImageFileListFromFile(XFile? value) {
_mediaFileList = value == null ? null : <XFile>[value];
}
dynamic _pickImageError;
bool isVideo = false;
VideoPlayerController? _controller;
VideoPlayerController? _toBeDisposed;
String? _retrieveDataError;
final ImagePicker _picker = ImagePicker();
final TextEditingController maxWidthController = TextEditingController();
final TextEditingController maxHeightController = TextEditingController();
final TextEditingController qualityController = TextEditingController();
final TextEditingController limitController = TextEditingController();
Future<void> _playVideo(XFile? file) async {
if (file != null && mounted) {
await _disposeVideoController();
final VideoPlayerController controller;
if (kIsWeb) {
controller = VideoPlayerController.networkUrl(Uri.parse(file.path));
} else {
controller = VideoPlayerController.file(File(file.path));
}
_controller = controller;
// In web, most browsers won't honor a programmatic call to .play
// if the video has a sound track (and is not muted).
// Mute the video so it auto-plays in web!
// This is not needed if the call to .play is the result of user
// interaction (clicking on a "play" button, for example).
const double volume = kIsWeb ? 0.0 : 1.0;
await controller.setVolume(volume);
await controller.initialize();
await controller.setLooping(true);
await controller.play();
setState(() {});
}
}
Future<void> _onImageButtonPressed(
ImageSource source, {
required BuildContext context,
bool allowMultiple = false,
bool isMedia = false,
}) async {
if (_controller != null) {
await _controller!.setVolume(0.0);
}
if (context.mounted) {
if (isVideo) {
final List<XFile> files;
if (allowMultiple) {
files = await _picker.pickMultiVideo();
} else {
final XFile? file = await _picker.pickVideo(
source: source,
maxDuration: const Duration(seconds: 10),
);
files = <XFile>[if (file != null) file];
}
// Just play the first file, to keep the example simple.
await _playVideo(files.firstOrNull);
} else if (allowMultiple) {
await _displayPickImageDialog(context, true, (
double? maxWidth,
double? maxHeight,
int? quality,
int? limit,
) async {
try {
final List<XFile> pickedFileList = isMedia
? await _picker.pickMultipleMedia(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
limit: limit,
)
: await _picker.pickMultiImage(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
limit: limit,
);
setState(() {
_mediaFileList = pickedFileList;
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else if (isMedia) {
await _displayPickImageDialog(context, false, (
double? maxWidth,
double? maxHeight,
int? quality,
int? limit,
) async {
try {
final List<XFile> pickedFileList = <XFile>[];
final XFile? media = await _picker.pickMedia(
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
if (media != null) {
pickedFileList.add(media);
setState(() {
_mediaFileList = pickedFileList;
});
}
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
} else {
await _displayPickImageDialog(context, false, (
double? maxWidth,
double? maxHeight,
int? quality,
int? limit,
) async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: source,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
setState(() {
_setImageFileListFromFile(pickedFile);
});
} catch (e) {
setState(() {
_pickImageError = e;
});
}
});
}
}
}
@override
void deactivate() {
if (_controller != null) {
_controller!.setVolume(0.0);
_controller!.pause();
}
super.deactivate();
}
@override
void dispose() {
_disposeVideoController();
maxWidthController.dispose();
maxHeightController.dispose();
qualityController.dispose();
super.dispose();
}
Future<void> _disposeVideoController() async {
if (_toBeDisposed != null) {
await _toBeDisposed!.dispose();
}
_toBeDisposed = _controller;
_controller = null;
}
Widget _previewVideo() {
final Text? retrieveError = _getRetrieveErrorWidget();
if (retrieveError != null) {
return retrieveError;
}
if (_controller == null) {
return const Text(
'You have not yet picked a video',
textAlign: TextAlign.center,
);
}
return Padding(
padding: const EdgeInsets.all(10.0),
child: AspectRatioVideo(_controller),
);
}
Widget _previewImages() {
final Text? retrieveError = _getRetrieveErrorWidget();
if (retrieveError != null) {
return retrieveError;
}
if (_mediaFileList != null) {
return Semantics(
label: 'image_picker_example_picked_images',
child: ListView.builder(
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
final String? mime = lookupMimeType(_mediaFileList![index].path);
// Why network for web?
// See https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform
return Semantics(
label: 'image_picker_example_picked_image',
child: kIsWeb
? Image.network(_mediaFileList![index].path)
: (mime == null || mime.startsWith('image/')
? Image.file(
File(_mediaFileList![index].path),
errorBuilder:
(
BuildContext context,
Object error,
StackTrace? stackTrace,
) {
return const Center(
child: Text(
'This image type is not supported',
),
);
},
)
: _buildInlineVideoPlayer(index)),
);
},
itemCount: _mediaFileList!.length,
),
);
} else if (_pickImageError != null) {
return Text(
'Pick image error: $_pickImageError',
textAlign: TextAlign.center,
);
} else {
return const Text(
'You have not yet picked an image.',
textAlign: TextAlign.center,
);
}
}
Widget _buildInlineVideoPlayer(int index) {
final VideoPlayerController controller = VideoPlayerController.file(
File(_mediaFileList![index].path),
);
const double volume = kIsWeb ? 0.0 : 1.0;
controller.setVolume(volume);
controller.initialize();
controller.setLooping(true);
controller.play();
return Center(child: AspectRatioVideo(controller));
}
Widget _handlePreview() {
if (isVideo) {
return _previewVideo();
} else {
return _previewImages();
}
}
Future<void> retrieveLostData() async {
final LostDataResponse response = await _picker.retrieveLostData();
if (response.isEmpty) {
return;
}
if (response.file != null) {
if (response.type == RetrieveType.video) {
isVideo = true;
await _playVideo(response.file);
} else {
isVideo = false;
setState(() {
if (response.files == null) {
_setImageFileListFromFile(response.file);
} else {
_mediaFileList = response.files;
}
});
}
} else {
_retrieveDataError = response.exception!.code;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title ?? "Image Pick")),
body: Center(
child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android
? FutureBuilder<void>(
future: retrieveLostData(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return const Text(
'You have not yet picked an image.',
textAlign: TextAlign.center,
);
case ConnectionState.done:
return _handlePreview();
case ConnectionState.active:
if (snapshot.hasError) {
return Text(
'Pick image/video error: ${snapshot.error}}',
textAlign: TextAlign.center,
);
} else {
return const Text(
'You have not yet picked an image.',
textAlign: TextAlign.center,
);
}
}
},
)
: _handlePreview(),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Semantics(
label: 'image_picker_example_from_gallery',
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'image0',
tooltip: 'Pick image from gallery',
child: const Icon(Icons.photo),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
allowMultiple: true,
);
},
heroTag: 'image1',
tooltip: 'Pick multiple images',
child: const Icon(Icons.photo_library),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
isMedia: true,
);
},
heroTag: 'media',
tooltip: 'Pick item from gallery',
child: const Icon(Icons.photo_outlined),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
allowMultiple: true,
isMedia: true,
);
},
heroTag: 'multipleMedia',
tooltip: 'Pick multiple items',
child: const Icon(Icons.photo_library_outlined),
),
),
if (_picker.supportsImageSource(ImageSource.camera))
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
onPressed: () {
isVideo = false;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'image2',
tooltip: 'Take a photo',
child: const Icon(Icons.camera_alt),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_onImageButtonPressed(ImageSource.gallery, context: context);
},
heroTag: 'video',
tooltip: 'Pick video from gallery',
child: const Icon(Icons.video_file),
),
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_onImageButtonPressed(
ImageSource.gallery,
context: context,
allowMultiple: true,
);
},
heroTag: 'multiVideo',
tooltip: 'Pick multiple videos',
child: const Icon(Icons.video_library),
),
),
if (_picker.supportsImageSource(ImageSource.camera))
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
isVideo = true;
_onImageButtonPressed(ImageSource.camera, context: context);
},
heroTag: 'takeVideo',
tooltip: 'Take a video',
child: const Icon(Icons.videocam),
),
),
],
),
);
}
Text? _getRetrieveErrorWidget() {
if (_retrieveDataError != null) {
final Text result = Text(_retrieveDataError!);
_retrieveDataError = null;
return result;
}
return null;
}
Future<void> _displayPickImageDialog(
BuildContext context,
bool isMulti,
OnPickImageCallback onPick,
) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Add optional parameters'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
controller: maxWidthController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
hintText: 'Enter maxWidth if desired',
),
),
TextField(
controller: maxHeightController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
hintText: 'Enter maxHeight if desired',
),
),
TextField(
controller: qualityController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
hintText: 'Enter quality if desired',
),
),
if (isMulti)
TextField(
controller: limitController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
hintText: 'Enter limit if desired',
),
),
],
),
actions: <Widget>[
TextButton(
child: const Text('CANCEL'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('PICK'),
onPressed: () {
final double? width = maxWidthController.text.isNotEmpty
? double.parse(maxWidthController.text)
: null;
final double? height = maxHeightController.text.isNotEmpty
? double.parse(maxHeightController.text)
: null;
final int? quality = qualityController.text.isNotEmpty
? int.parse(qualityController.text)
: null;
final int? limit = limitController.text.isNotEmpty
? int.parse(limitController.text)
: null;
onPick(width, height, quality, limit);
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
typedef OnPickImageCallback =
void Function(
double? maxWidth,
double? maxHeight,
int? quality,
int? limit,
);
class AspectRatioVideo extends StatefulWidget {
const AspectRatioVideo(this.controller, {super.key});
final VideoPlayerController? controller;
@override
AspectRatioVideoState createState() => AspectRatioVideoState();
}
class AspectRatioVideoState extends State<AspectRatioVideo> {
VideoPlayerController? get controller => widget.controller;
bool initialized = false;
void _onVideoControllerUpdate() {
if (!mounted) {
return;
}
if (initialized != controller!.value.isInitialized) {
initialized = controller!.value.isInitialized;
setState(() {});
}
}
@override
void initState() {
super.initState();
controller!.addListener(_onVideoControllerUpdate);
}
@override
void dispose() {
controller!.removeListener(_onVideoControllerUpdate);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (initialized) {
return Center(
child: AspectRatio(
aspectRatio: controller!.value.aspectRatio,
child: VideoPlayer(controller!),
),
);
} else {
return Container();
}
}
}
Shared Preferences
在安卓系统中,你可以使用 SharedPreferences API 存储一小组键值对。
在 Flutter 中,可以使用 Shared_Preferences 插件来访问此功能。该插件封装了 Shared Preferences 和 NSUserDefaults(iOS 上的等效功能)的功能。
shared_preferences 2.5.4
kotlin
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
Future<void> _incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
await prefs.setInt('counter', counter);
}
Sqlite
在安卓系统中,你可以使用 SQLite 来存储结构化数据,这些数据可以通过 SQL 进行查询。
在 Flutter 中,对于 macOS、Android 或 iOS 系统,可以使用 SQFlite 插件来获取此功能。
更多
插件系统类似于第三方库,如果有什么你认为比较复杂的功能,可以在pub上查找