Skip to content
This repository was archived by the owner on Dec 12, 2024. It is now read-only.

Commit fee8774

Browse files
authored
feat: add services and todo feature (#16)
1 parent be9196a commit fee8774

33 files changed

+753
-61
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ jobs:
1616
- name: Init Hermit
1717
uses: cashapp/activate-hermit@v1
1818

19+
- name: Cache Flutter
20+
uses: actions/cache@v2
21+
with:
22+
path: ~/.cache/hermit/cache/pkg
23+
key: ${{ runner.os }}-hermit-cache-${{ hashFiles('bin/.*.pkg') }}
24+
restore-keys: |
25+
${{ runner.os }}-hermit-cache-
26+
1927
- name: Install Dependencies
2028
run: make get
2129

Makefile

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ run:
77

88
.PHONY: get
99
get:
10-
flutter pub get
10+
@echo "Getting dependencies for main project"
11+
@flutter pub get
12+
@echo "Getting dependencies for packages"
13+
@for dir in packages/*; do \
14+
if [ -d $$dir ]; then \
15+
echo "Getting dependencies in $$dir"; \
16+
(cd $$dir && flutter pub get || dart pub get); \
17+
fi \
18+
done
19+
1120

1221
.PHONY: clean
1322
clean:
@@ -18,12 +27,31 @@ build:
1827
flutter build apk
1928

2029
.PHONY: test
21-
test:
22-
flutter test
30+
test: test-app test-packages
31+
32+
.PHONY: test-app
33+
test-app:
34+
@echo "Running Flutter tests"
35+
@flutter test
36+
37+
.PHONY: test-packages
38+
test-packages:
39+
@echo "Running Dart tests in packages"
40+
@for dir in packages/*; do \
41+
if [ -d $$dir ]; then \
42+
echo "Running tests in $$dir"; \
43+
(cd $$dir && dart test); \
44+
fi \
45+
done
2346

2447
.PHONY: analyze
2548
analyze:
26-
flutter analyze
49+
@flutter analyze
50+
@for dir in packages/*; do \
51+
if [ -d $$dir ]; then \
52+
(cd $$dir && dart analyze); \
53+
fi \
54+
done
2755

2856
.PHONY: generate
2957
generate:

lib/features/app/app.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter_starter/features/home/home_page.dart';
2+
import 'package:flutter_starter/features/app/app_tabs.dart';
33
import 'package:flutter_starter/l10n/app_localizations.dart';
44
import 'package:flutter_starter/shared/theme/theme.dart';
55

@@ -12,7 +12,7 @@ class App extends StatelessWidget {
1212
title: 'Flutter Starter App',
1313
theme: lightTheme(context),
1414
darkTheme: darkTheme(context),
15-
home: const HomePage(),
15+
home: const AppTabs(),
1616
localizationsDelegates: Loc.localizationsDelegates,
1717
supportedLocales: const [
1818
Locale('en', ''),

lib/features/app/app_tabs.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_hooks/flutter_hooks.dart';
3+
import 'package:flutter_starter/features/counter/counter_page.dart';
4+
import 'package:flutter_starter/features/todos/todos_page.dart';
5+
import 'package:flutter_starter/l10n/app_localizations.dart';
6+
import 'package:hooks_riverpod/hooks_riverpod.dart';
7+
8+
class _TabItem {
9+
final String label;
10+
final Icon icon;
11+
final Widget screen;
12+
13+
_TabItem(this.label, this.icon, this.screen);
14+
}
15+
16+
class AppTabs extends HookConsumerWidget {
17+
const AppTabs({super.key});
18+
19+
@override
20+
Widget build(BuildContext context, WidgetRef ref) {
21+
final selectedIndex = useState(0);
22+
23+
final tabs = [
24+
_TabItem(
25+
Loc.of(context).counter,
26+
const Icon(Icons.numbers),
27+
const CounterPage(),
28+
),
29+
_TabItem(
30+
Loc.of(context).todos,
31+
const Icon(Icons.check),
32+
const TodosPage(),
33+
),
34+
];
35+
36+
return Scaffold(
37+
body: IndexedStack(
38+
index: selectedIndex.value,
39+
children: tabs.map((tab) => tab.screen).toList(),
40+
),
41+
bottomNavigationBar: BottomNavigationBar(
42+
fixedColor: Theme.of(context).colorScheme.primary,
43+
selectedFontSize: 12,
44+
currentIndex: selectedIndex.value,
45+
onTap: (index) => selectedIndex.value = index,
46+
items: tabs
47+
.map(
48+
(tab) => BottomNavigationBarItem(
49+
icon: tab.icon,
50+
label: tab.label,
51+
),
52+
)
53+
.toList(),
54+
),
55+
);
56+
}
57+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_hooks/flutter_hooks.dart';
3+
import 'package:flutter_starter/l10n/app_localizations.dart';
4+
5+
class CounterPage extends HookWidget {
6+
const CounterPage({super.key});
7+
8+
@override
9+
Widget build(BuildContext context) {
10+
final counter = useState(0);
11+
12+
return Scaffold(
13+
appBar: AppBar(title: Text(Loc.of(context).appName)),
14+
body: Center(
15+
child: Column(
16+
mainAxisAlignment: MainAxisAlignment.center,
17+
children: <Widget>[
18+
Text(
19+
Loc.of(context).youHavePushedTheButton,
20+
textAlign: TextAlign.center,
21+
),
22+
Text(
23+
'${counter.value}',
24+
style: Theme.of(context).textTheme.headlineMedium,
25+
),
26+
],
27+
),
28+
),
29+
floatingActionButton: FloatingActionButton(
30+
heroTag: 'counter',
31+
onPressed: () => counter.value++,
32+
tooltip: Loc.of(context).increment,
33+
child: const Icon(Icons.add),
34+
),
35+
);
36+
}
37+
}

lib/features/home/home_page.dart

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_hooks/flutter_hooks.dart';
3+
import 'package:flutter_starter/features/todos/todos_notifier.dart';
4+
import 'package:flutter_starter/l10n/app_localizations.dart';
5+
import 'package:hooks_riverpod/hooks_riverpod.dart';
6+
import 'package:models/models.dart';
7+
8+
class TodoFormPage extends HookConsumerWidget {
9+
final Todo? todo;
10+
final bool isNew;
11+
12+
const TodoFormPage({this.todo, super.key}) : isNew = todo == null;
13+
14+
@override
15+
Widget build(BuildContext context, WidgetRef ref) {
16+
final title = useState(todo?.title);
17+
final description = useState(todo?.description);
18+
19+
return Scaffold(
20+
appBar: AppBar(title: Text(isNew ? 'New Todo' : 'Edit Todo')),
21+
body: Form(
22+
child: Padding(
23+
padding: const EdgeInsets.all(16),
24+
child: Stack(
25+
children: [
26+
SingleChildScrollView(
27+
child: Column(
28+
mainAxisSize: MainAxisSize.max,
29+
children: [
30+
TextFormField(
31+
autofocus: true,
32+
initialValue: title.value,
33+
textCapitalization: TextCapitalization.sentences,
34+
textInputAction: TextInputAction.next,
35+
onChanged: (value) => title.value = value,
36+
decoration: InputDecoration(
37+
labelText: Loc.of(context).title,
38+
),
39+
),
40+
TextFormField(
41+
initialValue: description.value,
42+
textCapitalization: TextCapitalization.sentences,
43+
textInputAction: TextInputAction.done,
44+
onChanged: (value) => description.value = value,
45+
decoration: InputDecoration(
46+
labelText: Loc.of(context).description,
47+
),
48+
maxLines: null,
49+
),
50+
],
51+
),
52+
),
53+
Align(
54+
alignment: Alignment.bottomCenter,
55+
child: SizedBox(
56+
width: double.infinity,
57+
child: FilledButton(
58+
child: Text(Loc.of(context).save),
59+
onPressed: () =>
60+
_save(context, ref, title.value, description.value),
61+
),
62+
),
63+
),
64+
],
65+
),
66+
),
67+
),
68+
);
69+
}
70+
71+
void _save(
72+
BuildContext context, WidgetRef ref, String? title, String? description) {
73+
if (title == null || title.isEmpty) {
74+
ScaffoldMessenger.of(context).showSnackBar(
75+
SnackBar(content: Text(Loc.of(context).titleRequired)),
76+
);
77+
return;
78+
}
79+
80+
if (isNew) {
81+
ref.read(todosProvider.notifier).add(
82+
Todo(
83+
title: title,
84+
description: description,
85+
),
86+
);
87+
} else {
88+
ref.read(todosProvider.notifier).update(
89+
todo!.copyWith(
90+
title: title,
91+
description: description,
92+
),
93+
);
94+
}
95+
96+
Navigator.of(context).pop();
97+
}
98+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:hooks_riverpod/hooks_riverpod.dart';
2+
import 'package:models/models.dart';
3+
import 'package:services/services.dart';
4+
5+
final todosProvider =
6+
NotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
7+
8+
class TodosNotifier extends Notifier<List<Todo>> {
9+
@override
10+
List<Todo> build() {
11+
return service.todos;
12+
}
13+
14+
TodosService get service => ref.read(todosServiceProvider);
15+
List<Todo> get serviceTodos => List.from(service.todos);
16+
17+
void add(Todo todo) {
18+
service.add(todo);
19+
state = serviceTodos;
20+
}
21+
22+
void update(Todo todo) {
23+
service.update(todo);
24+
state = serviceTodos;
25+
}
26+
27+
void remove(Todo todo) {
28+
service.remove(todo);
29+
state = serviceTodos;
30+
}
31+
}

0 commit comments

Comments
 (0)