Rad s datotekama

image

Prije nego što objasnimo i nakon što poslije toga sami isprobate primjer opisan u današnjem nastavku serijala, dobar uvodni korak trebala bi biti instalacija svih u međuvremenu pripremljenih nadogradnji alata koji se koriste u ovom serijalu - Fluttera, Darta i samog Android Studija.

Budući da ovaj serijal traje već neko vrijeme, u međuvremenu se pojavilo nekoliko zbilja zanimljivih nadogradnji poput podrške za novu verziju Androida (Android Q Beta) ili preglednijeg prikaza struktura u programskom kodu (vidi prateću sliku). Dok se nadogradnja Android Studija izvodi odabirom odgovarajuće automatski predložene opcije u samom razvojnom alatu, nadogradnju Fluttera i Darta najjednostavnije možete napraviti upisom sljedeće naredbe u komandnu liniju:

flutter upgrade

Kao rezultat izvođenja prethodne naredbe dobije se nešto poput sadržaja na pratećoj slici (ako je nadogradnja već prije toga uspješno dovršena) ili puno veći skup poruka ako treba izvesti cijeli postupak. Trenutna stabilna verzija Android Studija je 3.4 (u pripremi je 3.5), dok su u trenutku pisanja ovog teksta Flutter i Dart stigli do verzija 1.5.4 odnosno 2.3.0.

Vratimo se sada na glavnu temu današnjeg teksta. Riječ je o novom primjeru koji pokazuje kako se u okviru vlastite aplikacije mogu spremati i ponovo koristiti vrijednosti potrebne za normalno izvođenje programa. Trenutna verzija projekta nema izravne veze s primjerom iz prethodnog broja gdje smo započeli s izradom kontura „VIDI Chat“ aplikacije, ali će u budućnosti svakako zatrebati integracija oba navedena primjera. Kao i obično, da biste izbjegli prekucavanje, cjelokupni projekt možete preuzeti na Vidilabovom GitHubu.

Prije nego što počnemo s objašnjavanjem osnovne ideje i implementacije današnjeg primjera, spomenimo kako je za njegov normalni rad ponovo potrebno uključiti nekoliko dodatnih paketa (import naredbe na samom početku koda). Drugim riječima, opet treba nadopuniti datoteku pubspec.yaml.

Android Studio (nadogradnja): Poboljšani prikaz struktura u programskom kodu.

Nakon svih potrebnih dodataka za današnji primjer, odgovarajući dio navedene datoteke trebao bi imati sljedeći oblik:

...

dependencies:

  flutter:

    sdk: flutter

  # The following adds the Cupertino Icons font to your application.

  # Use with the CupertinoIcons class for iOS style icons.

  cupertino_icons: ^0.1.2

  intl: ^0.15.7

  path_provider: ^1.1.0

  shared_preferences: ^0.4.3

dev_dependencies:

  flutter_test:

    sdk: flutter

...

Ovo je prava prilika da razjasnimo što zapravo znače oznake poput ^1.1.0 ili ^0.4.3 jer ćete se s nečim sličnim često susretati u Flutteru. Objasnimo prvo značenje svakog od pojedinih brojeva u vrijednosti 1.1.0 kao primjeru oznake.

Prvi broj (1) označava glavnu oznaku verzije, nakon čije promjene se mogu pojaviti nekompatibilnosti u korištenju nekog paketa.

Drugi broj (1) označava manje promjene u verziji, nakon kojih bi trebala biti očuvana kompatibilnost bez obzira na promjenu broja verzije.

Treći broj (0) označava samo ispravke uočenih grešaka bez narušavanja kompatibilnosti.

Na temelju prethodnog može se zaključiti da se ^1.1.0 može ustvari tumačiti kao:

>= ^1.1.0 i

Verzija 2.0.0 u ovom slučaju nije uključena u uvjet jer se kod promjene prvog broja u označavanju verzija može pojaviti nekompatibilnost sa starijom verzijom programskog koda.

Situacija se dodatno komplicira kad je prvi broj u oznaci verzije 0 – na primjer, prije spomenuta vrijednost ^0.4.3. Broj 0 na samom početku oznake verzije znači da je još uvijek riječ o „nestabilnoj“ verziji paketa u razvoju, pa zato i manja promjena oznake verzije (u ovom slučaju vrijednosti 4 u 5) može također dovesti do određenih kompatibilnosti.

Za one koje to zanima, dodatne informacije o označavanju verzija u softveru mogu se pronaći na web adresi: https://semver.org/

Vratimo se sada konačno današnjem primjeru. Prvo je u nastavku naveden njegov cjelokupni kod, a onda slijedi objašnjenje. Kako izvođenje primjera izgleda u stvarnosti možete provjeriti  na pratećim slikama uz tekst ili samostalnim pokretanjem primjera.

import ‘package:flutter/foundation.dart’;

import ‘package:flutter/material.dart’;

import ‘package:path_provider/path_provider.dart’;

import “package:intl/intl.dart”;

import ‘package:shared_preferences/shared_preferences.dart’;

import ‘dart:async’;

import ‘dart:io’;

void main() {

  runApp(

    MaterialApp(

      title: ‘Reading and Writing Files’,

      home: FlutterFile(fo: FileOperation()),

    ),

  );

}

class FileOperation  {

  Future get currentDir async {

    final dir = await getApplicationDocumentsDirectory();

    return dir.path;

  }

  Future get systemFile async {

    final path = await currentDir;

    return File(‘$path/system.txt’);

  }

  Future get currentFile async {

    final path = await currentDir;

    return File(‘$path/data.txt’);

  }

  Future readMessageFile() async {

    try {

      final file = await currentFile;

      String message = await file.readAsString();

      return (message);

    } catch (e) {

      return “readMessageFile: “ + e.toString();

    }

  }

  Future readMessagePreferences() async {

    try {

      final prefs = await SharedPreferences.getInstance();

      final key = ‘last_message’;

      final message = prefs.getString(key) ?? “”;

      return (message);

    } catch (e) {

      return “readMessagePreferences: “ + e.toString();

    }

  }

  Future writeMessage(String message) async {

    try {

      final prefs = await SharedPreferences.getInstance();

      final key = ‘last_message’;

      prefs.setString(key, message);

      final file = await currentFile;

      file.writeAsString(‘$message’);

      return “”;

    } catch (e) {

      return “writeMessage: “ + e.toString();

    }

  }

  Future initSystem() async {

    try {

      final file = await systemFile;

      String sysMessage = “Aplikacija je pokrenuta: “ + DateFormat(‘dd.MM.yyyy kk:mm:ss’).format(DateTime.now());

      file.writeAsString(sysMessage);

      return “”;

    } catch (e) {

      return “initSystem: “ + e.toString();

    }

  }

  Future readSystem() async {

    try {

      final file = await systemFile;

      String sysMessage = await file.readAsString();

      return (sysMessage);

    } catch (e) {

      return “readSystem: “ + e.toString();

    }

  }

}

class FlutterFile extends StatefulWidget {

  final FileOperation fo;

  FlutterFile({Key key, @required this.fo}) : super(key: key);

  @override

  _FlutterFile createState() => _FlutterFile();

}

class _FlutterFile extends State {

  String sysdata = “”;

  String data = “”;

  @override

  void initState() {

    super.initState();

    widget.fo.initSystem();

  }

  void testRW() {

    widget.fo.writeMessage(“Nova poruka je zapisana u “ + DateFormat(‘dd.MM.yyyy kk:mm:ss’).format(DateTime.now()));

    //    widget.fo.readMessageFile().then((String value) {

    widget.fo.readMessagePreferences().then((String value) {

      setState(() {

        data = value;

      });

    });

  }

  @override

  Widget build(BuildContext context) {

      widget.fo.readSystem().then((String value) {

        setState(() {

          sysdata = value;

        });

      });

    return Scaffold(

      appBar: AppBar(title: Text(‘VIDI Demo - rad s datotekama’)),

        body: Center(

          child: Column(

            children: [

              Text(sysdata),

              Text(data),

            ],

          ),

        ),

      floatingActionButton: FloatingActionButton(

        onPressed: testRW,

        tooltip: ‘Zapis nove poruke u datoteku’,

        child: Icon(Icons.add),

      ),

    );

  }

}

Flutter i Dart: Postupak nadogradnje na zadnje verzije alata

Početno stanje aplikacije: Prikazuje se vrijeme njezina pokretanja prije toga zapisano u „sistemsku“ datoteku

Promjena načina čitanja: Mjesto u kodu za biranje između korištenja zajedničkih postavki i lokalnih datoteka

Tri uobičajena načina na koji Android program (uključujući verzije napravljene pomoću alata Dart / Flutter) može spremiti podatke za kasnije korištenje, ne koristeći pritom usluge nekog servera, su:

• korištenje zajedničkih postavki (shared preferences)

• zapisivanje u lokalne tekstualne datoteke

• zapisivanje u lokalnu SQLite bazu podataka

Današnji primjer demonstrira prve dvije mogućnosti, dok će treća mogućnost (korištenje lokalne baze podataka) biti objašnjena i potkrepljena primjerom sljedeći put.

Glavni dio programskog koda za rad sa zajedničkim postavkama i lokalnim tekstualnim datotekama nalazi se u klasi FileOperation, dok ostatak koda predstavlja primjer njezina korištenja preko minimalnog oblika korisničkog sučelja. Sučelje samo prikazuje prethodno spremljene poruke. Djelovanje klase najjednostavnije je objasniti preko namjene njezinih dijelova:

currentDir

Vraća aktivnu mapu (direktorij) u kojoj će biti kreirane lokalne tekstualne datoteke za operacije čitanja i zapisa podataka.

systemFile

Naziv aktivne „sistemske“ datoteke (system.txt) za inicijalni zapis sistemskih parametara ako već ne postoje od prije te njihovo naknadno čitanje. U punoj verziji aplikacije tu bi se mogle spremati različite prilagodbe u načinu djelovanja aplikacije koje može definirati sami korisnik.

currentFile

Naziv standardne podatkovne datoteke (data.txt) u koje aplikacija sprema podatke.

readMessageFile

Čita podatke iz standardne podatkovne datoteke.

readMessagePreferences

Čita podatke iz zajedničkih postavki programa.

writeMessage

Zapisuje iste podatke u tekstualnu datoteku i istovremeno u zajedničke postavke. Na taj način se zadnja vrijednost može pročitati s oba mjesta.

initSystem

Zapisuje početnu vrijednost sistemskih podataka u sistemsku datoteku.

readSystem

Čita podatke iz sistemske datoteke.

Pomoću sljedećeg dijela programskog koda u glavnom dijelu programa:

//    widget.fo.readMessageFile().then((String value) {

widget.fo.readMessagePreferences().then((String value) {

može se birati odakle se želi pročitati prethodno spremljeni podatak. Po potrebi možete komentirati ili ukloniti komentar s odgovarajućeg reda programa.

Objasnimo još čemu služe dijelovi future, async i await jer ih do sada nismo koristili. Prilikom čitanja i zapisivanja podataka u datoteke u današnjem primjeru koristi se asinkroni pristup. Na taj način glavni dio programa u Dartu može nastaviti svoje izvođenje dok se čeka izvođenje dijelova koji mogu potrajati (čitanje ili zapis u datoteku).

Objekt označen kao „future“ predstavlja rezultat izvođenja asinkrone operacije. U slučaju da izvođenje asinkrone operacije treba obustaviti dok se ne izvede neka druga operacija, za to se može koristiti navođenje riječi „await“ u okviru dijela koda označenog s async.

Na primjer, u sljedećem primjeru koda:

  Future get systemFile async {

    final path = await currentDir;

    return File(‘$path/system.txt’);

  }

da bi se u potpunosti mogla pripremiti putanja do sistemske datoteke, treba prvo osigurati dovršetak izvođenja dijela koji vraća aktivnu mapu ili direktorij.

Sljedeći put pozabavit ćemo se korištenjem lokalne baze podataka.