Skip to content

Commit

Permalink
Improve timetable responsiveness (#79)
Browse files Browse the repository at this point in the history
* Revert to stream-based event fetching

* Fix relevance picker bug

* Bump version
  • Loading branch information
IoanaAlexandru authored Oct 12, 2020
1 parent e035ca6 commit d0af893
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 74 deletions.
18 changes: 9 additions & 9 deletions lib/pages/filter/view/relevance_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ class RelevanceController {
}

class RelevancePicker extends StatefulWidget {
const RelevancePicker(
{@required this.filterProvider,
this.canBePrivate = true,
this.canBeForEveryone = true,
bool defaultPrivate,
this.controller})
: defaultPrivate = (defaultPrivate ?? true) && canBePrivate;
const RelevancePicker({
@required this.filterProvider,
this.canBePrivate = true,
this.canBeForEveryone = true,
bool defaultPrivate,
this.controller,
}) : defaultPrivate = (defaultPrivate ?? true) && canBePrivate;

final FilterProvider filterProvider;

Expand Down Expand Up @@ -189,8 +189,8 @@ class _RelevancePickerState extends State<RelevancePicker> {
..add(Selectable(
label: node,
controller: controller,
initiallySelected: (!_onlyMeController.isSelected &&
!_anyoneController.isSelected) ||
initiallySelected: (!(_onlyMeController?.isSelected ?? false) &&
!(_anyoneController?.isSelected ?? false)) ||
(!widget.canBePrivate && !widget.canBeForEveryone),
onSelected: (selected) => setState(() {
if (_user?.canAddPublicInfo ?? false) {
Expand Down
107 changes: 48 additions & 59 deletions lib/pages/timetable/service/uni_event_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart';
import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart';
import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart';
import 'package:acs_upb_mobile/widgets/toast.dart';
import 'package:async/async.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:rrule/rrule.dart';
import 'package:synchronized/synchronized.dart';
import 'package:time_machine/time_machine.dart';
import 'package:timetable/timetable.dart';

Expand Down Expand Up @@ -181,13 +181,11 @@ class UniEventProvider extends EventProvider<UniEventInstance>

final Map<String, AcademicCalendar> _calendars = {};
ClassProvider _classProvider;
FilterProvider _filterProvider;
final AuthProvider _authProvider;
List<String> _classIds = [];
Filter _filter;
List<UniEvent> eventsCache;
bool emptyCache;

var cacheLock = Lock();
bool empty;

Future<Map<String, AcademicCalendar>> fetchCalendars() async {
final QuerySnapshot query =
Expand All @@ -200,67 +198,68 @@ class UniEventProvider extends EventProvider<UniEventInstance>
return _calendars;
}

Future<bool> get empty async {
if (emptyCache != null) return emptyCache;
return (await _events).isEmpty;
Future<void> checkIfEmpty(List<Stream<List<UniEvent>>> streams) async {
for (final stream in streams) {
if ((await stream.first)?.isNotEmpty ?? false) {
empty = false;
return;
}
}
empty = true;
}

Future<List<UniEvent>> get _events async {
if (eventsCache != null) return eventsCache;

Stream<List<UniEvent>> get _events {
if (!_authProvider.isAuthenticatedFromCache ||
_filter == null ||
_calendars == null) return [];
_calendars == null) return Stream.value([]);

var events = <UniEvent>[];
// Set the cache to an empty list so that if [updateClasses] or [updateFilter]
// is called while fetching, we can invalidate the fetched data.
eventsCache = <UniEvent>[];
final streams = <Stream<List<UniEvent>>>[];

if (_filter.relevantNodes.length > 1) {
for (final classId in _classIds ?? []) {
final query = await Firestore.instance
final Stream<List<UniEvent>> stream = Firestore.instance
.collection('events')
.where('class', isEqualTo: classId)
.where('degree', isEqualTo: _filter.baseNode)
.where('relevance',
arrayContainsAny: _filter.relevantNodes..remove('All'))
.getDocuments();

for (final doc in query.documents) {
ClassHeader classHeader;
if (doc.data['class'] != null) {
classHeader =
await _classProvider.fetchClassHeader(doc.data['class']);
.snapshots()
.asyncMap((snapshot) async {
final events = <UniEvent>[];

try {
for (final doc in snapshot.documents) {
ClassHeader classHeader;
if (doc.data['class'] != null) {
classHeader =
await _classProvider.fetchClassHeader(doc.data['class']);
}

events.add(UniEventExtension.fromJSON(doc.documentID, doc.data,
classHeader: classHeader, calendars: _calendars));
}
return events.where((element) => element != null).toList();
} catch (e) {
print(e);
return events;
}

events.add(UniEventExtension.fromJSON(doc.documentID, doc.data,
classHeader: classHeader, calendars: _calendars));
}
});
streams.add(stream);
}
}

events = events.where((event) => event != null).toList();
return cacheLock.synchronized(() {
if (eventsCache != null) {
// Cache was not invalidated while fetching
emptyCache = events.isEmpty;
eventsCache = events;
notifyListeners();
return eventsCache;
} else {
// Cache was invalidated while fetching - that means the filter/classes
// changed, so we need to try again
// ignore: recursive_getters
return _events;
}
});
checkIfEmpty(streams);

final stream = StreamZip(streams);

// Flatten zipped streams
return stream.map((events) => events.expand((i) => i).toList());
}

@override
Stream<Iterable<UniEventInstance>> getAllDayEventsIntersecting(
DateInterval interval) {
return Stream<List<UniEvent>>.fromFuture(_events).map((events) => events
return _events.map((events) => events
.map((event) => event.generateInstances(intersectingInterval: interval))
.expand((i) => i)
.allDayEvents
Expand All @@ -279,40 +278,32 @@ class UniEventProvider extends EventProvider<UniEventInstance>
@override
Stream<Iterable<UniEventInstance>> getPartDayEventsIntersecting(
LocalDate date) {
return Stream<List<UniEvent>>.fromFuture(_events).map((events) => events
return _events.map((events) => events
.map((event) => event.generateInstances(
intersectingInterval: DateInterval(date, date)))
.expand((i) => i)
.partDayEvents);
}

void updateClasses(ClassProvider classProvider) {
classProvider.fetchUserClassIds(uid: _authProvider.uid).then((classIds) {
_classProvider = classProvider;
_classProvider = classProvider;
_classProvider.fetchUserClassIds(uid: _authProvider.uid).then((classIds) {
_classIds = classIds;
cacheLock.synchronized(() {
eventsCache = null;
emptyCache = null;
});
notifyListeners();
});
}

void updateFilter(FilterProvider filterProvider) {
filterProvider.fetchFilter().then((filter) {
_filterProvider = filterProvider;
_filterProvider.fetchFilter().then((filter) {
_filter = filter;
cacheLock.synchronized(() {
eventsCache = null;
emptyCache = null;
});
notifyListeners();
});
}

Future<bool> addEvent(UniEvent event, {BuildContext context}) async {
try {
await Firestore.instance.collection('events').add(event.toData());
eventsCache = null;
notifyListeners();
return true;
} catch (e) {
Expand All @@ -331,7 +322,6 @@ class UniEventProvider extends EventProvider<UniEventInstance>
}

await ref.updateData(event.toData());
eventsCache = null;
notifyListeners();
return true;
} catch (e) {
Expand All @@ -345,7 +335,6 @@ class UniEventProvider extends EventProvider<UniEventInstance>
DocumentReference ref;
ref = Firestore.instance.collection('events').document(event.id);
await ref.delete();
eventsCache = null;
notifyListeners();
return true;
} catch (e) {
Expand Down
4 changes: 1 addition & 3 deletions lib/pages/timetable/view/timetable_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ class _TimetablePageState extends State<TimetablePage> {
}
},
),
if (eventProvider.eventsCache == null)
const Center(child: CircularProgressIndicator()),
],
),
);
Expand Down Expand Up @@ -149,7 +147,7 @@ class _TimetablePageState extends State<TimetablePage> {
// Show dialog if there are no events
final eventProvider =
Provider.of<UniEventProvider>(context, listen: false);
if (await eventProvider.empty) {
if (eventProvider.empty) {
await showDialog<String>(
context: context,
builder: buildDialog,
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB.
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
#
# ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file.
version: 1.1.2+2
version: 1.1.3+1

environment:
sdk: ">=2.7.0 <3.0.0"
Expand Down
3 changes: 1 addition & 2 deletions test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,7 @@ void main() {
when(mockEventProvider.hasListeners).thenReturn(false);
when(mockEventProvider.getAllDayEventsIntersecting(any))
.thenAnswer((_) => Stream.fromIterable([]));
when(mockEventProvider.eventsCache).thenReturn([]);
when(mockEventProvider.empty).thenAnswer((_) => Future.value(true));
when(mockEventProvider.empty).thenReturn(true);

mockRequestProvider = MockRequestProvider();
when(mockRequestProvider.makeRequest(any, context: anyNamed('context')))
Expand Down

0 comments on commit d0af893

Please sign in to comment.