feat(availability-overview): create basic layout with options integration

This commit is contained in:
Joey Boerwinkel 2024-07-04 17:25:08 +02:00
parent 1b6a727305
commit 2465f84886
16 changed files with 586 additions and 481 deletions

View file

@ -9,8 +9,11 @@ class App extends StatelessWidget {
const App({super.key}); const App({super.key});
@override @override
Widget build(BuildContext context) => const MaterialApp( Widget build(BuildContext context) => MaterialApp(
home: Home(), home: AvailabilityUserStory(
userId: "",
options: AvailabilityOptions(),
),
); );
} }
@ -18,7 +21,16 @@ class Home extends StatelessWidget {
const Home({super.key}); const Home({super.key});
@override @override
Widget build(BuildContext context) => availabilityNavigatorUserStory( Widget build(BuildContext context) => Scaffold(
context, body: const Center(
child: Text("Hello World"),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
debugPrint("starting availability user story");
await openAvailabilitiesForUser(context, "anonymous", null);
debugPrint("finishing availability user story");
},
),
); );
} }

View file

@ -3,6 +3,5 @@ library flutter_availability;
export "src/config/availability_options.dart"; export "src/config/availability_options.dart";
export "src/config/availability_translations.dart"; export "src/config/availability_translations.dart";
export "src/screens/availability_overview.dart"; export "src/ui/screens/availability_overview.dart";
export "src/userstory/navigator_userstory.dart"; export "src/userstories.dart";
export "src/userstory/userstory_configuration.dart";

View file

@ -1,12 +1,72 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_availability/src/config/availability_translations.dart"; import "package:flutter_availability/src/config/availability_translations.dart";
import "package:flutter_availability/src/service/local_data_interface.dart";
import "package:flutter_availability/src/ui/widgets/default_base_screen.dart";
import "package:flutter_availability/src/ui/widgets/default_buttons.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
/// Class that holds all options for the availability userstory /// Class that holds all options for the availability userstory
class AvailabilityOptions { class AvailabilityOptions {
/// AvailabilityOptions constructor where everything is optional. /// AvailabilityOptions constructor where everything is optional.
const AvailabilityOptions({ AvailabilityOptions({
this.translations = const AvailabilityTranslations.empty(), this.translations = const AvailabilityTranslations.empty(),
}); this.baseScreenBuilder = DefaultBaseScreen.builder,
this.primaryButtonBuilder = DefaultPrimaryButton.builder,
this.textButtonBuilder = DefaultTextButton.builder,
this.spacing = const AvailabilitySpacing(),
AvailabilityDataInterface? dataInterface,
}) : dataInterface = dataInterface ?? LocalAvailabilityDataInterface();
/// The translations for the availability userstory /// The translations for the availability userstory
final AvailabilityTranslations translations; final AvailabilityTranslations translations;
/// The implementation for communicating with the persistance layer
final AvailabilityDataInterface dataInterface;
/// A method to wrap your availability screens with a base frame.
///
/// If you provide a screen here make sure to use a [Scaffold], as some
/// elements require a [Material] or other elements that a [Scaffold]
/// provides
final BaseScreenBuilder baseScreenBuilder;
/// A way to provide your own primary button implementation
final ButtonBuilder primaryButtonBuilder;
/// A way to provide your own text button implementation
final ButtonBuilder textButtonBuilder;
/// The spacing between elements
final AvailabilitySpacing spacing;
} }
/// All configurable paddings and whitespaces withing the userstory
class AvailabilitySpacing {
/// Creates an AvailabilitySpacing
const AvailabilitySpacing({
this.sidePadding = 32,
this.bottomButtonPadding = 40,
});
/// the padding below the button at the bottom
final double bottomButtonPadding;
/// the padding applied on both sides of the screen
final double sidePadding;
}
/// Builder definition for providing a base screen surrounding each page
typedef BaseScreenBuilder = Widget Function(
BuildContext context,
VoidCallback onBack,
Widget child,
);
/// Builder definition for providing a button implementation
typedef ButtonBuilder = Widget Function(
BuildContext context,
FutureOr<void>? Function() onPressed,
Widget child,
);

View file

@ -9,76 +9,35 @@ class AvailabilityTranslations {
/// You can copyWith the values to override some default translations. /// You can copyWith the values to override some default translations.
/// It is recommended to use this constructor. /// It is recommended to use this constructor.
const AvailabilityTranslations({ const AvailabilityTranslations({
required this.calendarTitle, required this.appbarTitle,
required this.addAvailableDayButtonText, required this.editAvailabilityButton,
required this.availabilityOverviewTitle, required this.templateLegendTitle,
required this.availabilityDate, required this.overviewScreenTitle,
required this.availabilityHours, required this.createTemplateButton,
required this.availabilityEmptyMessage,
required this.availabilitySubmit,
required this.availabilitySave,
}); });
/// AvailabilityTranslations constructor where everything is optional. /// AvailabilityTranslations constructor where everything is optional.
/// This provides default english values for the availability userstory. /// This provides default english values for the availability userstory.
const AvailabilityTranslations.empty({ const AvailabilityTranslations.empty({
this.calendarTitle = "Availability", this.appbarTitle = "Availability",
this.addAvailableDayButtonText = "Add availability", this.editAvailabilityButton = "Edit availability",
this.availabilityOverviewTitle = "Overview of your availabilities", this.templateLegendTitle = "Templates",
this.availabilityDate = "Date", this.createTemplateButton = "Create a new template",
this.availabilityHours = "Hours", this.overviewScreenTitle = "Availability",
this.availabilityEmptyMessage =
"No availabilities filled in for this month",
this.availabilitySubmit = "Submit",
this.availabilitySave = "Save",
}); });
/// The title shown above the calendar /// The title shown above the calendar
final String calendarTitle; final String appbarTitle;
/// The text shown on the button to add an available day /// The text shown on the button to edit availabilities for a range
final String addAvailableDayButtonText; final String editAvailabilityButton;
/// The title for the overview of the availabilities /// The title for the legend template section on the overview screen
final String availabilityOverviewTitle; final String templateLegendTitle;
/// The label for the date of an availability /// The title on the overview screen
final String availabilityDate; final String overviewScreenTitle;
/// The label for the hours of an availability /// The label on the button to go to the tempalte creation page
final String availabilityHours; final String createTemplateButton;
/// The text shown when there are no availabilities filled in for a month
final String availabilityEmptyMessage;
/// The text shown on the button to submit the availabilities
final String availabilitySubmit;
/// The text shown on the button to save a single availability
final String availabilitySave;
/// Method to update the translations with new values
AvailabilityTranslations copyWith({
String? calendarTitle,
String? addAvailableDayButtonText,
String? availabilityOverviewTitle,
String? availabilityDate,
String? availabilityHours,
String? availabilityEmptyMessage,
String? availabilitySubmit,
String? availabilitySave,
}) =>
AvailabilityTranslations(
calendarTitle: calendarTitle ?? this.calendarTitle,
addAvailableDayButtonText:
addAvailableDayButtonText ?? this.addAvailableDayButtonText,
availabilityOverviewTitle:
availabilityOverviewTitle ?? this.availabilityOverviewTitle,
availabilityDate: availabilityDate ?? this.availabilityDate,
availabilityHours: availabilityHours ?? this.availabilityHours,
availabilityEmptyMessage:
availabilityEmptyMessage ?? this.availabilityEmptyMessage,
availabilitySubmit: availabilitySubmit ?? this.availabilitySubmit,
availabilitySave: availabilitySave ?? this.availabilitySave,
);
} }

View file

@ -0,0 +1,27 @@
import "package:flutter/material.dart";
import "package:flutter_availability/flutter_availability.dart";
import "package:flutter_availability/src/ui/screens/template_availability_day_overview.dart";
///
final homePageRoute = MaterialPageRoute(
builder: (context) => AvailabilityOverview(
onBack: () => Navigator.of(context).pop(),
onEditDateRange: (range) {
Navigator.of(context).push(availabilityViewRoute(range.start));
},
onViewTemplates: () {},
),
);
///
MaterialPageRoute availabilityViewRoute(
DateTime date,
) =>
MaterialPageRoute(
builder: (context) => AvailabilityDayOverview(
date: date,
onAvailabilitySaved: () {
Navigator.of(context).pop();
},
),
);

View file

@ -1,174 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/config/availability_options.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
import "package:intl/intl.dart";
///
class AvailabilityOverview extends StatefulWidget {
///
const AvailabilityOverview({
required this.userId,
required this.service,
required this.options,
required this.onDayClicked,
required this.onAvailabilityClicked,
super.key,
});
/// The user whose availability is being managed
final String userId;
/// The service to use for managing availability
final AvailabilityDataInterface service;
/// The configuration option for the availability overview
final AvailabilityOptions options;
/// Callback for when the user clicks on a day
final void Function(DateTime date) onDayClicked;
/// Callback for when the user clicks on an availability
final void Function(AvailabilityModel availability) onAvailabilityClicked;
@override
State<AvailabilityOverview> createState() => _AvailabilityOverviewState();
}
class _AvailabilityOverviewState extends State<AvailabilityOverview> {
Stream<List<AvailabilityModel>>? _availabilityStream;
void _startLoadingAvailabilities(DateTime start, DateTime end) {
setState(() {
_availabilityStream = widget.service.getAvailabilityForUser(
userId: widget.userId,
start: start,
end: end,
);
});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Stack(
children: [
Column(
children: [
Text(
widget.options.translations.calendarTitle,
style: theme.textTheme.displaySmall,
),
Expanded(
child: _availabilityStream == null
? const Center(
child: Text("Press the button to load availabilities."),
)
: StreamBuilder<List<AvailabilityModel>>(
stream: _availabilityStream,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(
child: Text("Error: ${snapshot.error}"),
);
} else if (!snapshot.hasData ||
snapshot.data!.isEmpty) {
return const Center(
child: Text("No availabilities found."),
);
} else {
var sortedAvailabilities = snapshot.data!
..sort(
(a, b) => a.startDate.compareTo(b.startDate),
);
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
var availability = sortedAvailabilities[index];
return ListTile(
title: Text(
"Available from ${DateFormat(
"dd-MM-yyyy HH:mm",
).format(availability.startDate)} "
"\nto \n"
"${DateFormat("dd-MM-yyyy HH:mm").format(
availability.endDate,
)}",
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
if (availability.id == null) {
return;
}
await widget.service
.deleteAvailabilityForUser(
widget.userId,
availability.id!,
);
},
),
onTap: () =>
widget.onAvailabilityClicked(availability),
);
},
);
}
},
),
),
],
),
Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () async {
// ask the user to select a date
var date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate:
DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date == null) {
return;
}
widget.onDayClicked(date);
},
child: Text(
widget.options.translations.addAvailableDayButtonText,
),
),
ElevatedButton(
onPressed: () async {
// ask the user to select a date
var dateRange = await showDateRangePicker(
context: context,
firstDate:
DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (dateRange == null) {
return;
}
_startLoadingAvailabilities(dateRange.start, dateRange.end);
},
child: const Text("Load availabilities for a date range"),
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,16 @@
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
///
class AvailabilityService {
///
const AvailabilityService({
required this.userId,
required this.dataInterface,
});
///
final String userId;
///
final AvailabilityDataInterface dataInterface;
}

View file

@ -0,0 +1,97 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/util/scope.dart";
///
class AvailabilityOverview extends StatelessWidget {
///
const AvailabilityOverview({
required this.onEditDateRange,
required this.onViewTemplates,
required this.onBack,
super.key,
});
/// Callback for when the user clicks on a day
final void Function(DateTimeRange range) onEditDateRange;
/// Callback for when the user wants to navigate to the overview of templates
final VoidCallback onViewTemplates;
/// Callback invoked when a user attempts to go back
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var availabilityScope = AvailabilityScope.of(context);
var options = availabilityScope.options;
var translations = options.translations;
var spacing = options.spacing;
var title = Center(
child: Text(
translations.overviewScreenTitle,
style: theme.textTheme.displaySmall,
),
);
const calendar = SizedBox(
height: 320,
child: Placeholder(),
);
const templateLegend = SizedBox(
height: 40,
child: Placeholder(),
);
var startEditButton = options.primaryButtonBuilder(
context,
() {
onEditDateRange(DateTimeRange(start: DateTime(1), end: DateTime(2)));
},
Text(translations.editAvailabilityButton),
);
var body = CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: spacing.sidePadding),
sliver: SliverList.list(
children: [
const SizedBox(height: 40),
title,
const SizedBox(height: 24),
calendar,
const SizedBox(height: 32),
templateLegend,
const SizedBox(height: 32),
],
),
),
SliverFillRemaining(
fillOverscroll: false,
hasScrollBody: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: spacing.sidePadding,
).copyWith(
bottom: spacing.bottomButtonPadding,
),
child: Align(
alignment: Alignment.bottomCenter,
child: startEditButton,
),
),
),
],
);
return options.baseScreenBuilder(
context,
onBack,
body,
);
}
}

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_availability/src/config/availability_options.dart"; import "package:flutter_availability/src/util/scope.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
@ -7,24 +7,12 @@ import "package:intl/intl.dart";
class AvailabilityDayOverview extends StatefulWidget { class AvailabilityDayOverview extends StatefulWidget {
/// ///
const AvailabilityDayOverview({ const AvailabilityDayOverview({
required this.userId,
required this.service,
required this.options,
required this.date, required this.date,
required this.onAvailabilitySaved, required this.onAvailabilitySaved,
this.initialAvailability, this.initialAvailability,
super.key, super.key,
}); });
/// The user whose availability is being managed
final String userId;
/// The service to use for managing availability
final AvailabilityDataInterface service;
/// The configuration option for the availability overview
final AvailabilityOptions options;
/// The date for which the availability is being managed /// The date for which the availability is being managed
final DateTime date; final DateTime date;
@ -50,7 +38,7 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
super.initState(); super.initState();
_availability = widget.initialAvailability ?? _availability = widget.initialAvailability ??
AvailabilityModel( AvailabilityModel(
userId: widget.userId, userId: "",
startDate: widget.date, startDate: widget.date,
endDate: widget.date, endDate: widget.date,
breaks: [], breaks: [],
@ -86,6 +74,10 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var availabilityScope = AvailabilityScope.of(context);
var userId = availabilityScope.userId;
var service = availabilityScope.service;
Future<void> updateAvailabilityStart() async { Future<void> updateAvailabilityStart() async {
var selectedTime = await _selectTime(_startDateController); var selectedTime = await _selectTime(_startDateController);
if (selectedTime == null) return; if (selectedTime == null) return;
@ -125,15 +117,15 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
if (_clearAvailableToday) { if (_clearAvailableToday) {
// remove the availability for the user // remove the availability for the user
if (_availability.id != null) { if (_availability.id != null) {
await widget.service.deleteAvailabilityForUser( await service.dataInterface.deleteAvailabilityForUser(
widget.userId, userId,
_availability.id!, _availability.id!,
); );
} }
} else { } else {
// add an availability for the user // add an availability for the user
await widget.service.createAvailabilityForUser( await service.dataInterface.createAvailabilityForUser(
widget.userId, userId,
_availability, _availability,
); );
} }
@ -143,7 +135,8 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
} }
var theme = Theme.of(context); var theme = Theme.of(context);
return Padding( return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
@ -274,10 +267,11 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
const Spacer(), const Spacer(),
ElevatedButton( ElevatedButton(
onPressed: () async => onClickSave(), onPressed: () async => onClickSave(),
child: Text(widget.options.translations.availabilitySave), child: const Text(""),
), ),
], ],
), ),
),
); );
} }
} }

View file

@ -0,0 +1,35 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/util/scope.dart";
/// Default base screen for any availability screen
class DefaultBaseScreen extends StatelessWidget {
/// Create a base screen
const DefaultBaseScreen({
required this.child,
super.key,
});
/// Builder as default option
static Widget builder(
BuildContext context,
VoidCallback onBack,
Widget child,
) =>
DefaultBaseScreen(child: child);
/// Content of the page
final Widget child;
@override
Widget build(BuildContext context) {
var translations = AvailabilityScope.of(context).options.translations;
return Scaffold(
appBar: AppBar(
title: Text(translations.appbarTitle),
),
body: SafeArea(
child: child,
),
);
}
}

View file

@ -0,0 +1,65 @@
import "dart:async";
import "package:flutter/material.dart";
///
class DefaultPrimaryButton extends StatelessWidget {
///
const DefaultPrimaryButton({
required this.child,
required this.onPressed,
super.key,
});
///
static Widget builder(
BuildContext context,
FutureOr<void> Function()? onPressed,
Widget child,
) =>
DefaultPrimaryButton(
onPressed: onPressed,
child: child,
);
///
final Widget child;
///
final FutureOr<void> Function()? onPressed;
@override
Widget build(BuildContext context) =>
FilledButton(onPressed: onPressed, child: child);
}
///
class DefaultTextButton extends StatelessWidget {
///
const DefaultTextButton({
required this.child,
required this.onPressed,
super.key,
});
///
static Widget builder(
BuildContext context,
FutureOr<void> Function()? onPressed,
Widget child,
) =>
DefaultTextButton(
onPressed: onPressed,
child: child,
);
///
final Widget child;
///
final FutureOr<void> Function()? onPressed;
@override
Widget build(BuildContext context) =>
TextButton(onPressed: onPressed, child: child);
}

View file

@ -0,0 +1,83 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/config/availability_options.dart";
import "package:flutter_availability/src/routes.dart";
import "package:flutter_availability/src/service/availability_service.dart";
import "package:flutter_availability/src/util/scope.dart";
/// This pushes the availability user story to the navigator stack.
Future<void> openAvailabilitiesForUser(
BuildContext context,
String userId,
AvailabilityOptions? options,
) async {
var navigator = Navigator.of(context);
await navigator.push(
AvailabilityUserStory.route(
userId,
options ?? AvailabilityOptions(),
),
);
}
///
class AvailabilityUserStory extends StatefulWidget {
///
const AvailabilityUserStory({
required this.userId,
required this.options,
super.key,
});
///
final String userId;
///
final AvailabilityOptions options;
///
static MaterialPageRoute route(String userId, AvailabilityOptions options) =>
MaterialPageRoute(
builder: (context) => AvailabilityUserStory(
userId: userId,
options: options,
),
);
@override
State<AvailabilityUserStory> createState() => _AvailabilityUserStoryState();
}
class _AvailabilityUserStoryState extends State<AvailabilityUserStory> {
late AvailabilityService _service = AvailabilityService(
userId: widget.userId,
dataInterface: widget.options.dataInterface,
);
@override
Widget build(BuildContext context) => AvailabilityScope(
userId: widget.userId,
options: widget.options,
service: _service,
child: Navigator(
onGenerateInitialRoutes: (state, route) => [
homePageRoute,
],
),
);
@override
void didUpdateWidget(covariant AvailabilityUserStory oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId ||
oldWidget.options != widget.options) {
setState(() {
_service = AvailabilityService(
userId: widget.userId,
dataInterface: widget.options.dataInterface,
);
});
}
}
}

View file

@ -1,75 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/screens/availability_day_overview.dart";
import "package:flutter_availability/src/screens/availability_overview.dart";
import "package:flutter_availability/src/service/local_service.dart";
import "package:flutter_availability/src/userstory/userstory_configuration.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
///
Widget availabilityNavigatorUserStory(
BuildContext context, {
AvailabiltyUserstoryConfiguration? configuration,
}) =>
_availabiltyScreenRoute(
context,
configuration ??
AvailabiltyUserstoryConfiguration(
service: LocalAvailabilityDataInterface(),
getUserId: (_) => "no-user",
),
);
Widget _availabiltyScreenRoute(
BuildContext context,
AvailabiltyUserstoryConfiguration configuration,
) =>
SafeArea(
child: Scaffold(
body: AvailabilityOverview(
service: configuration.service,
options: configuration.options,
userId: configuration.getUserId(context),
onDayClicked: (date) async => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _avaibiltyDayOverviewRoute(
context,
configuration,
date,
null,
),
),
),
onAvailabilityClicked: (availability) async =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _avaibiltyDayOverviewRoute(
context,
configuration,
availability.startDate,
availability,
),
),
),
),
),
);
Widget _avaibiltyDayOverviewRoute(
BuildContext context,
AvailabiltyUserstoryConfiguration configuration,
DateTime date,
AvailabilityModel? availability,
) =>
SafeArea(
child: Scaffold(
appBar: AppBar(),
body: AvailabilityDayOverview(
service: configuration.service,
options: configuration.options,
userId: configuration.getUserId(context),
date: date,
initialAvailability: availability,
onAvailabilitySaved: () => Navigator.of(context).pop(),
),
),
);

View file

@ -1,22 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/config/availability_options.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
///
class AvailabiltyUserstoryConfiguration {
///
const AvailabiltyUserstoryConfiguration({
required this.service,
required this.getUserId,
this.options = const AvailabilityOptions(),
});
///
final AvailabilityOptions options;
///
final AvailabilityDataInterface service;
///
final Function(BuildContext context) getUserId;
}

View file

@ -0,0 +1,29 @@
import "package:flutter/widgets.dart";
import "package:flutter_availability/src/config/availability_options.dart";
import "package:flutter_availability/src/service/availability_service.dart";
///
class AvailabilityScope extends InheritedWidget {
///
const AvailabilityScope({
required this.userId,
required this.options,
required this.service,
required super.child,
super.key,
});
///
final String userId;
///
final AvailabilityOptions options;
///
final AvailabilityService service;
@override
bool updateShouldNotify(AvailabilityScope oldWidget) =>
oldWidget.userId != userId || options != options;
///
static AvailabilityScope of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<AvailabilityScope>()!;
}