Skip to content

Commit

Permalink
feat: add services and todo feature (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Dec 21, 2023
1 parent be9196a commit fee8774
Show file tree
Hide file tree
Showing 33 changed files with 753 additions and 61 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ jobs:
- name: Init Hermit
uses: cashapp/activate-hermit@v1

- name: Cache Flutter
uses: actions/cache@v2
with:
path: ~/.cache/hermit/cache/pkg
key: ${{ runner.os }}-hermit-cache-${{ hashFiles('bin/.*.pkg') }}
restore-keys: |
${{ runner.os }}-hermit-cache-
- name: Install Dependencies
run: make get

Expand Down
36 changes: 32 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ run:

.PHONY: get
get:
flutter pub get
@echo "Getting dependencies for main project"
@flutter pub get
@echo "Getting dependencies for packages"
@for dir in packages/*; do \
if [ -d $$dir ]; then \
echo "Getting dependencies in $$dir"; \
(cd $$dir && flutter pub get || dart pub get); \
fi \
done


.PHONY: clean
clean:
Expand All @@ -18,12 +27,31 @@ build:
flutter build apk

.PHONY: test
test:
flutter test
test: test-app test-packages

.PHONY: test-app
test-app:
@echo "Running Flutter tests"
@flutter test

.PHONY: test-packages
test-packages:
@echo "Running Dart tests in packages"
@for dir in packages/*; do \
if [ -d $$dir ]; then \
echo "Running tests in $$dir"; \
(cd $$dir && dart test); \
fi \
done

.PHONY: analyze
analyze:
flutter analyze
@flutter analyze
@for dir in packages/*; do \
if [ -d $$dir ]; then \
(cd $$dir && dart analyze); \
fi \
done

.PHONY: generate
generate:
Expand Down
4 changes: 2 additions & 2 deletions lib/features/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/home/home_page.dart';
import 'package:flutter_starter/features/app/app_tabs.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:flutter_starter/shared/theme/theme.dart';

Expand All @@ -12,7 +12,7 @@ class App extends StatelessWidget {
title: 'Flutter Starter App',
theme: lightTheme(context),
darkTheme: darkTheme(context),
home: const HomePage(),
home: const AppTabs(),
localizationsDelegates: Loc.localizationsDelegates,
supportedLocales: const [
Locale('en', ''),
Expand Down
57 changes: 57 additions & 0 deletions lib/features/app/app_tabs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/features/counter/counter_page.dart';
import 'package:flutter_starter/features/todos/todos_page.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class _TabItem {
final String label;
final Icon icon;
final Widget screen;

_TabItem(this.label, this.icon, this.screen);
}

class AppTabs extends HookConsumerWidget {
const AppTabs({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = useState(0);

final tabs = [
_TabItem(
Loc.of(context).counter,
const Icon(Icons.numbers),
const CounterPage(),
),
_TabItem(
Loc.of(context).todos,
const Icon(Icons.check),
const TodosPage(),
),
];

return Scaffold(
body: IndexedStack(
index: selectedIndex.value,
children: tabs.map((tab) => tab.screen).toList(),
),
bottomNavigationBar: BottomNavigationBar(
fixedColor: Theme.of(context).colorScheme.primary,
selectedFontSize: 12,
currentIndex: selectedIndex.value,
onTap: (index) => selectedIndex.value = index,
items: tabs
.map(
(tab) => BottomNavigationBarItem(
icon: tab.icon,
label: tab.label,
),
)
.toList(),
),
);
}
}
37 changes: 37 additions & 0 deletions lib/features/counter/counter_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';

class CounterPage extends HookWidget {
const CounterPage({super.key});

@override
Widget build(BuildContext context) {
final counter = useState(0);

return Scaffold(
appBar: AppBar(title: Text(Loc.of(context).appName)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
Loc.of(context).youHavePushedTheButton,
textAlign: TextAlign.center,
),
Text(
'${counter.value}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'counter',
onPressed: () => counter.value++,
tooltip: Loc.of(context).increment,
child: const Icon(Icons.add),
),
);
}
}
39 changes: 0 additions & 39 deletions lib/features/home/home_page.dart

This file was deleted.

98 changes: 98 additions & 0 deletions lib/features/todos/todo_form_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/features/todos/todos_notifier.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';

class TodoFormPage extends HookConsumerWidget {
final Todo? todo;
final bool isNew;

const TodoFormPage({this.todo, super.key}) : isNew = todo == null;

@override
Widget build(BuildContext context, WidgetRef ref) {
final title = useState(todo?.title);
final description = useState(todo?.description);

return Scaffold(
appBar: AppBar(title: Text(isNew ? 'New Todo' : 'Edit Todo')),
body: Form(
child: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
children: [
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TextFormField(
autofocus: true,
initialValue: title.value,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.next,
onChanged: (value) => title.value = value,
decoration: InputDecoration(
labelText: Loc.of(context).title,
),
),
TextFormField(
initialValue: description.value,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.done,
onChanged: (value) => description.value = value,
decoration: InputDecoration(
labelText: Loc.of(context).description,
),
maxLines: null,
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
child: FilledButton(
child: Text(Loc.of(context).save),
onPressed: () =>
_save(context, ref, title.value, description.value),
),
),
),
],
),
),
),
);
}

void _save(
BuildContext context, WidgetRef ref, String? title, String? description) {
if (title == null || title.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(Loc.of(context).titleRequired)),
);
return;
}

if (isNew) {
ref.read(todosProvider.notifier).add(
Todo(
title: title,
description: description,
),
);
} else {
ref.read(todosProvider.notifier).update(
todo!.copyWith(
title: title,
description: description,
),
);
}

Navigator.of(context).pop();
}
}
31 changes: 31 additions & 0 deletions lib/features/todos/todos_notifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:services/services.dart';

final todosProvider =
NotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);

class TodosNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() {
return service.todos;
}

TodosService get service => ref.read(todosServiceProvider);
List<Todo> get serviceTodos => List.from(service.todos);

void add(Todo todo) {
service.add(todo);
state = serviceTodos;
}

void update(Todo todo) {
service.update(todo);
state = serviceTodos;
}

void remove(Todo todo) {
service.remove(todo);
state = serviceTodos;
}
}
Loading

0 comments on commit fee8774

Please sign in to comment.