felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.84k stars 3.4k forks source link

question: fetch data periodically according to bloc pattern #3079

Closed FisherWL closed 2 years ago

FisherWL commented 2 years ago

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:

  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(
        const Duration(milliseconds: 10000),
        (Timer t) => setState(() {
              context.read<MyHttpCubit>().fetchData();
            }));

  @override
  void dispose() {
    timer.cancel();
    super.dispose();
  }

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!

Gene-Dana commented 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?

FisherWL commented 2 years ago

@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?

narcodico commented 2 years ago

Hi @FisherWL 👋

As you've already hinted, you can simply move the timer inside your bloc.

tarektaamali commented 2 years ago

Hi @FisherWL I suggest to use stream periodic inside event handling in bloc

Gene-Dana commented 2 years ago

as @narcodico stated, you can put the timer in the bloc.

If available, I'd look for stream support from the API itself.

felangel commented 2 years ago

Closing for now but feel free to comment with any additional questions and I'm happy to continue the conversation 👍

FisherWL commented 2 years ago

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.

felangel commented 2 years ago

@FisherWL you should override close in the bloc and dispose the timer there not from the widget tree.

FisherWL commented 2 years ago

@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();
  }
Aristidios commented 2 years ago

Hello @FisherWL would you mind sharing your complete implementation inside the bloc ? I'm not sure where to initialize the Timer

FisherWL commented 2 years ago

@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;
}
Aristidios commented 2 years ago

@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

Aristidios commented 2 years ago

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));
    }
  }
Aristidios commented 2 years ago

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,
      );
  }
SAGARSURI commented 1 year ago

So what was the fix? @Aristidios