攻克Flutter混合开发权限难题:EasyPermissions无缝集成方案

【免费下载链接】easypermissions Simplify Android M system permissions 【免费下载链接】easypermissions 项目地址: https://gitcode.com/gh_mirrors/ea/easypermissions

你是否在Flutter混合开发中遭遇过权限管理的"隐形墙"?原生Android使用EasyPermissions处理权限逻辑,而Flutter模块又依赖自身的权限插件,两套系统如同平行宇宙,导致权限状态不同步、重复申请弹窗、代码冗余等棘手问题。本文将提供一套经过实战验证的解决方案,通过构建统一权限管理层,实现原生Android与Flutter模块的权限共享,彻底解决混合开发中的权限协同难题。

读完本文你将掌握:

  • 原生Android与Flutter权限体系的深度对比分析
  • 基于EasyPermissions的跨端权限通信架构设计
  • 权限状态同步的三种核心实现方案及性能对比
  • 完整的代码实现与集成步骤(包含6个关键代码模块)
  • 边界场景处理策略(如"永不询问"状态同步、权限变更监听)

混合开发权限管理现状分析

原生与Flutter权限体系差异

Android原生与Flutter在权限管理上存在显著差异,这些差异是导致混合开发中权限混乱的根源:

特性 Android原生(EasyPermissions) Flutter权限插件
权限处理时机 运行时动态申请 运行时动态申请
权限存储位置 系统权限管理器 系统权限管理器+插件内存缓存
状态同步机制 系统广播通知 插件主动查询+事件分发
权限申请UI 系统原生对话框 系统原生对话框+自定义封装
"永不询问"处理 需手动检测并重定向设置页 部分插件自动处理但实现不一
权限组概念 严格遵循Android权限组机制 抽象封装,可能忽略组特性

典型冲突场景再现

场景一:权限状态不同步

Flutter模块通过permission_handler插件检测到相机权限已授予,但实际原生层因用户在设置中手动关闭而权限缺失,导致Flutter调用相机时崩溃。

场景二:重复申请弹窗

原生层已通过EasyPermissions申请并获得存储权限,Flutter模块因不知情再次调用权限申请,触发冗余弹窗,严重影响用户体验。

场景三:权限结果处理断裂

用户在Flutter页面拒绝权限并勾选"不再询问",原生层EasyPermissions无法感知此状态,后续仍尝试申请该权限,导致逻辑异常。

场景四:代码逻辑冗余

相同的权限检查逻辑在原生和Flutter层重复实现,维护成本加倍,且易产生逻辑不一致。

跨端权限共享架构设计

整体架构概览

基于EasyPermissions构建的跨端权限共享架构采用分层设计,通过双向通信通道实现权限状态的实时同步:

mermaid

核心设计思想:

  1. 单一权限源:以Android系统权限状态为唯一真实来源
  2. 统一申请入口:所有权限申请都通过原生层EasyPermissions处理
  3. 双向通信机制:实现权限状态的实时同步与结果回调
  4. 缓存优化:维护权限状态缓存,减少系统查询开销
  5. 事件驱动:基于观察者模式实现权限变更的即时通知

关键技术组件解析

1. 权限管理层(PermissionManager)

封装EasyPermissions核心功能,提供统一的权限查询、申请接口,并维护权限状态缓存。

2. Method Channel通信桥

实现Flutter与原生间的权限相关方法调用与结果返回,支持同步/异步调用模式。

3. 权限状态缓存(PermissionCache)

基于内存的权限状态缓存,减少对ContextCompat.checkSelfPermission的频繁调用,提升性能。

4. 权限变更监听器(PermissionChangeListener)

注册系统权限变更广播,实时捕获权限状态变化并同步至Flutter层。

5. Flutter权限服务(PermissionService)

Flutter侧抽象权限服务,封装Method Channel调用细节,提供与原生一致的API体验。

实现方案详解

方案一:Method Channel基础通信模式

这是最直接的实现方式,通过Flutter与原生间的Method Channel进行权限相关方法调用:

原生层实现

步骤1:创建PermissionManager封装EasyPermissions

public class PermissionManager {
    private static final String TAG = "PermissionManager";
    private static PermissionManager instance;
    private final Context context;
    private final PermissionCache permissionCache;
    
    private PermissionManager(Context context) {
        this.context = context.getApplicationContext();
        this.permissionCache = new PermissionCache();
        registerPermissionChangeListener();
    }
    
    public static synchronized PermissionManager getInstance(Context context) {
        if (instance == null) {
            instance = new PermissionManager(context);
        }
        return instance;
    }
    
    /**
     * 检查权限是否已授予
     */
    public boolean hasPermissions(String... perms) {
        // 先检查缓存
        if (permissionCache.areAllPermissionsGranted(perms)) {
            return true;
        }
        
        // 缓存未命中,查询系统
        boolean hasPerms = EasyPermissions.hasPermissions(context, perms);
        
        // 更新缓存
        if (hasPerms) {
            permissionCache.cachePermissions(perms, true);
        }
        return hasPerms;
    }
    
    /**
     * 请求权限
     */
    public void requestPermissions(Activity activity, String rationale, 
                                  int requestCode, String... perms) {
        EasyPermissions.requestPermissions(activity, rationale, requestCode, perms);
    }
    
    /**
     * 处理权限申请结果
     */
    public void onRequestPermissionsResult(int requestCode, String[] permissions, 
                                          int[] grantResults, PermissionResultCallback callback) {
        // 解析结果
        List<String> granted = new ArrayList<>();
        List<String> denied = new ArrayList<>();
        
        for (int i = 0; i < permissions.length; i++) {
            String perm = permissions[i];
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                granted.add(perm);
                permissionCache.cachePermission(perm, true);
            } else {
                denied.add(perm);
                permissionCache.cachePermission(perm, false);
            }
        }
        
        // 回调结果
        if (callback != null) {
            if (!granted.isEmpty()) {
                callback.onPermissionsGranted(requestCode, granted);
            }
            if (!denied.isEmpty()) {
                callback.onPermissionsDenied(requestCode, denied);
            }
        }
        
        // 通知Flutter层权限变更
        notifyFlutterPermissionChange(permissions);
    }
    
    // 其他辅助方法...
}

步骤2:实现Method Channel通信

public class PermissionChannelHandler implements MethodCallHandler {
    private static final String CHANNEL_NAME = "com.example/easy_permissions";
    private final PermissionManager permissionManager;
    private final Activity activity;
    private MethodChannel channel;
    
    public PermissionChannelHandler(Activity activity) {
        this.activity = activity;
        this.permissionManager = PermissionManager.getInstance(activity);
    }
    
    public void setupChannel(BinaryMessenger messenger) {
        channel = new MethodChannel(messenger, CHANNEL_NAME);
        channel.setMethodCallHandler(this);
    }
    
    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
        switch (call.method) {
            case "hasPermissions":
                handleHasPermissions(call, result);
                break;
            case "requestPermissions":
                handleRequestPermissions(call, result);
                break;
            case "openAppSettings":
                handleOpenAppSettings(call, result);
                break;
            default:
                result.notImplemented();
        }
    }
    
    private void handleHasPermissions(MethodCall call, Result result) {
        List<String> permissions = call.argument("permissions");
        if (permissions == null || permissions.isEmpty()) {
            result.error("INVALID_ARGUMENTS", "Permissions list cannot be empty", null);
            return;
        }
        
        boolean hasPerms = permissionManager.hasPermissions(
            permissions.toArray(new String[0])
        );
        result.success(hasPerms);
    }
    
    private void handleRequestPermissions(MethodCall call, Result result) {
        // 实现权限请求逻辑...
    }
    
    // 其他方法实现...
}
Flutter层实现

步骤1:创建权限服务封装

class EasyPermissions {
  static const MethodChannel _channel = MethodChannel('com.example/easy_permissions');
  
  /// 检查是否拥有指定权限
  static Future<bool> hasPermissions(List<String> permissions) async {
    final bool result = await _channel.invokeMethod(
      'hasPermissions',
      {'permissions': permissions},
    );
    return result;
  }
  
  /// 请求权限
  static Future<PermissionStatus> requestPermissions(
    List<String> permissions, {
    String rationale,
    int requestCode = 100,
  }) async {
    final Map<String, dynamic> result = await _channel.invokeMethod(
      'requestPermissions',
      {
        'permissions': permissions,
        'rationale': rationale,
        'requestCode': requestCode,
      },
    );
    
    return PermissionStatus.fromMap(result);
  }
  
  /// 打开应用设置页面
  static Future<void> openAppSettings() async {
    await _channel.invokeMethod('openAppSettings');
  }
  
  // 其他方法...
}

/// 权限状态封装类
class PermissionStatus {
  final int requestCode;
  final List<String> grantedPermissions;
  final List<String> deniedPermissions;
  final bool isPermanentlyDenied;
  
  PermissionStatus({
    required this.requestCode,
    required this.grantedPermissions,
    required this.deniedPermissions,
    this.isPermanentlyDenied = false,
  });
  
  factory PermissionStatus.fromMap(Map<String, dynamic> map) {
    return PermissionStatus(
      requestCode: map['requestCode'],
      grantedPermissions: List<String>.from(map['grantedPermissions'] ?? []),
      deniedPermissions: List<String>.from(map['deniedPermissions'] ?? []),
      isPermanentlyDenied: map['isPermanentlyDenied'] ?? false,
    );
  }
  
  bool get allGranted => deniedPermissions.isEmpty;
}

步骤2:在Flutter页面中使用

class CameraAccessPage extends StatefulWidget {
  @override
  _CameraAccessPageState createState() => _CameraAccessPageState();
}

class _CameraAccessPageState extends State<CameraAccessPage> {
  bool _hasCameraPermission = false;
  
  @override
  void initState() {
    super.initState();
    _checkCameraPermission();
  }
  
  Future<void> _checkCameraPermission() async {
    final hasPermission = await EasyPermissions.hasPermissions([
      'android.permission.CAMERA',
    ]);
    
    setState(() {
      _hasCameraPermission = hasPermission;
    });
  }
  
  Future<void> _requestCameraPermission() async {
    final status = await EasyPermissions.requestPermissions(
      ['android.permission.CAMERA'],
      rationale: '需要相机权限以拍摄照片',
      requestCode: 101,
    );
    
    setState(() {
      _hasCameraPermission = status.allGranted;
    });
    
    if (status.isPermanentlyDenied) {
      // 显示引导用户去设置页的对话框
      _showOpenSettingsDialog();
    }
  }
  
  void _showOpenSettingsDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('权限被拒绝'),
        content: Text('相机权限已被永久拒绝,请在设置中启用'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () => EasyPermissions.openAppSettings(),
            child: Text('去设置'),
          ),
        ],
      ),
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('相机访问')),
      body: Center(
        child: _hasCameraPermission 
            ? CameraPreview() 
            : ElevatedButton(
                onPressed: _requestCameraPermission,
                child: Text('请求相机权限'),
              ),
      ),
    );
  }
}

方案二:权限状态实时同步优化

基础方案存在权限状态滞后问题,当用户在设置中手动变更权限时,Flutter层无法及时感知。优化方案通过广播监听和事件流实现实时同步:

步骤1:原生层注册权限变更广播

public class PermissionChangeReceiver extends BroadcastReceiver {
    private static final String TAG = "PermissionChangeReceiver";
    private final PermissionChangeCallback callback;
    
    public interface PermissionChangeCallback {
        void onPermissionsChanged(String[] permissions);
    }
    
    public PermissionChangeReceiver(PermissionChangeCallback callback) {
        this.callback = callback;
    }
    
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null || !"android.intent.action.PACKAGE_CHANGED".equals(intent.getAction())) {
            return;
        }
        
        Uri data = intent.getData();
        if (data != null && data.getSchemeSpecificPart().equals(context.getPackageName())) {
            // 应用权限可能已变更,查询所有已知权限状态
            Set<String> trackedPermissions = PermissionManager.getInstance(context).getTrackedPermissions();
            if (!trackedPermissions.isEmpty()) {
                callback.onPermissionsChanged(trackedPermissions.toArray(new String[0]));
            }
        }
    }
}

步骤2:Flutter层实现事件流订阅

class PermissionStreamHandler {
  static const EventChannel _eventChannel = EventChannel('com.example/permission_events');
  static Stream<List<String>>? _permissionStream;
  
  static Stream<List<String>> get permissionChanges {
    _permissionStream ??= _eventChannel.receiveBroadcastStream().map((event) {
      return List<String>.from(event);
    });
    return _permissionStream!;
  }
}

// 在Widget中使用
class PermissionAwareWidget extends StatefulWidget {
  @override
  _PermissionAwareWidgetState createState() => _PermissionAwareWidgetState();
}

class _PermissionAwareWidgetState extends State<PermissionAwareWidget> {
  late StreamSubscription<List<String>> _permissionSubscription;
  
  @override
  void initState() {
    super.initState();
    _permissionSubscription = PermissionStreamHandler.permissionChanges.listen((permissions) {
      setState(() {
        // 更新UI以反映最新权限状态
        _checkPermissions();
      });
    });
  }
  
  @override
  void dispose() {
    _permissionSubscription.cancel();
    super.dispose();
  }
  
  // ...
}

步骤3:原生层发送权限变更事件

public class PermissionEventSender {
    private final EventChannel eventChannel;
    
    public PermissionEventSender(BinaryMessenger messenger) {
        this.eventChannel = new EventChannel(messenger, "com.example/permission_events");
    }
    
    private EventChannel.EventSink eventSink;
    
    public void setupEventStream() {
        eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
            @Override
            public void onListen(Object arguments, EventChannel.EventSink events) {
                eventSink = events;
            }
            
            @Override
            public void onCancel(Object arguments) {
                eventSink = null;
            }
        });
    }
    
    public void sendPermissionChangeEvent(String[] permissions) {
        if (eventSink != null) {
            eventSink.success(permissions);
        }
    }
}

方案三:全量权限状态缓存与同步

对于权限密集型应用,可维护全量权限状态缓存,并通过JSON序列化实现Flutter层完整状态同步:

// Kotlin实现更简洁的权限缓存
class PermissionCache {
    private val permissionStatus = mutableMapOf<String, Boolean>()
    private val lock = ReentrantLock()
    
    fun cachePermission(permission: String, granted: Boolean) {
        lock.lock()
        try {
            permissionStatus[permission] = granted
        } finally {
            lock.unlock()
        }
    }
    
    fun cachePermissions(permissions: Array<String>, granted: Boolean) {
        lock.lock()
        try {
            for (perm in permissions) {
                permissionStatus[perm] = granted
            }
        } finally {
            lock.unlock()
        }
    }
    
    fun areAllPermissionsGranted(permissions: Array<String>): Boolean {
        lock.lock()
        try {
            for (perm in permissions) {
                val status = permissionStatus[perm]
                if (status == null || !status) {
                    return false
                }
            }
            return true
        } finally {
            lock.unlock()
        }
    }
    
    fun getPermissionStatusMap(): Map<String, Boolean> {
        lock.lock()
        try {
            return HashMap(permissionStatus) // 返回副本避免并发修改问题
        } finally {
            lock.unlock()
        }
    }
    
    fun getTrackedPermissions(): Set<String> {
        lock.lock()
        try {
            return HashSet(permissionStatus.keys)
        } finally {
            lock.unlock()
        }
    }
}

Flutter层通过定期同步或按需获取完整权限状态映射,实现本地缓存,减少Method Channel通信次数:

class PermissionCacheManager {
  static final PermissionCacheManager _instance = PermissionCacheManager._internal();
  Map<String, bool> _permissionCache = {};
  
  factory PermissionCacheManager() {
    return _instance;
  }
  
  PermissionCacheManager._internal() {
    // 初始化时从原生获取全量权限状态
    _syncPermissionCache();
    
    // 订阅权限变更事件以更新缓存
    PermissionStreamHandler.permissionChanges.listen((permissions) {
      _syncPermissionCache(permissions);
    });
  }
  
  Future<void> _syncPermissionCache([List<String>? specificPermissions]) async {
    try {
      final Map<dynamic, dynamic> result = await EasyPermissions.getPermissionStatusMap();
      
      final Map<String, bool> newCache = {};
      result.forEach((key, value) {
        if (key is String && value is bool) {
          newCache[key] = value;
        }
      });
      
      _permissionCache = newCache;
      
      // 触发缓存变更通知
      _cacheChangeController.add(_permissionCache);
    } catch (e) {
      print('Error syncing permission cache: $e');
    }
  }
  
  // 缓存变更通知流
  final StreamController<Map<String, bool>> _cacheChangeController = StreamController.broadcast();
  Stream<Map<String, bool>> get cacheChanges => _cacheChangeController.stream;
  
  // 检查缓存中的权限状态
  bool hasPermission(String permission) => _permissionCache[permission] ?? false;
  
  // 检查多个权限
  bool hasPermissions(List<String> permissions) {
    return permissions.every((perm) => _permissionCache[perm] ?? false);
  }
}

集成步骤与最佳实践

完整集成流程

步骤1:添加依赖

在原生build.gradle中添加EasyPermissions依赖:

dependencies {
    // EasyPermissions核心库
    implementation 'pub.devrel:easypermissions:3.0.0'
    // Flutter相关依赖
    implementation 'io.flutter:flutter_embedding_release:1.0.0'
}

在Flutterpubspec.yaml中添加相关依赖:

dependencies:
  flutter:
    sdk: flutter
  # 其他依赖...

步骤2:初始化权限通道

FlutterActivity中初始化权限通信通道:

public class MainFlutterActivity extends FlutterActivity {
    private PermissionChannelHandler permissionChannel;
    private PermissionEventSender eventSender;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 初始化权限管理器
        PermissionManager.getInstance(this);
        
        // 设置Method Channel
        permissionChannel = new PermissionChannelHandler(this);
        permissionChannel.setupChannel(getFlutterEngine().getDartExecutor().getBinaryMessenger());
        
        // 设置Event Channel
        eventSender = new PermissionEventSender(getFlutterEngine().getDartExecutor().getBinaryMessenger());
        eventSender.setupEventStream();
        
        // 注册权限变更广播接收器
        PermissionManager.getInstance(this).setPermissionEventSender(eventSender);
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        
        // 委托给PermissionManager处理
        PermissionManager.getInstance(this).onRequestPermissionsResult(
            requestCode, permissions, grantResults, 
            new PermissionResultCallback() {
                @Override
                public void onPermissionsGranted(int requestCode, List<String> perms) {
                    // 发送结果到Flutter层
                    permissionChannel.sendPermissionResult(requestCode, perms, Collections.emptyList(), false);
                }
                
                @Override
                public void onPermissionsDenied(int requestCode, List<String> perms) {
                    boolean permanentlyDenied = EasyPermissions.somePermissionPermanentlyDenied(
                        MainFlutterActivity.this, perms);
                    permissionChannel.sendPermissionResult(requestCode, 
                        Collections.emptyList(), perms, permanentlyDenied);
                }
            }
        );
    }
}

步骤3:Flutter层初始化与使用

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化权限缓存管理器
  await PermissionCacheManager().initialize();
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PermissionDemoPage(),
    );
  }
}

class PermissionDemoPage extends StatelessWidget {
  final PermissionCacheManager _cacheManager = PermissionCacheManager();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('权限共享示例')),
      body: StreamBuilder<Map<String, bool>>(
        stream: _cacheManager.cacheChanges,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Center(child: CircularProgressIndicator());
          }
          
          final permissions = snapshot.data!;
          return ListView(
            children: [
              _buildPermissionItem(
                '相机权限', 
                'android.permission.CAMERA',
                permissions['android.permission.CAMERA'] ?? false,
                () => _requestCameraPermission(),
              ),
              _buildPermissionItem(
                '位置权限', 
                'android.permission.ACCESS_FINE_LOCATION',
                permissions['android.permission.ACCESS_FINE_LOCATION'] ?? false,
                () => _requestLocationPermission(),
              ),
              // 其他权限项...
            ],
          );
        },
      ),
    );
  }
  
  // 构建权限项UI...
  // 请求权限方法实现...
}

性能优化建议

  1. 权限分组批量处理:将同一功能所需的权限分组处理,减少通信次数
  2. 缓存预热:应用启动时预加载常用权限状态,减少首屏加载时间
  3. 通信数据压缩:对大量权限状态同步采用JSON压缩传输
  4. 避免UI线程阻塞:所有Method Channel调用放在异步方法中执行
  5. 事件节流:权限变更事件采用节流处理,避免短时间内多次触发

边界场景处理策略

1. "永不询问"状态处理

Future<void> _handlePermanentlyDenied(List<String> permissions) async {
  final shouldOpenSettings = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('权限被永久拒绝'),
      content: Text('需要在设置中启用以下权限才能继续: ${permissions.join(', ')}'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: Text('去设置'),
        ),
      ],
    ),
  );
  
  if (shouldOpenSettings == true) {
    await EasyPermissions.openAppSettings();
  }
}

2. 权限申请中断恢复

当权限申请过程中发生页面切换或配置变更时,需要保存和恢复申请状态:

// 原生层保存权限申请状态
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    permissionManager.savePermissionRequestState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    permissionManager.restorePermissionRequestState(savedInstanceState);
}

3. 跨页面权限共享

通过全局单例模式确保权限状态在应用内全局一致:

class PermissionService {
  static final PermissionService _instance = PermissionService._internal();
  
  factory PermissionService() {
    return _instance;
  }
  
  PermissionService._internal();
  
  // 全局权限服务实现...
}

方案对比与选型建议

方案 实现复杂度 性能 实时性 适用场景
基础通信模式 简单应用,权限变更少
实时同步优化 中等复杂度应用,需实时响应
全量缓存同步 权限密集型应用,复杂权限逻辑

选型建议

  • 小型应用:选择基础通信模式,以最低成本实现功能
  • 中型应用:采用实时同步优化方案,平衡实现成本与用户体验
  • 大型应用:全量缓存同步方案,提供最佳性能与用户体验

总结与展望

通过本文介绍的EasyPermissions跨端集成方案,我们成功构建了原生Android与Flutter模块间的权限共享桥梁,解决了混合开发中的权限状态同步、重复申请、代码冗余等核心问题。关键成果包括:

  1. 设计了基于单一权限源的跨端权限架构,确保权限状态一致性
  2. 实现了三种权限同步方案,适应不同复杂度的应用需求
  3. 提供了完整的代码实现与集成步骤,包含6个核心模块
  4. 总结了性能优化建议与边界场景处理策略

未来,随着Flutter对原生功能访问能力的增强,我们可以期待更优雅的权限共享方案,例如通过Pigeon实现类型安全的通信接口,或利用Flutter 3.0+的新特性进一步简化集成流程。无论如何,建立清晰的权限管理边界和通信协议,始终是混合开发中确保权限安全与用户体验的关键所在。

希望本文提供的方案能帮助你攻克Flutter混合开发中的权限难题。如果你在实施过程中遇到任何问题或有更好的实践经验,欢迎在评论区交流分享。别忘了点赞收藏本文,关注作者获取更多混合开发实战技巧!

【免费下载链接】easypermissions Simplify Android M system permissions 【免费下载链接】easypermissions 项目地址: https://gitcode.com/gh_mirrors/ea/easypermissions

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐