I am having trouble accessing files saved in getApplicationDocumentsDirectory on android only, I keep locale, theme and assets files stored there and when booting up the app those files are always inaccessible and as a result the app does not load, I tried using shared_prefs to track a first load scenario to use the files in assets/ and then later use the ones download but that seems to be an unreliable fix as at 2nd run they also inaccessible.
The only thing that seems to work reliable is the theme.json, but both copy functions run through the same helpers so what copies the theme over is what copies the locale over. Theme is loaded up in main.dart but the locale is loaded up with EasylocaLizationDelegate and if the files are in Document storage it can never load them up.
None of these issues are present on iOS, does anyone know of any special tricks to make this work for android, I have tried google but not really sure what to search for.
class FileHelpers {
static Future<Directory> getDownloadDirectory() async {
Directory dir = await getApplicationDocumentsDirectory();
globals.documentDirectory = dir;
return dir;
}
static verifyOrCreateDirectory(String folderName) async {
String path = globals.documentDirectory.path;
final Directory _folder = Directory('$path/$folderName/');
if (await _folder.exists()) {
//if folder already exists return path
return _folder.path;
} else {
//if folder not exists create folder and then return its path
final Directory _createdFolder = await _folder.create(recursive: true);
return _createdFolder.path;
}
}
static void writeStringToFile(String data, String path) {
new File(path).writeAsString(jsonEncode(data));
}
}
class ThemeHelpers {
static final _folderName = 'theme';
static final _fileName = 'theme.json';
static Future<void> loadTheme() async {
String documentPath = globals.documentDirectory.path;
try {
String assetTheme = 'assets/$_fileName';
String documentTheme = "$documentPath/$_folderName/$_fileName";
String loadPath = documentTheme;
/// We need to check if we can use the local assets as Android has some
/// issues using document assets this early on in the app run
if (!globals.canUseLocalAssets) {
loadPath = assetTheme;
}
Box box = await Hive.openBox(globals.environment.entryCode);
String data = await rootBundle.loadString(loadPath);
box.put(HIVE_COLLECTION_THEME, data);
} catch (e) {
print(e);
}
}
/// In order to support offline themes we ship the current version as at build
/// time with the app.
/// This method is to copy the files from the assets folder into the documents
/// directory. This gives us the option to then update when needed.
static copyTheme() async {
String path = await FileHelpers.verifyOrCreateDirectory(_folderName);
String themePath = '$path/$_fileName';
/// Check if the file already exists, if not copy it over
if (FileSystemEntity.typeSync(themePath) == FileSystemEntityType.notFound) {
String assetPath = "assets/$_fileName";
String data = await rootBundle.loadString(assetPath);
await new File(themePath).writeAsString(data);
print('theme copied');
}
}
static updateTheme() async {
String path = await FileHelpers.verifyOrCreateDirectory(_folderName);
var url = ApiEndpoint.uri('/theme').toString();
try {
bool shouldUpdate = await _checkIfUpdated();
if (shouldUpdate) {
/// If theme has been updated download the latest
/// version and store it.
///
/// Currently this happens as a non-blocking action
/// so updates will take effect the next time the user opens the app.
var response = await Session.apiGet(url);
String localePath = '$path/$_fileName';
await new File(localePath).writeAsString(jsonEncode(response));
/// Once updated reload the theme into [Hive]
loadTheme();
}
} catch (error) {
print('Unable to update theme');
}
}
/// This method makes alight call comparing the [__lastUpdated] in our local
/// copy of the Locale to the API version.
static Future<bool> _checkIfUpdated() async {
String assetPath =
'${globals.documentDirectory.path}/$_folderName/$_fileName';
if (FileSystemEntity.typeSync(assetPath) ==
FileSystemEntityType.notFound) throw "File not found";
try {
String data = await rootBundle.loadString(assetPath);
String currentThemeDate = json.decode(data)['__lastUpdated'];
var url = ApiEndpoint.uri(
'/theme/check',
queryParameters: {
"date": currentThemeDate,
},
).toString();
bool response = await Session.apiGet(url);
if (response.runtimeType == ErrorModel) throw response;
return response;
} catch (error) {
print("Unable to check for updated theme: $error");
return Future.value(false);
}
}
}
class LocaleHelpers {
static final _directory = DIRECTORY_LOCALE;
/// In order to support offline locale we ship the current version as at build
/// time with the app.
/// This method is to copy the files from the assets folder into the documents
/// directory. This gives us the option to then update when needed.
static copyLocaleFiles() async {
String path = await FileHelpers.verifyOrCreateDirectory(_directory);
for (var locale in globals.supportedLocale) {
String localeKey = locale.toString();
String localePath = '$path$localeKey.json';
/// Check if the file already exists, if not copy it over
if (FileSystemEntity.typeSync(localePath) ==
FileSystemEntityType.notFound) {
String assetPath = "assets/$_directory/$localeKey.json";
String data = await rootBundle.loadString(assetPath);
FileHelpers.writeStringToFile(data, localePath);
}
}
}
static updateLocale() async {
String path = await FileHelpers.verifyOrCreateDirectory(_directory);
/// Iterate through locale and check each supported
/// language if there is an updated version on the API
for (Locale locale in globals.supportedLocale) {
String localeString = locale.toString();
var url = ApiEndpoint.uri('/locale/$localeString.json').toString();
try {
bool shouldUpdate = await _checkIfUpdated(localeString);
if (shouldUpdate) {
/// If Locale has been updated download the latest
/// version and store it.
///
/// Currently this happens as a non-blocking action
/// so updates will take effect the next time the user opens the app.
var response = await Session.apiGet(url);
String localePath = '$path/$localeString.json';
FileHelpers.writeStringToFile(localePath, jsonEncode(response));
}
} catch (error) {
print('Unable to update locale: $localeString');
}
}
}
/// This method makes alight call comparing the [_sheetToFbDate] in our local
/// copy of the Locale to the API version.
static Future<bool> _checkIfUpdated(String langKey) async {
try {
String assetPath = '${globals.documentDirectory.path}/locale/$langKey.json';
String data = await rootBundle.loadString(assetPath);
var currentLocaleDate = json.decode(data)['_sheetToFbDate'];
var url = ApiEndpoint.uri(
'/locale/check/$langKey',
queryParameters: {
"date": currentLocaleDate,
},
).toString();
bool response = await Session.apiGet(url);
if (response.runtimeType == ErrorModel) throw response;
return response;
} catch (error) {
print("Unable to check for update: $langKey");
return Future.value(false);
}
}
}
I eventually realised that while not an issue on iOS, using rootBundle.loadString does not work for document storage on Android.
I needed to replace that bit with
File file = File('PATH_TO_FILE');
data = await file.readAsString();
Related
I am developing an app that can record and play sound. Everything works fine on my emulator. But when I try to run the app on my device, it always gives me this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'long android.os.storage.StorageVolume.getMaxFileSize()' on a null object reference
The way I implement the recording feature is to record the audio to a temporary file, and playback from it. This is the corresponding code, I am using flutter sound by the way:
String pathToSaveAudio = '';
class SoundRecorder {
FlutterSoundRecorder? _audioRecorder;
bool _isRecordingInitialised = false;
bool get isRecording => _audioRecorder!.isRecording;
// getters
bool getInitState() {
return _isRecordingInitialised;
}
FlutterSoundRecorder? getRecorder() {
return _audioRecorder;
}
/// init recorder
Future init() async {
_audioRecorder = FlutterSoundRecorder();
final status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('Microphone permission denied');
}
await _audioRecorder!.openRecorder();
_isRecordingInitialised = true;
var tempDir = await getTemporaryDirectory();
pathToSaveAudio = '${tempDir.path}/audio.mp4';
}
/// dipose recorder
void dispose() {
_audioRecorder!.closeRecorder();
_audioRecorder = null;
_isRecordingInitialised = false;
}
/// start record
Future _record() async {
assert(_isRecordingInitialised);
await _audioRecorder!
.startRecorder(toFile: pathToSaveAudio, codec: Codec.aacMP4);
}
/// stop record
Future _stop() async {
if (!_isRecordingInitialised) return;
await _audioRecorder!.stopRecorder();
}
I think what I'm doing is record the sound and put it in the file in that temp directory. But appearently the app want to acces the file before I even start recording. At this point, I don't know what to do, please help me.
I have a page that writes a color on file, called "colors.txt".Then the page is closed, when it will be opened again this file will be read and its content (String) printed on the screen.
This is the class that handles reads and writes :
class Pathfinder {
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
return File('$path/colors.txt');
}
Future<File> writeColor(String color) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$color');
}
Future<String> readColor() async {
try {
final file = await _localFile;
// Read the file
final contents = await file.readAsString();
return contents;
} catch (e) {
// If encountering an error, return 0
return "Error while reading colors";
}
}
}
Before page closure, the color has been saved with writeColor, we just need to read the file and print its content.
And this is how I read the color :
void initState() {
super.initState();
String colorRead;
() async {
pf = new Pathfinder();
colorRead = await pf.readColor();
}();
print("Color in initState: " + colorRead.toString());
}
The problem is that colorRead is always null. I already tried .then() and .whenCompleted() but nothing changed.
So my doubt is :
Am I not waiting read operation in right way or the file, for some reasons, is deleted when page is closed?
I think that if file wouldn't exists then readColor should throw an error.
EDIT : How writeColor is called :
Color bannerColor;
//some code
await pf.writeColor(bannerColor.value.toRadixString(16));
void initState() {
super.initState();
String colorRead;
() async {
pf = new Pathfinder();
colorRead = await pf.readColor();
}();
print("Color in initState: " + colorRead.toString()); /// << this will execute before the async code in the function is executed
}
It's null because of how async/await works. The print statement is going to be called before the anonymous async function finishes executing. If you print in inside the function you should see the color if everything else is working correctly.
My flutter app user data takes up a lot of space. I'm currently using the following code to save the user data
class FileUtil {
static Future<String> get getFilePath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
static Future<File> get getFile async {
final path = await getFilePath;
return File('$path/user.txt');
}
static Future<File> saveToFile(String data) async {
final file = await getFile;
return file.writeAsString(data);
}
static Future readFromFile() async {
try {
final file = await getFile;
String fileContents = await file.readAsString();
log(fileContents);
return json.decode(fileContents);
} catch (e) {
return "";
}
}
String formatData() {
String formattedString;
Map x = {};
x['a'] = a;
// other variables
formattedString = json.encode(x);
return formattedString;
}
void saveData() async {
try {
await saveToFile(formatData());
//print('DATA SAVED');
} catch (e) {
//print('Could not save data due to: $e');
}
}
}
Whenever the user interacts with something in the app that needs to be saved, I run saveData(). This happens quite often in my app. However, after using the app for a while, the user data can jump to a few hundred MB. I've used a JSON calculator to estimate the space of the formatData() output string and it's much less than 1MB. What should I do to minimise user data?
I'm reading the google_fonts documentation and I'm mostly here for clarification.
"With the google_fonts package, .ttf or .otf files do not need to be stored in your assets folder and mapped in the pubspec. Instead, they can be fetched once via http at runtime, and cached in the app's file system. This is ideal for development, and can be the preferred behavior for production apps that are looking to reduce the app bundle size."
Do I have to follow an extra step to cache the fonts, or should it happen automatically assuming I use the following code from the documentation:
Text(
'This is Google Fonts',
style: GoogleFonts.lato(),
),
I think I'm tripped up by the wording here. When it says the fonts can be fetched once at runtime, I want to know if that's what happens by default.
I am currently doing some font library research for custom fonts, and what I see in Google Fonts per:
https://github.com/material-foundation/google-fonts-flutter/blob/develop/lib/src/google_fonts_variant.dart
Looks pretty directly that it does per the code:
Future<void> saveFontToDeviceFileSystem(String name, List<int> bytes) async {
final file = await _localFile(name);
await file.writeAsBytes(bytes);
}
Future<ByteData> loadFontFromDeviceFileSystem(String name) async {
try {
final file = await _localFile(name);
final fileExists = file.existsSync();
if (fileExists) {
List<int> contents = await file.readAsBytes();
if (contents != null && contents.isNotEmpty) {
return ByteData.view(Uint8List.fromList(contents).buffer);
}
}
} catch (e) {
return null;
}
return null;
}
At runtime it checks in:
https://github.com/material-foundation/google-fonts-flutter/blob/develop/lib/src/google_fonts_base.dart
Future<void> loadFontIfNecessary(GoogleFontsDescriptor descriptor) async {
final familyWithVariantString = descriptor.familyWithVariant.toString();
final fontName = descriptor.familyWithVariant.toApiFilenamePrefix();
// If this font has already already loaded or is loading, then there is no
// need to attempt to load it again, unless the attempted load results in an
// error.
if (_loadedFonts.contains(familyWithVariantString)) {
return;
} else {
_loadedFonts.add(familyWithVariantString);
}
try {
Future<ByteData> byteData;
// Check if this font can be loaded by the pre-bundled assets.
final assetManifestJson = await assetManifest.json();
final assetPath = _findFamilyWithVariantAssetPath(
descriptor.familyWithVariant,
assetManifestJson,
);
if (assetPath != null) {
byteData = rootBundle.load(assetPath);
}
if (await byteData != null) {
return _loadFontByteData(familyWithVariantString, byteData);
}
// Check if this font can be loaded from the device file system.
byteData = file_io.loadFontFromDeviceFileSystem(familyWithVariantString);
if (await byteData != null) {
return _loadFontByteData(familyWithVariantString, byteData);
While it doesn't work for me directly, it is the base of dynamic_fonts which I am evaluating:
https://pub.dev/packages/dynamic_fonts/
I agree though, the wording isn't totally clear if its handling caching or not but looks to be per the code.
I am using a flutter plugin named path_provider. I have to store image file at path_provider.getTemporaryDirectory(). Is the image stored here is deleted automatically or I have to do it explicitly.
from the documentation of path_provider
Files in this directory may be cleared at any time. This does not return
a new temporary directory. Instead, the caller is responsible for creating
(and cleaning up) files or directories within this directory. This
directory is scoped to the calling application.
So you are responsible for cleaning up, which means it is not automatically cleared, but it may be cleared any time
Edit
You can clear the temporary the directory as follows:
import 'dart:io';
....
Directory dir = await getTemporaryDirectory();
dir.deleteSync(recursive: true);
dir.create(); // This will create the temporary directory again. So temporary files will only be deleted
To keep things safe, I store all the file paths in list with flutter_secure_storage, then when I launch the app I browse all the file paths, check if file still exists (can be previously deleted by system) and delete it. Finally, I clear the list from flutter_secure_storage for next time.
AppSecureStorage.dart
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AppSecureStorage {
static const _storage = FlutterSecureStorage();
static const TEMP_FILES_PATH = 'temp-files-path';
static Future<String?> get(String key) async {
return await _storage.read(key: key);
}
static Future<void> set(String key, String? value) async {
if (value == null) {
await _storage.delete(key: key);
} else {
await _storage.write(
key: key,
value: value,
);
}
}
static Future<List<String>> getTempFilesPath() async {
final tempFilesPathValue = await get(
AppSecureStorage.TEMP_FILES_PATH,
);
List<String> tempFilesPathStrList = [];
if (tempFilesPathValue != null) {
List<dynamic> tempFilesPathDynamicList = jsonDecode(tempFilesPathValue);
for (var element in tempFilesPathDynamicList) {
tempFilesPathStrList.add(element.toString());
}
}
return tempFilesPathStrList;
}
static Future<void> addTempFile(String filePath) async {
final tempFilesPath = await AppSecureStorage.getTempFilesPath();
tempFilesPath.add(filePath);
AppSecureStorage.set(
AppSecureStorage.TEMP_FILES_PATH,
jsonEncode(tempFilesPath),
);
}
}
main.dart
void _handleTempFiles() async {
List<String> tempFilesPath = await AppSecureStorage.getTempFilesPath();
for (var element in tempFilesPath) {
final file = File(element);
// check if still exist : tmp file can be deleted by system
if (await file.exists()) {
file.delete();
}
}
AppSecureStorage.set(AppSecureStorage.TEMP_FILES_PATH, jsonEncode([]));
}