skip to Main Content

I am using these packages for the following codes: flutter_bloc, oxidized, freezed, and get_it

tmdb_bloc.dart

typedef TmdbState = PageState<List<MovieEntity>>;

class TmdbBloc extends Bloc<TmdbEvent, TmdbState> {
  final TmdbPublicUseCases useCases;

  TmdbBloc(this.useCases) : super(const PageState.initial()) {
    on<TmdbEvent>((event, emit) async {
      event.map(
        getPopularMovies: (_) => _runEvent(emit, useCases.proxy.getPopularMovies),
        getTrendingMovies: (_) => _runEvent(emit, useCases.proxy.getTrendingMovies),
        searchMovies: (e) => _runEvent(emit, () => useCases.proxy.searchMovies(e.query)),
      );
    });
  }

  void _runEvent(Emitter<TmdbState> emit, Future<TMovieListResult> Function() caller) async {
    emit(const PageState.loading());
    final response = await caller();
    response.when(
      ok: (list) => emit(PageState.loaded(list)), 
      err: (error) => emit(PageState.error(error.message)),
    );
  }
}

tmdb_events.dart

part 'tmdb_events.freezed.dart';

@freezed
sealed class TmdbEvent with _$TmdbEvent {
  const factory TmdbEvent.getPopularMovies() = PopularMoviesEvent;
  const factory TmdbEvent.getTrendingMovies() = TrendingMoviesEvent;
  const factory TmdbEvent.searchMovies(String query) = SearchMoviesEvent;
}

page_states.dart

part 'page_states.freezed.dart';

@freezed
sealed class PageState<T extends Object> with _$PageState {
  const factory PageState.initial() = InitialState;
  const factory PageState.loading() = LoadingState;
  const factory PageState.loaded(T data) = LoadedState;
  const factory PageState.error([String? message]) = ErrorState;
}

di.dart

final sl = GetIt.instance;

void getItInit() {
  // external connector
  sl.registerLazySingleton<TApiDataSourceClient>(() => DioClient());
  // data sources
  sl.registerLazySingleton<TBaseApiClient>(() => RestApiClient(sl()));
  sl.registerLazySingleton<TBaseDataProxy>(() => DataProxy(sl()));
  // use cases
  sl.registerLazySingleton(() => TmdbPublicUseCases(sl()));
}

main.dart

void main() async {
  await TMDB.init();
  getItInit();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: BlocProvider(
        create: (_) => TmdbBloc(sl<TmdbPublicUseCases>())..add(const TmdbEvent.getPopularMovies()),
        child: const HomeScreen(),
      ),
    );
  }
}

home_screen.dart

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(),
      body: Container(
        color: Colors.black,
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildDefaultImage(),
              const SizedBox(height: 30),
              const Text(
                'Popular Movies',
                style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
              ),
              BlocBuilder<TmdbBloc, TmdbState>(
                builder: (context, state) {
                  return state.when(
                    initial: () => const Text('No data to show'), 
                    loading: () => const CircularProgressIndicator(), 
                    loaded: (list) => Text(list.runtimeType.toString()), 
                    error: (e) => Text('Something went wrong: $e'),
                  );
                },
              ),
              const SizedBox(height: 20),
            ],
          ),
        ),
      ),
    );
  }
  ...
}

After running my app it keeps showing the CircularProgressIndicator widget with the following text in Debug Console:

Launching lib/main.dart on sdk gphone64 x86 64 in debug mode...
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
E/flutter ( 4527): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: 'package:bloc/src/emitter.dart': Failed assertion: line 114 pos 7: '!_isCompleted':
E/flutter ( 4527):
E/flutter ( 4527): emit was called after an event handler completed normally.
E/flutter ( 4527): This is usually due to an unawaited future in an event handler.
E/flutter ( 4527): Please make sure to await all asynchronous operations with event handlers
E/flutter ( 4527): and use emit.isDone after asynchronous operations before calling emit() to
E/flutter ( 4527): ensure the event handler has not completed.
E/flutter ( 4527):
E/flutter ( 4527):   **BAD**
E/flutter ( 4527):   on<Event>((event, emit) {
E/flutter ( 4527):     future.whenComplete(() => emit(...));
E/flutter ( 4527):   });
E/flutter ( 4527):
E/flutter ( 4527):   **GOOD**
E/flutter ( 4527):   on<Event>((event, emit) async {
E/flutter ( 4527):     await future.whenComplete(() => emit(...));
E/flutter ( 4527):   });
E/flutter ( 4527):
E/flutter ( 4527): #0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:51:61)
E/flutter ( 4527): #1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5)
E/flutter ( 4527): #2      _Emitter.call (package:bloc/src/emitter.dart:114:7)
E/flutter ( 4527): #3      TmdbBloc._runEvent.<anonymous closure> (package:movie_app_tdd_di/features/tmdb_public/3_presentation/1_bloc/tmdb_bloc.dart:27:25)
E/flutter ( 4527): #4      Ok.when (package:oxidized/src/result.dart:264:75)
E/flutter ( 4527): #5      TmdbBloc._runEvent (package:movie_app_tdd_di/features/tmdb_public/3_presentation/1_bloc/tmdb_bloc.dart:26:14)
E/flutter ( 4527): <asynchronous suspension>
E/flutter ( 4527):
D/EGL_emulation( 4527): app_time_stats: avg=18.48ms min=5.36ms max=91.61ms count=44
D/EGL_emulation( 4527): app_time_stats: avg=13.80ms min=5.36ms max=31.71ms count=61
D/EGL_emulation( 4527): app_time_stats: avg=7.21ms min=3.08ms max=12.55ms count=60
D/EGL_emulation( 4527): app_time_stats: avg=14.61ms min=9.96ms max=26.73ms count=60
D/EGL_emulation( 4527): app_time_stats: avg=16.67ms min=10.15ms max=22.99ms count=61
D/EGL_emulation( 4527): app_time_stats: avg=16.73ms min=14.36ms max=18.81ms count=60
D/EGL_emulation( 4527): app_time_stats: avg=18.50ms min=5.40ms max=100.80ms count=54
D/EGL_emulation( 4527): app_time_stats: avg=8.58ms min=3.11ms max=27.14ms count=61
Application finished.

Exited.

I have no idea what is happening, I think my TmdbBloc class is right.

Regarding this code:

              BlocBuilder<TmdbBloc, TmdbState>(
                builder: (context, state) {
                  return state.when(
                    initial: () => const Text('No data to show'), 
                    loading: () => const CircularProgressIndicator(), 
                    loaded: (list) => Text(list.runtimeType.toString()), 
                    error: (e) => Text('Something went wrong: $e'),
                  );
                },
              ),

I think it is not correctly defined because it does not indicate a specific associated event. How to fix it if that snippet is associated with TmdbEvent.getPopularMovies() or PopularMoviesEvent?

2

Answers


  1. Chosen as BEST ANSWER

    I made these changes and it is working like charm:

    tmdb_cubit.dart (Replaced Bloc with Cubit)

    typedef TmdbState = PageState<List<MovieEntity>>;
    
    
    sealed class TmdbCubit<T extends TmdbEvent> extends Cubit<TmdbState> {
      final TmdbPublicUseCases useCases;
    
      TmdbCubit(this.useCases) : super(const PageState.initial());
    
      Future<void> _runEvent(Future<TMovieListResult> Function() caller) async {
        emit(const PageState.loading());
    
        final response = await caller();
    
        response.when(
          ok: (list) => emit(PageState.loaded(list)), 
          err: (error) => emit(PageState.error(error.message)),
        );
      }
    }
    
    final class PopularMoviesCubit extends TmdbCubit<PopularMoviesEvent> {
      PopularMoviesCubit(super.useCases);
      void call() async => await _runEvent(useCases.proxy.getPopularMovies);
    }
    
    final class TrendingMoviesCubit extends TmdbCubit<TrendingMoviesEvent> {
      TrendingMoviesCubit(super.useCases);
      void call() async => await _runEvent(useCases.proxy.getTrendingMovies);
    }
    
    final class SearchMoviesCubit extends TmdbCubit<SearchMoviesEvent> {
      SearchMoviesCubit(super.useCases);
      void call(String query) async => await _runEvent(() => useCases.proxy.searchMovies(query));
    }
    

    home_screen.dart

    ...
                  BlocBuilder<TrendingMoviesCubit, TmdbState>(
                    builder: (context, state) {
                      return state.when(
                        initial: () => const Text('No data to show'), 
                        loading: () => const CircularProgressIndicator(), 
                        loaded: (list) => Text((list as List<MovieEntity>).runtimeType.toString()),
                        error: (e) => Text('Something went wrong: $e'),
                      );
                    },
                  ),
    ...
    

    main.dart

    ....
          home: BlocProvider(
            create: (_) => TrendingMoviesCubit(sl<TmdbPublicUseCases>())..call(),
    ....
    

  2. You declared _runEvent to be async and return void instead of a Future<>.
    That’s a bad pattern in general, because a caller of this function does not know about its asynchronous nature and cannot await the result.

    void _runEvent(Emitter<TmdbState> emit, Future<TMovieListResult> Function() caller) async {}
    

    If you change void to an appropriate Future<>, BloC should be able to await the call and avoid any race conditions

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search