How to represent shared state with freezed without casting

214

I think the problem you are facing could be related to Dart type promotion that does not always work as you could expect. It is thoroughly explained here.

However, how I do handle this with freezed is by using the generated union methods. When rendering the UI, you could use them like this:

class ResultsReport extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ResultsReportBloc, ResultsReportState>(
      builder: (context, state) => state.maybeWhen(
        loading: () => ResultsScreenLoadingSkeleton(),
        success: (report) => SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              var serviceCategory = report.serviceCategories[index];
              return ServiceCategoryBlock(
                viewModel: serviceCategory,
              );
            },
            childCount: report.serviceCategories.length,
          ),
        ),
        refreshing: (report) => SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              var serviceCategory = report.serviceCategories[index];
              return ServiceCategoryBlock(
                viewModel: serviceCategory,
              );
            },
            childCount: report.serviceCategories.length,
          ),
        ),
        error: () => SliverFillRemaining(
          child: ErrorStateContent(
            onErrorRetry: () {
              context
                  .read<ResultsReportBloc>()
                  .add(ResultsReportEvent.retryButtonTapped());
            },
          ),
        ),
      ),
    );
  }
}

Notice that success and refreshing states' code is duplicated, hence you should probably extract it to a separate Widget.

Share:
214
Daniel Allen
Author by

Daniel Allen

Full stack mobile and web application developer with experience in Flutter, Java, Spring, HTML, CSS, JavaScript, and AngularJS.

Updated on December 19, 2022

Comments

  • Daniel Allen
    Daniel Allen over 1 year

    I'm using the freezed package to generate state objects which are consumed by the bloc library.

    I like the ability to define union classes for a widget's state so that I can express the different and often disjoint states that a widget has. For example:

    @freezed
    class ResultsReportState with _$ResultsReportState {
      const factory ResultsReportState.loading() = ResultsReportLoading;
    
      const factory ResultsReportState.success({
        required ReportViewViewModel report,
      }) = ResultsReportSuccess;
    
      const factory ResultsReportState.refreshing({
        required ReportViewViewModel report,
      }) = ResultsReportRefreshing;
    
      const factory ResultsReportState.error() = ResultsReportError;
    }
    

    In the snippet above, my intent is to not show any data when there was an error or the widget is loading, but I do still want to show data if it successfully loads or if the user is refreshing the widget. So the ResultsReportSuccess and ResultsReportRefreshing states have a shared state which is ReportViewViewModel. However, I have no ability to access those shared properties even after performing a type check as suggested here.

    For example, this does not work without an explicit type-cast:

    class ResultsReport extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<ResultsReportBloc, ResultsReportState>(
          builder: (context, state) {
            if (state is ResultsReportSuccess || state is ResultsReportRefreshing) {
              return SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    var serviceCategory = state.report.serviceCategories[index];
                    return ServiceCategoryBlock(
                      viewModel: serviceCategory,
                    );
                  },
                  childCount: state.report.serviceCategories.length,
                ),
              );
            } else if (state is ResultsReportLoading) {
              return ResultsScreenLoadingSkeleton();
            } else {
              return SliverFillRemaining(
                child: ErrorStateContent(
                  onErrorRetry: () {
                    context
                        .read<ResultsReportBloc>()
                        .add(ResultsReportEvent.retryButtonTapped());
                  },
                ),
              );
            }
          },
        );
      }
    }
    

    But there is nothing for me to explicitly type-cast to since it could be either type. So, I tried this approach instead which introduces an interface that I can refer to:

    part of 'results_report_bloc.dart';
    
    abstract class ReportPopulated {
      ReportViewViewModel get report;
    }
    
    @freezed
    class ResultsReportState with _$ResultsReportState {
      const factory ResultsReportState.loading() = ResultsReportLoading;
    
      @Implements<ReportPopulated>()
      const factory ResultsReportState.success({
        required ReportViewViewModel report,
      }) = ResultsReportSuccess;
    
      @Implements<ReportPopulated>()
      const factory ResultsReportState.refreshing({
        required ReportViewViewModel report,
      }) = ResultsReportRefreshing;
    
      const factory ResultsReportState.error() = ResultsReportError;
    }
    
    class ResultsReport extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<ResultsReportBloc, ResultsReportState>(
          builder: (context, state) {
            if (state is ReportPopulated) {
              return SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    var serviceCategory = state.report.serviceCategories[index];
                    return ServiceCategoryBlock(
                      viewModel: serviceCategory,
                    );
                  },
                  childCount: state.report.serviceCategories.length,
                ),
              );
            } else if (state is ResultsReportLoading) {
              return ResultsScreenLoadingSkeleton();
            } else {
              return SliverFillRemaining(
                child: ErrorStateContent(
                  onErrorRetry: () {
                    context
                        .read<ResultsReportBloc>()
                        .add(ResultsReportEvent.retryButtonTapped());
                  },
                ),
              );
            }
          },
        );
      }
    }
    

    But this also requires a type-cast. So, I could do this:

    class ResultsReport extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<ResultsReportBloc, ResultsReportState>(
          builder: (context, state) {
            if (state is ReportPopulated) {
              ReportPopulated currentState = state as ReportPopulated;
              return SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    var serviceCategory = currentState.report.serviceCategories[index];
                    return ServiceCategoryBlock(
                      viewModel: serviceCategory,
                    );
                  },
                  childCount: currentState.report.serviceCategories.length,
                ),
              );
            } else if (state is ResultsReportLoading) {
              return ResultsScreenLoadingSkeleton();
            } else {
              return SliverFillRemaining(
                child: ErrorStateContent(
                  onErrorRetry: () {
                    context
                        .read<ResultsReportBloc>()
                        .add(ResultsReportEvent.retryButtonTapped());
                  },
                ),
              );
            }
          },
        );
      }
    }
    

    But I'm left wondering why the type-cast is necessary, as it just feels cumbersome. Any insight someone can provide on how to accomplish my goal of shared state differently is certainly welcomed.

  • Daniel Allen
    Daniel Allen about 2 years
    After reading the mechanics of type promotion in Dart in your linked documentation, I agree that is the limitation of the language that I'm running into here. Thank you for confirming, and thank you for the suggestion on using freezed's built-in union methods.