Danny Allen
Danny Allen

Reputation: 949

How to represent shared state with freezed without casting

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.

Upvotes: 2

Views: 2050

Answers (1)

mkobuolys
mkobuolys

Reputation: 5333

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.

Upvotes: 1

Related Questions