From 8a6737bf00e94ad68f9b8159ea3d87994200b081 Mon Sep 17 00:00:00 2001 From: SethCohen Date: Tue, 28 Mar 2023 02:27:07 -0400 Subject: [PATCH] feat: updated lesson pagination to use new `firebase_ui_firestore` dependency --- src/lib/common/utils/data_provider.dart | 72 ---------- src/lib/common/utils/routes.dart | 4 +- src/lib/features/home/home_page.dart | 1 - .../features/lesson/lesson_details_page.dart | 29 ++-- src/lib/features/lesson/lesson_model.dart | 49 ------- .../features/lesson/lessons_list_page.dart | 125 ++++++++++++------ 6 files changed, 104 insertions(+), 176 deletions(-) delete mode 100644 src/lib/features/lesson/lesson_model.dart diff --git a/src/lib/common/utils/data_provider.dart b/src/lib/common/utils/data_provider.dart index aacb159..babd043 100644 --- a/src/lib/common/utils/data_provider.dart +++ b/src/lib/common/utils/data_provider.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:spaced_repetition/sm.dart'; import '../../features/dictionary/dictionary_model.dart'; -import '../../features/lesson/lesson_model.dart'; import '../../features/flashcard/flashcard_model.dart'; import '../../features/review/review_model.dart'; @@ -12,80 +11,14 @@ final currentUser = FirebaseAuth.instance.currentUser!; const pageSize = 5; class DataProvider extends ChangeNotifier { - List _lessons = []; Map> _reviews = {}; List _dictionary = []; - late DocumentSnapshot> _lastDeckDoc; late DocumentSnapshot> _lastCardDoc; - List get lessons => _lessons; Map> get reviews => _reviews; List get dictionary => _dictionary; - Future loadLessons() async { - final lessons = await FirebaseFirestore.instance - .collection('decks') - .orderBy('title') - .limit(pageSize) - .get(); - - _lessons = await Future.wait(lessons.docs.map((doc) async { - // Gets the progress of the current user for each deck - final userProgress = await FirebaseFirestore.instance - .collection('users') - .doc(currentUser.uid) - .collection('deckProgress') - .doc(doc.id) - .get(); - - // If the user has not started the lesson, the cardCount will be 0 - return userProgress.exists - ? LessonModel.fromMap(doc.id, userProgress['cardCount'], doc.data()) - : LessonModel.fromMap(doc.id, 0, doc.data()); - }).toList()); - - _lastDeckDoc = lessons.docs.last; - - notifyListeners(); - } - - Future> loadMoreLessons() async { - final decks = await FirebaseFirestore.instance - .collection('decks') - .orderBy('name') - .startAfterDocument(_lastDeckDoc) - .limit(pageSize) - .get(); - - // If there are no more decks to fetch, return the current list of lessons - if (decks.docs.isEmpty) { - return _lessons; - } - - // Gets the progress of the current user for each deck - final currentUser = FirebaseAuth.instance.currentUser!; - final deckProgressDocs = await FirebaseFirestore.instance - .collection('users') - .doc(currentUser.uid) - .collection('deckProgress') - .where(FieldPath.documentId, - whereIn: decks.docs.map((doc) => doc.id).toList()) - .get(); - _lessons = []; - for (final deck in decks.docs) { - final deckProgress = deckProgressDocs.docs - .firstWhere((doc) => doc.id == deck.id)['cardCount']; - - // Adds the deck to the list of lessons - _lessons.add(LessonModel.fromMap(deck.id, deckProgress, deck.data())); - } - - notifyListeners(); - - return _lessons; - } - Future loadReviews() async { try { final reviews = await FirebaseFirestore.instance @@ -177,11 +110,6 @@ class DataProvider extends ChangeNotifier { .doc(flashcard.deckId); batch.set(deckRec, {'cardCount': FieldValue.increment(1)}, SetOptions(merge: true)); - _lessons = _lessons - .map((lesson) => lesson.id == flashcard.deckId - ? lesson.copyWith(cardsCompleted: lesson.cardsCompleted + 1) - : lesson) - .toList(); await batch.commit(); diff --git a/src/lib/common/utils/routes.dart b/src/lib/common/utils/routes.dart index 549a5cd..825ba29 100644 --- a/src/lib/common/utils/routes.dart +++ b/src/lib/common/utils/routes.dart @@ -10,7 +10,9 @@ Route generateRoute(RouteSettings settings) { } else if (settings.name!.contains("lesson")) { final args = settings.arguments as Map; return MaterialPageRoute( - builder: (_) => Lesson(lesson: args['lesson']), settings: settings); + builder: (_) => LessonDetails( + lessonId: args['lessonId'], lessonData: args['lessonData']), + settings: settings); } else if (settings.name!.contains("review")) { final args = settings.arguments as Map; return MaterialPageRoute( diff --git a/src/lib/features/home/home_page.dart b/src/lib/features/home/home_page.dart index 3d81c66..89a731c 100644 --- a/src/lib/features/home/home_page.dart +++ b/src/lib/features/home/home_page.dart @@ -20,7 +20,6 @@ class _HomePageState extends State { void initState() { super.initState(); - context.read().loadLessons(); context.read().loadDictionary(); } diff --git a/src/lib/features/lesson/lesson_details_page.dart b/src/lib/features/lesson/lesson_details_page.dart index c592a82..85faddb 100644 --- a/src/lib/features/lesson/lesson_details_page.dart +++ b/src/lib/features/lesson/lesson_details_page.dart @@ -1,18 +1,20 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'lesson_model.dart'; import '../flashcard/flashcard_model.dart'; import '../flashcard/flashcard.dart'; +import 'lessons_list_page.dart'; -class Lesson extends StatefulWidget { - const Lesson({super.key, required this.lesson}); - final LessonModel lesson; +class LessonDetails extends StatefulWidget { + const LessonDetails( + {super.key, required this.lessonId, required this.lessonData}); + final String lessonId; + final Lesson lessonData; @override - State createState() => _LessonState(); + State createState() => _LessonDetailsState(); } -class _LessonState extends State { +class _LessonDetailsState extends State { int _currentCardIndex = 0; @override @@ -25,7 +27,7 @@ class _LessonState extends State { }, child: Scaffold( appBar: AppBar( - title: Text(widget.lesson.title), + title: Text(widget.lessonData.title), ), body: FutureBuilder( future: _getCardsFuture(), @@ -45,12 +47,12 @@ class _LessonState extends State { Future _getCardsFuture() => FirebaseFirestore.instance .collection("decks") - .doc(widget.lesson.id) + .doc(widget.lessonId) .collection('cards') .get(); void _handleIndex() => setState(() { - bool isCompleted = _currentCardIndex == widget.lesson.cardsTotal - 1; + bool isCompleted = _currentCardIndex == widget.lessonData.cardCount - 1; if (isCompleted) { Navigator.pop(context); } else { @@ -61,7 +63,7 @@ class _LessonState extends State { List _getFlashcards(List cards) => cards .map((card) => Flashcard( card: FlashcardModel.fromMap(card.data() as Map, - card.id, widget.lesson.id, widget.lesson.title), + card.id, widget.lessonId, widget.lessonData.title), handleIndex: _handleIndex, isReview: false, )) @@ -80,14 +82,15 @@ class _LessonState extends State { return Container( padding: const EdgeInsets.all(8.0), alignment: Alignment.centerRight, - child: Text('${_currentCardIndex + 1} / ${widget.lesson.cardsTotal}')); + child: + Text('${_currentCardIndex + 1} / ${widget.lessonData.cardCount}')); } Widget _buildProgressBarIndicator() { return TweenAnimationBuilder( tween: Tween( - begin: _currentCardIndex / widget.lesson.cardsTotal, - end: (_currentCardIndex + 1) / widget.lesson.cardsTotal), + begin: _currentCardIndex / widget.lessonData.cardCount, + end: (_currentCardIndex + 1) / widget.lessonData.cardCount), duration: const Duration(milliseconds: 1000), builder: (context, double value, child) => LinearProgressIndicator(value: value)); diff --git a/src/lib/features/lesson/lesson_model.dart b/src/lib/features/lesson/lesson_model.dart deleted file mode 100644 index cf5f774..0000000 --- a/src/lib/features/lesson/lesson_model.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class LessonModel { - final String title; - final String description; - final String id; - final int cardsCompleted; - final int cardsTotal; - - LessonModel({ - required this.cardsCompleted, - required this.cardsTotal, - required this.title, - required this.description, - required this.id, - }); - - factory LessonModel.fromMap(id, cardsCompleted, Map data) => - LessonModel( - cardsCompleted: cardsCompleted, - cardsTotal: data['cardCount'], - title: data['title'], - description: data['description'], - id: id, - ); - - LessonModel copyWith({ - int? cardsCompleted, - int? cardsTotal, - String? title, - String? description, - String? id, - }) => - LessonModel( - cardsCompleted: cardsCompleted ?? this.cardsCompleted, - cardsTotal: cardsTotal ?? this.cardsTotal, - title: title ?? this.title, - description: description ?? this.description, - id: id ?? this.id, - ); - - void navigateToLesson(BuildContext context) => Navigator.pushNamed( - context, - '/${title.toLowerCase().replaceAll(' ', '_')}', - arguments: { - 'lesson': this, - }, - ); -} diff --git a/src/lib/features/lesson/lessons_list_page.dart b/src/lib/features/lesson/lessons_list_page.dart index ffc9b85..f2f7fa3 100644 --- a/src/lib/features/lesson/lessons_list_page.dart +++ b/src/lib/features/lesson/lessons_list_page.dart @@ -1,42 +1,77 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_firestore/firebase_ui_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'lesson_model.dart'; -import '../../common/utils/data_provider.dart'; -class LessonsPage extends StatefulWidget { - const LessonsPage({super.key}); +class Lesson { + Lesson( + {required this.cardCount, + required this.title, + required this.description}); - @override - State createState() => _LessonsPageState(); -} + Lesson.fromMap(Map data) + : this( + cardCount: data['cardCount'] as int, + title: data['title'] as String, + description: data['description'] as String, + ); -class _LessonsPageState extends State { - final ScrollController _scrollController = ScrollController(); + final int cardCount; + final String title; + final String description; - @override - void initState() { - super.initState(); - _scrollController.addListener(_scrollListener); + Map toMap() { + return { + 'cardCount': cardCount, + 'title': title, + 'description': description, + }; } +} + +class LessonsPage extends StatelessWidget { + const LessonsPage({super.key}); @override Widget build(BuildContext context) { - final decks = context.watch().lessons; + final decksQuery = FirebaseFirestore.instance + .collection('decks') + .orderBy('title') + .withConverter( + fromFirestore: (snapshot, _) => Lesson.fromMap(snapshot.data()!), + toFirestore: (deck, _) => deck.toMap(), + ); + return FirestoreListView( + query: decksQuery, + itemBuilder: (context, deckSnapshot) { + Lesson deckData = deckSnapshot.data(); + String deckId = deckSnapshot.id; + + return FutureBuilder( + future: _getUserDeckProgress(deckId), + builder: (context, snapshot) { + int userDeckCardCount = 0; + + if (snapshot.hasData && snapshot.data!.exists) { + userDeckCardCount = snapshot.data!['cardCount']; + } - // TODO fix loading more lessons - // TODO add loading indicator - return ListView.builder( - controller: _scrollController, - itemCount: decks.length, - itemBuilder: (context, index) => _buildListItem(decks[index])); + return _buildListItem(context, deckId, deckData, userDeckCardCount); + }, + ); + }, + ); } - void _scrollListener() { - if (_scrollController.offset >= - _scrollController.position.maxScrollExtent && - !_scrollController.position.outOfRange) { - context.read().loadMoreLessons(); - } + Future _getUserDeckProgress(deckId) { + final currentuser = FirebaseAuth.instance.currentUser!; + + return FirebaseFirestore.instance + .collection('users') + .doc(currentuser.uid) + .collection('deckProgress') + .doc(deckId) + .get(); } Color _lessonColour(cardsCompleted, cardsTotal) => @@ -46,21 +81,31 @@ class _LessonsPageState extends State { ? Colors.orange : Colors.black38; - Widget _buildListItem(LessonModel deck) => Card( + Widget _buildListItem(BuildContext context, String deckId, Lesson deck, + int cardsCompleted) => + Card( child: ListTile( title: Text(deck.title), - trailing: _buildTrailing(deck), - onTap: () => deck.navigateToLesson(context)), + subtitle: Text(deck.description), + trailing: Row(mainAxisSize: MainAxisSize.min, children: [ + Text( + '${deck.cardCount - cardsCompleted}', + style: TextStyle( + color: _lessonColour(cardsCompleted, deck.cardCount)), + ), + Icon(Icons.check_circle, + color: _lessonColour(cardsCompleted, deck.cardCount)), + ]), + onTap: () => _navigateToLesson(context, deckId, deck)), ); - Widget _buildTrailing(LessonModel deck) => - Row(mainAxisSize: MainAxisSize.min, children: [ - Text( - '${deck.cardsTotal - deck.cardsCompleted}', - style: TextStyle( - color: _lessonColour(deck.cardsCompleted, deck.cardsTotal)), - ), - Icon(Icons.check_circle, - color: _lessonColour(deck.cardsCompleted, deck.cardsTotal)), - ]); + void _navigateToLesson(BuildContext context, String deckId, Lesson deck) => + Navigator.pushNamed( + context, + '/${deck.title.toLowerCase().replaceAll(' ', '_')}', + arguments: { + 'lessonId': deckId, + 'lessonData': deck, + }, + ); }