在Flutter中,您可能会遇到在 Android 10+ 上将文件保存在公共存储中的问题。这可能令人沮丧,因为无法使用path_provider插件或任何其他插件保存文件。也可能是没有授予足够的权限。在本文中,我们将看到如何在使用 Flutter 在 Android 上进行范围存储后将文件保存到下载文件夹中。
我们将创建一个示例,首先使用Dio包从 URL 下载文件,然后将其存储在其中Download/AppName文件夹。
步骤:
1、在pubspec.yaml中添加相关包。
2、在AndroidManifest.xml中添加相关的存储和网络权限。
3、 从flutter调用一个原生的android函数。
4、请求存储运行时权限。
5、下载文件到本地目录。
6、将文件移动到 Download/AppName 文件夹。
第1步:
添加相关包pubspec.yaml
path_provider: ^2.0.11
dio: ^4.0.6
device_info_plus: ^4.0.1
第2步:
在AndroidManifest.xml.
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
第 3 步:
现在,我们必须请求运行时存储权限。我们不会向 Flutter 请求权限,而是要从 Flutter 调用 android 的原生函数,这将请求权限。我们必须向本机函数请求权限,否则这将不起作用。
创建一个Button我们将请求权限的对象。
ElevatedButton(
onPressed: requestStoragePermission,
child: Text('Request Storage Permissions'),
),
创建一个将调用本机 android 方法的函数。
Future<void> requestStoragePermission() async {
try {
await platform.invokeMethod('requestStoragePermission');
} on PlatformException catch (e) {
print(e);
}
}
在您的 MainActivity 中创建一个名为 CHANNEL 的变量并为其设置一个值。此值应与 dart invokeMethod 值匹配。如果它们不匹配,颤振代码将无法调用 android 方法。
class MainActivity: FlutterActivity() {
private val CHANNEL ="com.example.androidstorage.android_12_flutter_storage/storage"
}
覆盖configureFlutterEngine内部MainActivity并添加MethodChannel代码。
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "requestStoragePermission") {
requestPermission(this@MainActivity as Context)
} else {
result.notImplemented()
}
}
}
创建一个名为的新 kt 文件FileUtils.kt并添加一个名为requestPermission.在该方法中,调用ActivityCompat.requestPermissions以请求存储运行时权限。
object FileUtils {
fun requestPermission(context: Context) {
ActivityCompat.requestPermissions(
context as Activity, arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.ACCESS_MEDIA_LOCATION
), 101
);
}
}
第4步:
下载文件到本地目录。
ElevatedButton(
onPressed: downloadRecording,
child: const Text('Download Recording!'),
),
void downloadRecording() async {
String url =
"https://file-examples.com/storage/fe8faa459062eec049e62d4/2017/11/file_example_MP3_700KB.mp3";
String fileName = "Audio-Recording.mp3";
String path = await _getFilePath(fileName);
await dio.download(
url,
path,
onReceiveProgress: (receivedBytes, totalBytes) {
print("Rec: $receivedBytes , Total: $totalBytes");
setState(() {
progress = ((receivedBytes / totalBytes) * 100);
if (progress == 100.0) {
_saveFileToRecordings(path);
}
});
},
deleteOnError: true,
).then((value) => print(value.toString()));
}
Future<void> _saveFileToRecordings(String path) async {
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
final androidInfo = await deviceInfoPlugin.androidInfo;
print("Device Version: ${androidInfo.version.sdkInt}");
if (Platform.isAndroid && androidInfo.version.sdkInt! >= 29) {
try {
await platform.invokeMethod('saveFile', {'path': path});
} on PlatformException catch (e) {
print(e);
}
} else {}
}
Future<String> _getFilePath(String fileName) async {
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
final androidInfo = await deviceInfoPlugin.androidInfo;
print("Device Version: ${androidInfo.version.sdkInt}");
if (Platform.isAndroid && androidInfo.version.sdkInt! >= 29) {
final dir = await getExternalStorageDirectory();
print("File Name: ${dir!.path}/$fileName");
return "${dir.path}/$fileName";
} else {
var dir = Directory('/storage/emulated/0/Download/AppName');
print("File Name: ${dir.path}/$fileName");
return "${dir.path}/$fileName";
}
}
第5步:
将文件移动到 Download/AppName 文件夹。
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "saveFile") {
val hashMap = call.arguments as HashMap<*,*> //Get the arguments as a HashMap
val path = hashMap["path"]
FileUtils.saveBitmapToStorage(this@MainActivity as Context,path.toString())
} else if (call.method == "requestStoragePermission") {
requestPermission(this@MainActivity as Context)
} else {
result.notImplemented()
}
}
}
fun checkPermissionForExternalStorage(context: Context): Boolean {
return ActivityCompat.checkSelfPermission(
context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE
) === PackageManager.PERMISSION_GRANTED
}
fun saveBitmapToStorage(context: Context, bitmap: String): Uri? {
var result: Uri? = null
if (checkPermissionForExternalStorage(context)) {
var filename: File? = null
val outputStream: java.io.OutputStream?
val DEFAULT_IMAGE_NAME: String = java.util.UUID.randomUUID().toString()
try {
/*Check if the android version is equal or greater than Android 10*/
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
val resolver: ContentResolver = context.getContentResolver()
val contentValues = ContentValues()
contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, DEFAULT_IMAGE_NAME)
contentValues.put(MediaStore.Audio.Media.TITLE, DEFAULT_IMAGE_NAME)
contentValues.put(
MediaStore.Audio.Media.MIME_TYPE,
getMIMEType(context, bitmap)
)
contentValues.put(MediaStore.Audio.Media.RELATIVE_PATH, "Music/" + "AppName")
val imageUri: Uri? =
resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
var file = File(bitmap);
outputStream = resolver.openOutputStream(imageUri!!)
val bytesArray: ByteArray = file.readBytes()
outputStream!!.write(bytesArray)
outputStream!!.flush()
result = imageUri
Log.d("FileUtils", bitmap);
SingleMediaScanner(context, file)
} else {
}
} catch (e: java.lang.Exception) {
Log.d("FileUtils", e.message!!);
e.printStackTrace()
}
}
return result
}
fun getMIMEType(con: Context, url: String?): String? {
var mType: String? = null
val mExtension = MimeTypeMap.getFileExtensionFromUrl(url)
if (mExtension != null) {
mType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(mExtension)
}
return mType
}
第6步:
SingleMediaScanner在 android 目录中创建。
import android.content.Context;
import android.media.MediaScannerConnection;
import android.net.Uri;
import java.io.File;
public class SingleMediaScanner implements MediaScannerConnection.MediaScannerConnectionClient {
private MediaScannerConnection mMs;
private File mFile;
public SingleMediaScanner(Context context, File f) {
mFile = f;
mMs = new MediaScannerConnection(context, this);
mMs.connect();
}
@Override
public void onMediaScannerConnected() {
mMs.scanFile(mFile.getAbsolutePath(), null);
}
@Override
public void onScanCompleted(String path, Uri uri) {
mMs.disconnect();
}
}