added timestamp to bloc cache, showing age in offline mode

This commit is contained in:
Elias Müller 2024-05-12 02:39:35 +02:00
parent 3281b134e0
commit ebbb70dc96
13 changed files with 302 additions and 40 deletions

View File

@ -4,6 +4,6 @@ import 'data_loader.dart';
abstract class MhslDataLoader<TResult> extends DataLoader<TResult> { abstract class MhslDataLoader<TResult> extends DataLoader<TResult> {
MhslDataLoader() : super(Dio(BaseOptions( MhslDataLoader() : super(Dio(BaseOptions(
baseUrl: 'https://mhsl.eu/marianum/marianummobile/' baseUrl: 'https://mhsl.eu/marianum/marianummobile/'
))); )));
} }

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'loadable_state_event.dart'; import 'loadable_state_event.dart';
import 'loadable_state_state.dart'; import 'loadable_state_state.dart';
@ -29,7 +30,6 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
bool connectivityStatusKnown() => state.connections != null; bool connectivityStatusKnown() => state.connections != null;
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true); bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
bool allowRetry() => reFetch != null; bool allowRetry() => reFetch != null;
bool showErrorMessage() => isConnected() && reFetch != null;
IconData connectionIcon() => connectivityStatusKnown() IconData connectionIcon() => connectivityStatusKnown()
? isConnected() ? isConnected()
@ -37,10 +37,10 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
: Icons.signal_wifi_connected_no_internet_4 : Icons.signal_wifi_connected_no_internet_4
: Icons.device_unknown; : Icons.device_unknown;
String connectionText() => connectivityStatusKnown() String connectionText({int? lastUpdated}) => connectivityStatusKnown()
? isConnected() ? isConnected()
? 'Verbindung fehlgeschlagen' ? 'Verbindung fehlgeschlagen'
: 'Offline' : 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}'
: 'Unbekannte Fehlerursache'; : 'Unbekannte Fehlerursache';
@override @override

View File

@ -11,6 +11,7 @@ class LoadableState<TState> with _$LoadableState {
const factory LoadableState({ const factory LoadableState({
@Default(true) bool isLoading, @Default(true) bool isLoading,
@Default(null) TState? data, @Default(null) TState? data,
@Default(null) int? lastFetch,
@Default(null) void Function()? reFetch, @Default(null) void Function()? reFetch,
@Default(null) LoadingError? error, @Default(null) LoadingError? error,
}) = _LoadableState; }) = _LoadableState;

View File

@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$LoadableState<TState> { mixin _$LoadableState<TState> {
bool get isLoading => throw _privateConstructorUsedError; bool get isLoading => throw _privateConstructorUsedError;
TState? get data => throw _privateConstructorUsedError; TState? get data => throw _privateConstructorUsedError;
int? get lastFetch => throw _privateConstructorUsedError;
void Function()? get reFetch => throw _privateConstructorUsedError; void Function()? get reFetch => throw _privateConstructorUsedError;
LoadingError? get error => throw _privateConstructorUsedError; LoadingError? get error => throw _privateConstructorUsedError;
@ -35,6 +36,7 @@ abstract class $LoadableStateCopyWith<TState, $Res> {
$Res call( $Res call(
{bool isLoading, {bool isLoading,
TState? data, TState? data,
int? lastFetch,
void Function()? reFetch, void Function()? reFetch,
LoadingError? error}); LoadingError? error});
@ -57,6 +59,7 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
$Res call({ $Res call({
Object? isLoading = null, Object? isLoading = null,
Object? data = freezed, Object? data = freezed,
Object? lastFetch = freezed,
Object? reFetch = freezed, Object? reFetch = freezed,
Object? error = freezed, Object? error = freezed,
}) { }) {
@ -69,6 +72,10 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
as TState?, as TState?,
lastFetch: freezed == lastFetch
? _value.lastFetch
: lastFetch // ignore: cast_nullable_to_non_nullable
as int?,
reFetch: freezed == reFetch reFetch: freezed == reFetch
? _value.reFetch ? _value.reFetch
: reFetch // ignore: cast_nullable_to_non_nullable : reFetch // ignore: cast_nullable_to_non_nullable
@ -104,6 +111,7 @@ abstract class _$$LoadableStateImplCopyWith<TState, $Res>
$Res call( $Res call(
{bool isLoading, {bool isLoading,
TState? data, TState? data,
int? lastFetch,
void Function()? reFetch, void Function()? reFetch,
LoadingError? error}); LoadingError? error});
@ -125,6 +133,7 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
$Res call({ $Res call({
Object? isLoading = null, Object? isLoading = null,
Object? data = freezed, Object? data = freezed,
Object? lastFetch = freezed,
Object? reFetch = freezed, Object? reFetch = freezed,
Object? error = freezed, Object? error = freezed,
}) { }) {
@ -137,6 +146,10 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
as TState?, as TState?,
lastFetch: freezed == lastFetch
? _value.lastFetch
: lastFetch // ignore: cast_nullable_to_non_nullable
as int?,
reFetch: freezed == reFetch reFetch: freezed == reFetch
? _value.reFetch ? _value.reFetch
: reFetch // ignore: cast_nullable_to_non_nullable : reFetch // ignore: cast_nullable_to_non_nullable
@ -155,6 +168,7 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
const _$LoadableStateImpl( const _$LoadableStateImpl(
{this.isLoading = true, {this.isLoading = true,
this.data = null, this.data = null,
this.lastFetch = null,
this.reFetch = null, this.reFetch = null,
this.error = null}) this.error = null})
: super._(); : super._();
@ -167,6 +181,9 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
final TState? data; final TState? data;
@override @override
@JsonKey() @JsonKey()
final int? lastFetch;
@override
@JsonKey()
final void Function()? reFetch; final void Function()? reFetch;
@override @override
@JsonKey() @JsonKey()
@ -174,7 +191,7 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
@override @override
String toString() { String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, reFetch: $reFetch, error: $error)'; return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
} }
@override @override
@ -185,13 +202,15 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
(identical(other.isLoading, isLoading) || (identical(other.isLoading, isLoading) ||
other.isLoading == isLoading) && other.isLoading == isLoading) &&
const DeepCollectionEquality().equals(other.data, data) && const DeepCollectionEquality().equals(other.data, data) &&
(identical(other.lastFetch, lastFetch) ||
other.lastFetch == lastFetch) &&
(identical(other.reFetch, reFetch) || other.reFetch == reFetch) && (identical(other.reFetch, reFetch) || other.reFetch == reFetch) &&
(identical(other.error, error) || other.error == error)); (identical(other.error, error) || other.error == error));
} }
@override @override
int get hashCode => Object.hash(runtimeType, isLoading, int get hashCode => Object.hash(runtimeType, isLoading,
const DeepCollectionEquality().hash(data), reFetch, error); const DeepCollectionEquality().hash(data), lastFetch, reFetch, error);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -205,6 +224,7 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
const factory _LoadableState( const factory _LoadableState(
{final bool isLoading, {final bool isLoading,
final TState? data, final TState? data,
final int? lastFetch,
final void Function()? reFetch, final void Function()? reFetch,
final LoadingError? error}) = _$LoadableStateImpl<TState>; final LoadingError? error}) = _$LoadableStateImpl<TState>;
const _LoadableState._() : super._(); const _LoadableState._() : super._();
@ -214,6 +234,8 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
@override @override
TState? get data; TState? get data;
@override @override
int? get lastFetch;
@override
void Function()? get reFetch; void Function()? get reFetch;
@override @override
LoadingError? get error; LoadingError? get error;

View File

@ -53,13 +53,13 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
bloc.reFetch = loadableState.reFetch; bloc.reFetch = loadableState.reFetch;
return Column( return Column(
children: [ children: [
LoadableStateErrorBar(visible: loadableState.showErrorBar()), LoadableStateErrorBar(visible: loadableState.showErrorBar(), message: loadableState.error?.message, lastUpdated: loadableState.lastFetch),
Expanded( Expanded(
child: Stack( child: Stack(
children: [ children: [
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()), LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()), LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
LoadableStateErrorScreen(visible: loadableState.showError()), LoadableStateErrorScreen(visible: loadableState.showError(), message: loadableState.error?.message),
AnimatedOpacity( AnimatedOpacity(
opacity: loadableState.showContent() ? 1.0 : 0.0, opacity: loadableState.showContent() ? 1.0 : 0.0,

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/infoDialog.dart';
import '../bloc/loadable_state_bloc.dart'; import '../bloc/loadable_state_bloc.dart';
class LoadableStateErrorBar extends StatelessWidget { class LoadableStateErrorBar extends StatelessWidget {
final bool visible; final bool visible;
const LoadableStateErrorBar({required this.visible, super.key}); final String? message;
final int? lastUpdated;
const LoadableStateErrorBar({required this.visible, this.message, this.lastUpdated, super.key});
final Duration animationDuration = const Duration(milliseconds: 200); final Duration animationDuration = const Duration(milliseconds: 200);
@ -29,25 +32,31 @@ class LoadableStateErrorBar extends StatelessWidget {
builder: (context) { builder: (context) {
var bloc = context.watch<LoadableStateBloc>(); var bloc = context.watch<LoadableStateBloc>();
var status = ( var status = (
icon: bloc.connectionIcon(), icon: bloc.connectionIcon(),
text: bloc.connectionText(), text: bloc.connectionText(lastUpdated: lastUpdated),
color: bloc.connectivityStatusKnown() && !bloc.isConnected() color: bloc.connectivityStatusKnown() && !bloc.isConnected()
? Colors.grey.shade600 ? Colors.grey.shade600
: Theme.of(context).primaryColor : Theme.of(context).primaryColor
); );
return Container( return InkWell(
height: 20, onTap: () {
decoration: BoxDecoration( if(!bloc.isConnected()) return;
color: status.color, InfoDialog.show(context, 'Exception: ${message.toString()}');
), },
child: Row( child: Container(
mainAxisAlignment: MainAxisAlignment.center, height: 20,
children: [ decoration: BoxDecoration(
Icon(status.icon, size: 14), color: status.color,
const SizedBox(width: 10), ),
Text(status.text, style: const TextStyle(fontSize: 12)) child: Row(
], mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(status.icon, size: 14),
const SizedBox(width: 10),
Text(status.text, style: const TextStyle(fontSize: 12))
],
),
), ),
); );
}, },

View File

@ -6,7 +6,8 @@ import 'loadable_state_consumer.dart';
class LoadableStateErrorScreen extends StatelessWidget { class LoadableStateErrorScreen extends StatelessWidget {
final bool visible; final bool visible;
const LoadableStateErrorScreen({required this.visible, super.key}); final String? message;
const LoadableStateErrorScreen({required this.visible, this.message, super.key});
@override @override
@ -28,11 +29,19 @@ class LoadableStateErrorScreen extends StatelessWidget {
if(bloc.allowRetry()) ...[ if(bloc.allowRetry()) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')), TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')),
],
if(bloc.showErrorMessage()) ...[
const SizedBox(height: 40), const SizedBox(height: 40),
Text("bloc.loadingError!.message", style: TextStyle(color: Theme.of(context).hintColor, fontSize: 12)) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
message ?? 'Task failed successfully :)',
style: TextStyle(
color: Theme.of(context).hintColor,
fontSize: 12
),
maxLines: 10,
overflow: TextOverflow.ellipsis,
),
),
], ],
], ],
), ),

View File

@ -6,6 +6,7 @@ import '../../loadableState/loading_error.dart';
import '../../repository/repository.dart'; import '../../repository/repository.dart';
import 'loadable_hydrated_bloc_event.dart'; import 'loadable_hydrated_bloc_event.dart';
import '../../loadableState/loadable_state.dart'; import '../../loadableState/loadable_state.dart';
import 'loadable_save_context.dart';
abstract class LoadableHydratedBloc< abstract class LoadableHydratedBloc<
TEvent extends LoadableHydratedBlocEvent<TState>, TEvent extends LoadableHydratedBlocEvent<TState>,
@ -17,10 +18,29 @@ abstract class LoadableHydratedBloc<
> { > {
late TRepository _repository; late TRepository _repository;
LoadableHydratedBloc() : super(const LoadableState()) { LoadableHydratedBloc() : super(const LoadableState()) {
on<Emit<TState>>((event, emit) => emit(LoadableState(isLoading: event.loading, data: event.state(innerState ?? fromNothing()), reFetch: retry)));
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(isLoading: true, data: innerState))); on<Emit<TState>>((event, emit) => emit(LoadableState(
isLoading: event.loading,
data: event.state(innerState ?? fromNothing()),
lastFetch: DateTime.now().millisecondsSinceEpoch,
reFetch: retry
)));
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
isLoading: true,
data: innerState,
lastFetch: state.lastFetch
)));
on<ClearState<TState>>((event, emit) => emit(const LoadableState())); on<ClearState<TState>>((event, emit) => emit(const LoadableState()));
on<Error<TState>>((event, emit) => emit(LoadableState(isLoading: false, data: innerState, reFetch: retry, error: event.error)));
on<Error<TState>>((event, emit) => emit(LoadableState(
isLoading: false,
data: innerState,
lastFetch: state.lastFetch,
reFetch: retry,
error: event.error
)));
_repository = repository(); _repository = repository();
fetch(); fetch();
@ -41,19 +61,26 @@ abstract class LoadableHydratedBloc<
(e) { (e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}'); log('Error while fetching ${TState.toString()}: ${e.toString()}');
add(Error(LoadingError( add(Error(LoadingError(
message: e.toString(), message: e.message,
allowRetry: true, allowRetry: true,
))); )));
} },
).then((value) { ).then((value) {
log('Fetch for ${TState.toString()} completed!'); log('Fetch for ${TState.toString()} completed!');
}); });
} }
@override @override
fromJson(Map<String, dynamic> json) => LoadableState(isLoading: true, data: fromStorage(json)); fromJson(Map<String, dynamic> json) {
var rawData = LoadableSaveContext.unwrap(json);
return LoadableState(isLoading: true, lastFetch: rawData.meta.timestamp, data: fromStorage(rawData.data));
}
@override @override
Map<String, dynamic>? toJson(LoadableState<TState> state) => state.data == null ? {} : state.data.toJson(); Map<String, dynamic>? toJson(LoadableState<TState> state) => LoadableSaveContext.wrap(
toStorage(state.data),
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
);
Future<void> gatherData(); Future<void> gatherData();
TRepository repository(); TRepository repository();

View File

@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'loadable_save_context.freezed.dart';
part 'loadable_save_context.g.dart';
@freezed
class LoadableSaveContext with _$LoadableSaveContext {
const LoadableSaveContext._();
const factory LoadableSaveContext({
required int timestamp,
}) = _LoadableSaveContext;
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) => _$LoadableSaveContextFromJson(json);
static String dataKey = 'data';
static String metaKey = 'meta';
static Map<String, dynamic> wrap(Map<String, dynamic>? data, int lastFetch) =>
{dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()};
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey]));
}

View File

@ -0,0 +1,155 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'loadable_save_context.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
LoadableSaveContext _$LoadableSaveContextFromJson(Map<String, dynamic> json) {
return _LoadableSaveContext.fromJson(json);
}
/// @nodoc
mixin _$LoadableSaveContext {
int get timestamp => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LoadableSaveContextCopyWith<LoadableSaveContext> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoadableSaveContextCopyWith<$Res> {
factory $LoadableSaveContextCopyWith(
LoadableSaveContext value, $Res Function(LoadableSaveContext) then) =
_$LoadableSaveContextCopyWithImpl<$Res, LoadableSaveContext>;
@useResult
$Res call({int timestamp});
}
/// @nodoc
class _$LoadableSaveContextCopyWithImpl<$Res, $Val extends LoadableSaveContext>
implements $LoadableSaveContextCopyWith<$Res> {
_$LoadableSaveContextCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? timestamp = null,
}) {
return _then(_value.copyWith(
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$LoadableSaveContextImplCopyWith<$Res>
implements $LoadableSaveContextCopyWith<$Res> {
factory _$$LoadableSaveContextImplCopyWith(_$LoadableSaveContextImpl value,
$Res Function(_$LoadableSaveContextImpl) then) =
__$$LoadableSaveContextImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int timestamp});
}
/// @nodoc
class __$$LoadableSaveContextImplCopyWithImpl<$Res>
extends _$LoadableSaveContextCopyWithImpl<$Res, _$LoadableSaveContextImpl>
implements _$$LoadableSaveContextImplCopyWith<$Res> {
__$$LoadableSaveContextImplCopyWithImpl(_$LoadableSaveContextImpl _value,
$Res Function(_$LoadableSaveContextImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? timestamp = null,
}) {
return _then(_$LoadableSaveContextImpl(
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LoadableSaveContextImpl extends _LoadableSaveContext {
const _$LoadableSaveContextImpl({required this.timestamp}) : super._();
factory _$LoadableSaveContextImpl.fromJson(Map<String, dynamic> json) =>
_$$LoadableSaveContextImplFromJson(json);
@override
final int timestamp;
@override
String toString() {
return 'LoadableSaveContext(timestamp: $timestamp)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoadableSaveContextImpl &&
(identical(other.timestamp, timestamp) ||
other.timestamp == timestamp));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, timestamp);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith =>
__$$LoadableSaveContextImplCopyWithImpl<_$LoadableSaveContextImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LoadableSaveContextImplToJson(
this,
);
}
}
abstract class _LoadableSaveContext extends LoadableSaveContext {
const factory _LoadableSaveContext({required final int timestamp}) =
_$LoadableSaveContextImpl;
const _LoadableSaveContext._() : super._();
factory _LoadableSaveContext.fromJson(Map<String, dynamic> json) =
_$LoadableSaveContextImpl.fromJson;
@override
int get timestamp;
@override
@JsonKey(ignore: true)
_$$LoadableSaveContextImplCopyWith<_$LoadableSaveContextImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'loadable_save_context.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LoadableSaveContextImpl _$$LoadableSaveContextImplFromJson(
Map<String, dynamic> json) =>
_$LoadableSaveContextImpl(
timestamp: json['timestamp'] as int,
);
Map<String, dynamic> _$$LoadableSaveContextImplToJson(
_$LoadableSaveContextImpl instance) =>
<String, dynamic>{
'timestamp': instance.timestamp,
};

View File

@ -72,10 +72,6 @@ class Overhang extends StatelessWidget {
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()), onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
), ),
const ListTile(
leading: Icon(Icons.science_outlined),
// onTap: () => pushScreen(context, withNavBar: false, screen: const GradeAveragesScreen()),
)
], ],
), ),
); );