So, recently while working on the Skica Flutter application, I struggled with a problem of randomizing data and calling it the way I wanted to. Below goes through the various transformations of my code
The 'tips' Cloudstore collection reference, a Random, and a 'next' function that randomizes a number between a given min and max range.
class _TipsCardState extends State<TipsCard> {
CollectionReference tips = FirebaseFirestore.instance.collection('tips');
final _random = new Random();
String next(int min, int max) =>
(min + _random.nextInt(max - min)).toString();
String _randomTip;
int _tipsCount;
Future<int> countDocuments() async {
QuerySnapshot _myDoc = await tips.get();
List<DocumentSnapshot> _myDocCount = _myDoc.docs;
print(_myDocCount.length);
return _myDocCount.length;
}
Future<DocumentSnapshot> getTips() async {
_tipsCount = await countDocuments();
_randomTip = next(1, _tipsCount);
return await tips.doc(_randomTip).get();
}
The problem I ran in to was trying to accomplish this all in a single function. There really is no reason to over complicate functions. Though, I'm pretty sure it can be done, it's better to just seperate things if it makes the code more readable, imo.
First is the countDocuments
function.
This one is very simple. We're simply querying the Tips Collection
, then getting the documents that belong to that collection and returning it's length. This gives us the amount of Documents
in the Tips Collection
.
Note: In Firestore, a Collection contains Documents.
In the body of the app, the getTips
function is called within a FutureBuilder
widget. Since it returns the DocumentSnapshot
of the database.
But! I ended up simplifying this even more by using a provider! I added a StreamProvder<Tip>
to my app's MultiProvider
like so. The code to randomize the tips remains the same, however now I manually set the range instead of generating it. dynamically. No biggie.
class App extends StatelessWidget {
static FirebaseAnalytics analytics = FirebaseAnalytics();
final _random = new Random();
String next(int min, int max) =>
(min + _random.nextInt(max - min)).toString();
String _randomTip;
@override
Widget build(BuildContext context) {
_randomTip = next(1, 33);
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthProvider>(create: (_) => AuthProvider()),
StreamProvider<User>.value(
value: FirebaseAuth.instance.authStateChanges(),
),
StreamProvider<Tip>.value(
value: TipProvider().streamTip(_randomTip),
),
],
The StreamProvider
's value is TipProvider().streamTip(_randomTip)
which is the following code below:
class TipProvider with ChangeNotifier {
final String uid;
TipProvider({this.uid});
final FirebaseFirestore firestore = FirebaseFirestore.instance;
bool loading = false;
Stream<Tip> streamTip(String id) {
return firestore
.collection('tips')
.doc(id)
.snapshots()
.map((snap) => Tip.fromFirestore(snap));
}
The TipProvider
also contains other useful methods related to tips such as addTipLike
, removeTipLike
, addTipBookmark
, removeTipBookmark
, etc.
This is a much more clean way of organizing this code, imo. We can store methods/functions related to the Tips in the TipProvider
file.
Plus, having it set up as a StreamProvider
allows the data to be kept updated in real time! And there is virtually no extra cost in Firebase, as compared to calling it manually each time.
The end result is displaying that tip within the app like so, with real time updates to the likes count! Neat right!?