In a Flutter app the user must be able to see the retrieved data from an API also when there is no network, e.g. in a house basement. I'm able to retrieve the data from the Api and to store it in a local sqflite db. I'm also able to check whether there is network or not. But how do I implement the flow in order to show local data or remote data? Is it possible to do it in the same screen or do I need two screens? Any help is appreciated.
EDIT
Thank you for all the answers, but I don't check where to fill in the getData() method now. I post what I've done so far:
class WorkAtPop extends StatefulWidget {
#override
_WorkAtPop createState() => _WorkAtPop();
}
class _WorkAtPop extends State<WorkAtPop> {
final String title = 'Work#Pop';
final bgcolor = HexToColor('#ffffff');
final list = List();
final isLoading = false;
List<DropdownChoices> workatpopdropdownchoices = <DropdownChoices>[
DropdownChoices(title: 'Refresh', action: 'refresh', route: '/workatpop'),
];
bool _isVpnEnabled = false;
bool _isLoading = true;
void checkVpn() async {
var isEnabled = await ApiService().isVpnEnabled();
setState(() => _isVpnEnabled = isEnabled);
setState(() => _isLoading = false);
}
#override
void initState() {
super.initState();
checkVpn();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: BaseAppBar(title: title, appBar: AppBar(), dropdownChoices: workatpopdropdownchoices),
backgroundColor: bgcolor,
body: new Container(
padding: const EdgeInsets.all(30.0),
color: bgcolor,
child: (_isLoading)
? new Center(
child: new CircularProgressIndicator(),
)
: new Container(
child: new Text('here my data displayed as list, vpn=$_isVpnEnabled'),
)
),
);
}
}
Now if VPN is enabled I retrieve the data from the api else from the local db. The API is storing the data into the db each time it is called.
Since you're handling everything, it will be easy for you to do so. It's possible only in a single screen.
List<PodoClass> data;
if(internet()) {
data = fecthDataFromNetwork(); // load fron internet
} else {
data = loadLocalData(); // load from database
}
processData(data); // process your data
I hope you got some idea.
If internet is available get data from server and store it in localdb, and if internet is not available get already stored data from localdb.
List<PodoClass> data;
void feachdata() async{
if(isInternet()) {
data = await DataFromNetwork(); // load from internet
storetolocaldb(data); // store to local for later use
} else {
data = getLocalData(); // load from database
}
displaydata(data); // process your data
}
hope it helps..
you can store retrieved data from an API to a local db and can fetch it on both cases offline/online.. Also you can update the db if any new data available in the api
void getData() {
getStoredDataFromDb().then((data){
if(data !=null){
populateUI();
checkForAnyUpdate();
}
else {
if(hasInternetconnection)
getDatafromApi();
}
else{
loadMockOrOldData();}
};
getDatafromApi().then((response){
insertDataIntoTheDatabase(response);
};
}
Related
I'm trying to build a note app, all data and other things is working perfectly, cos the data is displaying to the screen when the code file is saving, its weird , first time facing this problem
in short, the valuelistanble is not listening when the data adding from app, but when just hot reloading the data is displaying
how can i fix this,
here is the code
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback((_) async {
final value = await NoteDB.instance.getAllNotes();
});
____________________________________________
____________________________________________
//code line for aligment
Expanded(
child: ValueListenableBuilder(
valueListenable: NoteDB.instance.noteListNotifier,
builder: (context, List<NoteModel> newNotes, _) {
return GridView.count(
childAspectRatio: 3 / 4,
crossAxisCount: 2,
mainAxisSpacing: 34,
crossAxisSpacing: 30,
padding: const EdgeInsets.all(20),
//generating list for all note
children: List.generate(
newNotes.length,
(index) {
//setting the notelist to a variable called [note]
final note = newNotes[index];
if (note.id == null) {
//if the note's id is null set to sizedbox
//the note id never be null
const SizedBox();
}
return NoteItem(
id: note.id!,
//the ?? is the statement (if null)
content: note.content ?? 'No Content',
title: note.title ?? 'No Title',
);
},
),
);
},
)),
here is the NoteDB.instance.getAllNotes(); function
#override
Future<List<NoteModel>> getAllNotes() async {
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
final noteResponse = GetAllNotes.fromJson(_result.data);
noteListNotifier.value.clear();
noteListNotifier.value.addAll(noteResponse.data.reversed);
noteListNotifier.notifyListeners();
return noteResponse.data;
} else {
noteListNotifier.value.clear();
return [];
}
}
and also there is a page to create note , and when create note button pressed there is only one function calling here is function
Future<void> saveNote() async {
final title = titleController.text;
final content = contentController.text;
final _newNote = NoteModel.create(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
content: content,
);
final newNote = await NoteDB().createNote(_newNote);
if (newNote != null) {
print('Data Added to the DataBase Succesfully!');
Navigator.of(scaffoldKey.currentContext!).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => HomePage()),
(Route<dynamic> route) => false);
} else {
print('Error caught while data adding to the DataBase');
}
}
everything work fine, but while add the data the UI isn't refreshing even tho notifier is active
and if you need full code please have a look at this github link : https://github.com/Mishalhaneef/Note-app
Since this ValueNotifier has a type of List<NoteModel>, the value will not change when you add new items to the list or delete from it or clear all. The value here is a reference to the list which does not change.
You have to assign a new value to it, like:
noteListNotifier.value = List<NoteModel>[<add your current items here>];
You can manipulate your current list with List.from, removeWhere, add etc., and then re-assign the complete list.
Besides you don't need to call notifyListeners in case of a ValueNotifier, the framework handles it, see here.
Another approach would be to use a custom ChangeNotifierProvider where you can call notifyListeners when the contents of your list are changed.
Some further suggestions:
In your homescreen.dart file, instead of NoteDB.instance.noteListNotifier.value[index] you can use newNotes[index].
In data.dart, within getAllNotes, you have to set a new value for noteListNotifier in order to get the changes propagated. Currently you are just modifying items in this list and that is not considered to be a change. Try this code:
#override
Future<List<NoteModel>> getAllNotes() async {
//patching all data from local server using the url from [Post Man]
final _result = await dio.get(url.baseUrl+url.getAllNotes);
if (_result.data != null) {
//if the result data is not null the rest operation will be operate
//recived data's data decoding to json map
final _resultAsJsonMap = jsonDecode(_result.data);
//and that map converting to dart class and storing to another variable
final getNoteResponse = GetAllNotes.fromJson(_resultAsJsonMap);
noteListNotifier.value = getNoteResponse.data.reversed;
//and returning the class
return getNoteResponse.data;
} else {
noteListNotifier.value = <NoteModel>[];
return [];
}
}
I want steps count and calories burned data in my flutter app. I am using health: ^3.1.1+1 package but I'm getting "Authorization not granted" even after giving all permission. I even used permission handler for permission and I was successfully getting permission with permission handler, still I am not getting data from health package. Please help me with the process to authorize my app to fetch data from Google Fit API.
I have successfully generate my OAuth client id from google console and added the json file in my project. Please let me know if there is any other place where I need to add my client id.
I am using given below sample code provided in with the package.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:health/health.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
enum AppState {
DATA_NOT_FETCHED,
FETCHING_DATA,
DATA_READY,
NO_DATA,
AUTH_NOT_GRANTED
}
class _MyAppState extends State<MyApp> {
List<HealthDataPoint> _healthDataList = [];
AppState _state = AppState.DATA_NOT_FETCHED;
#override
void initState() {
super.initState();
}
/// Fetch data from the healt plugin and print it
Future fetchData() async {
// get everything from midnight until now
DateTime startDate = DateTime(2020, 11, 07, 0, 0, 0);
DateTime endDate = DateTime(2025, 11, 07, 23, 59, 59);
HealthFactory health = HealthFactory();
// define the types to get
List<HealthDataType> types = [
HealthDataType.STEPS,
HealthDataType.WEIGHT,
HealthDataType.HEIGHT,
HealthDataType.BLOOD_GLUCOSE,
HealthDataType.DISTANCE_WALKING_RUNNING,
];
setState(() => _state = AppState.FETCHING_DATA);
// you MUST request access to the data types before reading them
bool accessWasGranted = await health.requestAuthorization(types);
int steps = 0;
if (accessWasGranted) {
try {
// fetch new data
List<HealthDataPoint> healthData =
await health.getHealthDataFromTypes(startDate, endDate, types);
// save all the new data points
_healthDataList.addAll(healthData);
} catch (e) {
print("Caught exception in getHealthDataFromTypes: $e");
}
// filter out duplicates
_healthDataList = HealthFactory.removeDuplicates(_healthDataList);
// print the results
_healthDataList.forEach((x) {
print("Data point: $x");
steps += x.value.round();
});
print("Steps: $steps");
// update the UI to display the results
setState(() {
_state =
_healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
});
} else {
print("Authorization not granted");
setState(() => _state = AppState.DATA_NOT_FETCHED);
}
}
Widget _contentFetchingData() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(
strokeWidth: 10,
)),
Text('Fetching data...')
],
);
}
Widget _contentDataReady() {
return ListView.builder(
itemCount: _healthDataList.length,
itemBuilder: (_, index) {
HealthDataPoint p = _healthDataList[index];
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
trailing: Text('${p.unitString}'),
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
);
});
}
Widget _contentNoData() {
return Text('No Data to show');
}
Widget _contentNotFetched() {
return Text('Press the download button to fetch data');
}
Widget _authorizationNotGranted() {
return Text('''Authorization not given.
For Android please check your OAUTH2 client ID is correct in Google Developer Console.
For iOS check your permissions in Apple Health.''');
}
Widget _content() {
if (_state == AppState.DATA_READY)
return _contentDataReady();
else if (_state == AppState.NO_DATA)
return _contentNoData();
else if (_state == AppState.FETCHING_DATA)
return _contentFetchingData();
else if (_state == AppState.AUTH_NOT_GRANTED)
return _authorizationNotGranted();
return _contentNotFetched();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.file_download),
onPressed: () {
fetchData();
},
)
],
),
body: Center(
child: _content(),
)),
);
}
}
Go to Google Cloud Platform > Apis & Services > OAuth consent screen and check if your app is in Testing mode and add gmails to allow user to use the Oauth
Remember to upload the android debug key sha1!
New version of the health package solved my problem.
I'm learning Flutter and there is something I cannot grasp my head around.
I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination),
it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.
My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.
Example:
Let's take a contact list, that loads 10 contacts at a time:
class ContactsBody extends StatefulWidget {
#override
_ContactsBodyState createState() => _ContactsBodyState();
}
class _ContactsBodyState extends State<ContactsBody> {
static const _pageSize = 10;
final PagingController<int, Contact> pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
pagingController.appendLastPage(newItems.contacts);
} else {
final nextPageKey = pageKey + 1;
pagingController.appendPage(newItems.contacts, nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
#override
Widget build(BuildContext context) {
return ContactsList(pagingController);
}
#override
void dispose() {
pagingController.dispose();
super.dispose();
}
So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:
Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
final response = await http.get(.....);
if (response.statusCode == 200) {
return Contacts.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load contacts');
}
}
And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.
Now my Provider class:
class Contacts extends ChangeNotifier {
List<Contact> _contacts = <Contact>[];
Contacts();
factory Contacts.fromJson(final Map<String, dynamic> json) {
final Contacts contacts = Contacts();
if (json['data'] != null) {
json['data'].forEach((contact) {
contacts.add(Contact.fromJson(contact));
});
}
return contacts;
}
void add(final Contact contact) {
this._contacts.add(contact);
this.notifyListeners();
}
The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),
How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?
Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere
EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.
I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191
There are a few confusing things in your code :
When to create instances of your objects (ContactsService, Contacts)
Provider usage
(Accessing the list of the pagingController ?)
Parsing a JSON / using a factory method
The repository results in the following :
When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites.
If you click on a Contact, it becomes a favorite and the yellow container is also updated.
I commented the repository to explain you each part.
Note: the Contacts class in your code became ContactProvider in mine.
The ContactsService class to make the API call :
class ContactsService {
static Future<List<Contact>> fetchContactsPaged(
int pageKey, int pageSize) async {
// Here, you should get your data from your API
// final response = await http.get(.....);
// if (response.statusCode == 200) {
// return Contacts.fromJson(jsonDecode(response.body));
// } else {
// throw Exception('Failed to load contacts');
// }
// I didn't do the backend part, so here is an example
// with what I understand you get from your API:
var responseBody =
"{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
Map<String, dynamic> decoded = json.decode(responseBody);
List<dynamic> contactsDynamic = decoded["data"];
List<Contact> listOfContacts =
contactsDynamic.map((c) => Contact.fromJson(c)).toList();
// you can return listOfContacts, for this example, I will add
// more Contacts for the Pagination plugin since my json only has 2 contacts
for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
listOfContacts.add(Contact(name: "Name $i"));
}
return listOfContacts;
}
}
Usage of Provider :
Consumer<ContactProvider>(
builder: (_, foo, __) => Container(
child: Text(
"${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
color: Colors.amber,
)),
Expanded(child: ContactsBody())
]),
)
Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :
Future<void> _fetchPage(int pageKey) async {
try {
// Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
final newItems =
await ContactsService.fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
// Important : we add the contacts to our provider so we can get
// them in other parts of our app
context.read<ContactProvider>().addContacts(newItems);
} catch (error) {
print(error);
_pagingController.error = error;
}
}
ContactItem widget, in which we update the favorite statuts and notify the listeners :
class ContactItem extends StatefulWidget {
final Contact contact;
ContactItem({this.contact});
#override
_ContactItemState createState() => _ContactItemState();
}
class _ContactItemState extends State<ContactItem> {
#override
Widget build(BuildContext context) {
return InkWell(
child: Padding(child: Row(children: [
Expanded(child: Text(widget.contact.name)),
if (widget.contact.isFavorite) Icon(Icons.favorite)
]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
onTap: () {
// the below code updates the item
// BUT others parts of our app won't get updated because
// we are not notifying the listeners of our ContactProvider !
setState(() {
widget.contact.isFavorite = !widget.contact.isFavorite;
});
// To update other parts, we need to use the provider
context.read<ContactProvider>().notifyContactUpdated(widget.contact);
});
}
}
And the ContactProvider :
class ContactProvider extends ChangeNotifier {
final List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;
void addContacts(List<Contact> newContacts) {
_contacts.addAll(newContacts);
notifyListeners();
}
void notifyContactUpdated(Contact contact) {
// You might want to update the contact in your database,
// send it to your backend, etc...
// Here we don't have these so we just notify our listeners :
notifyListeners();
}
}
I have class which will fetch the data from an API and store the result in a LIST and display the content in my text widget. When I hot reload it's displaying 'Default'. But is working fine when just reload again though I guard the text widget against the NULL.
The following is my code:
class Sample extends StatefulWidget {
#override
SampleState createState() => SampleState();
}
class SampleState extends State<Sample> {
var selected = [];
#override
void initState() {
super.initState();
callAsyncFetch();
}
callAsyncFetch() async {
var url = 'http://10.0.2.2/abc.php';
var response = await http.get(url);
var jsonData = json.decode(response.body);
print(jsonData);
for (var u in jsonData) {
if (u.substring(0, 2) == 'ABC') {
selected.add(u);
}
}
for (var u in selected) {
print(u);
}
print(selected.length); // working fine
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(children: [
selected.isEmpty
? CircularProgressIndicator()
: new Text(selected[0].toString()),
// i have tried this also but no luck
new Text(selectedStudentsIT.length!=0?selectedStudentsIT[0].toString():'Default Value'),
]),
),
);
}
}
There is simple issue is that, in hot reload it will not run initState again. you can check it by printing something in initState, while in full reload it will call initState.
i think you want to display data when you get response from api.
i think this can be easily achieved by calling setState at the end of callAsyncFetch function, so when you get data from server then it will update ui.
I'm writing a Flutter app and I decided to use RxDart to pass my data and events along the managers, services and UI.
Basically I have a service which fetches data from a web service and returns it. Let's assume it returns a List of a model called ExploreEntity.
class ExploreMockService extends ExploreServiceStruct {
final String response = /** a sample json **/;
#override
Future<List<ExploreEntity>> loadExploreData(PaginationInput input) async {
await Future.delayed(new Duration(seconds: 2));
return List<ExploreEntity>.from(jsonDecode(response));
}
}
Now in my manager class I call the loadExploreData method inside a RxCommand.
class ExploreManagerImplementation extends ExploreManager {
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input) =>
sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input));
}
}
And finally I get the result by a RxLoader and pass it to a GridView if data was fetched successfully.
class ExplorePageState extends State<ExplorePage>{
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Explore"),
),
body: Column(children: <Widget>[
Expanded(
child: RxLoader<List<ExploreEntity>>(
commandResults:
sl.get<ExploreManager>().loadExploreDataCommand.results,
dataBuilder: (context, data) => ExploreGridView(data),
placeHolderBuilder: (context) => Center(
child: Center(
child: CircularProgressIndicator(),
),
),
errorBuilder: (context, error) => Center(
child: Text("Error"),
)),
)
]));
}
}
It works like a charm but when I wanted to load the data of the next page from web service and append it to the list, I couldn't find a solution to store the content of previous pages and just append the new page's contents to them, since data is passed along the RxCommand and RxLoader automatically.
When loadExploreData sends the reponse to the manager, I need to firstly append the result to a list, and then send that list as the result to RxLoader. Any suggestions?
Hmm that's a good question if this can be done just using Rx. What I would do is keeping a list of the received items in the manager. So when triggering the command to get the next page the command would first add the new data to the list and then push the whole list to the UI.
I"m curious if there is another solution.
My described approach in a rough code sample
class ExploreManagerImplementation extends ExploreManager {
List<ExploreEntity>> receivedData = <ExploreEntity>[];
#override
RxCommand<void, List<ExploreEntity>> loadExploreDataCommand;
ExploreManagerImplementation() {
loadExploreDataCommand = RxCommand.createAsync<PaginationInput, List<ExploreEntity>>((input)
async {
var newData = await sl //Forget about this part
.get<ExploreServiceStruct>() //and this part if you couldn't understand it
.loadExploreData(input);
receivedData.addAll(newData);
return receivedData;
};
}
}