Database path in main isolate and other isolate - flutter

I am writing database from background thread for this I have used isolate.But after writing data unable to get updated data from main thread to update UI.
I have checked it like right after inserting data I am getting count of inserted entries its give me right count. and write after this whole operation I am getting count from main thread but here the count is 0. Even I am building app again means not using hot reload.
I have registered my database like this in main thread:
LazyDatabase openConnection({bool logStatements = false}) {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'myDatabase.sqlite'));
return NativeDatabase(file,logStatements: logStatements);
});
}
and in isolate accessing it like
Future<DatabaseConnection> _backgroundConnection() async {
final dbFolder = await getApplicationDocumentsDirectory();// here getting error
final file = File(p.join(dbFolder.path, 'myDatabase.sqlite'));
final database =NativeDatabase(file, logStatements: true);
return DatabaseConnection.fromExecutor(database);
}

Related

Riverpod provider list updates from firebase only if the code is in the provider

I have a Riverpod Streamprovider that manages how a number of different Firebase documents are presented to the user. The user can then access each document, make some changes and return to the list of their documents. Once they have made, changes the row for that document should have a tick showing. The only wrinkle is that these documents in a different collection, each with their own identifier. So its not as easy as just streaming a whole collection, my function needs to get the identifier for each item and then get a list of documents to send to the user.
I have the code so it 'just works' but what I can't work out is why updating the record works when all the code is inside the provider vs when the provider calls it a external code. For example this StreamProvider works as I want and updated documents are recognised
final outputStreamProvider = StreamProvider.autoDispose((ref) async* {
final List<itemModelTest> itemList = [];
final user = ref.watch(loggedInUserProvider);
final uid = ref.watch(authStateProvider).value!.uid;
for (String ident in user.value!.idents) {
# get each item by its own identifier
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
# Listen for changes in the items
item.listen((event) async {
dev.log('event changed');
for (var change in event.docChanges) {
if (change.type == DocumentChangeType.modified) {
itemModelTest updatedModel =
itemModelTest.fromFirebaseQuery(test, uid);
itemList
.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}
yield itemList;
});
But as you can see it contains a lot of logic that doesn't belong there and should be with my firebase database class. So I tried to split it so now in my firebase crud class I have almost identical code:
Stream<List<itemModelTest>> itemsToReviewStream(LoggedInUser user, String uid) async*{
final List<itemModelTest> itemList = [];
for (String ident in user.idents) {
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
item.listen((event) async {
dev.log('event changed ${event.docChanges.first.doc}');
for(var change in event.docChanges){
if(change.type == DocumentChangeType.modified){
itemModelTest updatedModel = itemModelTest.fromFirebaseQuery(test, uid);
itemList.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}yield itemList;
}
and my StreamProvider now looks like this
// Get a list of the currently logged in users papers to review
final testitemStreamProvider = StreamProvider.autoDispose((ref) {
final user = ref.watch(loggedInUserProvider).value;
final uid = ref.watch(authStateProvider).value!.uid;
return DataBase().itemsToReviewStream(user!, uid);
});
The only problem is using this second approach the updates to firebase don't trigger any updates to the ui so when the user returns to their list of documents they cant see which have been processed already. I have been round the houses trying to work out what I am doing wrong but cant see it.
Edit: just a quick edit in case it matters but this is for FlutterWeb not iOS or Android
I am leaving this in case anyone else has the same problem. The real problem with this project was that the database structure was not fit for purpose and a further restriction was to not duplicate data on the database.
The simplest solution (and if you happen to be reading this because you fixing a similar problem) is to make a copy of the documents the user is supposed to have access to in their own collection, this can then be streamed as an entire collection. Checking which documents have and have not been looked at by users was always going to have to be done via an admin account anyway, so it's not as though this would have incurred a penalty.
All the same to manage my particular data repo i ended up
1 make a simple provider to stream a single document
final getSinglePageProvider = StreamProvider.family((ref, String pageId){
return DataBase().getSinglePage(pageId);});
Then once you have a list of all the documents the user has access to make a provider that provides a list of providers above
final simpleOutputsStreamsProvier = StreamProvider((ref) async* {
final user = ref.watch(loggedInUserProvider);
final items = user.value!.items;
yield items.map((e) => ref.watch(getSinglePageProvider(e))).toList();
});
You can then use this in a consumer as normal, but it has to be 'consumed' twice. In my case, I watched the ListProvider in the build method of a ConsumerWidget. That gives you a list of StreamProviders for individual pages. Finally I used ListView to get each StreamProvide (listofProviders[index]) and unwrapped that with provider.when(...
I have no idea how brittle this approach will turn out to be however!

How to overwrite files in google drive using API flutter

The Problem
I'm using Google Drive to store application-specific data. I can read and write data to my drive buy whenever I want to update the config in the drive a new file is created instead of updating the old one.
I have tried the following:
Gave the file an ID but the following error appears
Unhandled Exception: DetailedApiRequestError(status: 400, message: The provided file ID is not usable.)
Got the ID generated by the API and used the same code and got the following error
Unhandled Exception: DetailedApiRequestError(status: 409, message: A file already exists with the provided ID.)
My Code:
final signIn.GoogleSignInAccount account = await _googleSignIn.signIn();
final authHeaders = await account.authHeaders;
final authenticateClient = GoogleAuthClient(authHeaders);
final driveApi = drive.DriveApi(authenticateClient);
File _data = await _report.backupData(); //gets data to be overwriten
final Stream<List<int>> mediaStream = _data.openRead();
var media = new drive.Media(mediaStream, _data.lengthSync());
var driveFile = new drive.File();
driveFile.parents=['appDataFolder'];
driveFile.name = "backup.mw";
final result = await driveApi.files.create(driveFile, uploadMedia: media,);
print("Upload result: ${result.id}");
The code you are using does a files.create file create will create a new file every time it is run.
final result = await driveApi.files.create(driveFile, uploadMedia: media,);
If you want to update an existing file then you will need to do Files.update

Sqlite onCreate isn't invoked after dropping/deleting the table from the database/storage

Minimal reproducible code:
class Helper {
Database _db;
Future<Database> initDb() async {
if (_db == null) {
final directory = await getApplicationDocumentsDirectory();
_db = await openDatabase(join(directory.path, 'foo.db'), version: 1, onCreate: _onCreate);
}
return _db;
}
Future<void> _onCreate(Database db, _) async {
print('onCreate');
await db.transaction((txn) async {
await txn.execute('CREATE TABLE tableName(abc TEXT)');
});
}
}
Here's my main method:
void main() async {
final directory = await getApplicationDocumentsDirectory();
final file = File(join(directory.path, 'foo.db'));
if (await file.exists()) {
await file.delete();
}
// If there had been a file, it's deleted now.
final helper = Helper();
await helper.initDb(); // This must fire `onCreate` but it doesn't.
}
Every time you run the main method, it should execute the onCreate method in Helper class but it only does that once. What am I doing wrong here?
The issue description has changed since the beginning and it is not easy to make a proper explanation in a comment so here is yet another answer.
All existing responses remain valid but the issue now is moving to something like onCreate not called after deleting the database.
Every time you run the main method, it should execute the onCreate method in Helper class but it only does that once. What am I doing wrong here?
You don't really specify how you run (i.e. do you stop the application before), so I'm assuming you are just pressing the run button on your IDE which performs a hot-restart.
While you might be enclined to simply delete the file, you should however use
deleteDatabase to properly delete a database.
// Do not call File.delete, it will not work in a hot restart scenario
await File(path).delete();
// Instead do
await deleteDatabase(path);
it will properly close any existing database connection
it will properly handle the hot-restart scenario which put SQLite in a
weird state (basically the 'dart' side think the database is closed while
the database is in fact open on the native side)
If you call File.delete, while you might think it work (i.e. the file does not
exist anymore), since the database might still be opened in a hot restart scenario
the next open will re-use the open connection and at some point will get written
with the old data and onCreate will not be called the next time you open
the database.
onCreate will be executed only when there is no database file. Not when there is no table in the db. If you delete database file then there will be a print in console with message onCreate. Here is an example:
void main() async {
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'foo.db');
await File(path).delete();
final helper = Helper();
await helper.initDb();
// await helper.dropTable();
}
This code prints a log message every run of a program.
This is not how Sqflite works.
The very first time when you create a database you give it a version number. In your case the version is 1.
Sqflite will check if database exists on the devices if it exists it checks the version number of db on device and version number of your code, if version is not the same it will call onUpgrade if new version is greater than old db version. and on downgrade if old version is greater than new version. OnCreate won't be called. onCreate is only called the very first time user install your app. Even if you drop your tables afterwards. If you need to update your database in future on a production app, you have to write onUpgrade method, in which you have to explicitly drop your tables and call onCreate yourself. and upgrade the database version.
See the following code. Use this code setup and change version to a higher version number whenever you need to dropAll tables and createAgain.
If you don't want to use the versions and explicitly drop tables and recreate them. You have to call _onCreate yourself after dropping a table.
class Helper {
Database _db;
Future<Database> initDb() async {
if (_db == null) {
final directory = await getDatabasesPath();
final path = join(directory, 'foo.db');
_db = await openDatabase(
path,
version: 2, //+1 to this number whenever you want to update
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
return _db;
}
Future<void> _onUpgrade(
Database db, int previousVersion, int newVersion) async {
_db = db;
await dropTable();
await _onCreate(db, newVersion);
}
Future<void> _onCreate(Database db, int version) async {
print('onCreate');
await db.transaction((txn) async {
await txn.execute('CREATE TABLE tableName(abc TEXT)');
});
}
Future<void> dropTable() async {
await _db.transaction((txn) async {
await txn.execute('DROP TABLE IF EXISTS tableName');
});
}
It is the default behavior because onCreate Method is executed when you create the database. Since the database is created before it is not executed again.
If you intent to clean the rows of the table then use the TRUNCATE command. For more information check the link comparing drop table and truncate table here

Flutter, getting database records and then internet json

I have a simple table from which I'm fetching a list of records. Once I get the records, then I have to get information online for each of the records. The code to do this is as follows:
class UserStationList {
List<UserStationListItem> _userStations = [];
final StreamController<HomeViewState> stateController;
UserStationList({#required this.stateController});
Future fetchUserStations() async {
stateController.add(HomeViewState.Busy);
//Fetch stations from table.
List<Map<String, dynamic>> stations =
await UserStationDatabase.instance.queryAllRows();
//If there are no stations, return and tell the screen to display the no data message.
if (stations.length == 0) {
stateController.add(HomeViewState.NoData);
return;
}
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
//When done, let the screen know.
stateController.add(HomeViewState.DataRetrieved);
}
Future<UserStationListItem> _getPurpleAirSiteData(
int id, int stationId) async {
var response = await http.get('$kURL$stationId');
var data = json.decode(response.body);
return UserStationListItem(
id: id, stationId: stationId, stationName: data['results'][0]['Label']);
}
}
The problem that I am running into involves the futures. I am processing the loop in a forEach and calling into the _getPurpleAirSiteData function for each. Within that function I have to await on the http.get to bring in the data. The stateController.add(HomeViewState.DataRetrieved) function is being called and the function exits long before the loop is completed. This is resulting in the data not being available when the StreamBuilder that I have receiving the data is run.
How can I set this up so that the loop runs completely before calling stateController.add?
I would change this part of code to a list of Futures and await-ing on it.
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
To:
List<Future<UserStationListItem>> listOfFutures = [];
stations.forEach((station) {
listOfFutures.add(_getPurpleAirSiteData(station['_id'], station['stationid']));
});
var stationItems = await Future.wait(listOfFutures);
stationItems.forEach((userStationListItem) {
_userStations.add(userStationListItem);
});
What I am essentially doing creating a list of Futures with your server request. Then await on it which returns a list of item result Maintaining index, which in turn ensures that requests are completed before you hit statecontroller.add. You also gain a performance gain since all request are not going one by one and instead asynchronously. Then you just iterate through the future result and add it to your item list.

Transaction/batch in a newly created SQFlite database

The following only happens with the database just created as in the code. A previously existing database works all right.
I have the usual singleton setup for the database helper, the relevant part being:
Future<Database> get database async {
// ...
db ??= await openDatabase(
path.join(await getDatabasesPath(), 'database.db'),
onCreate: (db, version) async {
final batch = db.batch();
batch.execute('CREATE TABLE table1 ...');
batch.execute('CREATE TABLE table2 ...');
await batch.commit(noResult: true);
},
// ...
return db;
}
Let's suppose the database doesn't exist yet. I call the following routine:
final db = await database;
await db.transaction((txn) async {
await txn.delete('table1');
final batch = txn.batch();
for (data in newData1)
batch.insert('table1', data.toJson()));
await batch.commit(noResult: true);
await txn.delete('table2');
final batch = txn.batch();
for (data in newData2)
batch.insert('table2', data.toJson()));
await batch.commit(noResult: true);
});
The transaction and batch calls execute without error. When the whole operation is actually executed at the end, it stops on the first DELETE FROM table1 SQL operation with a DatabaseException(attempt to write a readonly database(Sqlite code 1032) (running on Android).
I checked that the singleton is a singleton, openDatabase is not called twice. I also tried the transaction with exclusive: false, no difference.
The likely cause of the issue is that the table can't be accessed. A known workaround for this case is to open the database and close it before deleting, as mentioned in this GitHub issue thread.