Reputation: 949
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
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