0

I am currently displaying the data by calling the JSON file from Firebase Storage, but I want that instead of download JSON file every single time to show data => I will check if the JSON file from the Firebase Store has changed:

  • If it changed => download the new JSON file to Local directory and display it.
  • Otherwise => display the old JSON file in Local directory (This old JSON file will be downloaded when first time App open)

About JSON File

This is JSON link after I upload JSON to Firebase Storage:

https://firebasestorage.googleapis.com/v0/b/tft-test-48c87.appspot.com/o/loadData.json?alt=media&token=92e3d416-62dc-4137-93a3-59ade95ac38f

As far as I know, this link is made up of 2 parts:

First part: https://firebasestorage.googleapis.com/v0/b/tft-test-48c87.appspot.com/o/loadData.json

Last part: ?alt=media&token= + 2e3d416-62dc-4137-93a3-59ade95ac38f (it is value of String: "downloadTokens" in First part)

In the First part of the link, there is all information about JSON file, and especially I think that value of String "updated" can be used as a condition for the purpose of downloading files or not.

Ex. "updated": "2020-08-04T14:30:10.920Z",

The value of this String updated will change every time I upload a new JSON file with the same name as the old JSON file but the link download will not change.


Steps

So I want to do the following:

  1. Create file to store String "updated" in Local directory (Ex. "updated": null) and where to store the JSON file after download to Local directory
  2. Open App
  3. Check String "updated" in link First Part:
  • Case A: if value of String "updated" in First Part != value of String "updated" in Local directory =>

    • Step 1: download JSON file (by link: First part + ?alt=media&token= + downloadTokens) to Local directory (If the old json file already exists, it will be replaced)
    • Step 2: overwrite value of String "updated" in Local directory by value of String "updated" in Firebase Storage
    • Step 3: access JSON file in Local directory to display data
  • Case B: if value of String "updated" in First Part == value of String "updated" in Local directory => do nothing, just access JSON file in Local directory to display data


I know this is a lot of questions for one post, I'm a newbie with code and if I split it up into a few posts then it is very difficult to combine them for me. So I hope the answer with full code, that would be great. Thanks. This is the main file:

import 'package:ask/model/load_data_model.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class LoadDataPage extends StatefulWidget {
  @override
  _LoadDataPageState createState() => _LoadDataPageState();
}

class DataServices {
  static const String url = 'https://firebasestorage.googleapis.com/v0/b/tft-test-48c87.appspot.com/o/loadData.json?alt=media&token=92e3d416-62dc-4137-93a3-59ade95ac38f';

  static Future<List<Data>> getData() async {
    try {
      final response = await http.get(url);
      if (200 == response.statusCode) {
        final List<Data> data = dataFromJson(response.body);
        return data;
      } else {
        return List<Data>();
      }
    } catch (e) {
      return List<Data>();
    }
  }
}

class _LoadDataPageState extends State<LoadDataPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Load Data')),
        body: FutureBuilder(
            future: DataServices.getData(),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              List<Widget> children;
              List<Data> _data = snapshot.data;
              if (snapshot.hasData) {
                return ListView.builder(
                  itemCount: _data.length,
                  itemBuilder: (context, index) {
                    return Column(
                      children: [Text(_data[index].data)],
                    );
                  },
                );
              } else {
                children = <Widget>[SizedBox(child: CircularProgressIndicator(), width: 60, height: 60), const Padding(padding: EdgeInsets.only(top: 16), child: Text('Loading...'))];
              }
              return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: children));
            }));
  }
}


Another Steps

EdwynZN's answer worked great for me, however, I edit the post to add one more case which I think will make load page ASAP, So please help me again:

After open Page => readFile > compareLastUpdate > _lastUpdateDB & _createFile

  • Case A: The first time the app opens => readFile: false > _lastUpdateDB & _createFile > readFile again
  • Case B: Not the first time the app opens:
    • the data is still loaded immediately from the old JSON, at the same time, run in background: compareLastUpdate:
      • If update times are the same => do nothing
      • If update times are different => _lastUpdateDB & _createFile

P/S: With this flow, the second time they open the page then new data will be displayed, right? But I wonder that if using StatefulWidget => after the new JSON file is overwritten to the old JSON file => will the phone screen display new data after that?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Kel
  • 453
  • 1
  • 11
  • 29

1 Answers1

1

I would recommend using shared_preferences to save the last updated date as a String

import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:convert';


/// Move them outside of the class as Top Level functions
List<Data> readFile(File file) {
  try{
    String data = file.readAsStringSync();
    return dataFromJson(data);
  } catch(e){
    print(e.toString());
    return List<Data>(); // or return an empty list, up to you
  }
}

// No need of encoder now because response body is already a String
void writeFile(Map<String, dynamic> arg) =>
  arg['file']?.writeAsStringSync(arg['data'], flush: true);

class DataServices {

  DateTime dateApi;

  static const String url = 'https://firebasestorage.googleapis.com/v0/b/tft-test-48c87.appspot.com/o/loadData.json?alt=media&token=92e3d416-62dc-4137-93a3-59ade95ac38f';
  static const String urlUpdate = 'https://firebasestorage.googleapis.com/v0/b/tft-test-48c87.appspot.com/o/loadData.json';

  Future<List<Data>> getData() async {
    bool update = await compareLastUpdate;
    if(update) { // that means the update times are the same, so retrieving form json file is better than doing http request
       final file  = await _createFile();
       if(await file.exists()) return await compute(readFile, file);
       else return null; //or an empty List
       // If it doesn't exists (probably first time running the app)
       // then retrieve an empty list, null or check how to fill the list from somewhere else
    }
    try {
      final response = await http.get(url);
      final SharedPreferences preferences = await SharedPreferences.getInstance();
      if (200 == response.statusCode) {
        final String utfData = utf8.decode(response.bodyBytes); //just decode it yourself instead of using response.body which uses [latin1] by default
        final List<Data> data = await compute(dataFromJson, utfData);
        final file  = await _createFile();
        Map<String, dynamic> args = {
          'file': file,
          'data': utfData
          //'data': response.body // pass the return body instead of the data
        };
        await compute(writeFile, args);
        await preferences.setString('updateDate', dateApi.toString()); //Save the new date
        return data;
      } else {
        return List<Data>();
      }
    } catch (e) {
      return List<Data>();
    }
  }

 File _createFile() async{
   Directory tempDir = await getTemporaryDirectory(); // or check for a cache dir also
   return File('${tempDir.path}/Data.json');
 }


Future<bool> get compareLastUpdate async{
  final dateCache = await _lastUpdateDB;
  dateApi = await _lastUpdateApi;

  if(dateCache == null) return false;    
  return dateApi?.isAtSameMomentAs(dateCache) ?? false; // or just isAfter()
  // If dateApi is null (an error conection or some throw) just return false or throw an error and 
  // catch it somewhere else (and give info to the user why it couldn't update)
}

Future<DateTime> get _lastUpdateApi async{
  try {
     final response = await http.get(urlUpdate);
     DateTime dateTime;
     if (200 == response.statusCode) {
       final data = jsonDecode(response.body));
       dateTime = DateTime.tryParse(data['updated'] ?? '');
     } 
     return dateTime;
   } catch (e) {
     return null;
   }
}

  Future<DateTime> get _lastUpdateDB async{
    final SharedPreferences preferences = await SharedPreferences.getInstance();
    return DateTime.tryParse(preferences.getString('updateDate') ?? ''); // Or if it's null use an old date
    // The first time the app opens there is no updateDate value, so it returns null, if that
    // happens replace it by an old date, one you know your api will be always newer,
    // Ex: 1999-08-06 02:07:53.973 Your Api/App didn't even exist back then
    // Or just use an empty String so the tryParser returns null
  }
}

Then in the widget you just call it the same

class _LoadDataPageState extends State<LoadDataPage> {
  final DataServices services = DataServices();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('Load Data')),
        body: FutureBuilder(
            future: services.getData(),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              List<Widget> children;
              List<Data> _data = snapshot.data;
              if (snapshot.hasData) {
                return ListView.builder(
                  itemCount: _data.length,
                  itemBuilder: (context, index) {
                    return Column(
                      children: [Text(_data[index].data)],
                    );
                  },
                );
              } else {
                children = <Widget>[SizedBox(child: CircularProgressIndicator(), width: 60, height: 60), const Padding(padding: EdgeInsets.only(top: 16), child: Text('Loading...'))];
              }
              return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: children));
            }));
  }
}

Also yu could check Dio package which have some functions over http that let you add parameters to the url

EdwynZN
  • 4,895
  • 2
  • 12
  • 15
  • Yeahhhh! I was looking forward to your answer and I know you will help me xD. One thing is, there are [two errors](https://i.imgur.com/N9Qb3x1.jpg) (I think `return` is missing something, and `dataApi` instead of `dataDB`, right?) – Kel Aug 06 '20 at 15:32
  • Yeah you're right, should return null or an empty List and dateApi instead of dateDB – EdwynZN Aug 06 '20 at 16:42
  • It seems the snapshot cannot retrieve data, it keeps loading like [this](https://imgur.com/HrwkzTW). Btw, I saw [this line](https://i.imgur.com/C4jdsp0.jpg) have warning, I am not sure if it's because of it – Kel Aug 06 '20 at 17:09
  • yeah add the return of dateTime in the if, also in `if(!update) return ;` it should be the logic to retrieve the data from a cached file (because there is no need to update) – EdwynZN Aug 06 '20 at 17:13
  • It worked!!! I found [it](https://i.imgur.com/3xutVGP.jpg), However, I am not sure if this code includes downloading the newest JSON to cache? if so, Where will it be located? If not, we probably can't use `SharedPreferences` because it saves the file as `Map `, right? And also we can't overwrite JSON file in `assets`, right? I have no experience with caching, please help me with this, I believe you will recommend the best way for me :D – Kel Aug 06 '20 at 17:50
  • shared_preferences saves data as a key value pair (as a HashMap) so it can be better than just creating, opening and writing/rewriting a json file each time just to check the update value, but for what you want is maybe use a temporary Directory or cache directory using path_provider to create a json file with your data after that – EdwynZN Aug 06 '20 at 18:28
  • Ah, Got it, so please help me by updating your answer to combine with `path_provider` and how to access it following Case A & Case B in my question :"> – Kel Aug 06 '20 at 18:54
  • I added some new methods to read and write a json file from a temporary directory, maybe you can take a hint from that code and improve it for what you want – EdwynZN Aug 06 '20 at 19:19
  • There are [2 errors](https://i.imgur.com/0hCEYY1.jpg), with `File`, I think that replace by `Future`, but with `writeAsBytesSync` method use `List` => I have no idea how to fix it :P – Kel Aug 06 '20 at 19:44
  • Yeah change it to a Future and a writeAsStringSync – EdwynZN Aug 06 '20 at 20:01
  • It keeps loading... again, I doubt the cause is missing of `return` [here](https://i.imgur.com/uVBwcTJ.jpg)? – Kel Aug 06 '20 at 21:18
  • Yeah check for the lints and return the object where is missed – EdwynZN Aug 06 '20 at 22:25
  • I mean it keeps loading like [this](https://imgur.com/HrwkzTW) when start App, seems it can't retrieve the data, pls help – Kel Aug 06 '20 at 22:49
  • Did you add the return in _createFile as you said? – EdwynZN Aug 06 '20 at 23:44
  • Yup, I try to add `return file;` [here](https://i.imgur.com/YWZ3JTC.jpg) but it is still loading.., I'm not sure if it correct? – Kel Aug 07 '20 at 00:08
  • I changed the writeFile method, the jsonEconder was unnecesary, anyway you should try to debug the code at some breakpoints to make sure if something is taking a lot of time to finish, specially the try catch with the http request – EdwynZN Aug 07 '20 at 17:02
  • Ahh, Thanks for letting me know about debugging, I catched all 3 breakpoints and it seems to have an error in the [first one](https://i.imgur.com/zaHP9gA.jpg) :D `(object is a closure - Function 'writeFile':.)` Did I track this bug properly? and how to fix it :"> – Kel Aug 07 '20 at 18:35
  • oh yeah compute and isolates only accepts top level functions, no methods (isolates cannot interact with the values inside the class because they're working on a different thread), try moving the writeFile method outside the class, just like a simple function, do the same with readFile, just like dataFromJson I suppose it's already a function to decode the json and return a List – EdwynZN Aug 07 '20 at 19:04
  • I have tried but my knowledge is not enough to do it, please update your answer when you have time... plsssssss, It's too close – Kel Aug 07 '20 at 22:44
  • Ahhhh, your last edit, it worked! Thanks to you I can save a lot of data from firebase haha and user experience is also faster. You saved my life xD – Kel Aug 08 '20 at 03:48
  • I combined this code with `CachedNetworkImage` then it loads the page like from Pentium dual-core => Intel Core i9-9900K :D. However I think there is one more case that won't be delayed at all, that is reverse `readFile` to the first, I updated the question, please help me again – Kel Aug 08 '20 at 18:40
  • is LoadDataPage your first route in flutter? the only possible way to do what you want is to load the file before (as a future complete) in the main or a step before LoadDataPage, pass it to LoadDataPage and then use that value in the initialData in the FutureBuilder – EdwynZN Aug 09 '20 at 15:59
  • My route in the main file is like [this](https://i.imgur.com/fLWpIOc.jpg), as far as I understand what you are saying, the main file will load all the data files (ex. data.json; data2.json; data3.json ...) which should do when open each Page (ex. LoadDataPage; LoadDataPage2; LoadDataPage3), right? Or does it apply to only the first route (ex. loadDataPage)? And how about just reverse `readFile` to the first so that new data will definitely display when open the page for the second time? Is it possible? – Kel Aug 09 '20 at 18:10
  • I didn't understand what you mean by reverse readFile, but maybe you should check some state management (redux, bloc, provider) if you want to keep the data across your app and update it when changing page, as I understand at the beginning of the app, you can use readFile to get the data, then pass it to `MyApp(data1: data1)`, then to `LoadDataPage(data1)` and so on until FutureBuilder(initialData: data1, ...)` – EdwynZN Aug 09 '20 at 18:26
  • Yup, got it, I will learn about it. Again, thank you very much, this code of yours has been of great help to my current app and also to all my future App xD – Kel Aug 09 '20 at 18:37
  • just another stupid question: in `LoadDataPage`, I want to access another data from another JSON file (eg. `final Data2Services services2 = Data2Services();`), Can I add it into `future: services.getData(),`? if can't, then what is the best way to access it? :D – Kel Aug 14 '20 at 00:11
  • I suppose you can, if it's part of your logic app, but it is related to the List returned from getData() ? – EdwynZN Aug 14 '20 at 01:36
  • It is separate but I will use `retainWhere` to combine it, just like [this post](https://stackoverflow.com/questions/63220842/flutter-how-to-display-data-from-2-json-based-on-the-same-value-of-array) I asked, up to now, I only know how to call 2nd data in 1 page by putting it in `initState()` like that :P – Kel Aug 14 '20 at 02:06
  • If it's separate then it's better to keep it that way, maybe a future method in load Page that deals with logic of the 2 services – EdwynZN Aug 14 '20 at 03:46
  • Oh got it, thanks for your confirmation, now I can be sure to use it :D – Kel Aug 14 '20 at 05:40
  • I have a new question about Toolbar Search View [here](https://stackoverflow.com/questions/63426193/flutter-how-to-implement-toolbar-search-view-to-show-data-between-2-state), if you can help me then great :"> – Kel Aug 15 '20 at 12:50
  • I have a problem with showing specials characters from API (eg `⇒`). I have researched and found that must decode `utf-8` from the rest API, I tried but failed, can you help me update it to the above code? – Kel Sep 15 '20 at 14:08
  • What have you tried? For example those special characters come from the API in what format? As a list? – EdwynZN Sep 16 '20 at 00:57
  • Example `’`; `“`; `⇒` I found them to be used quite commonly on web platforms. I have updated the JSON file in this post, and here is the [result displayed](https://i.imgur.com/XP93EyT.jpg) when running the app. I tried searching it seems like these links([1](https://stackoverflow.com/questions/51368663/flutter-fetched-japanese-character-from-server-decoded-wrong), [2](https://stackoverflow.com/questions/56105646/how-to-decode-utf-8-from-rest-api-in-dart-code) ) are the solution, but I don't know how to combine it with the above code :(. – Kel Sep 16 '20 at 01:40
  • http.Response.body uses its own decoder if the header doesnt say the format, so instead of using body just decode it yourself by calling utf8.decode(response.bodyBytes); the rest is the same, pass it as a String to json decoder and etc. – EdwynZN Sep 16 '20 at 06:05