Note - это простое и интуитивно понятное приложение Flutter для управления вашими повседневными заметками. Легко записывайте свои мысли, идеи, списки дел и важную информацию с помощью этого удобного приложения для ведения заметок. Проект был сдела в качестве первой части домашнего задания.
-
Отображение списка всех заметок: На главном экране приложения вы увидите список всех созданных заметок. Каждая заметка будет содержать описание, приоритет выполнения и дату создания. Вы также сможете отметить выполненные задачи и удалить ненужные заметки.
-
Добавление новой заметки: Чтобы добавить новую заметку, вы можете воспользоваться быстрым созданием (внизу списка) или нажать на FloatingActionButton и перейти к детальному созданию задачи. Здесь вы сможете указать текст заметки, приоритет выполнения и как срок выполнения.
-
Редактирование существующей заметки: Если вы выберете заметку из списка на главном экране, вы будете перенаправлены на экран редактирования заметки с предварительно заполненными данными этой заметки. Вы сможете внести изменения в заголовок, приоритет и дату.
Реализована поддержка тёмной темы, анимация и обрезания текста, как по макету, поддержка лендскейп-ориентации и больших экранов
Для разработки проекта используются следующие зависимости:
environment:
sdk: '>=3.0.3 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# helper library
cupertino_icons: ^1.0.2
equatable: ^2.0.5
logger: ^1.4.0
intl: ^0.18.0
uuid: ^3.0.7
device_info_plus: ^9.0.2
path_provider: ^2.0.15
get_it: ^7.6.0
# remote connection
dio: ^5.2.0
# local storage
shared_preferences: ^2.1.2
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
# state manager
provider: ^6.0.5
bloc: ^8.1.2
flutter_bloc: ^8.1.3
# annotation
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# firebase
firebase_core: ^2.14.0
firebase_crashlytics: ^3.3.3
firebase_analytics: ^10.4.3
firebase_remote_config: ^4.2.4
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.4.6
isar_generator: ^3.1.0+1
test: ^1.24.1
bloc_test: ^9.1.3
freezed: ^2.4.1
json_serializable: ^6.7.1- core
- api // Dio, интерсепторы, apiClient
- di // DI с настройкой сущностей
- firebase // Работа с Firebase
- exception // Исключения прилоложения
- l10n // Локализация приложения
- navigation // Навигация 2.0
- data
- local_datasource // Данные из локального хранилища
- remote_datasource // Данные из сервера
- mock_datasource // Моканые данные
- domain
- entity // Сущности проекта
- repository // Интерфейсы для работы с [data]
- screen // Папка экранов
- note // Экран
- cubit // Модель экрана
- view // Главные элементы экрана
- widget // Дополнительные элементы экрана
- notes
- ...
- utils
- logger // Логирование приложения
- theme // Тема приложения
- app.dart // app файл с мультирепозиторием
- main.dart // main запуск приложения с реальными данными
- main_development.dart// main_development запуск с мокаными данными
- main_production.dart // main_production запуск с мокаными данными
Следуйте этим инструкциям, чтобы установить и запустить проект на вашем локальном компьютере:
- Клонируйте репозиторий с проектом с помощью следующей команды:
git clone https://github.com/latrygin/note.git- Перейдите в каталог проекта:
cd note- Запустите следующую команду, чтобы создать необходимые файлы проекта для работы с firebase:
touch ./android/app/google-services.json
touch ./android/app/src/development/google-services.json
touch ./android/app/src/production/google-services.json-
Заполните эти файлы своими уникальными ключами.
-
Запустите следующую команду, чтобы загрузить зависимости проекта:
flutter pub get- Подключите свое устройство или эмулятор и выполните следующую команду:
flutter run --flavor production --target lib/main_production.dart --dart-define=TOKEN=YOUR_TOKEN --dart-define=PATH=YOUR_PATHПример, модель Task
@collection
class Task {
Id get isarId => fastHash(id);
final String id;
final String text;
@enumerated
final TaskImportant importance;
final DateTime? deadline;
final bool done;
final String? color;
final DateTime? createdAt;
final DateTime? changedAt;
final String? lastUpdatedBy;
Task({
required this.id,
required this.text,
this.importance = TaskImportant.basic,
this.deadline,
this.done = false,
this.color,
this.createdAt,
this.changedAt,
this.lastUpdatedBy,
});
}abstract class TaskLocalDatasource {
Future<List<Task>> getAll();
Future<Task> getAt(String id);
Future<Task> updateAt(Task task);
Future<void> removeAt(String id);
Future<Task> create(Task task);
Future<List<Task>> patch(List<Task> tasks);
}
abstract class TaskRemoteDatasource {
Future<TaskListResponse> getAll();
Future<TaskResponse> get(String id);
Future<TaskResponse> put(Task request);
Future<TaskResponse> delete(String id);
Future<TaskResponse> post(Task request);
Future<TaskListResponse> patch(TaskListRequest request);
}Минимально настроено логирование в файле lib/utils/logger/logger.dart , но использовано во всех необходимых местах
import 'package:logger/logger.dart';
Logger logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
printTime: true,
),
);Логирование для работы с API:
class ApiClient {
Future<Response<Map<String, dynamic>>> get(String path) async {
try {
logger.v('GET: $path');
final response = await _https.get<Map<String, dynamic>>(path);
logger.v(response.data);
return response;
} on DioException catch (e) {
throw e.error as Exception;
}
}
Future<Response<Map<String, dynamic>>> post(
String path, {
Map<String, dynamic>? data,
}) async {
try {
logger
..v('POST: $path')
..v(data);
final response = await _https.post<Map<String, dynamic>>(
path,
data: data,
);
logger.v(response.data);
return response;
} on DioException catch (e) {
throw e.error as Exception;
}
}
...И main.dart
import 'app.dart';
Future<void> main() async {
logger.i('Start main_production');
/// Инициализация Binding
WidgetsFlutterBinding.ensureInitialized();
/// Инициализация DI
await DI.setUpDI(DIOptions.production);
/// Запуск проекта
runApp(const App());
}
Протестированы все сущности от работы с репозиторием, до работы со стейт менеджером внутри экрана, успешную работу тестов можно увидеть в GitHub Actions
В GitHub Actions так же проходит тест интеграционный с проходом по задачи, создать заметки, сохранить, выйти и найти по тексту с мокаными данными
DI релирован с помощью пакета get_it и определяет сущности на старте проекта:
di.registerSingleton<ConfigRepositoryProd>(configRepo);
di.registerSingleton<Nav>(TaskRouterDelegate());
di.registerSingleton<RouteInformationParser<NavigationStateDTO>>(
TaskRouteInformationParser(),
);
di.registerSingleton<PlatformRouteInformationProvider>(
DebugRouteInformationProvider(),
);
di.registerSingleton<RouteObserver>(RouteObserver());
/// Repository
di.registerLazySingleton<TaskLocalDatasource>(
() => TaskLocal(
device: DeviceInfoPlugin(),
),
);
di.registerLazySingleton<TaskRemoteDatasource>(
() => TaskRemote(
https: ApiClient(),
revision: RevisionRemote(),
device: DeviceInfoPlugin(),
),
);
di.registerLazySingleton<RevisionLocalDatasource>(RevisionLocal.new);
di.registerLazySingleton<RevisionRemoteDatasource>(RevisionRemote.new);А используется через GetIt в экранах:
class NotesPage extends StatelessWidget {
const NotesPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => NotesCubit(
taskLocalDatasource: GetIt.I<TaskLocalDatasource>(),
taskRemoteDatasource: GetIt.I<TaskRemoteDatasource>(),
revisionLocalDatasource: GetIt.I<RevisionLocalDatasource>(),
)..initial(),
child: const NotesBody(),
);
}
}В стейт менеджере при нициализации работает try cache, которые ловят все возсожные ошибки:
Future<void> initial(){
try {
...
// Any code
...
} on NotInternetException catch (_) {
logger.i('DO TASK: Нет интернета');
await _revisionLocalDatasource.set(true);
emit(
NotesSuccessState(
tasks: state.tasks,
filter: state.filter,
),
);
} on Exception catch (error, stackTrace) {
/// Передача стейта с ошибкой
logger.e('DONE TASK:', error, stackTrace);
emit(
NotesFailureState(
error: error,
tasks: state.tasks,
filter: state.filter,
),
);
} finally {
emit(
NotesSuccessState(
tasks: state.tasks,
filter: state.filter,
),
);
}
}Вывод ошибки происходит в Widget BlocListener, который запускает AlertDialog:
class NotePage extends StatelessWidget {
final String? id;
const NotePage({
super.key,
this.id,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => NoteCubit(
taskRemoteDatasource: context.read<TaskRemoteDatasource>(),
taskLocalDatasource: context.read<TaskLocalDatasource>(),
revisionLocalDatasource: context.read<RevisionLocalDatasource>(),
)..initial(id),
child: BlocListener<NoteCubit, NoteState>(
listener: (context, state) {
if (state is NoteFailureState) {
late String message;
switch (state.error) {
case BadRequestException():
message = S.of(context).get(SName.badRequest);
case UnauthorizedException():
message = S.of(context).get(SName.unauthorized);
case NotFoundException():
message = S.of(context).get(SName.notFound);
case ServerErrorException():
message = S.of(context).get(SName.serverError);
case NotInternetException():
message = S.of(context).get(SName.notInternet);
case UnknownException():
message = S.of(context).get(SName.unknownException);
default:
message = S.of(context).get(SName.veryUnknownException);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
},
child: const Scaffold(
appBar: NoteHeader(),
body: NoteBody(),
),
),
);
}
}Настроены flavor для проекта:
flavorDimensions "default"
productFlavors {
production {
dimension "default"
resValue "string", "app_name", "Note"
applicationIdSuffix ""
}
development {
dimension "default"
resValue "string", "app_name", "Note dev"
applicationIdSuffix ".dev"
}
}Навигация инкапсулирована в отдельной сущности:
abstract class Nav {
void gotoHome();
void gotoTask(String id);
void gotoCreateTask();
}