gpt4 book ai didi

android - 使用 Android SDK 30+ Flutter 读/写外部存储

转载 作者:行者123 更新时间:2023-12-05 05:55:07 26 4
gpt4 key购买 nike

在 Flutter 中开发面向 Android SDK 30+ 的 Android 应用。

我想将数据(xml 文件)读写到如下内容:

/storage/emulated/0/CustomDirectory/example.xml

四处阅读我想我应该使用 Intent.ACTION_OPEN_DOCUMENT_TREE 所以我写了一个 MethodChannel 允许我打开 SelectDialog美好的。 (为简洁起见,我删除了所有的 try-catch 和错误处理)

private fun selectDirectory() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startActivityForResult(intent, 100)
}

@RequiresApi(Build.VERSION_CODES.Q)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val uri = data.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return uri.toString())
}

我可以从 Flutter 中调用它,它会打开“选择目录”对话框,我可以选择我的 CustomDirectory,然后返回一个内容 URI:

content://com.android.externalstorage.documents/tree/primary%3ACustomDirectory

如何将其转换为 Flutter 目录?

在 Flutter 中,我可以调用 Directory.fromUri(...) 但这只是抛出

Unsupported operation: Cannot extract a file path from a content URI

所以我有点不确定从这里去哪里,我是否需要更改我的 Intent 的标志,或者我在某处做错了什么?

最佳答案

这将是一个很长的答案,并且很多代码都是针对我的用例的,因此如果有人想重用它,您可能需要进行一些调整。

基本上随着 Android 30+ 的变化,我无法在不请求可怕的 manage_external_storage 的情况下获得写入用户手机上不是我的应用程序自己目录的目录的权限。

我通过使用原生 Kotlin 然后通过 Dart 中的接口(interface)调用这些方法来解决这个问题。

首先从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 的接口(interface)并实现了

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;

// invoke a method with given arguments
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');

// release currently selected directory if new directory selected successfully
if (result.error == FileOperationError.success) {
if (_root != null) {
await _invoke('releaseDirectory', root: _root, returnVoid: true);
}
_root = result.result;
}

return result;
}

//...

它基本上通过 _platform.invokeMethod 发送请求,传递方法的名称和要发送的参数。

使用工厂模式,您可以实现运行 30+ 的接口(interface)设备,并为 Apple 和运行 29 及以下的设备使用标准内容。

类似于:

abstract class IOInterface {

//...

/// Select a subdirectory of the root directory
Future<void> selectDirectory(String? message, String? buttonText);
}

还有一个工厂来决定使用什么接口(interface)

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();
//...
}

希望这足以让您入门并指明正确的方向。

关于android - 使用 Android SDK 30+ Flutter 读/写外部存储,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69533579/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com