Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 82 additions & 11 deletions lib/ui/screens/data_loading.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'dart:async';

import 'package:app/app_state.dart';
import 'package:app/enums.dart';
import 'package:app/providers/providers.dart';
import 'package:app/ui/screens/screens.dart';
import 'package:app/ui/widgets/widgets.dart';
Expand All @@ -14,37 +18,104 @@ class DataLoadingScreen extends StatefulWidget {
}

class _DataLoadingScreen extends State<DataLoadingScreen> {
static const _stillLoadingAfter = Duration(seconds: 6);
static const _loadTimeout = Duration(seconds: 30);

var _hasError = false;
var _stillLoading = false;
Timer? _stillLoadingTimer;
Timer? _timeoutTimer;

@override
void initState() {
super.initState();
_loadData();
}

@override
void dispose() {
_cancelTimers();
super.dispose();
}

void _cancelTimers() {
_stillLoadingTimer?.cancel();
_timeoutTimer?.cancel();
}

Future<void> _loadData() async {
_stillLoadingTimer = Timer(_stillLoadingAfter, () {
if (mounted) setState(() => _stillLoading = true);
});
_timeoutTimer = Timer(_loadTimeout, () {
if (mounted) setState(() => _hasError = true);
});

try {
await context.read<DataProvider>().init();
await Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
if (!mounted || _hasError) return;
_cancelTimers();
Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
} catch (e) {
print(e);
setState(() => _hasError = true);
_cancelTimers();
if (mounted) setState(() => _hasError = true);
}
}

void _retry() {
_cancelTimers();
setState(() {
_hasError = false;
_stillLoading = false;
});
_loadData();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

bool get _hasDownloads =>
context.read<DownloadProvider>().playables.isNotEmpty;

void _viewDownloads() {
_cancelTimers();
AppState.set('mode', AppMode.offline);
Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: GradientDecoratedContainer(
child: _hasError
? OopsBox(
showLogOutButton: true,
onRetry: () {
setState(() => _hasError = false);
_loadData();
},
)
: const ContainerWithSpinner(),
? OopsBox(showLogOutButton: true, onRetry: _retry)
: _buildLoading(),
),
);
}

Widget _buildLoading() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Spinner(),
if (_stillLoading) ...[
const SizedBox(height: 28),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
'This is taking longer than usual…',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70, fontSize: 15),
),
),
if (_hasDownloads) ...[
const SizedBox(height: 20),
ElevatedButton(
onPressed: _viewDownloads,
child: const Text('View Downloads'),
),
],
],
],
),
);
}
Expand Down
100 changes: 100 additions & 0 deletions test/ui/screens/data_loading_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import 'dart:async';

import 'package:app/app_state.dart';
import 'package:app/enums.dart';
import 'package:app/models/models.dart';
import 'package:app/providers/data_provider.dart';
import 'package:app/providers/download_provider.dart';
import 'package:app/ui/screens/data_loading.dart';
import 'package:app/ui/screens/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';

import '../../extensions/widget_tester_extension.dart';
import 'data_loading_test.mocks.dart';

@GenerateMocks([DataProvider, DownloadProvider])
void main() {
late MockDataProvider dataProvider;
late MockDownloadProvider downloadProvider;
late Completer<void> initCompleter;

setUp(() {
AppState.clear();
dataProvider = MockDataProvider();
downloadProvider = MockDownloadProvider();
initCompleter = Completer<void>();
when(dataProvider.init()).thenAnswer((_) => initCompleter.future);
});

Future<void> mount(
WidgetTester tester, {
required List<Playable> downloads,
}) async {
when(downloadProvider.playables).thenReturn(downloads);

await tester.pumpAppWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<DataProvider>.value(value: dataProvider),
Provider<DownloadProvider>.value(value: downloadProvider),
],
child: const DataLoadingScreen(),
),
routes: {MainScreen.routeName: (_) => const Text('MAIN')},
);
}

// Completes the still-hanging load so no timers outlive the test.
Future<void> settle(WidgetTester tester) async {
if (!initCompleter.isCompleted) initCompleter.complete();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
}

testWidgets('does not nag while the load is still fresh', (tester) async {
await mount(tester, downloads: []);
await tester.pump();

expect(find.text('This is taking longer than usual…'), findsNothing);

await settle(tester);
});

testWidgets(
'shows the still-loading message after a delay, without a downloads button',
(tester) async {
await mount(tester, downloads: []);
await tester.pump();
await tester.pump(const Duration(seconds: 7));

expect(find.text('This is taking longer than usual…'), findsOneWidget);
expect(find.text('View Downloads'), findsNothing);

await settle(tester);
},
);

testWidgets(
'offers View Downloads after a delay and enters offline mode',
(tester) async {
await mount(tester, downloads: [Song.fake()]);
await tester.pump();
await tester.pump(const Duration(seconds: 7));

expect(find.text('View Downloads'), findsOneWidget);

await tester.tap(find.text('View Downloads'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));

expect(find.text('MAIN'), findsOneWidget);
expect(AppState.get('mode'), AppMode.offline);

await settle(tester);
},
);
}
Loading
Loading