Closed FisherWL closed 2 years ago
Hi there 👋 It's recommended that all data layer transactions occur in a separate package (repository) as to abstract BLoCs from working outside their intended scope.
Note: There is a difference between data providers and data sources, and the purpose of the repository layer is to interface purely with the data sources.
From there, blocs can interact with those repos and provide the necessary data for the UI.
Above all of this, if you needed to receive an update every 10 seconds, I would consider having simply a stream from the repo->bloc->UI which reacts to an API that sends updates every 10 secs.
What api are you calling?
@Gene-Dana wow, thx for the quick response! In terms of architecture, I'm actually following the data repository-> data provider structure, to be more specific, following the weather app example structure.
What api are you calling?
a live sports data provider. I'm calling it from a page where live game data is constantly updating and presenting.
a stream from the repo
can you be more specific?
are you referring to the Stream.periodic
?
Hi @FisherWL 👋
As you've already hinted, you can simply move the timer inside your bloc.
Hi @FisherWL I suggest to use stream periodic inside event handling in bloc
as @narcodico stated, you can put the timer in the bloc.
If available, I'd look for stream support from the API itself.
Closing for now but feel free to comment with any additional questions and I'm happy to continue the conversation 👍
How to dispose/ cancel timer inside a bloc/cubit?
so I moved the timer into cubit:
Future<void> fetchFixture(int fixtureId) async {
emit(state.copyWith(status: TrendsStatus.loading));
try {
timer = Timer.periodic(const Duration(milliseconds: 10000), (Timer t) async {
Fixture fixture = await _soccerRepository.getFixtureById(fixtureId);
if (fixture.trends!.isEmpty) {
emit(
state.copyWith(
status: TrendsStatus.empty,
),
);
} else {
emit(
state.copyWith(
status: TrendsStatus.success,
fixture: fixture,
),
);
}
});
} on Exception {
emit(state.copyWith(status: TrendsStatus.failure));
}
}
void disposeTimer() {
timer.cancel();
print('timer is disposed');
}
and it makes periodical HTTP calls as expected.
But the timer keeps alive, thus making calls.
I tried to call the disposeTimer() from the State
class,
@override
void dispose() {
context.read<TrendsCubit>().timer.cancel();
// or
context.read<TrendsCubit>().disposeTimer();
super.dispose();
}
but exception throws because the bloc has already automatically disposed.
@FisherWL you should override close in the bloc and dispose the timer there not from the widget tree.
@felangel Thx Felix, Merry Christmas! I added the following code and the timer can be closed now.
@override
Future<void> close() {
timer.cancel();
print('timer is cancelled');
return super.close();
}
Hello @FisherWL would you mind sharing your complete implementation inside the bloc ? I'm not sure where to initialize the Timer
@Aristidios
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:soccer_repository/soccer_repository.dart';
import 'package:futchart/app/app.dart';
import 'dart:async';
part 'trends_cubit.g.dart';
part 'trends_state.dart';
class TrendsCubit extends Cubit<TrendsState> {
TrendsCubit(this._soccerRepository) : super(const TrendsState());
final SoccerRepository _soccerRepository;
late Timer timer;
Future<void> fetchFixture(int fixtureId) async {
emit(state.copyWith(status: TrendsStatus.loading));
try {
timer = makePeriodicCall(
const Duration(milliseconds: 25000),
(Timer t) async {
Fixture fixture = await _soccerRepository.getFixtureById(fixtureId);
if (fixture.trends!.isEmpty) {
emit(
state.copyWith(
status: TrendsStatus.empty,
),
);
} else {
emit(
state.copyWith(
status: TrendsStatus.success,
fixture: fixture,
),
);
}
},
fireNow: true,
);
} on Exception {
emit(state.copyWith(status: TrendsStatus.failure));
}
}
@override
Future<void> close() {
timer.cancel();
print('getFixtureById timer is cancelled');
return super.close();
}
TrendsState fromJson(Map<String, dynamic> json) => TrendsState.fromJson(json);
Map<String, dynamic> toJson(TrendsState state) => state.toJson();
}
import 'dart:async';
/// default timer.periodic does not fire right away
Timer makePeriodicCall(
Duration duration,
void Function(Timer timer) callback, {
bool fireNow = false,
}) {
var timer = Timer.periodic(duration, callback);
if (fireNow) {
callback(timer);
}
return timer;
}
@FisherWL Thanks so much I've followed exactly your code but using it inside a Bloc event rather than cubit & I get this exception :
Exception has occurred.
_AssertionError ('package:bloc/src/emitter.dart': Failed assertion: line 114 pos 7: '!_isCompleted':
emit was called after an event handler completed normally.
This is usually due to an unawaited future in an event handler.
Please make sure to await all asynchronous operations with event handlers
and use emit.isDone after asynchronous operations before calling emit() to
ensure the event handler has not completed.
**BAD**
on<Event>((event, emit) {
future.whenComplete(() => emit(...));
});
**GOOD**
on<Event>((event, emit) async {
await future.whenComplete(() => emit(...));
});
)
Any ideas why ? : )
@Gene-Dana @felangel
my bloc event is async
Here is my code :
Future<void> _onAppleRequested(
AppleRequested event,
Emitter<MyBlocState> emit,
) async {
emit(state.copyWith(status: MyBlocStatus.loading));
try {
timer = await makePeriodicCall(
const Duration(milliseconds: 25000),
(Timer t) async {
List<Apples>? apples =
await _applesRepository.getApples();
if (apples == null || apples.isEmpty) {
emit(
state.copyWith(
status: MyBlocStatus.success,
),
);
} else {
emit(
state.copyWith(
status: MyBlocStatus.success,
currentApple: apples.first),
);
}
},
fireNow: true,
);
} on Exception {
emit(state.copyWith(status: MyBlocStatus.failure));
}
}
Alright I've figured it out :
I created another event ApplesRequested()
& all the async
calls happen in it & states are emitted there, this event gets triggered by the Timer
Future<void> _onAppleStreamRequested(
AppleRequested event,
Emitter<MyBlocState> emit,
) async {
timer = await makePeriodicCall(
const Duration(milliseconds: 25000),
(Timer t) async {
add(ApplesRequested());
}
},
fireNow: true,
);
}
So what was the fix? @Aristidios
Description Need to make a http call every 10 seconds.
My current solution Inside a stateful widget, setup a periodic timer during init, from there to trigger function in repo layer, then http call in data layer:
A better practice? Current problem: the timer isn't supposed to be a part of the presentation layer. How to separate it from the UI layer, maybe integrate the timer into the bloc, and get rid of the stateful widget? Thx!