Favoriting Items in Flutter Using Sqlite - sqlite

I want to be able to favorite/mark items in a already populated local sqlite database to display on a "favorites page" that shows a list of favorited items.
In this sqlite database, I have a table of genres and a table of books relating to a specific genre. I am not using a model "book" class to display book information in a list. I have using a rawQuery to place a ROW of book information into a List to display that book information in a bookView page. Like so..
Future getDescriptionData() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "asset_sample_sqlite.db");
ByteData data =
await rootBundle.load(join("assets", "sample_sqlite.db"));
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes);
Database db = await openDatabase(path);
await db
.rawQuery('SELECT * FROM books WHERE id = "${widget.id}"') // id passed on from previous page where it shows a list of books
.then((results) {
setState(() {
loading = false;
initialized = true;
bookDescription = results;
});
then I display the information like so..
body: Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: !loading
? new ListView.builder(
itemCount: bookDescription.length,
itemBuilder: (BuildContext context, int index) {
return new Card(
elevation: 2.0,
child: new ListTile(
title: new Text(${bookDescription[index]['title']}",
textAlign: TextAlign.center,
),
subtitle: new Text(
"${bookDescription[index]['summary']}",
),
),
),
),
);
},
)
What is the best method to include an iconButton in this card, and then have an option to favorite this "book" to add it to a shortlist of books that are favorited in a different favorite book list page?

You could create a Favorites table that only holds id values of the books table that have been favored. You should read that table also in getDescriptionData (or a similar getFavoriteData method) so that you have a list of favored book ids in memory, available when the build function is called. In the Card add a leading argument with an Icon representing a filled or unfilled star (depending on whether the bookDescription[index]['id'] is in the favored list) and wrap it in a GestureDetector where in its onTap you add or remove the book id from the list in memory and in your SQL table, then call setState to rebuild the list with the new favorite information.

Related

Updating element ordering in Firestore(NoSQL) of an ordered collection (Flutter-Firestore)

As the title suggests, I have a stream that delivers through a StreamBuilder an AsyncSnapshot<List>, a list of elements. The elements then are built in a ReorderableListView.builder in order to rearrange them through drag & drop.
Widget - View
body: Column(children: [
StreamBuilder(
initialData: controller.listOfProjectTasks,
stream: controller.retrieveTasksOfProject(),
builder: (BuildContext context,
AsyncSnapshot<List<Task>> dataSnapshot) {
return Expanded(
child: GetBuilder<BacklogController>(
init: Get.find<BacklogController>(),
builder: (value) {
if (dataSnapshot.hasData) {
return ReorderableListView.builder(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final Task? taskToOrder =
dataSnapshot.data?.removeAt(oldIndex);
dataSnapshot.data?.insert(newIndex, taskToOrder!);
controller.saveNewTaskPositions(dataSnapshot.data);
},
itemCount: dataSnapshot.data!.length,
itemBuilder: (context, int index) {
GlobalKey globalKey = GlobalKey();
return Draggable(
feedback: Container(
width: 10,
),
child: TaskTile(
dataSnapshot.data![index]),
key: globalKey,
);
});
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
}));
})
]),
View model Stream generator
yield* Stream.periodic(const Duration(seconds: 5), (_) {
return backlogRepository.retrieveTasks(projectId);
}).asyncMap((value) async => await value);
Problem I: I need to save the Task ordering and I face 2 challenges, on one side whenever the Builder delivers the list and I reorder it, the stream yields the same data again with the previous order, and the ListView gets built with the previous order.
Problem II: the list ordering would need to be preserved on App restart
Possible hack: I'm using the Flutter Firestore combo so I have a document-oriented DB, no Autoincrement on IDs like in relational db's. The solution that I came up with is trivial and essentially I add a new field to the Task element named "order".
Sample Task Document Model, should be a toJson method in Model class
Future<void> addNewTask(String projectId, String createdProjectName,
String startDate, String endDate) async {
firestore.collection('task').add({
'name': createdProjectName,
'startDate': startDate,
'endDate': endDate,
'teamId': '',
'projectId': projectId,
'epicId': '',
'order': 1,
'teamMemberId': '',
'position': null
});
}
Now problem is that the List is preserving a Scrum Backlog, for scalability issues I'm assuming that it could hold 1000 Tasks related to 1 Project.
With 1 rearrangement of Tasks I could on worst case assume 999 Firestore calls to just update each Task document field "order", and it's only on 1 Project on Firestore.
Is there a better way to act on this problem?
Possible changes that I see are on:
How Task documents are stored -> how to address the order persistence
How the field "order" of each Task document can be updated through the Firestore call on the Repository
Any suggestions?

Delete a document from firebase

I'm building an app that contains pet adoption offers. Each pet document has an ID that's generated by DateTime.now() + the user ID to make it unique, anyway, I'm trying to write a deleting method within the Slidable widget to delete the adoption offer.
The problem is that I'm unable to reach the document ID to delete it.
Is there a way to delete a document without getting the ID?
This is the Firebase database
Here is my current code
Future getOffersList() async {
List<PetTile> tiles = [];
List<Slidable> slidables = [];
var data = await FirebaseFirestore.instance
.collection('pets')
.where('owner',
isEqualTo: FirebaseAuth.instance.currentUser!.uid.toString())
.get();
_petsList = List.from(data.docs.map((doc) => Pet.fromSnapshot(doc)));
for (var pet in _petsList) {
tiles.add(PetTile(pet: pet));
}
for (var tile in tiles) {
slidables.add(
Slidable(
child: tile,
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (value) async {
var ref = FirebaseFirestore.instance
.collection('pets')
.where('id', isEqualTo: tile.pet.id)
.get();
// Deleting...
},
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
],
),
),
);
}
}
You can get the id of the document by doing the following steps:
Add await infront when you're accessing the conditioned data from firebase collection.. in your case in front of FirebaseFirestore.instance
*This will return a QuerySnapshot rather than a Future instance of the same.
You need to get the doc and the id of that doc.. write:
final id= ref.docs[0].id
*Using first index(0) because i am assuming that only one pet id matches with other pet id.
since you have the id now.. you can perform the delete function

Getting a specific String from List.generate with onTap

I am using WordPress REST API to fetch posts from a WordPress site with this piece of code with the help of wordpress_api package.
class ProvideTitle with ChangeNotifier {
List<String> Titles = [];
List<String> Descriptions = [];
List<String> Urls = [];
void ClearTitle() { //clears lists when called
Titles.clear();
Descriptions.clear();
Urls.clear();
}
void TitleFetcher(term) async { //takes term from query and adds parameters
final String searchTerm=term;
final api = WordPressAPI('https://wordpresswebsite.com');
WPResponse res = await api.posts.fetch(args: {
"search" : searchTerm,
"per_page" : "20",
"page" : "1"
});
for (final post in res.data){ //returns String to fill Lists below for each post found
Titles.add(post.title);
Descriptions.add(post.content);
Urls.add(post.link);
}
notifyListeners();
}
}
Everything is working as expected and class notifies all listeners with each List<//Strings> respectively.
Problem here is I build a list with List.generate using List<//String> Title in search bar recommendations with material_floating_search_bar, code looks like this.
class SearchPage extends StatefulWidget {
const SearchPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => SearchPageState();
}
class SearchPageState extends State<SearchPage> {//search bar from package mentioned
late String resultValue= "";
final controller = FloatingSearchBarController(); //controller for search bar
#override
Widget build(BuildContext context) {
return SafeArea(
child: Consumer<ProvideTitle>(builder: (context, provideTitle, child) {//Provider used for listening to ProvideTitle class
return FloatingSearchBar(
debounceDelay: const Duration(milliseconds: 165),
onQueryChanged: (query) async { //registers keyboard input
query != "" ? provideTitle.TitleFetcher(query) //sends input to TitleFetcher function inside ProvideTitle class
: provideTitle.ClearTitle(); //clears lists to stop showing fetced data with empty query(please feel free to recommend me a better way to do)
},
controller: controller,
hint: "Search",
backdropColor: Colors.transparent,
borderRadius: BorderRadius.circular(24),
physics: const BouncingScrollPhysics(),
builder: (context, _) => BuildBody(), //calls list builder as recommendations
body: resultValue != "" ? SearchResults(): //result body going to be called when some entry chosen from list
SizedBox());//page stays empty when there is no action
}),
);
}
Widget BuildBody() {
ProvideTitle model = Provider.of<ProvideTitle>(context); //ProvideTitle called for its lists as model
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
child: Column(
children:
List.generate(model.Titles.length, (index) => model.Titles[index].toString()) //list generated as long as Title[index] which is 20 max because we only call 20 post from api
.map((e) => ListTile(
onTap: () { // i want this onTap to register which tile I chosed
model.ClearTitle();//clears lists for next search
controller.close();//closes search bar recommendations
},
title: Text(e),
))
.toList(),
),
),
);
}
}
After that list generated based on search, I want to chose one of the tiles to build a widget that contains fetched content of that post.
I created a class for contents to be displayed based on the tile we've chosen earlier but everything is also okay with that class.
How can I effectively call my class to show tiles contents that I chosen earlier, preferably without further fetching to stop wasting server resources and my devices resources.
I am completely new at flutter and developing this app for not more than three weeks without any previous coding experience other than few basic java apps that works on console, so please please please feel free to correct me in any way rather it's about my problem or not. Much thanks in advance.
Also this is my first question on here so excuse if formatting is not adequate enough for site.
Using indexOf() successfully returns the int value of chosen tile.
List.generate(model.titles.length,
(index) => model.titles[index].toString())
.map((e) => ListTile(
onTap: () {
setState(() {});
controller.close();
model.resultValue = model.titles.indexOf(e); // we are returning the int value of chosen tile to another class here
model.parsData();
model.clearFetchedData();
},
title: Text(e),
))
.toList(),

How to store data of collection from firestore into a list?

I am looking to store fetch data from firestore into a List which would contain data from all of its documents.
I defined list as :
List retrievedData = List();
next, on press of button, I wanted to print data in all documents of a specific collection. So, I did this:
RaisedButton(
onPressed: () async {
var collectionReferece = await Firestore.instance.collection('insults');
collectionReferece.getDocuments().then((collectionSnapshot){
retrievedData = collectionSnapshot.documents.toList();
});
print(retrievedData);
},
I am expecting this in console:
I/flutter (11351): [{index: 200, title: This is a test 1},{index: 100, title: This is a test 2}]
But I get this:
I/flutter (11351): [Instance of 'DocumentSnapshot', Instance of 'DocumentSnapshot']
Also, I just want to store this data in a list or any other variable. Help me out. Thank you.
Edit:
I tried to use forEach but it keeps on adding on every press of button.
If you want to:
retrieve data from firestore
add to list
create listview.builder
Then you can do the following, first declare the following variables under your State class:
class _MyHomePageState extends State<MyHomePage> {
bool isFirstTime = false;
List<DocumentSnapshot> datas = List<DocumentSnapshot>();
Next, create a method called getData() which will be referenced in onPressed:
floatingActionButton: FloatingActionButton(
onPressed: getData,
tooltip: 'Increment',
child: Icon(Icons.add),
),
getData() async {
if (!isFirstTime) {
QuerySnapshot snap =
await Firestore.instance.collection("insults").getDocuments();
isFirstTime = true;
setState(() {
datas.addAll(snap.documents);
});
}
}
Here on press of the FAB, you will get the data inside the insults collection. We use the boolean to only retrieve once per click. Inside the method dispose which you override:
#override
void dispose() {
super.dispose();
this.isFirstTime = false;
}
}
You can assign isFirstTime to false again. Then to display the data, You can use the property body of AppBar, assign it to Center widget, and the Center widget will contain the listview:
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: ListView.builder(
itemCount: datas.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${datas[index]["index"]}'),
subtitle: Text('${datas[index]["title"]}'),
);
},
),
Using listview.builder, you will have a list in your screen and you dont have to use forEach to iterate a list. You just have to use the get operator [] to be able to get the data inside the list.
Any code that needs access to the data from Firestore, need to be inside the then. So:
var collectionReferece = await Firestore.instance.collection('insults');
collectionReferece.getDocuments().then((collectionSnapshot){
retrievedData = collectionSnapshot.documents.toList();
print(retrievedData);
});
But you'll typically want to separate the data loading out of the build() method, and use state or FutureBuilder to get the data from the database into the rendered output. Some examples of that:
Flutter/Firebase_Auth: a build function returned null for using state
How to use one field of firebase to login for another example of using state
how do i call async property in Widget build method for an example of using a FutureBuilder
i think its because the .toList() method put those 2 documents just same datatype as "DocumentSnapshot" in the List. try printing this to be sure.
print(retrievedData[0]['title']);

Firestore replicating a SQL Join for noSQL and Flutter

I realise there is many questions in regards to replicating joins with NoSql document databases such as FireStore, however i'm unable to find a thorough solution utilising Dart/Flutter with FireStore.
I have done some research i feel that in the following example i would be looking for a 'many to many' relationship (please correct me if this is wrong) as there may be a future need to look at all profiles as well as all connections.
In firebase, i have two root level collections (profile & connection):
profile
> documentKey(Auto Generated)
> name = "John Smith"
> uid = "xyc4567"
> documentKey(Auto Generated)
> name = "Jane Doe"
> uid = "abc1234"
> documentKey(Auto Generated)
> name = "Kate Dee"
> uid = "efg8910"
connection
> documentKey(Auto Generated)
> type = "friend"
> profileuid = "abc1234"
> uid = "xyc4567"
> documentKey(Auto Generated)
> type = "family"
> profileuid = "abc1234"
> uid = "efg8910"
For this example the 'connection' documents have been created hypothetically for the user John Smith (uid: xyc4567) when he connected to Jane Doe (uid: abc1234) and Kate Dee (uid: efg8910).
Here is the relational SQL i'm looking to replicate to show a list of profiles which John Smith has connected with:
Select * FROM profile, connection
WHERE profile.uid = connection.profileuid
AND profile.uid = "xyc4567"
In flutter my flutter app i have a fireStore query starting point:
stream: Firestore.instance.collection('profile')
.where('uid', isEqualTo: "xyc4567").snapshots(),
Obviously it only returns from one collection. How do i join the collections in a many to many relationship?
Unfortunately, there is no JOIN clause in Cloud Firestore nor in others NoSQL databases. In Firestore queries are shallow. This means that they only get items from the collection that the query is run against. There is no way to get documents from two top-level collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection.
So the most simple solution I can think of is to query the database to get the uid of a user from the profile collection. Once you have that id, make another database call (inside the callback), and get the corresponding data that you need from the connection collection using the following query:
stream: Firestore.instance.collection('connection').where('uid', isEqualTo: "xyc4567").snapshots(),
Another solution would be to create a subcollection named connection under each user and add all connection objects beneath it. This practice is called denormalization and is a common practice when it comes to Firebase. If you are new to NoQSL databases, I recommend you see this video, Denormalization is normal with the Firebase Database for a better understanding. It is for Firebase realtime database but same rules apply to Cloud Firestore.
Also, when you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.
Suppose, you want to use a Stream that depends on some Future objcets.
Stories
Document ID (Auto Generated) //Suppose, "zddgaXmdadfHs"
> name = "The Lion & the Warthog"
> coverImage = "https://...."
> author = "Furqan Uddin Fahad"
> publisDate = 123836249234
Favorites
Document ID (Auto Generated)
> storyDocID = "zddgaXmdadfHs" //Document ID of a story
> userId = "adZXkdfnhoa" //Document ID of a user
Sql equivalent query should look like this
SELECT * FROM Favorites AS f, Stories AS s
WHERE f.storyDocID = s.DocumentID
AND f.userId = user.userId
And Firestore query like this
final _storeInstance = Firestore.instance;
Stream <List<Favorite>> getFavorites() async* {
final user = await _getUser(); //_getUser() Returns Future<User>
yield* _storeInstance
.collection('Favorites')
.where('userId', isEqualTo: user.userId)
.snapshots()
.asyncMap((snapshot) async {
final list = snapshot.documents.map((doc) async {
final story = await _getStory(doc['storyDocID']);
return Favorite.from(doc, story); //Favorite.from(DocumentSnapshot doc, Story story) returns an instance of Favorite
}).toList(); //List<Future<Favorite>>
return await Future.wait(list); //Converts List<Future<Favorite>> to Future<List<Favorite>>
});
}
Future<Story> _getStory(String storyDocID) async {
final docRef = _storeInstance
.collection('Stories')
.document(storyDocID);
final document = await docRef.get();
final story = Story.from(document);
return story;
}
I did some like this to join results from two colections objects and categories.
i did two StreamBuilders to show in a list, in the first one i got the categories and put in a map, then i query the objects and get the category object from the map using the categoryID:
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('categoryPath')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> categorySnapshot) {
//get data from categories
if (!categorySnapshot.hasData) {
return const Text('Loading...');
}
//put all categories in a map
Map<String, Category> categories = Map();
categorySnapshot.data.documents.forEach((c) {
categories[c.documentID] =
Category.fromJson(c.documentID, c.data);
});
//then from objects
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('objectsPath')
.where('day', isGreaterThanOrEqualTo: _initialDate)
.where('day', isLessThanOrEqualTo: _finalDate)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> objectsSnapshot) {
if (!objectsSnapshot.hasData)
return const Text('Loading...');
final int count =
objectsSnapshot.data.documents.length;
return Expanded(
child: Container(
child: Card(
elevation: 3,
child: ListView.builder(
padding: EdgeInsets.only(top: 0),
itemCount: count,
itemBuilder: (_, int index) {
final DocumentSnapshot document =
objectsSnapshot.data.documents[index];
Object object = Object.fromJson(
document.documentID, document.data);
return Column(
children: <Widget>[
Card(
margin: EdgeInsets.only(
left: 0, right: 0, bottom: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(0)),
),
elevation: 1,
child: ListTile(
onTap: () {},
title: Text(object.description,
style: TextStyle(fontSize: 20)),
//here is the magic, i get the category name using the map
of the categories and the category id from the object
subtitle: Text(
categories[object.categoryId] !=
null
? categories[
object.categoryId]
.name
: 'Uncategorized',
style: TextStyle(
color: Theme.of(context)
.primaryColor),
),
),
),
],
);
}),
),
),
);
I'm not sure if is what you want or is clear but i hope it help you.
I think denominational should not be preferred because to maintain the it you have to make extra writes to firestore
instead jorge vieira is correct since you are allowed to make double reads as compare to the writes
so its better to read twice instead of writing writing data twice and its also very impractical to remember every demoralized thing in a large project

Resources