1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +00:00

Compass app (#2446)

This commit is contained in:
Eric Windmill
2024-09-27 18:49:27 -04:00
committed by GitHub
parent fcf2552cda
commit 46b5a26b26
326 changed files with 53272 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../../core/ui/back_button.dart';
import '../../core/ui/home_button.dart';
class ActivitiesHeader extends StatelessWidget {
const ActivitiesHeader({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
top: true,
bottom: false,
child: Padding(
padding: EdgeInsets.only(
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenHorizontal,
top: Dimens.of(context).paddingScreenVertical,
bottom: Dimens.paddingVertical,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomBackButton(
onTap: () {
// Navigate to ResultsScreen and edit search
context.go(Routes.results);
},
),
Text(
AppLocalization.of(context).activities,
style: Theme.of(context).textTheme.titleLarge,
),
const HomeButton(),
],
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../core/themes/dimens.dart';
import '../view_models/activities_viewmodel.dart';
import 'activity_entry.dart';
import 'activity_time_of_day.dart';
class ActivitiesList extends StatelessWidget {
const ActivitiesList({
super.key,
required this.viewModel,
required this.activityTimeOfDay,
});
final ActivitiesViewModel viewModel;
final ActivityTimeOfDay activityTimeOfDay;
@override
Widget build(BuildContext context) {
final list = switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
ActivityTimeOfDay.evening => viewModel.eveningActivities,
};
return SliverPadding(
padding: EdgeInsets.only(
top: Dimens.paddingVertical,
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenHorizontal,
bottom: Dimens.paddingVertical,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final activity = list[index];
return Padding(
padding:
EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
child: ActivityEntry(
key: ValueKey(activity.ref),
activity: activity,
selected: viewModel.selectedActivities.contains(activity.ref),
onChanged: (value) {
if (value!) {
viewModel.addActivity(activity.ref);
} else {
viewModel.removeActivity(activity.ref);
}
},
),
);
},
childCount: list.length,
),
),
);
}
}

View File

@@ -0,0 +1,188 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../../core/ui/error_indicator.dart';
import '../view_models/activities_viewmodel.dart';
import 'activities_header.dart';
import 'activities_list.dart';
import 'activities_title.dart';
import 'activity_time_of_day.dart';
const String confirmButtonKey = 'confirm-button';
class ActivitiesScreen extends StatefulWidget {
const ActivitiesScreen({
super.key,
required this.viewModel,
});
final ActivitiesViewModel viewModel;
@override
State<ActivitiesScreen> createState() => _ActivitiesScreenState();
}
class _ActivitiesScreenState extends State<ActivitiesScreen> {
@override
void initState() {
super.initState();
widget.viewModel.saveActivities.addListener(_onResult);
}
@override
void didUpdateWidget(covariant ActivitiesScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.saveActivities.removeListener(_onResult);
widget.viewModel.saveActivities.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.saveActivities.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, r) {
if (!didPop) context.go(Routes.results);
},
child: Scaffold(
body: ListenableBuilder(
listenable: widget.viewModel.loadActivities,
builder: (context, child) {
if (widget.viewModel.loadActivities.completed) {
return child!;
}
return Column(
children: [
const ActivitiesHeader(),
if (widget.viewModel.loadActivities.running)
const Expanded(
child: Center(child: CircularProgressIndicator())),
if (widget.viewModel.loadActivities.error)
Expanded(
child: Center(
child: ErrorIndicator(
title: AppLocalization.of(context)
.errorWhileLoadingActivities,
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.loadActivities.execute,
),
),
),
],
);
},
child: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) {
return Column(
children: [
Expanded(
child: CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: ActivitiesHeader(),
),
ActivitiesTitle(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.daytime,
),
ActivitiesList(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.daytime,
),
ActivitiesTitle(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.evening,
),
ActivitiesList(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.evening,
),
],
),
),
_BottomArea(viewModel: widget.viewModel),
],
);
},
),
),
),
);
}
void _onResult() {
if (widget.viewModel.saveActivities.completed) {
widget.viewModel.saveActivities.clearResult();
context.go(Routes.booking);
}
if (widget.viewModel.saveActivities.error) {
widget.viewModel.saveActivities.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileSavingActivities),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.saveActivities.execute,
),
),
);
}
}
}
class _BottomArea extends StatelessWidget {
const _BottomArea({
required this.viewModel,
});
final ActivitiesViewModel viewModel;
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: true,
child: Material(
elevation: 8,
child: Padding(
padding: EdgeInsets.only(
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenVertical,
top: Dimens.paddingVertical,
bottom: Dimens.of(context).paddingScreenVertical,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalization.of(context)
.selected(viewModel.selectedActivities.length),
style: Theme.of(context).textTheme.labelLarge,
),
FilledButton(
key: const Key(confirmButtonKey),
onPressed: viewModel.selectedActivities.isNotEmpty
? viewModel.saveActivities.execute
: null,
child: Text(AppLocalization.of(context).confirm),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../view_models/activities_viewmodel.dart';
import 'activity_time_of_day.dart';
class ActivitiesTitle extends StatelessWidget {
const ActivitiesTitle({
super.key,
required this.activityTimeOfDay,
required this.viewModel,
});
final ActivitiesViewModel viewModel;
final ActivityTimeOfDay activityTimeOfDay;
@override
Widget build(BuildContext context) {
final list = switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
ActivityTimeOfDay.evening => viewModel.eveningActivities,
};
if (list.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox());
}
return SliverToBoxAdapter(
child: Padding(
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
child: Text(_label(context)),
),
);
}
String _label(BuildContext context) => switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => AppLocalization.of(context).daytime,
ActivityTimeOfDay.evening => AppLocalization.of(context).evening,
};
}

View File

@@ -0,0 +1,68 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../domain/models/activity/activity.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/ui/custom_checkbox.dart';
class ActivityEntry extends StatelessWidget {
const ActivityEntry({
super.key,
required this.activity,
required this.selected,
required this.onChanged,
});
final Activity activity;
final bool selected;
final ValueChanged<bool?> onChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 80,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: activity.imageUrl,
height: 80,
width: 80,
errorListener: imageErrorListener,
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
activity.timeOfDay.name.toUpperCase(),
style: Theme.of(context).textTheme.labelSmall,
),
Text(
activity.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(width: 20),
CustomCheckbox(
key: ValueKey('${activity.ref}-checkbox'),
value: selected,
onChanged: onChanged,
)
],
),
);
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
enum ActivityTimeOfDay { daytime, evening }