Search code examples
fluttereventsblocflutter-bloc

How to manage an enum-typed BLoC event?


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?


Solution

  • 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(),
    ....