Skip to content

Instantly share code, notes, and snippets.

@Ehesp
Last active November 9, 2021 15:20
Show Gist options
  • Save Ehesp/7a185a4cbda0681e276b7076cc4d3e51 to your computer and use it in GitHub Desktop.
Save Ehesp/7a185a4cbda0681e276b7076cc4d3e51 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0',
appId: '1:448618578101:ios:2bc5c1fe2ec336f8ac3efc',
messagingSenderId: '448618578101',
projectId: 'react-native-firebase-testing',
),
);
runApp(FirestoreExampleApp());
}
/// A reference to the list of movies.
/// We are using `withConverter` to ensure that interactions with the collection
/// are type-safe.
final moviesRef = FirebaseFirestore.instance
.collection('firestore-example-app')
.withConverter<Movie>(
fromFirestore: (snapshots, _) => Movie.fromJson(snapshots.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
/// The different ways that we can filter/sort movies.
enum MovieQuery {
year,
likesAsc,
likesDesc,
score,
sciFi,
fantasy,
}
extension on Query<Movie> {
/// Create a firebase query from a [MovieQuery]
Query<Movie> queryBy(MovieQuery query) {
switch (query) {
case MovieQuery.fantasy:
return where('genre', arrayContainsAny: ['Fantasy']);
case MovieQuery.sciFi:
return where('genre', arrayContainsAny: ['Sci-Fi']);
case MovieQuery.likesAsc:
case MovieQuery.likesDesc:
return orderBy('likes', descending: query == MovieQuery.likesDesc);
case MovieQuery.year:
return orderBy('year', descending: true);
case MovieQuery.score:
return orderBy('score', descending: true);
}
}
}
/// The entry point of the application.
///
/// Returns a [MaterialApp].
class FirestoreExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firestore Example App',
theme: ThemeData.dark(),
home: const Scaffold(
body: Center(child: FilmList()),
),
);
}
}
/// Holds all example app films
class FilmList extends StatefulWidget {
const FilmList({Key? key}) : super(key: key);
@override
_FilmListState createState() => _FilmListState();
}
class _FilmListState extends State<FilmList> {
MovieQuery query = MovieQuery.year;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Firestore Example: Movies'),
// This is a example use for 'snapshots in sync'.
// The view reflects the time of the last Firestore sync; which happens any time a field is updated.
StreamBuilder(
stream: FirebaseFirestore.instance.snapshotsInSync(),
builder: (context, _) {
return Text(
'Latest Snapshot: ${DateTime.now()}',
style: Theme.of(context).textTheme.caption,
);
},
)
],
),
actions: <Widget>[
PopupMenuButton<MovieQuery>(
onSelected: (value) => setState(() => query = value),
icon: const Icon(Icons.sort),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem(
value: MovieQuery.year,
child: Text('Sort by Year'),
),
const PopupMenuItem(
value: MovieQuery.score,
child: Text('Sort by Score'),
),
const PopupMenuItem(
value: MovieQuery.likesAsc,
child: Text('Sort by Likes ascending'),
),
const PopupMenuItem(
value: MovieQuery.likesDesc,
child: Text('Sort by Likes descending'),
),
const PopupMenuItem(
value: MovieQuery.fantasy,
child: Text('Filter genre Fantasy'),
),
const PopupMenuItem(
value: MovieQuery.sciFi,
child: Text('Filter genre Sci-Fi'),
),
];
},
),
PopupMenuButton<String>(
onSelected: (_) => _resetLikes(),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem(
value: 'reset_likes',
child: Text('Reset like counts (WriteBatch)'),
),
];
},
),
],
),
body: StreamBuilder<QuerySnapshot<Movie>>(
stream: moviesRef.queryBy(query).snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.requireData;
return ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
return _MovieItem(
data.docs[index].data(),
data.docs[index].reference,
);
},
);
},
),
);
}
Future<void> _resetLikes() async {
final movies = await moviesRef.get();
WriteBatch batch = FirebaseFirestore.instance.batch();
for (final movie in movies.docs) {
batch.update(movie.reference, {'likes': 0});
}
await batch.commit();
}
}
/// A single movie row.
class _MovieItem extends StatelessWidget {
_MovieItem(this.movie, this.reference);
final Movie movie;
final DocumentReference<Movie> reference;
/// Returns the movie poster.
Widget get poster {
return SizedBox(
width: 100,
child: Center(
child: Image.network(movie.poster),
),
);
}
/// Returns movie details.
Widget get details {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
metadata,
genres,
Likes(
reference: reference,
currentLikes: movie.likes,
)
],
),
);
}
/// Return the movie title.
Widget get title {
return Text(
'${movie.title} (${movie.year})',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
);
}
/// Returns metadata about the movie.
Widget get metadata {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text('Rated: ${movie.rated}'),
),
Text('Runtime: ${movie.runtime}'),
],
),
);
}
/// Returns a list of genre movie tags.
List<Widget> get genreItems {
return [
for (final genre in movie.genre)
Padding(
padding: const EdgeInsets.only(right: 2),
child: Chip(
backgroundColor: Colors.lightBlue,
label: Text(
genre,
style: const TextStyle(color: Colors.white),
),
),
)
];
}
/// Returns all genres.
Widget get genres {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
children: genreItems,
),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4, top: 4),
child: Row(
children: [
poster,
Flexible(child: details),
],
),
);
}
}
/// Displays and manages the movie 'like' count.
class Likes extends StatefulWidget {
/// Constructs a new [Likes] instance with a given [DocumentReference] and
/// current like count.
Likes({
Key? key,
required this.reference,
required this.currentLikes,
}) : super(key: key);
/// The reference relating to the counter.
final DocumentReference<Movie> reference;
/// The number of current likes (before manipulation).
final int currentLikes;
@override
_LikesState createState() => _LikesState();
}
class _LikesState extends State<Likes> {
/// A local cache of the current likes, used to immediately render the updated
/// likes count after an update, even while the request isn't completed yet.
late int _likes = widget.currentLikes;
Future<void> _onLike() async {
final currentLikes = _likes;
// Increment the 'like' count straight away to show feedback to the user.
setState(() {
_likes = currentLikes + 1;
});
try {
// Update the likes using a transaction.
// We use a transaction because multiple users could update the likes count
// simultaneously. As such, our likes count may be different from the likes
// count on the server.
int newLikes = await FirebaseFirestore.instance
.runTransaction<int>((transaction) async {
DocumentSnapshot<Movie> movie =
await transaction.get<Movie>(widget.reference);
if (!movie.exists) {
throw Exception('Document does not exist!');
}
int updatedLikes = movie.data()!.likes + 1;
transaction.update(widget.reference, {'likes': updatedLikes});
return updatedLikes;
});
// Update with the real count once the transaction has completed.
setState(() => _likes = newLikes);
} catch (e, s) {
print(s);
print('Failed to update likes for document! $e');
// If the transaction fails, revert back to the old count
setState(() => _likes = currentLikes);
}
}
@override
void didUpdateWidget(Likes oldWidget) {
super.didUpdateWidget(oldWidget);
// The likes on the server changed, so we need to update our local cache to
// keep things in sync. Otherwise if another user updates the likes,
// we won't see the update.
if (widget.currentLikes != oldWidget.currentLikes) {
_likes = widget.currentLikes;
}
}
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
iconSize: 20,
onPressed: _onLike,
icon: const Icon(Icons.favorite),
),
Text('$_likes likes'),
],
);
}
}
@immutable
class Movie {
Movie({
required this.genre,
required this.likes,
required this.poster,
required this.rated,
required this.runtime,
required this.title,
required this.year,
});
Movie.fromJson(Map<String, Object?> json)
: this(
genre: (json['genre']! as List).cast<String>(),
likes: json['likes']! as int,
poster: json['poster']! as String,
rated: json['rated']! as String,
runtime: json['runtime']! as String,
title: json['title']! as String,
year: json['year']! as int,
);
final String poster;
final int likes;
final String title;
final int year;
final String runtime;
final String rated;
final List<String> genre;
Map<String, Object?> toJson() {
return {
'genre': genre,
'likes': likes,
'poster': poster,
'rated': rated,
'runtime': runtime,
'title': title,
'year': year,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment