flutter-permission_handler、flutter_blue(蓝牙使用)、image_picker、flutter_progress_hud

4,632 阅读9分钟

前言

本篇文章主要介绍 permission_handler、flutter_blue使用,案例中会使用 image_picker、flutter_progress_hud,因此后面两个也会简单介绍下

image_picker(选择照片或者拍照库)

flutter_progress_hud(hud加载库)

permission_handler(权限请求判断库)

flutter_blue(蓝牙库)]

image_gallery_saver(单纯的保存图片到相册)

案例demo

蓝牙广播外设demo-swift版本:如果想念 Object-C 版本,前面也有,这个就是那个翻译改动而来

flutter_progress_hud、image_picker

先简单介绍一下这两个,一个是加载框(flutter_progress_hud),一个是图片选择器(支持照片和图片)(flutter_progress_hud)

flutter_progress_hud

使用比较简单不多说了,用的也比较多,一般请求网络数据使用,如果还有不满足,可以搜索一下其他的,这个只是最基础的

Scaffold(
  appBar: AppBar(
    title: const Text("ProgressHUD"),
  ),
  //嵌套在外层,展示加载框时会在中间,并盖住
  //里面有些属性可以微调,可以点进去查看
  body: ProgressHUD(
    child: Container(),
  ),
);

显示或者隐藏

//显示HUD
ProgressHUD.of(context)?.show();
//显示带文字的HUD
ProgressHUD.of(context)?.showWithText(text);
//隐藏HUD
ProgressHUD.of(context)?.dismiss();

简单看一下效果吧

image.png

image_picker

这个用的也比较多,拍照、相册选择图片,一般只要用户头像、上传反馈的都会用到,也比较常见

import 'package:image_picker/image_picker.dart';

//声明参数
final ImagePicker _picker = ImagePicker();

使用相机拍摄照片

_picker.pickImage(source: ImageSource.camera).then((value) {
  print(value);
}).catchError((error) {
    //失败了走这里,可以在这里判断权限
    Permission.camera.status
});

使用相册选择图片

_picker.pickImage(source: ImageSource.gallery).then((value) {
  print(value);
}).catchError((error) {
  //注意相册使用的是这个权限
  Permission.photos.status
});

可以看到image_picker使用过程中设计到了权限,后面会给出权限的使用

permission_handler

介绍权限的使用,会给出几个案例,同时也是蓝牙使用时判断权限时会用到的一个库

permission_handler(权限请求判断库)

参数与基础使用

默认的权限状态

//常见的有下面几种状态
/*
denied, //没授权默认是这个,也保不准特殊情况是表示拒绝的,最好是先请求权限后用于判断
granted, //正常使用
restricted, //被操作系统拒绝,例如家长控制等
limited, //被限制了部分功能,适用于部分权限,例如相册的
permanentlyDenied, //这个权限表示永久拒绝,不显示弹窗,用户可以手动调节(也有可能是系统关闭了该权限导致的)
*/

请求权限,在使用前,可以通过 request 来请求权限,避免有些权限没办法给出准确提示,且可能出错

//提前请求权限,如果没给过权限,可以触发权限,以便于获取权限信息
final status = await Permission.camera.request(); //请求单个权限
Map<Permission, PermissionStatus> statuses = await [
  Permission.photosAddOnly,
  Permission.photos
].request(); //同时请求多个全新啊

//可以同时请求多个 await,被限制的部分权限理论也可以使用才是,根据情况作出判断
if (status != PermissionStatus.granted) {
  //无相机权限,请前往设置打开
  //openAppSettings();
  return;
}
//使用对应功能即可
await _picker.pickImage(source: ImageSource.camera)

直接获取权限,但是初次使用并不会触发对应的权限请求弹窗,因此可能判断出错,适用于使用对应功能失败后给出相应的提示

//直接拍照获取图片
_picker.pickImage(source: ImageSource.camera).then((value) {
  print(value);
}).catchError((error) {
  //可能用户拒绝了权限,因此会走到这里
  print(error);
  //可以请求用失败的时候在提示权限问题,或者打开授权页面
  Permission.camera.status.then((status) {
    print(status);
    if (status == PermissionStatus.denied || status == PermissionStatus.permanentlyDenied) {
      //拒绝,可以跳转权限页面
      showToast(context, "相机权限被拒绝");
    }else if (status == PermissionStatus.granted) {
      //正常访问
      showToast(context, "相机权限可以正常使用");
    }
  });
});

注意:不是所有的三方没给权限都会走到catch,也可能直接报错闪退,因此最好先判断权限后给出提示

//简单给出几个常用权限,里面还有很多
Map<Permission, PermissionStatus> statuses = await [
  Permission.camera, //相机
  Permission.photosAddOnly, //写入相册
  Permission.photos, //读取相册
  Permission.locationWhenInUse, //使用应用期间获取定位
  Permission.bluetooth, //获取蓝牙
].request();

跳转权限操作界面

如果用户没打开权限,可以提示给用户打开权限,下面可以跳转到应用的权限界面,方便用户更新权限

openAppSettings();

android权限声明

需要注意的是,使用什么权限搜索一下即可,使用如下所示(需要注意的是,其有些权限跟ios端是不一样的,例如)

image.png

<uses-permission android:name="android.permission.CAMERA" />
<!--相册的读写权限,跟ios端不一样哈,其他的有些也是,根据平台动态调整即可-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

ios端权限声明

info.plist 中设置对应权限即可,$(PRODUCT_BUNDLE_NAME)为app的名字,动态最好

image.png

<string>$(PRODUCT_BUNDLE_NAME)想在app使用期间获取您的定位,以便于更新位置</string>
<key>NSBluetoothPeripheralUsageDescription</key>

此外,还需要在 podfile 中加入下面的脚本(放到最后即可),根据对应权限需要,将 PERMISSION_???=1 前面的注释 # 去掉即可(上面的不要取消),这里假设去掉的是 camera

post_install do |installer|
  installer.pods_project.targets.each do |target|
    ... # Here are some configurations automatically generated by flutter

    # Start of the permission_handler configuration
    target.build_configurations.each do |config|

      # You can enable the permissions needed here. For example to enable camera
      # permission, just remove the `#` character in front so it looks like this:
      #
      # ## dart: PermissionGroup.camera
      # 'PERMISSION_CAMERA=1'
      #
      #  Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',

        ## dart: PermissionGroup.calendar
        # 'PERMISSION_EVENTS=1',

        ## dart: PermissionGroup.reminders
        # 'PERMISSION_REMINDERS=1',

        ## dart: PermissionGroup.contacts
        # 'PERMISSION_CONTACTS=1',

        ## dart: PermissionGroup.camera
        ## 假设这里面用到了相机权限,只取消掉其前面的注释即可
        'PERMISSION_CAMERA=1',

        ## dart: PermissionGroup.microphone
        # 'PERMISSION_MICROPHONE=1',

        ## dart: PermissionGroup.speech
        # 'PERMISSION_SPEECH_RECOGNIZER=1',

        ## dart: PermissionGroup.photos
        # 'PERMISSION_PHOTOS=1',

        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
        # 'PERMISSION_LOCATION=1',

        ## dart: PermissionGroup.notification
        # 'PERMISSION_NOTIFICATIONS=1',

        ## dart: PermissionGroup.mediaLibrary
        # 'PERMISSION_MEDIA_LIBRARY=1',

        ## dart: PermissionGroup.sensors
        # 'PERMISSION_SENSORS=1',   

        ## dart: PermissionGroup.bluetooth
        # 'PERMISSION_BLUETOOTH=1',

        ## dart: PermissionGroup.appTrackingTransparency
        # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',

        ## dart: PermissionGroup.criticalAlerts
        # 'PERMISSION_CRITICAL_ALERTS=1'
      ]

    end 
    # End of the permission_handler configuration
  end
end

再具体点的权限,都可以通过搜索一下获取即可,具体的就不多讲述了(ios的文档给出了一些,先贴出来方便大家使用吧)

PermissionInfo.plistMacro
PermissionGroup.calendarNSCalendarsUsageDescriptionPERMISSION_EVENTS
PermissionGroup.remindersNSRemindersUsageDescriptionPERMISSION_REMINDERS
PermissionGroup.contactsNSContactsUsageDescriptionPERMISSION_CONTACTS
PermissionGroup.cameraNSCameraUsageDescriptionPERMISSION_CAMERA
PermissionGroup.microphoneNSMicrophoneUsageDescriptionPERMISSION_MICROPHONE
PermissionGroup.speechNSSpeechRecognitionUsageDescriptionPERMISSION_SPEECH_RECOGNIZER
PermissionGroup.photosNSPhotoLibraryUsageDescriptionPERMISSION_PHOTOS
PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUseNSLocationUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription, NSLocationWhenInUseUsageDescriptionPERMISSION_LOCATION
PermissionGroup.notificationPermissionGroupNotificationPERMISSION_NOTIFICATIONS
PermissionGroup.mediaLibraryNSAppleMusicUsageDescription, kTCCServiceMediaLibraryPERMISSION_MEDIA_LIBRARY
PermissionGroup.sensorsNSMotionUsageDescriptionPERMISSION_SENSORS
PermissionGroup.bluetoothNSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescriptionPERMISSION_BLUETOOTH
PermissionGroup.appTrackingTransparencyNSUserTrackingUsageDescriptionPERMISSION_APP_TRACKING_TRANSPARENCY
PermissionGroup.criticalAlertsPermissionGroupCriticalAlertsPERMISSION_CRITICAL_ALERTS

flutter_blue 与 蓝牙权限

官方提供的蓝牙库,使用非常简单,使用前需要先判断权限

:ios系统有些版本是没有权限弹窗的,声明了就可以直接使用,但不代表不需要判断申请了,后面大多数版本还是需要用到的

flutter_blue(蓝牙库)

外设端demo:被连接的外设广播端,由swift编写,前面也有object-c编写的版本

蓝牙广播外设demo-swift版本:根据上面的广播端代码翻译改动而来(都是自己的😂)

本案例demo:中心设备端,在本案例中的bluetooth文件中

PS: 为什么没有 flutter 的广播端案例,因为没提供,flutter 跨平台作为一个中心设备连接已经很给力了,目前蓝牙对android、ios支持不错,对鸿蒙支持很拉跨😂

蓝牙权限配置

ios端配置

这里面配置就比较固定,只不过有一个alway权限的,表示允许后台持续运行蓝牙,如果不是必须的话无需填写,否则易造成审核问题,目前无需申请定位,也不用打开即可正常使用蓝牙

info.plist

<key>NSBluetoothAlwaysUsageDescription</key>
<string>$(PRODUCT_BUNDLE_NAME)想使用您的蓝牙,以便于持续跟设备连接发送信息</string>
<key>NSBluetoothPeripheralUsageDescription</key>
   <string>$(PRODUCT_BUNDLE_NAME)想使用您的蓝牙,以便于跟设备发送信息</string>

podfile

//取消这一行的注释即可
'PERMISSION_BLUETOOTH=1',

安卓端配置

安卓端还有点不一样,android 12 后,推出新权限,可以在在 manifest.xml 中填写如下代码,也可以在代码中动态申请

前面两个是兼容 android12 之前的版本,所以限制了最大版本,后面则是分开两个,需要注意的是,现在都需要申请定位权限了,否则功能无法正常使用

manifest.xml

<!-- android6之前使用该权限即可 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
    
<!-- Required if your app derives physical location from Bluetooth scan results.-->
<!-- android6之后需要使用蓝牙也要声明定位权限,且用户还要打开 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Request legacy Bluetooth permissions on older devices.-->
<!-- android6~12 之间使用该权限-->
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />

<!-- android12 之后权限分开-->
<!-- android12 扫描周边设备需要该权限-->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- android12 连接交互使用的权限-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- android12 广播功能,让别人也能连接搞设备,一般和connect一起使用-->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

build.gradle

Android {
  defaultConfig {
     minSdkVersion: 19

flutter 端中心设备使用

设备最为中心设备扫描周边设备,大致经过了下面几个阶段:

扫描 -> 连接 -> 查找服务 -> 查找特征 -> 使用特征进行交互

细节直接看代码即可,下面简单上一个权限的判断,android 实际还要请求定位,即:

ios 请求一个 bluetooth 权限,android 请求 bluetooth、bluetoothScan、bluetoothConnect、locationWhenInUse四个权限,自己动态设置即可

ps:除了要判断权限还有蓝牙是否打开,此外这里面权限判断不如原生端准确,一些系统版本的不需要权限,但是这里可能会返回拒绝,因此最好给这些特殊版本一些提示,以便于"没给权限"也能继续执行(否则他们永远无法使用蓝牙了,只能更新系统)

//仅仅是一个案例,实际可以写的严谨简单一些,先判断打开,后判断权限都可以,扫描前检测即可
//请求一下权限,某些机型蓝牙默认可以直接使用,无需权限,但是权限会反馈被拒绝状态
//因此一些机型,可以请求后给出继续向后执行的弹窗,避免用户永远无法使用该功能,失败了注意反馈即可
[Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetooth].request().then((status) {
  print(status);
  //只会有一个弹窗,android端多个权限,但一般一般显示了一个就结束了,主要看ios的
  //android端注意定位权限, Permission.locationWhenInUse 即可
  if (status[Permission.bluetooth] != PermissionStatus.granted) {
    print("没有蓝牙权限"); //这一个是都有的,可以用这个判断,当然最好分平台判断
  }
});

//还要判断蓝牙是否已经打开
_bluetooth.isOn.then((value) {
  //获取蓝牙打开状态
}); 

扫描、连接、查找服务和特征逻辑如下所示,此外还设置类的通知,以便于能够及时收到消息

//开始扫描并连接设备
void startConnectDevice({isRepeat = false}) {
  bool isSearched = false; //为了避免连接过程中没有停止扫描导致的多次连接问题,但不得不连接后在取消扫描
  try {
    _bluetooth.scanResults.listen((results) async {
      //扫描回调,try-catch是统一处理里面的失败情况
      if (isSearched) return; //已经找到了就结束
      for (ScanResult res in results) {
        final device = res.device;
        //检查是否是我们需要的设备,device.name有时候不一定会有,最好使用advertisementData.localName
        final name = res.advertisementData.localName;
        if (name != "") print(name);
        if (!isSearched && name.contains("marshal_")) {
          print("找到了我们需要的设备");
          isSearched = true; //标记已经找到了
          //找到了我们要连接的设备,并连接,autoConnect需要设置为 false,默认为true其不会自动连接
          print("开始连接");
          //鸿蒙这个 autoConnect 会一次也连接不上,设置为 false 则会出现时而连得上时而连不上,支持不太好,根据情况选择
          await device.connect(timeout: const Duration(seconds: 10), autoConnect: true);
          print("连接成功");
          // 连接成功后停止扫描,据说有些android手机停止扫描会出现连接功能异常,连接成功后在停止扫描
          await _bluetooth.stopScan();
          //开始查找服务
          print("开始查找服务");
          List<BluetoothService> services = await device.discoverServices();
          //遍历服务使用我们想要的特征值
          print("遍历服务找特征值");
          print(services.length);
          services.forEach((service) async {
            var characteristics = service.characteristics;
            //找我们的特征值
            for(BluetoothCharacteristic char in characteristics) {
              print(char.uuid.toString());
              //通过uuid过滤出我们需要的特征值
              if (char.uuid.toString().substring(0, 2) == "12") {
                print("找到我们特征值");
                //假设我们用这个,获取到特征值后保存,可以用来读写
                _currentCharacteristic = char;
                await setNotifiy();//设置通知,可以接收传递过来的消息
                await readMessage(); //随便读取一下消息吧
              }
            }
          });
        }
      }
    });
  }catch(error) {
    print(error);
    isSearched = false;
    print('一般连接失败后会走这里,然后停止扫描');
    _bluetooth.stopScan();
  }

  print("开始扫描");
  _bluetooth.startScan(scanMode: ScanMode.balanced, timeout: const Duration(seconds: 30),);
}

接下来看看读写消息代码,比较简单,需要注意的是,发送接收的字符串一般使用 utf8 转化

此外,如果传输数据量比较大的话,需要多次传递就行了(因为设备缘故一次不能传递数据过,由于传输长度限制问题,单次长度建议跟 UUID 长度一样16字节最佳,当然自己可以尝试更长的字符串(一般超过20字节就会丢失了))
//设置通知
Future<void> setNotifiy() async {
  //可以监听外设端主动发送过来的消息
  await _currentCharacteristic!.setNotifyValue(true);
  _currentCharacteristic!.value.listen((event) {
    print(event);
  });
}

//读取外设消息
Future<void> readMessage() async {
  if (_currentCharacteristic == null) return;

  List<int> value = await _currentCharacteristic!.read();
  print("value");
  print(value);
  final string = utf8.decode(value);
  print(string);
}

//向外设发送消息
void sendMessage(String text) {
  print(text);
  _controller.clear();
  _currentCharacteristic?.write(utf8.encode(text)).then((value) => print(value));
}

最后

快来尝试一下吧,两个案例都是通的,如果想玩更有趣的,可以更新两端代码,相互交互,写一个情侣间的小型传输工具也不是未尝不可😂