improve loading speed. (#414)

This commit is contained in:
Jojo Feng
2024-06-01 19:40:37 -07:00
committed by GitHub
parent 9763a94e1d
commit e46432b86c
6 changed files with 109 additions and 68 deletions

View File

@ -13,6 +13,7 @@ import 'package:responsive_builder/responsive_builder.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
part 'stories_event.dart'; part 'stories_event.dart';
part 'stories_state.dart'; part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> { class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
@ -95,7 +96,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
storiesToBeDownloaded: state.storiesToBeDownloaded, storiesToBeDownloaded: state.storiesToBeDownloaded,
), ),
); );
for (final StoryType type in StoryType.values) {
for (final StoryType type in _preferenceCubit.state.tabs) {
add(LoadStories(type: type)); add(LoadStories(type: type));
} }
} }
@ -111,28 +113,44 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit( emit(
state state
.copyWithStoryIdsUpdated(type: type, to: ids) .copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0), .copyWithCurrentPageUpdated(type: type, to: 0)
.copyWithStatusUpdated(type: type, to: Status.inProgress),
); );
_offlineRepository _offlineRepository
.getCachedStoriesStream( .getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)), ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
) )
.listen((Story story) { .listen((Story story) => add(StoryLoaded(story: story, type: type)))
add(StoryLoaded(story: story, type: type)); .onDone(() => add(StoriesLoaded(type: type)));
}).onDone(() {
add(StoriesLoaded(type: type));
});
} else { } else {
final List<int> ids = final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type); await _hackerNewsRepository.fetchStoryIds(type: type);
emit( emit(
state state
.copyWithStoryIdsUpdated(type: type, to: ids) .copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0), .copyWithCurrentPageUpdated(type: type, to: 0)
.copyWithStatusUpdated(type: type, to: Status.inProgress),
); );
bool hasFirstArrived = false;
await _hackerNewsRepository await _hackerNewsRepository
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize)) .fetchStoriesStream(
ids: ids.sublist(0, state.currentPageSize),
sequential: !event.isRefreshing,
)
.listen((Story story) { .listen((Story story) {
/// When it's refreshing, only flush
/// previous stories when new stories has been fetched.
if (event.isRefreshing && !hasFirstArrived) {
hasFirstArrived = true;
final Map<StoryType, List<Story>> newStoriesMap =
Map<StoryType, List<Story>>.from(state.storiesByType);
newStoriesMap[type] = <Story>[];
emit(
state.copyWith(
storiesByType: newStoriesMap,
),
);
}
add(StoryLoaded(story: story, type: type)); add(StoryLoaded(story: story, type: type));
}).asFuture<void>(); }).asFuture<void>();
add(StoriesLoaded(type: type)); add(StoriesLoaded(type: type));
@ -161,11 +179,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
); );
} else { } else {
emit(state.copyWithRefreshed(type: event.type)); emit(state.copyWithRefreshed(type: event.type));
add(LoadStories(type: event.type)); add(LoadStories(type: event.type, isRefreshing: true));
} }
} }
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) { void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
if (state.statusByType[event.type] == Status.inProgress) return;
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,
@ -195,16 +215,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
upper, upper,
), ),
) )
.listen((Story story) { .listen(
add( (Story story) => add(StoryLoaded(story: story, type: event.type)),
StoryLoaded( )
story: story, .onDone(() => add(StoriesLoaded(type: event.type)));
type: event.type,
),
);
}).onDone(() {
add(StoriesLoaded(type: event.type));
});
} else { } else {
_hackerNewsRepository _hackerNewsRepository
.fetchStoriesStream( .fetchStoriesStream(
@ -213,16 +227,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
upper, upper,
), ),
) )
.listen((Story story) { .listen(
add( (Story story) => add(StoryLoaded(story: story, type: event.type)),
StoryLoaded( )
story: story, .onDone(() => add(StoriesLoaded(type: event.type)));
type: event.type,
),
);
}).onDone(() {
add(StoriesLoaded(type: event.type));
});
} }
} else { } else {
emit( emit(

View File

@ -6,12 +6,16 @@ abstract class StoriesEvent extends Equatable {
} }
class LoadStories extends StoriesEvent { class LoadStories extends StoriesEvent {
LoadStories({required this.type}); LoadStories({required this.type, this.isRefreshing = false});
final StoryType type; final StoryType type;
final bool isRefreshing;
@override @override
List<Object?> get props => <Object?>[type]; List<Object?> get props => <Object?>[
type,
isRefreshing,
];
} }
class StoriesInitialize extends StoriesEvent { class StoriesInitialize extends StoriesEvent {

View File

@ -150,12 +150,6 @@ class StoriesState extends Equatable {
} }
StoriesState copyWithRefreshed({required StoryType type}) { StoriesState copyWithRefreshed({required StoryType type}) {
final Map<StoryType, List<Story>> newStoriesMap =
Map<StoryType, List<Story>>.from(storiesByType);
newStoriesMap[type] = <Story>[];
final Map<StoryType, List<int>> newStoryIdsMap =
Map<StoryType, List<int>>.from(storyIdsByType);
newStoryIdsMap[type] = <int>[];
final Map<StoryType, Status> newStatusMap = final Map<StoryType, Status> newStatusMap =
Map<StoryType, Status>.from(statusByType); Map<StoryType, Status>.from(statusByType);
newStatusMap[type] = Status.inProgress; newStatusMap[type] = Status.inProgress;
@ -163,8 +157,6 @@ class StoriesState extends Equatable {
Map<StoryType, int>.from(currentPageByType); Map<StoryType, int>.from(currentPageByType);
newCurrentPageMap[type] = 0; newCurrentPageMap[type] = 0;
return copyWith( return copyWith(
storiesByType: newStoriesMap,
storyIdsByType: newStoryIdsMap,
statusByType: newStatusMap, statusByType: newStatusMap,
currentPageByType: newCurrentPageMap, currentPageByType: newCurrentPageMap,
); );

View File

@ -281,13 +281,21 @@ class HackiApp extends StatelessWidget {
.instance.platformDispatcher.platformBrightness, .instance.platformDispatcher.platformBrightness,
mode, mode,
); );
final bool isDarkModeEnabled = mode == null || final bool isDarkModeEnabled = () {
mode == AdaptiveThemeMode.dark || if (mode == null) {
return View.of(context)
.platformDispatcher
.platformBrightness ==
Brightness.dark;
} else {
return mode == AdaptiveThemeMode.dark ||
(mode == AdaptiveThemeMode.system && (mode == AdaptiveThemeMode.system &&
View.of(context) View.of(context)
.platformDispatcher .platformDispatcher
.platformBrightness == .platformBrightness ==
Brightness.dark); Brightness.dark);
}
}();
final ColorScheme colorScheme = ColorScheme.fromSeed( final ColorScheme colorScheme = ColorScheme.fromSeed(
brightness: brightness:
isDarkModeEnabled ? Brightness.dark : Brightness.light, isDarkModeEnabled ? Brightness.dark : Brightness.light,

View File

@ -329,7 +329,11 @@ class HackerNewsRepository {
/// Fetch a list of [Story] based on ids and return results /// Fetch a list of [Story] based on ids and return results
/// using a stream. /// using a stream.
Stream<Story> fetchStoriesStream({required List<int> ids}) async* { Stream<Story> fetchStoriesStream({
required List<int> ids,
bool sequential = false,
}) async* {
if (sequential) {
for (final int id in ids) { for (final int id in ids) {
final Story? story = final Story? story =
await _fetchItemJson(id).then((Map<String, dynamic>? json) async { await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
@ -342,6 +346,18 @@ class HackerNewsRepository {
yield story; yield story;
} }
} }
} else {
final List<Map<String, dynamic>?> responses = await Future.wait(
<Future<Map<String, dynamic>?>>[
...ids.map(_fetchItemJson),
],
);
for (final Map<String, dynamic>? json in responses) {
if (json == null) continue;
final Story story = Story.fromJson(json);
yield story;
}
}
} }
/// Fetch a list of [PollOption] based on ids and return results /// Fetch a list of [PollOption] based on ids and return results

View File

@ -3,10 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
class StoryTile extends StatelessWidget { class StoryTile extends StatelessWidget {
@ -123,6 +125,17 @@ class StoryTile extends StatelessWidget {
excludeSemantics: true, excludeSemantics: true,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: () {
if (story.url.isNotEmpty) {
LinkUtil.launch(
story.url,
context,
useReader: context.read<PreferenceCubit>().state.readerEnabled,
offlineReading:
context.read<StoriesBloc>().state.isOfflineReading,
);
}
},
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: Dimens.pt12), padding: const EdgeInsets.only(left: Dimens.pt12),
child: Column( child: Column(