What is the best way to work with files in Flutter? - flutter

I'm a junior working with flutter and hit a problem.
I need to open a file, read and compare some data everytime the app opens and then change some of that data as the app progress. We tried using .txt files to read and write some text, but when we had to look for something in the file was too complicated to change it and the file is not accessibe only on the device running the app. We also thought of using xml files but I don't know if is a good idea.
What would be a pratical solution for this situation, as the file needs to be opened all the time. Thanks.

Let's say our JSON looks like this:
{
title: 'some text',
values: [1,5,2,4,1,3],
}
And we want to make a UI that allows us to add values and to edit the title. First let's write a method to write and read from the JSON file:
Future<void> _write(Map<String, dynamic> value) async {
File f = File(_fileLocation);
String jsonStr = jsonEncode(value);
await f.writeAsString(jsonStr);
}
Future<Map<String, dynamic>> _read() async {
File f = File(_fileLocation); // maybe move this into a getter
final jsonStr = await f.readAsString();
return jsonDecode(jsonStr) as Map<String, dynamic>;
}
This way, the rest of the app should be trivial, but let's add a method to update the title and a method to add a new number:
Future<void> _updateTitle(String title) async {
var values = await _read();
values['title'] = title;
await _write(values);
}
Future<void> _addNumber(int number) async {
var values = await _read();
values['values'].push(number);
await _write(values);
}
Types with JSON and Dart can be a bit weird, so it is possible you need to use the as keyword when reading from the list:
Future<void> _addNumber(int number) async {
var values = await _read();
var valueList = (values['values'] as List<int>);
valueList.push(number);
values['values'] = valueList;
await _write(values);
}
Hopefully, this example helps

Related

save map locally and use it elsewhere

I'm converting a map to a string in order to save it to the device memory
_read() async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/answers.txt');
String text = await file.readAsString();
print(text);
} catch (e) {
print("Couldn't read file");
}
}
_save() async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/answers.txt');
await file.writeAsString(answers.toString());
print('saved');
}
now I want to use it as a map to access the data on the map. is there a way to do it?
my map looks like this {Everyone should read...: Harry Potter, Two truths and a lie...: something, I can quote every line from...: the alchemist}
What you want is a JSON file.
JSON is a special syntax that can be used to store maps and lists in a file.
There is a catch though: You may only store maps and lists of primitive values like string, int or bool, a custom class, for example, cannot be stored in a JSON file. You would have to convert it into a map first.
In order to turn a JSON string into a map, you can use the jsonDecode function. Similarly, the jsonEncode function will return a string from a map.
Here goes the code:
Future<Map<String, dynamic>> _read() async {
final file = File(filePath);
final jsonStr = await file.readAsString()
return jsonDecode(jsonStr) as Map<String, dynamic>>;
}
Future<void> _write(Map<String, dynamic> map) async {
final jsonStr = jsonEncode(map);
final file = File(filePath);
await file.writeAsString(jsonStr);
}
In my code I skipped the try-catch block and the Directory thing, that's just to make the example simpler.

Flutter - How to write String into json files

I followed the guide in the flutter doc and didnt understand how it works.
I have a existing file containing users info, and I want to be able to write/update the file.
Here's the relevant parts of my code:
Future<bool> sendMessage(String sender, String target, String body) async {
String tempAccountData = await loadMessage();
List<Map<String, dynamic>> accountData =
List<Map<String, dynamic>>.from(jsonDecode(tempAccountData));
//find valid target
print(accountData);
print(target);
for (Map<String, dynamic> accountInfo in accountData) {
if (accountInfo["username"] == target) {
//write message
accountInfo["messages"]
.add({"time": DateTime.now(), "sender": sender, "message": body});
writeMessage(accountData);
return true;
}
}
return false;
}
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
print(path);
return File('$path/accountInfo.json');
}
Future<File> writeMessage(List<Map<String, dynamic>> accountData) async {
File file = await _localFile;
// Write the file
return file.writeAsString('$accountData');
}
I didnt know what the path is, and I put the file into assets/data as seen in the screenshot below: (I'm trying to read/write accountInfo.json from accountInfo.dart)
It's not possible to modify a file in the assets during runtime.
What you can do is write all the data in the Json to a file in one of the device's directories and then modify that file when needed or use a database to store all the data.
There are a few steps that need to happen and things you'll need to know. First of all, assets are immutable. This means you can read from them, but you're not able to write to them. What you'll need is a local file. A file that lives on the file system of the device where the app is installed. This file can be modified, and will allow writing to it.
The process would go as follows:
Check if there's already a local file
If not, create it and put the contents of an initial asset file onto it.
Now that the mutable local file exists, read and write from it.
Here's a code sample of a class that takes care of that:
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
const initialAssetFile = 'assets/initial.json';
const localFilename = 'data.json';
class Repository {
/// Initially check if there is already a local file.
/// If not, create one with the contents of the initial json in assets
Future<File> _initializeFile() async {
final localDirectory = await getApplicationDocumentsDirectory();
final file = File('$localDirectory/$localFilename');
if (!await file.exists()) {
// read the file from assets first and create the local file with its contents
final initialContent = await rootBundle.loadString(initialAssetFile);
await file.create();
await file.writeAsString(initialContent);
}
return file;
}
Future<String> readFile() async {
final file = await _initializeFile();
return await file.readAsString();
}
Future<void> writeToFile(String data) async {
final file = await _initializeFile();
await file.writeAsString(data);
}
}
It will basically check before every read or write if the local file has already been created, and will take care of that if not.
Note that there's no JSON specific things happening here, this is just default file IO. JSON encoding/decoding should happen before/after this.
You'll just need to add an initial file into the assets/ folder and specify that in your pubspec.yaml as follows:
flutter:
assets:
- assets/initial.json # the file containing the initial data
# - assets/ # you can also just add the whole directory
Once that's done, you should be able to use readFile() and writeFile(String data) from this Repository class.
(It might be important to make sure WidgetsFlutterBinding.ensureInitialized(); has been called in order to interact with the local file system)

Flutter - How to add and retrieve data to/from hive?

I know it sounds simple and I went through the example given in the documentation. Yet somehow I am unable to get it right.
This is what I have:
void main() async {
await Hive.initFlutter();
//Hive.openBox('workoutBox');
runApp(const MyApp());
}
...
Next Screen:
var box;
...
Trying to add to the box
Future<void> _save() async{
// save doc id somewhere
final Id = doc.id;
//box = await Hive.openBox('workoutBox');
box.put("Id", Id);
}
Trying to retrieve in another function:
var someId = box.get("Id");
Current error: get was called on null
My confusion is, where/how do you declare, open and retrieve from the box in this situation?
It seems you are forgetting to initialize a Box param and assign the value returned by the openBox function to it.
After Hive initialization you should have something like this:
Box<myValue> boxValue = await Hive.openBox("myKey");
Important: the retrieval method will dependend based on what you need to do and, more importantly, how you saved your data in the first place.
Let's say you saved data like this:
await boxValue.add(value);
By adding data like this, the key assigned to the value will be an auto-incremented one, so that trying to retrieve it with a specific key that never was assigned in the first place will fail.
If you did add the data like this:
await boxValue.put("myKey", value);
then you will be able to successfully fetch it using the intended key.
You can do the following:
void main() async {
await Hive.initFlutter();
await Hive.openBox('workoutBox'); //<- make sure you await this
runApp(const MyApp());
}
...
_save() { // <- can be a synchronous function
final box = Hive.box('workoutBox'); //<- get an already opened box, no await necessary here
// save doc id somewhere
final Id = doc.id;
box.put("Id", Id);
}
I have written an example app and a Flutter Cubits + Hooks + Hive DB tutorial. I have the following AppDatabase class there:
const String _bookBox = 'book';
#Singleton()
class AppDatabase {
AppDatabase._constructor();
static final AppDatabase _instance = AppDatabase._constructor();
factory AppDatabase() => _instance;
late Box<BookDb> _booksBox;
Future<void> initialize() async {
await Hive.initFlutter();
Hive.registerAdapter<BookDb>(BookDbAdapter());
_booksBox = await Hive.openBox<BookDb>(_bookBox);
}
Future<void> saveBook(Book book) async {
await _booksBox.put(
book.id,
BookDb(
book.id,
book.title,
book.author,
book.publicationDate,
book.about,
book.readAlready,
));
}
Future<void> deleteBook(int id) async {
await _booksBox.delete(id);
}
...

How to use a String returned by a Future<String> method in flutter?

I'm trying to read a json file's components using the following method:
import 'dart:io';
class CharacterDataReader {
Future<String> read() async {
final file = File('assets/data/character_data.json');
String data = await file.readAsString();
return data;
}
}
Now, I'm trying to assign the read values to a String named data and json.decode() it in another class using the following:
Future<String> data = CharacterDataReader().read();
Map<String, dynamic> characterData = json.decode(data);
However, this doesn't work since json.decode() only accepts Strings as a parameter. Therefore, can someone please tell me how do I convert this Future into an actual string?
since its a future you have to add await keyword
String data= await CharacterDataReader().read();
check out dart official doc on asynchronous programming

Saving DateTime type in Local File in Flutter

I have a form with a TextField and a submit button that is able to save and read the data.
class Storage {
Future<String> get localPath async {
final dir = await getApplicationDocumentsDirectory();
return dir.path;
}
Future<File> get localFile async {
final path = await localPath;
return File('$path/db.txt');
}
Future<String> readData() async {
try {
final file = await localFile;
String body = await file.readAsString();
return body;
} catch (e) {
return e.toString();
}
}
Future<File> writeData(String data) async {
final file = await localFile;
return file.writeAsString('$data');
}
}
#override
void initState() {
super.initState();
widget.storage.readData().then((String value) {
setState(() {
name = value;
});
});
}
Future<File> writeData() async {
setState(() {
name = oneController.text;
oneController.text = '';
});
}
With this I was able to save data with String values. I tried doing the same thing for DateTime and I get this error:
"The argument type 'Null Function(DateTime)' can't be assigned to the parameter type 'FutureOr Function(String)'"
Does saving to local file only work for String Data?
The error you are getting seems to be because you are trying to do this:
widget.storage.readData().then((DateTime value) {
setState(() {
name = value;
});
});
Using DateTime as an argument, but it seems you forgot to change the return type of readData() to Future<DateTime>. Anyway, that's seems to be the error.
But as #Adrian mentioned, you could store the int property millisecondsSinceEpoch instead of DateTime, and then you could do this:
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampSaved);
To answer the other issue, you have to save strings to files. An object must be converted to a string. A typical way to save DateTime is to do something like millisecondsSinceEpoch and save that in your file.
Rather than reinvent what's been done before, I suggest looking into packages/plugins for persistent storage. One promising one is hive. You can search for more at pubdev. There you can find persistent storage options like shared_preferences, sqflite, and sembast. A couple of these (sembast and hive) are basically object stores within files. Kind of like what you're trying to do here.