perf(ui): retain platform link lookup across rebuilds

Keep the platform lookup Future in widget state and replace it only when track identifiers change. Theme rebuilds retain loaded content without another native call.

Add widget regressions for theme and identifier changes.
This commit is contained in:
zarzet
2026-09-06 14:04:05 +07:00
parent 4a4de098c8
commit a89f786960
2 changed files with 91 additions and 5 deletions
+31 -5
View File
@@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
/// Bottom sheet listing every streaming platform song.link resolves for a
/// track. Data-driven from the generic platform-link map so the app core
/// stays service-agnostic; tapping a row opens the link externally.
class OpenOnPlatformSheet extends StatelessWidget {
class OpenOnPlatformSheet extends StatefulWidget {
final String spotifyId;
final String isrc;
@@ -31,6 +31,35 @@ class OpenOnPlatformSheet extends StatelessWidget {
);
}
@override
State<OpenOnPlatformSheet> createState() => _OpenOnPlatformSheetState();
}
class _OpenOnPlatformSheetState extends State<OpenOnPlatformSheet> {
late Future<Map<String, String>> _links;
@override
void initState() {
super.initState();
_loadLinks();
}
@override
void didUpdateWidget(OpenOnPlatformSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.spotifyId != widget.spotifyId ||
oldWidget.isrc != widget.isrc) {
_loadLinks();
}
}
void _loadLinks() {
_links = PlatformBridge.getTrackPlatformLinks(
spotifyId: widget.spotifyId,
isrc: widget.isrc,
);
}
static const Map<String, String> _platformNames = {
'spotify': 'Spotify',
'appleMusic': 'Apple Music',
@@ -83,10 +112,7 @@ class OpenOnPlatformSheet extends StatelessWidget {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return FutureBuilder<Map<String, String>>(
future: PlatformBridge.getTrackPlatformLinks(
spotifyId: spotifyId,
isrc: isrc,
),
future: _links,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Padding(
+60
View File
@@ -0,0 +1,60 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/l10n/app_localizations.dart';
import 'package:spotiflac_android/widgets/open_on_platform_sheet.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
testWidgets(
'theme rebuild retains completed lookup; identifiers replace it',
(tester) async {
const channel = MethodChannel('com.zarz.spotiflac/backend');
final calls = <MethodCall>[];
final pending = Completer<Map<String, dynamic>>();
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
call,
) async {
calls.add(call);
if (calls.length == 1) {
return {
'platforms': {'spotify': 'https://example.test/one'},
};
}
return pending.future;
});
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
Widget app(Brightness brightness, String id) => MaterialApp(
theme: ThemeData(brightness: brightness),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: OpenOnPlatformSheet(spotifyId: id)),
);
await tester.pumpWidget(app(Brightness.light, 'one'));
await tester.pumpAndSettle();
expect(find.text('Spotify'), findsOneWidget);
await tester.pumpWidget(app(Brightness.dark, 'one'));
await tester.pumpAndSettle();
expect(calls.length, 1);
expect(find.text('Spotify'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
await tester.pumpWidget(app(Brightness.dark, 'two'));
await tester.pump();
expect(calls.length, 2);
expect(calls.last.arguments, {'spotify_id': 'two', 'isrc': ''});
expect(find.byType(CircularProgressIndicator), findsOneWidget);
pending.complete({
'platforms': {'appleMusic': 'https://example.test/two'},
});
await tester.pumpAndSettle();
expect(find.text('Apple Music'), findsOneWidget);
expect(find.text('Spotify'), findsNothing);
},
);
}