{
+ @override
+ Widget buildEntityListCard(BuildContext context, T contact) {
+ var address = Utils.contactToShortAddress(contact);
+ return Container(
+ padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ child: Row(
+ mainAxisSize: MainAxisSize.max,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Flexible(
+ fit: FlexFit.tight,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ FittedBox(
+ fit: BoxFit.fitWidth,
+ alignment: Alignment.centerLeft,
+ child: Text('${contact.getName()}',
+ style: TextStyle(
+ color: Color(0xFF282828),
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ height: 20 / 14))),
+ Text(
+ entityDateFormat.format(
+ DateTime.fromMillisecondsSinceEpoch(
+ contact.createdTime!)),
+ style: TextStyle(
+ color: Color(0xFFAFAFAF),
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ height: 16 / 12))
+ ]),
+ SizedBox(height: 4),
+ if (contact.email != null)
+ Text(contact.email!,
+ style: TextStyle(
+ color: Color(0xFFAFAFAF),
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ height: 16 / 12)),
+ if (contact.email == null) SizedBox(height: 16),
+ if (address != null) SizedBox(height: 4),
+ if (address != null)
+ Text(address,
+ style: TextStyle(
+ color: Color(0xFFAFAFAF),
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ height: 16 / 12)),
+ ],
+ )),
+ SizedBox(width: 16),
+ Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
+ SizedBox(width: 8)
+ ],
+ ),
+ );
+ }
+}
+
+abstract class PageKeyController extends ValueNotifier> {
+ PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey));
+
+ P nextPageKey(P pageKey);
+}
+
+class PageKeyValue {
+ final P pageKey;
+
+ PageKeyValue(this.pageKey);
+}
+
+class PageLinkController extends PageKeyController {
+ PageLinkController({int pageSize = 20, String? searchText})
+ : super(PageLink(
+ pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
+
+ @override
+ PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink();
+
+ onSearchText(String searchText) {
+ value.pageKey.page = 0;
+ value.pageKey.textSearch = searchText;
+ notifyListeners();
+ }
+}
+
+class TimePageLinkController extends PageKeyController {
+ TimePageLinkController({int pageSize = 20, String? searchText})
+ : super(TimePageLink(
+ pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
+
+ @override
+ TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink();
+
+ onSearchText(String searchText) {
+ value.pageKey.page = 0;
+ value.pageKey.textSearch = searchText;
+ notifyListeners();
+ }
+}
+
+abstract class BaseEntitiesWidget extends TbContextWidget
+ with EntitiesBase {
+ final bool searchMode;
+ final PageKeyController pageKeyController;
+
+ BaseEntitiesWidget(TbContext tbContext, this.pageKeyController,
+ {this.searchMode = false})
+ : super(tbContext);
+
+ @override
+ Widget? buildHeading(BuildContext context) => searchMode
+ ? Text('Search results',
+ style: TextStyle(
+ color: Color(0xFFAFAFAF), fontSize: 16, height: 24 / 16))
+ : null;
+}
+
+abstract class BaseEntitiesState
+ extends TbContextState> {
+ late final PagingController pagingController;
+ Completer? _refreshCompleter;
+ bool _dataLoading = false;
+ bool _scheduleRefresh = false;
+ bool _reloadData = false;
+
+ BaseEntitiesState();
+
+ @override
+ void initState() {
+ super.initState();
+ pagingController =
+ PagingController(firstPageKey: widget.pageKeyController.value.pageKey);
+ widget.pageKeyController.addListener(_didChangePageKeyValue);
+ pagingController.addPageRequestListener((pageKey) {
+ _fetchPage(pageKey);
+ });
+ }
+
+ @override
+ void didUpdateWidget(BaseEntitiesWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.pageKeyController != oldWidget.pageKeyController) {
+ oldWidget.pageKeyController.removeListener(_didChangePageKeyValue);
+ widget.pageKeyController.addListener(_didChangePageKeyValue);
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.pageKeyController.removeListener(_didChangePageKeyValue);
+ pagingController.dispose();
+ super.dispose();
+ }
+
+ void _didChangePageKeyValue() {
+ _reloadData = true;
+ _refresh();
+ }
+
+ Future _refresh() {
+ if (_refreshCompleter == null) {
+ _refreshCompleter = Completer();
+ }
+ if (_dataLoading) {
+ _scheduleRefresh = true;
+ } else {
+ _refreshPagingController();
+ }
+ return _refreshCompleter!.future;
+ }
+
+ void _refreshPagingController() {
+ if (_reloadData) {
+ pagingController.refresh();
+ _reloadData = false;
+ } else {
+ _fetchPage(widget.pageKeyController.value.pageKey, refresh: true);
+ }
+ }
+
+ Future _fetchPage(P pageKey, {bool refresh = false}) async {
+ if (mounted) {
+ _dataLoading = true;
+ try {
+ hideNotification();
+ final pageData = await widget.fetchEntities(pageKey);
+ final isLastPage = !pageData.hasNext;
+ if (refresh) {
+ var state = pagingController.value;
+ if (state.itemList != null) {
+ state.itemList!.clear();
+ }
+ }
+ if (isLastPage) {
+ pagingController.appendLastPage(pageData.data);
+ } else {
+ final nextPageKey = widget.pageKeyController.nextPageKey(pageKey);
+ pagingController.appendPage(pageData.data, nextPageKey);
+ }
+ } catch (error) {
+ if (mounted) {
+ pagingController.error = error;
+ }
+ } finally {
+ _dataLoading = false;
+ if (refresh) {
+ _refreshCompleter!.complete();
+ _refreshCompleter = null;
+ }
+ if (_scheduleRefresh) {
+ _scheduleRefresh = false;
+ if (mounted) {
+ _refreshPagingController();
+ }
+ }
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshIndicator(
+ onRefresh: () => Future.wait([widget.onRefresh(), _refresh()]),
+ child: pagedViewBuilder(context));
+ }
+
+ Widget pagedViewBuilder(BuildContext context);
+
+ Widget firstPageProgressIndicatorBuilder(BuildContext context) {
+ return Stack(children: [
+ Positioned(
+ top: 20,
+ left: 0,
+ right: 0,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [RefreshProgressIndicator()],
+ ),
+ )
+ ]);
+ }
+
+ Widget newPageProgressIndicatorBuilder(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(
+ top: 16,
+ bottom: 16,
+ ),
+ child: Center(child: RefreshProgressIndicator()),
+ );
+ }
+
+ Widget noItemsFoundIndicatorBuilder(BuildContext context) {
+ return FirstPageExceptionIndicator(
+ title: widget.noItemsFoundText,
+ message: '${S.of(context).listIsEmptyText}',
+ onTryAgain: widget.searchMode ? null : () => pagingController.refresh(),
+ );
+ }
+}
+
+class FirstPageExceptionIndicator extends StatelessWidget {
+ const FirstPageExceptionIndicator({
+ required this.title,
+ this.message,
+ this.onTryAgain,
+ Key? key,
+ }) : super(key: key);
+
+ final String title;
+ final String? message;
+ final VoidCallback? onTryAgain;
+
+ @override
+ Widget build(BuildContext context) {
+ final message = this.message;
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
+ child: Column(
+ children: [
+ Text(
+ title,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ if (message != null)
+ const SizedBox(
+ height: 16,
+ ),
+ if (message != null)
+ Text(
+ message,
+ textAlign: TextAlign.center,
+ ),
+ if (onTryAgain != null)
+ const SizedBox(
+ height: 48,
+ ),
+ if (onTryAgain != null)
+ SizedBox(
+ height: 50,
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: onTryAgain,
+ icon: const Icon(
+ Icons.refresh,
+ color: Colors.white,
+ ),
+ label: Text(
+ '${S.of(context).tryAgain}',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/entity/entities_grid.dart b/lib/core/entity/entities_grid.dart
new file mode 100644
index 0000000..d0808e9
--- /dev/null
+++ b/lib/core/entity/entities_grid.dart
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
+
+import 'entities_base.dart';
+import 'entity_grid_card.dart';
+
+mixin EntitiesGridStateBase on StatefulWidget {
+ @override
+ _EntitiesGridState createState() => _EntitiesGridState();
+}
+
+class _EntitiesGridState extends BaseEntitiesState {
+ _EntitiesGridState() : super();
+
+ @override
+ Widget pagedViewBuilder(BuildContext context) {
+ final heading = widget.buildHeading(context);
+ final gridChildAspectRatio = widget.gridChildAspectRatio() ?? 156 / 150;
+
+ final slivers = [];
+ if (heading != null) {
+ slivers.add(
+ SliverPadding(
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
+ sliver: SliverToBoxAdapter(child: heading),
+ ),
+ );
+ }
+
+ slivers.add(
+ SliverPadding(
+ padding: EdgeInsets.all(16),
+ sliver: PagedSliverGrid(
+ showNewPageProgressIndicatorAsGridChild: false,
+ showNewPageErrorIndicatorAsGridChild: false,
+ showNoMoreItemsIndicatorAsGridChild: false,
+ pagingController: pagingController,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ childAspectRatio: gridChildAspectRatio,
+ crossAxisSpacing: 16,
+ mainAxisSpacing: 16,
+ crossAxisCount: 2,
+ ),
+ builderDelegate: PagedChildBuilderDelegate(
+ itemBuilder: (context, item, index) => EntityGridCard(
+ item,
+ key: widget.getKey(item),
+ entityCardWidgetBuilder: widget.buildEntityGridCard,
+ onEntityTap: widget.onEntityTap,
+ settings: widget.entityGridCardSettings(item),
+ ),
+ firstPageProgressIndicatorBuilder:
+ firstPageProgressIndicatorBuilder,
+ newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
+ noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder,
+ ),
+ ),
+ ),
+ );
+
+ return CustomScrollView(slivers: slivers);
+ }
+}
diff --git a/lib/core/entity/entities_list.dart b/lib/core/entity/entities_list.dart
new file mode 100644
index 0000000..0590716
--- /dev/null
+++ b/lib/core/entity/entities_list.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
+import 'package:thingsboard_app/core/entity/entities_base.dart';
+
+import 'entity_list_card.dart';
+
+mixin EntitiesListStateBase on StatefulWidget {
+ @override
+ _EntitiesListState createState() => _EntitiesListState();
+}
+
+class _EntitiesListState extends BaseEntitiesState {
+ _EntitiesListState() : super();
+
+ @override
+ Widget pagedViewBuilder(BuildContext context) {
+ var heading = widget.buildHeading(context);
+ List slivers = [];
+ if (heading != null) {
+ slivers.add(SliverPadding(
+ padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
+ sliver: SliverToBoxAdapter(child: heading)));
+ }
+ slivers.add(SliverPadding(
+ padding: EdgeInsets.all(16),
+ sliver: PagedSliverList.separated(
+ pagingController: pagingController,
+ separatorBuilder: (context, index) => SizedBox(height: 8),
+ builderDelegate: PagedChildBuilderDelegate(
+ itemBuilder: (context, item, index) => EntityListCard(
+ item,
+ key: widget.getKey(item),
+ entityCardWidgetBuilder: widget.buildEntityListCard,
+ onEntityTap: widget.onEntityTap,
+ ),
+ firstPageProgressIndicatorBuilder:
+ firstPageProgressIndicatorBuilder,
+ newPageProgressIndicatorBuilder:
+ newPageProgressIndicatorBuilder,
+ noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder))));
+ return CustomScrollView(slivers: slivers);
+ }
+}
diff --git a/lib/core/entity/entities_list_widget.dart b/lib/core/entity/entities_list_widget.dart
new file mode 100644
index 0000000..272f1f3
--- /dev/null
+++ b/lib/core/entity/entities_list_widget.dart
@@ -0,0 +1,218 @@
+import 'dart:async';
+
+import 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
+import 'package:flutter/material.dart';
+import 'package:thingsboard_app/core/context/tb_context.dart';
+import 'package:thingsboard_app/core/context/tb_context_widget.dart';
+import 'package:thingsboard_app/core/entity/entities_base.dart';
+import 'package:thingsboard_client/thingsboard_client.dart';
+
+import 'entity_list_card.dart';
+
+class EntitiesListWidgetController {
+ final List<_EntitiesListWidgetState> states = [];
+
+ void _registerEntitiesWidgetState(
+ _EntitiesListWidgetState entitiesListWidgetState) {
+ states.add(entitiesListWidgetState);
+ }
+
+ void _unregisterEntitiesWidgetState(
+ _EntitiesListWidgetState entitiesListWidgetState) {
+ states.remove(entitiesListWidgetState);
+ }
+
+ Future refresh() {
+ return Future.wait(states.map((state) => state._refresh()));
+ }
+
+ void dispose() {
+ states.clear();
+ }
+}
+
+abstract class EntitiesListPageLinkWidget
+ extends EntitiesListWidget {
+ EntitiesListPageLinkWidget(TbContext tbContext,
+ {EntitiesListWidgetController? controller})
+ : super(tbContext, controller: controller);
+
+ @override
+ PageKeyController createPageKeyController() =>
+ PageLinkController(pageSize: 5);
+}
+
+abstract class EntitiesListWidget extends TbContextWidget
+ with EntitiesBase {
+ final EntitiesListWidgetController? _controller;
+
+ EntitiesListWidget(TbContext tbContext,
+ {EntitiesListWidgetController? controller})
+ : _controller = controller,
+ super(tbContext);
+
+ @override
+ _EntitiesListWidgetState createState() =>
+ _EntitiesListWidgetState(_controller);
+
+ PageKeyController createPageKeyController();
+
+ void onViewAll();
+}
+
+class _EntitiesListWidgetState
+ extends TbContextState> {
+ final EntitiesListWidgetController? _controller;
+
+ late final PageKeyController _pageKeyController;
+
+ final StreamController?> _entitiesStreamController =
+ StreamController.broadcast();
+
+ _EntitiesListWidgetState(EntitiesListWidgetController? controller)
+ : _controller = controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _pageKeyController = widget.createPageKeyController();
+ if (_controller != null) {
+ _controller._registerEntitiesWidgetState(this);
+ }
+ _refresh();
+ }
+
+ @override
+ void dispose() {
+ if (_controller != null) {
+ _controller._unregisterEntitiesWidgetState(this);
+ }
+ _pageKeyController.dispose();
+ _entitiesStreamController.close();
+ super.dispose();
+ }
+
+ Future _refresh() {
+ _entitiesStreamController.add(null);
+ var entitiesFuture = widget.fetchEntities(_pageKeyController.value.pageKey);
+ entitiesFuture.then((value) => _entitiesStreamController.add(value));
+ return entitiesFuture;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 120,
+ margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
+ child: Card(
+ margin: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(6),
+ ),
+ elevation: 0,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ children: [
+ Container(
+ height: 24,
+ margin: const EdgeInsets.only(bottom: 8),
+ child: Row(
+ children: [
+ StreamBuilder?>(
+ stream: _entitiesStreamController.stream,
+ builder: (context, snapshot) {
+ var title = widget.title;
+ if (snapshot.hasData) {
+ var data = snapshot.data;
+ title += ' (${data!.totalElements})';
+ }
+ return Text(title,
+ style: TextStyle(
+ color: Color(0xFF282828),
+ fontSize: 16,
+ fontWeight: FontWeight.normal,
+ height: 1.5));
+ },
+ ),
+ Spacer(),
+ TextButton(
+ onPressed: () {
+ widget.onViewAll();
+ },
+ style: TextButton.styleFrom(
+ padding: EdgeInsets.zero),
+ child: Text('View all'))
+ ],
+ ),
+ ),
+ Container(
+ height: 64,
+ child: StreamBuilder?>(
+ stream: _entitiesStreamController.stream,
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ var data = snapshot.data!;
+ if (data.data.isEmpty) {
+ return _buildNoEntitiesFound(); //return Text('Loaded');
+ } else {
+ return _buildEntitiesView(context, data.data);
+ }
+ } else {
+ return Center(
+ child: RefreshProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(
+ Theme.of(tbContext.currentState!.context)
+ .colorScheme
+ .primary),
+ ));
+ }
+ }),
+ )
+ ],
+ ))),
+ decoration: BoxDecoration(
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withAlpha(25),
+ blurRadius: 10.0,
+ offset: Offset(0, 4)),
+ BoxShadow(
+ color: Colors.black.withAlpha(18),
+ blurRadius: 30.0,
+ offset: Offset(0, 10)),
+ ],
+ ));
+ }
+
+ Widget _buildNoEntitiesFound() {
+ return Container(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: Color(0xFFDEDEDE), style: BorderStyle.solid, width: 1),
+ borderRadius: BorderRadius.circular(4)),
+ child: Center(
+ child: Text(widget.noItemsFoundText,
+ style: TextStyle(
+ color: Color(0xFFAFAFAF),
+ fontSize: 14,
+ )),
+ ),
+ );
+ }
+
+ Widget _buildEntitiesView(BuildContext context, List entities) {
+ return FadingEdgeScrollView.fromScrollView(
+ gradientFractionOnStart: 0.2,
+ gradientFractionOnEnd: 0.2,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ controller: ScrollController(),
+ children: entities
+ .map((entity) => EntityListCard(entity,
+ entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
+ onEntityTap: widget.onEntityTap,
+ listWidgetCard: true))
+ .toList()));
+ }
+}
diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart
new file mode 100644
index 0000000..a871175
--- /dev/null
+++ b/lib/core/entity/entity_details_page.dart
@@ -0,0 +1,207 @@
+import 'package:flutter/material.dart';
+import 'package:thingsboard_app/core/context/tb_context.dart';
+import 'package:thingsboard_app/core/context/tb_context_widget.dart';
+import 'package:thingsboard_app/widgets/tb_app_bar.dart';
+import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
+import 'package:thingsboard_client/thingsboard_client.dart';
+
+abstract class EntityDetailsPage extends TbPageWidget {
+ final labelTextStyle =
+ TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14);
+
+ final valueTextStyle =
+ TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14);
+
+ final String _defaultTitle;
+ final String _entityId;
+ final String? _subTitle;
+ final bool _showLoadingIndicator;
+ final bool _hideAppBar;
+ final double? _appBarElevation;
+
+ EntityDetailsPage(TbContext tbContext,
+ {required String defaultTitle,
+ required String entityId,
+ String? subTitle,
+ bool showLoadingIndicator = true,
+ bool hideAppBar = false,
+ double? appBarElevation})
+ : this._defaultTitle = defaultTitle,
+ this._entityId = entityId,
+ this._subTitle = subTitle,
+ this._showLoadingIndicator = showLoadingIndicator,
+ this._hideAppBar = hideAppBar,
+ this._appBarElevation = appBarElevation,
+ super(tbContext);
+
+ @override
+ _EntityDetailsPageState createState() => _EntityDetailsPageState();
+
+ Future fetchEntity(String id);
+
+ ValueNotifier? detailsTitle() {
+ return null;
+ }
+
+ Widget buildEntityDetails(BuildContext context, T entity);
+}
+
+class _EntityDetailsPageState
+ extends TbPageState> {
+ late Future entityFuture;
+ late ValueNotifier titleValue;
+
+ @override
+ void initState() {
+ super.initState();
+ entityFuture = widget.fetchEntity(widget._entityId);
+ ValueNotifier? detailsTitle = widget.detailsTitle();
+ if (detailsTitle == null) {
+ titleValue = ValueNotifier(widget._defaultTitle);
+ entityFuture.then((value) {
+ if (value is HasName) {
+ titleValue.value = (value as HasName).getName();
+ }
+ });
+ } else {
+ titleValue = detailsTitle;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: widget._hideAppBar
+ ? null
+ : TbAppBar(
+ tbContext,
+ showLoadingIndicator: widget._showLoadingIndicator,
+ elevation: widget._appBarElevation,
+ title: ValueListenableBuilder(
+ valueListenable: titleValue,
+ builder: (context, title, _widget) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ FittedBox(
+ fit: BoxFit.fitWidth,
+ alignment: Alignment.centerLeft,
+ child: Text(title,
+ style: widget._subTitle != null
+ ? Theme.of(context)
+ .primaryTextTheme
+ .titleLarge!
+ .copyWith(fontSize: 16)
+ : null)),
+ if (widget._subTitle != null)
+ Text(widget._subTitle!,
+ style: TextStyle(
+ color: Theme.of(context)
+ .primaryTextTheme
+ .titleLarge!
+ .color!
+ .withAlpha((0.38 * 255).ceil()),
+ fontSize: 12,
+ fontWeight: FontWeight.normal,
+ height: 16 / 12))
+ ]);
+ },
+ ),
+ ),
+ body: FutureBuilder(
+ future: entityFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ var entity = snapshot.data;
+ if (entity != null) {
+ return widget.buildEntityDetails(context, entity);
+ } else {
+ return Center(child: Text('Requested entity does not exists.'));
+ }
+ } else {
+ return Center(
+ child: TbProgressIndicator(
+ size: 50.0,
+ ));
+ }
+ },
+ ),
+ );
+ }
+}
+
+abstract class ContactBasedDetailsPage
+ extends EntityDetailsPage