Для работы с файлами Dart можно использовать потоки. Поток (stream) представляет абстракцию, которая применяется для передачи набора данных. Например, если надо считать данные с файла, приложение можно использовать файловый поток для взаимодействия с файлом и получения от него данных.
Если файл большой, его можно прочитать как поток. Это позволяет вам начать обработку данных быстрее, так как не нужно ждать, пока будет прочитан весь файл. Потому что при чтении файла в виде потока считывание файла производится отдельными кусочками или чанками (chunks). Размер каждого такого чанка зависит от конкретной системы, в рамках которой работает приложение на Dart, но обычно это 65 536 байт. Если файл меньшего размера, то не будет большой разницы - считывать файл целиком или как поток.
Для получения файлового потока может применяться один из методов класса File в зависимости от того, что мы собираемся делать с файлом:
openRead(): возвращает поток для чтения файла в виде объекта Stream<T>
Stream<List<int>> openRead([int? start, int? end]);
Первый параметр - start
представляет индекс байта от начала файла, начиная с которого надо считывать данные (по умолчанию данные считываются с самого начала).
Второй параметр - end
представляет индекс байта в файле, до которого надо считывать данные (по умолчанию данные считываются до конца файла).
openWrite(): возвращает поток для чтения файла в виде объекта IOSink
IOSink openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8});
Первый параметр - mode
представляет режим открытия файла. По умолчанию это значение FileMode.write
, которое представляет перезапись файла.
Если же надо выполнить дозапись, применяется значение FileMode.append
Второй параметр - encoding
устанавливает кодировку записи. По умолчанию это UTF-8.
open(): возвращает поток для произвольного доступа к файлу в виде объекта RandomAccessFile
Futureopen({FileMode mode = FileMode.read});
В качестве параметра принимает режим открытия файла. По умолчанию это значение FileMode.read
, то есть файл открывается для чтения.
Поток для записи представляет тип IOSink, который представляет ряд методов записи данных:
void write(Object? object)
: записывает в поток произвольный объект
void writeAll(Iterable objects, [String separator = ""])
: записывает в поток набор objects. Для отделения объектов можно передать separator - разделитель
void writeCharCode(int charCode)
: записывает в поток числовой код символа
void writeln([Object? object = ""])
: преобразует объект в строку, вызывая у него метод toString()
и записывает строку
в поток. К строке добавляется символ перевода строки \n.
Например, запишем в файл строку:
import 'dart:io'; void main() async { final file = File("file.txt"); var sink = file.openWrite(); sink.write("Hello Metanit.com\n"); // закрываем IOSink и освобождаем все ресурсы системы await sink.close(); print("File has been written"); }
В данном случае для записи строки применяем метод write()
, в который передаем записываемую строку.
Стоит отметить, что после окончания работы с потоком записи его надо закрыть методом close(), который освобождает все связанные ресурсы системы.
Чтобы открыть файл для записи, применяется режим открытия FileMode.append
:
import 'dart:io'; void main() async { final file = File("file.txt"); // открываем файл для дозаписи var sink = file.openWrite(mode: FileMode.append); sink.write("Hello Metanit.com\n"); await sink.close(); print("File has been written"); }
При записи для объектов классов в файл записывается строковое представление объекта, которое определяется с помощью метода toString()
. Например:
import 'dart:io'; void main() async{ Person tom = Person("Tom", 38); final file = File("person.dat"); var sink = file.openWrite(); sink.write(tom); await sink.close(); print("File has been written"); } class Person{ String name; int age; Person(this.name, this.age); void display() => print("Name: $name \tAge: $age"); @override toString() => "{name: $name, age: $age}"; }
В данном случае при записи объекта tom в файл будет записана строка "{name: Tom, age: 38}".
Для чтения данных применяется поток Stream<List<int>>, который передает данные в виде списка байт. Причем получение даннз из потока происходит через подписку на уведомления - когда в потоке появляется новая порция данных, поток посылает уведомление. И чтобы подписаться на уведомления, надо использовать метод listen() класса Stream:
StreamSubscription<T> listen(void onData(T event)?, {Function? onError, void onDone()?, bool? cancelOnError});
В качестве обязательного параметра в метод передается функция onData
, которая вызывается при каждом событии в потоке и через параметр получает полученную
порцию данных.
Дополнительно метод принимает функции onError()
(вызывается при возникновении ошибки) и onDone()
(вызывается после закрытия потока)
Последний параметр - cancelOnError
позволяет отменить подписку при генерации оишбки (при значении true
)
Например, пусть у нас есть файл file.txt
, в который записана строка "Hello Metanit.com". Считаем его через поток:
import 'dart:io'; void main(){ final file = File("file.txt"); final stream = file.openRead(); stream.listen( (data) => print(data)); }
В качестве обработчика получения данных в метод listen()
передана анонимная функция, которая просто выводит данные на консоль. В данном случае консольный вывод будет
следующим:
[72, 101, 108, 108, 111, 32, 77, 101, 116, 97, 110, 105, 116, 46, 99, 111, 109, 10]
Поскольку поток для чтения получает данные в виде списка байт, то в анонимную функцию в качестве аргумента передается список байт, который потом выводится на консоль. То есть фактически этот список байт представляет строку "Hello Metanit.com". А отдельные числа в этом списке - это числовые кода символов строки.
Поскольку записанные данные в реальности представляют строку, а не бинарные данные, то мы можем их преобразовать в строку с помощью встроенной функции
utf8.decode()
из пакета dart:convert
:
import 'dart:io'; import 'dart:convert'; void main(){ final file = File("file.txt"); final stream = file.openRead(); // преобразуем в строку и выводим на консоль stream.listen( (data) => print(utf8.decode(data))); }
Также есть еще один способ получения данных - в цикле for-in
:
import 'dart:io'; import 'dart:convert'; void main() { final file = File("file.txt"); final stream = file.openRead(); await for (var data in stream) { print(utf8.decode(data)); } }
Ключевое слово await перед определением цикла позволяет циклу приостановить выполнение, пока не будет получена новая порция данных.
Метод listen()
класса Stream позволяет через необязательные параметры передать функции обратного вызова, которые будут срабатывать при возникновении
ошибки или при завершение получения данных
import 'dart:io'; import 'dart:convert'; void main() { final file = File("file.txt"); final stream = file.openRead(); stream.listen( (data) => print(utf8.decode(data)), onError: (error) {print(error);}, // срабатывает при ошибке onDone: () { print("End of reading");} // срабатывает при завершении ); }
Обработчик onError
принимает объект ошибки и срабатывает при возникновении ошибки.
Обработчик onDone
не принимает параметров и срабатывает при завершении работы функции.
Последний необязательный параметр - cancelOnError
прекращает работу потока, если этот параметр равен true
:
stream.listen( (data) => print(utf8.decode(data)), onError: (error) {print(error);}, // срабатывает при ошибке onDone: () { print("End of reading");}, // срабатывает при завершении cancelOnError: true // прекращаем работу потока при возникновении ошибки );
По умолчанию cancelOnError равен false, что означает, что даже если возникнет ошибка, обработчик onDone
все равно будет срабатывать.
Естественно для обработки ошибки также можно использовать конструкцию try-catch
, особенно если мы считываем данные в цикле for
import 'dart:io'; import 'dart:convert'; void main(){ try { final file = File("file.txt"); final stream = file.openRead(); await for (var data in stream) { print(utf8.decode(data)); } } on Exception catch (error) { print(error); } finally { print("End of reading"); } }
В примерах выше считываемый файл представлял по сути текст. Однако поскольку для потока Stream все считываемые данные представляют набор байт - тип
List<int>
, то, чтобы получить собственно строку, нам приходилось применять к набору байтов функцию utf8.decode()
.
Класс Stream с помошью метода transform() позволяет автоматически преобразовать данные в нужную форму. В качестве параметра он принимает объект StreamTransformer, который управляет преобразованием данных. Применим метод transform():
import 'dart:io'; import 'dart:convert'; void main(){ final file = File("file.txt"); final stream = file.openRead(); await for (var data in stream.transform(utf8.decoder)) { print(data); // data - уже представляет строку } }
В данном случае в метод stream.transform()
передается объект utf8.decoder
, который преобразует данные из байтов в строку. Поэтому на выходе
мы получим строку.
Аналогичное преобразование ср считыванием с помошью метода stream.listen()
import 'dart:io'; import 'dart:convert'; void main(){ final file = File("file.txt"); final stream = file.openRead(); stream .transform(utf8.decoder) // преобразование данных .listen((data) => print(data) ); }