How to represent shared state with freezed without casting
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.
Daniel Allen
Full stack mobile and web application developer with experience in Flutter, Java, Spring, HTML, CSS, JavaScript, and AngularJS.
Updated on December 19, 2022Comments
-
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
andResultsReportRefreshing
states have a shared state which isReportViewViewModel
. 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 about 2 yearsAfter 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.