这将是一个较长的答案,其中许多代码都与我的用例有关,因此如果有人想要重用它,可能需要进行一些调整。
基本上,在Android 30+中的更改中,我无法在用户手机上写入非自己应用程序目录的目录而不请求可怕的manage_external_storage权限。
我通过使用本地Kotlin来解决这个问题,然后通过Dart中的接口调用这些方法。
首先从Kotlin代码开始。
class MainActivity : FlutterActivity() {
private val CHANNEL = "package/Main"
private var pendingResult: MethodChannel.Result? = null
private var methodCall: MethodCall? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
val handlers = mapOf(
"getSavedRoot" to ::getSavedRoot,
"selectDirectory" to ::copyDirectoryToCache,
"createDirectory" to ::createDirectory,
"writeFile" to ::writeFile,
)
if (call.method in handlers) {
handlers[call.method]!!.invoke(call, result)
} else {
result.notImplemented()
}
}
}
这将设置我们的MainActivity
以便监听在setMethodCallHandler
方法中命名的方法。
有很多示例可以找到如何在Kotlin中实现基本IO功能,因此我不会在这里全部发布它们,但是以下是如何打开设置内容根并处理结果的示例:
class MainActivity : FlutterActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun selectContentRoot(call: MethodCall, result: MethodChannel.Result) {
pendingResult = result
try {
val browseIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(browseIntent, 100)
} catch (e: Throwable) {
Log.e("selectDirectory", " error", e)
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 100 && resultCode == RESULT_OK) {
val uri: Uri = data?.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return pendingResult!!.success(uri.toString())
}
return
}
现在,为了在Dart中调用该代码,我创建了一个名为
AndroidInterface
的接口并实现了它。
class AndroidInterface {
final _platform = const MethodChannel('package/Main');
final _errors = {
'no persist document tree': FileOperationError.noSavedPersistRoot,
'pending': FileOperationError.pending,
'access error': FileOperationError.accessError,
'exists': FileOperationError.alreadyExists,
'creation failed': FileOperationError.creationFailed,
'canceled': FileOperationError.canceled,
};
String? _root;
Future<FileOperationResult<String>> _invoke(
String method, {
bool returnVoid = false,
String? root,
String? directory,
String? subdir,
String? name,
Uint8List? bytes,
bool? overwrite,
}) async {
try {
final result = await _platform.invokeMethod<String>(method, {
'root': root,
'directory': directory,
'subdir': subdir,
'name': name,
'bytes': bytes,
'overwrite': overwrite,
});
if (result != null || returnVoid) {
final fileOperationResult = FileOperationResult(result: result);
fileOperationResult.result = result;
return fileOperationResult;
}
return FileOperationResult(error: FileOperationError.unknown);
} on PlatformException catch (e) {
final error = _errors[e.code] ?? FileOperationError.unknown;
return FileOperationResult(
error: error,
result: e.code,
message: e.message,
);
}
}
Future<FileOperationResult<String>> selectContentRoot() async {
final result = await _invoke('selectContentRoot');
if (result.error == FileOperationError.success) {
if (_root != null) {
await _invoke('releaseDirectory', root: _root, returnVoid: true);
}
_root = result.result;
}
return result;
}
基本上,它通过
_platform.invokeMethod
发送请求,传递方法名和要发送的参数。使用工厂模式,您可以实现此接口设备运行30+并使用标准的Apple和设备运行29及以下的东西。类似于:
abstract class IOInterface {
Future<void> selectDirectory(String? message, String? buttonText);
}
并且一个工厂来决定使用哪个接口
class IOFactory {
static IOInterface? _interface;
static IOInterface? get instance => _interface;
IOFactory._create();
static Future<IOFactory> create() async {
final component = IOFactory._create();
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
_interface = sdkInt > 29 ? AndroidSDKThirty() : AndroidSDKTwentyNine();
}
if (Platform.isIOS) {
_interface = AppleAll();
}
return component;
}
}
最后,30+的实现可能如下所示:
class AndroidSDKThirty implements IOInterface {
final AndroidInterface _androidInterface = AndroidInterface();
@override
Future<void> selectDirectory(String? message, String? buttonText) async {
final contentRoot = await _androidInterface.getContentRoot();
}
希望这些足以让你入门并指向正确的方向。
MethodChannel
调用,因为我无法在Dart中使其正常工作。 - Chris