FutureBuilder displays that data is null even tho it has value and still proceeds as normal [Flutter] - json

Hi currently Im working with local json file where I want to use some data from it. I have modeled it accordingly to that json file and when I print it out it works as normal. But when I run it in debug mode it stops at this points saying this :_CastError (type 'Null' is not a subtype of type 'List<UrlCheckModel>' in type cast)
This is my code:
FutureBuilder(
future: readJsonData(),
builder: (context, data) {
if (data.hasError) {
//in case if error found
return Center(child: Text("${data.error}"));
}
List<UrlCheckModel> items = data.data as List<UrlCheckModel>;
Where readJsonData is:
Future<List<UrlCheckModel>> readJsonData() async {
//read json file
final jsondata =
await rootBundle.rootBundle.loadString('jsonfile/malicious.json');
//decode json data as list
final list = json.decode(jsondata) as List<dynamic>;
//map json and initialize using DataModel
return list.map((e) => UrlCheckModel.fromJson(e)).toList();
}
My question is why is this error happening even tho when I run the app it works fine (at brief 1 second time period the error appears) and how can I resolve this. Looking forward to your replies. (If needed I can post the whole code for this page).

Ok so it seams even tho the json file is stored localy and Im not fetching it via API calls it needs to be initialized, so with help of #temp_ one viable solution is like this:
if (data.data == null || data.hasError) {
//in case if error found
return const Center(
child: Text(
'Please wait',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26,
color: Color.fromRGBO(255, 125, 84, 1),
fontWeight: FontWeight.bold,
),
),
);
}

First off
Before all, you have a error in your widget configuration:
FutureBuilder(
// Do not call the async function directly
future: readJsonData(),
// ...
)
This will result on new Future being instantiated every rebuild, which kills device memory and your app will eventually be killed by the OS.
By the docs:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
Then you should do something like this:
Future<List<UrlCheckModel>> _future;
void initState() {
super.initState();
_future = readJsonData();
}
Widget build(BuildContext context) {
return FutureBuilder<List<UrlCheckModel>>(
future: _future,
// ..
);
}
Now fixing the actual error
Since the rendering process is sync, which means Flutter can't wait your async process finish to render the first frame of your widget, you should be aware the data will be null in the first snapshot (AsyncSnapshot<T>.nothing()), in the docs:
For a future that completes successfully with data, assuming initialData is null, the builder will be called with either both or only the latter of the following snapshots:
AsyncSnapshot<String>.withData(ConnectionState.waiting, null)
AsyncSnapshot<String>.withData(ConnectionState.done, 'some data')
As you see, your 'data' is actually of type AsyncSnapshot<T>, then:
FutureBuilder<List<UrlCheckModel>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Loading();
}
if (snapshot.hasError) {
return HandleYourError();
}
List<UrlCheckModel> items = snapshot.data as List<UrlCheckModel>;
return HappyEnd(items: items);
}
);
See FutureBuilder<T> class reference for details

Related

Flutter getx json

First of all, I apologize to everyone for my low level of English. I want to make an application for a live broadcasting radio station. json link of radio site "http://radyo.comu.edu.tr/json/". This link is refreshed every 5 seconds. and json data is also changing. How can I follow a path from the application?
You can cache the response by using flutter cache manager Flutter Cache Manager, store it somewhere and use it. For storage you can use shared preferences,sqlite, hive etc there are a lot of options afterwards
you can use stream builder where data is continually updating
https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
StreamBuilder<int>(
stream: generateNumbers,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.red, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),

PopupMenuButton sending Buildcontext to handleclick function

I am pretty new to flutter and I found the code for my PopupMenuButton online and modified it slightly. I want the logout option to run Navigator.pop(context), but I don't understand how to get it get it there.
Code:
PopupMenuButton<String>(
onSelected: handleClick,
itemBuilder: (context) {
return {'Logout', 'Settings'}.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice)
);
}).toList();
}
),
And I want to send it in here:
void handleClick(String value, BuildContext context) {
switch (value) {
case 'Logout':
storage.deleteAll();
//Navigator.pop();
break;
case 'Settings':
break;
}
}
I also tried making Buildcontext a global final late variable, but it seemed to cause a lot of problems.
To be honest, I don't understand how the PopupMenuButton works in regards to calling the funtion, thus me not knowing how to pass another argument
Thanks for the help
Write onSelected like this instead:
onSelected: (value) {
handleClick(value, context);
},

Flutter: How to use Firebase Storage to get new data if JSON file in Firebase Storage updated

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:
Create file to store String "updated" in Local directory (Ex. "updated": null) and where to store the JSON file after download to Local directory
Open App
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?
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

How to save my current state by using json or something else in Flutter

I want to know how to save my current state by modifying the existing JSON file. but I can't find a way to saving..... So I want to ask you about that.
The size of the JSON file is not so big. So I considered total - loading and total - saving when the loading and saving state is called. but I think it is not efficient and unstable... So can I get some help?
please use this package https://pub.dev/packages/shared_preferences
Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing a persistent store for simple data. Data is persisted to disk asynchronously
example code
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
));
}
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
await prefs.setInt('counter', counter);
}

Flutter: How can I store user created objects in local storage and retrieve them into a ListView

I am recreating my app Call Manager in Flutter and I am having a very difficult time storing objects and retrieving them into a list. Using SharedPreferences and GSON works well for the full Android app, but as GSON does not exist in Flutter, it seems to me that storing onjects as JSON data is the way to go.
The flow should be as follows:
The user add a new call on the AddNewCall screen. This includes a name, phone number, and optional description'
When the user clicks the 'Save' button, a Call object is created from the text in those fields. The object is encoded into a String like this: String jsonData = json.encode(newCallObject);
The call JSON data is saved into the app's local storage using getApplicationDocumentsDirectory()
The user is taken back to the main screen, where the JSON file will be loaded into the app asynchronously in the initState() method.
The JSON data is decoded and added to a ListView
The problem I am experiencing is that as the JSON data is added to the file is Step 3, it will look like this: {sampleData}{sampleData} instead of being surrounded in [], as an array. The app hiccups when reading in the file because of this. My solution thus far has been to parse the contents of the file into an array, stick an underscore in between the }{, and split the resulting String into an array of type String. It looks like this:
String jsonCalls = await file.readAsString();
jsonCalls = jsonCalls.replaceAll('}{', '}_{');
List<String> temp = jsonCalls.split('_');
So now I get an array with JSON strings in them, but I am stuck on being able to access the values in each JSON string. This whole approach that I'm taking seems way over-complicated too. There must be a better way, but I have googled like crazy and found nothing. Are you guys able to help me out?
Thanks!
Here are some suggestions
Flutter supports shared preferences using a plugin, so it should feel familiar from Android.
initState is only called when the StatefulWidget is initialised the first time, so may not be a good place to expect to re-load changed information
Consider keeping your working state in memory, for example in a List<Call> to which you can readily add new Calls.
When you serialize this List to json it will be wrapped in brackets as expected. Write the whole List, overwriting the previous version.
Use the backing store (file or shared preferences) as the persistence between uses of your app, so read it once at startup, and write it each time you modify it in memory. The in-memory copy is the master while the app is running.
This should alleviate the need to hack the json as strings.
Maybe something like:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Call Manager',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Call> calls = [];
#override
void initState() {
super.initState();
load();
}
load() async {
// initially populate calls by getting last value from shared preferences and parsing it
}
persist() async {
// encode calls as json and write to share prefs
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Calls'),
),
body: new ListView(
scrollDirection: Axis.horizontal,
children: calls
.map((call) => new ListTile(
title: new Text(call.phoneNumber),
))
.toList(),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
Navigator.push<Call>(
context,
new MaterialPageRoute(
builder: (context) => new SecondPage(),
),
).then((newCall) {
if (newCall != null) {
setState(() {
calls.add(newCall);
});
persist();
}
});
},
tooltip: 'New',
child: new Icon(Icons.add),
),
);
}
}