├── gen └── todo_api │ ├── analysis_options.yaml │ ├── .openapi-generator │ ├── VERSION │ └── FILES │ ├── .travis.yml │ ├── .gitignore │ ├── pubspec.yaml │ ├── doc │ ├── Priority.md │ ├── PriorityEnum.md │ ├── TodoParams.md │ ├── TodoResponse.md │ └── TodoApi.md │ ├── test │ ├── priority_test.dart │ ├── priority_enum_test.dart │ ├── todo_params_test.dart │ ├── todo_response_test.dart │ └── todo_api_test.dart │ ├── lib │ ├── auth │ │ ├── authentication.dart │ │ ├── oauth.dart │ │ ├── http_basic_auth.dart │ │ ├── api_key_auth.dart │ │ └── http_bearer_auth.dart │ ├── api_exception.dart │ ├── api.dart │ ├── model │ │ ├── priority_enum.dart │ │ ├── todo_params.dart │ │ └── todo_response.dart │ └── api_helper.dart │ ├── .openapi-generator-ignore │ ├── git_push.sh │ └── README.md ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── Flutter.podspec │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift ├── .gitignore ├── Podfile.lock └── Podfile ├── .fvm └── fvm_config.json ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_todo │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── images └── 2.0x │ ├── checkbox_checked.png │ └── checkbox_unchecked.png ├── lib ├── Repository │ ├── Abstraction │ │ ├── NetworkIssue.dart │ │ ├── TodoValues.dart │ │ ├── Result.dart │ │ ├── TodoManager.dart │ │ ├── LongResult.dart │ │ └── EntityGateway.dart │ ├── Entities │ │ ├── Entity.dart │ │ ├── Todo.dart │ │ └── Priority.dart │ ├── Ephemeral │ │ ├── EphemeralEntityGateway.dart │ │ ├── EphemeralData.dart │ │ ├── EphemeralTodo.dart │ │ └── EphemeralTodoManager.dart │ ├── Db │ │ ├── SqlLiteEntityGateway.dart │ │ ├── SqlLiteManager.dart │ │ └── SqlLiteTodoManager.dart │ └── Network │ │ ├── NetworkClient.dart │ │ ├── NetworkEntityGateway.dart │ │ ├── RealNetworkClient.dart │ │ ├── NetworkExceptionGuard.dart │ │ └── NetworkTodoManager.dart ├── Scenes │ ├── AppState │ │ ├── TodoAppState.dart │ │ ├── TodoItemStartMode.dart │ │ └── AppState.dart │ ├── Common │ │ ├── Bloc.dart │ │ ├── ActionDecoratedScene.dart │ │ ├── ErrorMessages.dart │ │ ├── Localize.dart │ │ ├── FullScreenLoadingIndicator.dart │ │ ├── StarterBloc.dart │ │ ├── ErrorScene.dart │ │ ├── BaseBlocBuilder.dart │ │ ├── BlocBuilder.dart │ │ ├── Waiting.dart │ │ ├── BlocProvider.dart │ │ ├── BlocConsumer.dart │ │ ├── TodoOkDialog.dart │ │ ├── TodoSwitch.dart │ │ ├── BaseBlocConsumer.dart │ │ ├── TodoTextField.dart │ │ ├── TodoExclusive.dart │ │ ├── CupertinoPopoverDatePicker.dart │ │ └── Localization │ │ │ └── TodoLocalizationsDelegate.dart │ ├── TodoItem │ │ ├── TodoItemDisplay │ │ │ ├── Router │ │ │ │ └── Router.dart │ │ │ ├── Presenter │ │ │ │ ├── ViewModel.dart │ │ │ │ ├── PresenterOutput.dart │ │ │ │ └── Presenter.dart │ │ │ ├── UseCase │ │ │ │ ├── UseCaseOutput.dart │ │ │ │ ├── PresentationModel.dart │ │ │ │ └── UseCase.dart │ │ │ ├── Assembly │ │ │ │ └── Assembly.dart │ │ │ ├── TodoItemDisplay.dart │ │ │ └── View │ │ │ │ └── Scene.dart │ │ ├── TodoItemRouter │ │ │ ├── Router │ │ │ │ └── Router.dart │ │ │ ├── UseCase │ │ │ │ ├── UseCaseOutput.dart │ │ │ │ └── UseCase.dart │ │ │ ├── Presenter │ │ │ │ ├── PresenterOutput.dart │ │ │ │ └── Presenter.dart │ │ │ ├── Assembly │ │ │ │ └── Assembly.dart │ │ │ ├── TodoItemRouter.dart │ │ │ └── View │ │ │ │ └── Scene.dart │ │ └── TodoItemEdit │ │ │ ├── Router │ │ │ └── Router.dart │ │ │ ├── Presenter │ │ │ ├── PresenterOutput.dart │ │ │ ├── ViewModel.dart │ │ │ └── Presenter.dart │ │ │ ├── Assembly │ │ │ └── Assembly.dart │ │ │ ├── UseCase │ │ │ ├── UseCaseOutput.dart │ │ │ ├── PresentationModel.dart │ │ │ └── UseCase.dart │ │ │ └── TodoItemEdit.dart │ ├── TodoRootRouter │ │ ├── Presenter │ │ │ ├── PresenterOutput.dart │ │ │ └── Presenter.dart │ │ ├── Assembly │ │ │ └── Assembly.dart │ │ ├── TodoRootRouter.dart │ │ └── View │ │ │ └── Scene.dart │ └── TodoList │ │ ├── Router │ │ └── Router.dart │ │ ├── Presenter │ │ ├── PresenterOutput.dart │ │ ├── ViewModel.dart │ │ └── Presenter.dart │ │ ├── UseCase │ │ ├── UseCaseOutput.dart │ │ ├── PresentationModel.dart │ │ └── UseCase.dart │ │ ├── Assembly │ │ └── Assembly.dart │ │ ├── TodoList.dart │ │ └── View │ │ ├── CheckBox.dart │ │ ├── Cell.dart │ │ └── Scene.dart └── main.dart ├── api_generator ├── todo_api.sh └── todo_api.yaml ├── analysis_options.yaml ├── .metadata ├── .gitignore ├── README.md └── pubspec.yaml /gen/todo_api/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gen/todo_api/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.0.0 -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.10.4", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /images/2.0x/checkbox_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/images/2.0x/checkbox_checked.png -------------------------------------------------------------------------------- /images/2.0x/checkbox_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/images/2.0x/checkbox_unchecked.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/NetworkIssue.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | enum NetworkIssue { noNetwork } -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /lib/Scenes/AppState/TodoAppState.dart: -------------------------------------------------------------------------------- 1 | import 'AppState.dart'; 2 | 3 | class TodoAppState extends AppState { 4 | TodoAppState._(); 5 | static final instance = TodoAppState._(); 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyleresnick/FlutterCleanTodo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_todo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_todo 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/Scenes/Common/Bloc.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Lyle Resnick. All rights reserved. 2 | 3 | abstract interface class Bloc { 4 | 5 | void emit(Output value); 6 | Stream get stream; 7 | void dispose(); 8 | } -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/Router/Router.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | abstract interface class Router { 6 | void routeEditView(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/Router/Router.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | abstract interface class Router { 6 | void routeCreateItemCancelled(); 7 | } 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /api_generator/todo_api.sh: -------------------------------------------------------------------------------- 1 | openapi-generator generate -i todo_api.yaml -g dart -o ../gen/todo_api --additional-properties=pubName=todo_api 2 | cd ../gen/todo_api/lib 3 | sed "s/'yyyy-MM-dd'/\"yyyy-MM-dd'T'HH:mm:ss'Z'\"/g" api.dart >api.dart.out 4 | mv api.dart.out api.dart -------------------------------------------------------------------------------- /lib/Scenes/TodoRootRouter/Presenter/PresenterOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoRootRouter.dart'; 4 | 5 | @visibleForTesting 6 | enum PresenterOutput { 7 | showRowDetail, 8 | showPop 9 | } 10 | 11 | -------------------------------------------------------------------------------- /gen/todo_api/.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # AUTO-GENERATED FILE, DO NOT MODIFY! 3 | # 4 | # https://docs.travis-ci.com/user/languages/dart/ 5 | # 6 | language: dart 7 | dart: 8 | # Install a specific stable release 9 | - "2.12" 10 | install: 11 | - pub get 12 | 13 | script: 14 | - pub run test 15 | -------------------------------------------------------------------------------- /gen/todo_api/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://dart.dev/guides/libraries/private-files 2 | 3 | .dart_tool/ 4 | .packages 5 | build/ 6 | pubspec.lock # Except for application packages 7 | 8 | doc/api/ 9 | 10 | # IntelliJ 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea/ 15 | 16 | # Mac 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/Router/Router.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | typedef void ChangedItemCallback(PresentationRowModel model); 6 | 7 | abstract interface class Router { 8 | void routeShowItemDetail(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/Scenes/Common/ActionDecoratedScene.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | import 'package:flutter/material.dart'; 3 | 4 | abstract interface class ActionDecoratedScene { 5 | Widget get title; 6 | Widget? get leading; 7 | List? get actions; 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/Router/Router.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | abstract interface class Router { 6 | void routeEditingCancelled(); 7 | void routeSaveCompleted(); 8 | void routeCreateCancelled(); 9 | } 10 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/Scenes/Common/ErrorMessages.dart: -------------------------------------------------------------------------------- 1 | enum ErrorMessage { 2 | titleIsEmpty( 3 | titleToken: "titleRequiredTitle", messageToken: "titleRequiredMessage"); 4 | 5 | final String titleToken; 6 | final String messageToken; 7 | 8 | const ErrorMessage({required this.titleToken, required this.messageToken}); 9 | } 10 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/Presenter/ViewModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | @visibleForTesting 6 | class RowViewModel { 7 | 8 | final String fieldName; 9 | final String value; 10 | 11 | RowViewModel(this.fieldName, this.value); 12 | } 13 | -------------------------------------------------------------------------------- /lib/Scenes/Common/Localize.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'Localization/TodoLocalizationsDelegate.dart'; 4 | 5 | String localizedString(String string) => TodoLocalizationsDelegate.localize(string); 6 | String localizedDate(DateTime date) => TodoLocalizationsDelegate.outboundDateFormatter.format(date); 7 | 8 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/Presenter/PresenterOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | sealed class _PresenterOutput {} 6 | 7 | @visibleForTesting 8 | class showModel extends _PresenterOutput { 9 | final ViewModel model; 10 | showModel(this.model); 11 | } 12 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/Scenes/AppState/TodoItemStartMode.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | sealed class TodoItemStartMode {} 4 | 5 | class TodoItemStartModeCreate extends TodoItemStartMode {} 6 | 7 | class TodoItemStartModeUpdate extends TodoItemStartMode { 8 | String itemId; 9 | TodoItemStartModeUpdate(this.itemId); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/Presenter/PresenterOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | sealed class _PresenterOutput {} 6 | 7 | @visibleForTesting 8 | class showFieldList extends _PresenterOutput { 9 | final List model; 10 | showFieldList(this.model); 11 | } 12 | -------------------------------------------------------------------------------- /lib/Repository/Entities/Entity.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | void jsonNullCheck(Map json, List propertyNameList, 4 | String entityName) { 5 | propertyNameList.forEach((name) { 6 | if (json[name] == null) 7 | throw FormatException("missing json property: '$name' in: $entityName"); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /lib/Repository/Ephemeral/EphemeralEntityGateway.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Abstraction/EntityGateway.dart'; 4 | import '../Abstraction/TodoManager.dart'; 5 | import 'EphemeralTodoManager.dart'; 6 | 7 | class EphemeralEntityGateway extends EntityGateway { 8 | TodoManager get todoManager => EphemeralTodoManager(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/UseCase/UseCaseOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | sealed class _UseCaseOutput {} 6 | 7 | @visibleForTesting 8 | class presentModel extends _UseCaseOutput { 9 | final List<_RowPresentationModel> modelList; 10 | presentModel(this.modelList); 11 | } 12 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: 3 | - "**/*.g.dart" 4 | - "**/*.freezed.dart" 5 | errors: 6 | invalid_use_of_visible_for_testing_member: error 7 | 8 | 9 | linter: 10 | rules: 11 | always_declare_return_types: true 12 | unawaited_futures: true 13 | avoid_void_async: true 14 | await_only_futures: true 15 | exhaustive_cases: true 16 | 17 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/Scenes/TodoList/Presenter/PresenterOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | sealed class _PresenterOutput {} 6 | 7 | @visibleForTesting 8 | class showLoading extends _PresenterOutput {} 9 | 10 | @visibleForTesting 11 | class showModel extends _PresenterOutput { 12 | final ViewModel model; 13 | showModel(this.model); 14 | } 15 | -------------------------------------------------------------------------------- /gen/todo_api/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # AUTO-GENERATED FILE, DO NOT MODIFY! 3 | # 4 | 5 | name: 'todo_api' 6 | version: '1.0.0' 7 | description: 'OpenAPI API client' 8 | homepage: 'homepage' 9 | environment: 10 | sdk: '>=2.12.0 <3.0.0' 11 | dependencies: 12 | collection: '^1.17.0' 13 | http: '>=0.13.0 <0.14.0' 14 | intl: '^0.18.0' 15 | meta: '^1.1.8' 16 | dev_dependencies: 17 | test: '>=1.16.0 <1.18.0' 18 | -------------------------------------------------------------------------------- /lib/Scenes/AppState/AppState.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_todo/Repository/Entities/Todo.dart'; 2 | import 'package:rxdart/rxdart.dart'; 3 | import 'TodoItemStartMode.dart'; 4 | 5 | abstract class AppState { 6 | final toDoSceneRefreshSubject = PublishSubject(); 7 | final itemStartModeSubject = BehaviorSubject(); 8 | final currentTodoSubject = BehaviorSubject(); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /lib/Repository/Db/SqlLiteEntityGateway.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Abstraction/EntityGateway.dart'; 4 | import '../Abstraction/TodoManager.dart'; 5 | import '../Db/SqlLiteTodoManager.dart'; 6 | import '../Db/SqlLiteManager.dart'; 7 | 8 | class SqlLiteEntityGateway extends EntityGateway { 9 | TodoManager get todoManager => SqlLiteTodoManager(SqlLiteManager()); 10 | } 11 | -------------------------------------------------------------------------------- /lib/Repository/Network/NetworkClient.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:todo_api/api.dart'; 3 | 4 | abstract interface class NetworkClient { 5 | Future?> getAllTodos(); 6 | Future getTodo(String id); 7 | Future createTodo(TodoParams params); 8 | Future updateTodo(String id, TodoParams params); 9 | Future deleteTodo(String id); 10 | 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/Scenes/TodoRootRouter/Assembly/Assembly.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoRootRouter.dart'; 4 | 5 | class Assembly { 6 | 7 | final Scene scene; 8 | Assembly._(this.scene); 9 | 10 | factory Assembly() { 11 | 12 | final presenter = Presenter(); 13 | final scene = Scene(presenter); 14 | 15 | return Assembly._(scene); 16 | } 17 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/Repository/Network/NetworkEntityGateway.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import '../Abstraction/EntityGateway.dart'; 4 | import '../Abstraction/TodoManager.dart'; 5 | 6 | import 'RealNetworkClient.dart'; 7 | import 'NetworkTodoManager.dart'; 8 | 9 | class NetworkEntityGateway extends EntityGateway { 10 | TodoManager get todoManager => NetworkTodoManager(RealNetworkClient()); 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gen/todo_api/doc/Priority.md: -------------------------------------------------------------------------------- 1 | # todo_api.model.Priority 2 | 3 | ## Load the model package 4 | ```dart 5 | import 'package:todo_api/api.dart'; 6 | ``` 7 | 8 | ## Properties 9 | Name | Type | Description | Notes 10 | ------------ | ------------- | ------------- | ------------- 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/Scenes/Common/FullScreenLoadingIndicator.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class FullScreenLoadingIndicator extends StatelessWidget { 6 | const FullScreenLoadingIndicator({ 7 | super.key, 8 | }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Center(child: CircularProgressIndicator(color: Colors.lightGreen,)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gen/todo_api/doc/PriorityEnum.md: -------------------------------------------------------------------------------- 1 | # todo_api.model.PriorityEnum 2 | 3 | ## Load the model package 4 | ```dart 5 | import 'package:todo_api/api.dart'; 6 | ``` 7 | 8 | ## Properties 9 | Name | Type | Description | Notes 10 | ------------ | ------------- | ------------- | ------------- 11 | 12 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 13 | 14 | 15 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/UseCase/UseCaseOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | sealed class _UseCaseOutput {} 6 | 7 | @visibleForTesting 8 | class presentLoading extends _UseCaseOutput {} 9 | 10 | @visibleForTesting 11 | class presentModel extends _UseCaseOutput { 12 | final PresentationModel model; 13 | presentModel(this.model); 14 | } 15 | 16 | @visibleForTesting 17 | class presentItemDetail extends _UseCaseOutput {} 18 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/TodoValues.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Entities/Priority.dart'; 4 | 5 | class TodoValues { 6 | final String title; 7 | final String note; 8 | final DateTime? completeBy; 9 | final Priority priority; 10 | final bool completed; 11 | 12 | TodoValues( 13 | {required this.title, 14 | required this.note, 15 | required this.completeBy, 16 | required this.priority, 17 | required this.completed}); 18 | } -------------------------------------------------------------------------------- /lib/Scenes/TodoRootRouter/TodoRootRouter.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../Common/ErrorScene.dart'; 6 | import '../Common/StarterBloc.dart'; 7 | import '../TodoItem/TodoItemRouter/TodoItemRouter.dart' as TodoItemRouter; 8 | import '../TodoList/TodoList.dart' as TodoList; 9 | 10 | part 'Assembly/Assembly.dart'; 11 | part 'View/Scene.dart'; 12 | part 'Presenter/Presenter.dart'; 13 | part 'Presenter/PresenterOutput.dart'; 14 | -------------------------------------------------------------------------------- /lib/Scenes/Common/StarterBloc.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:async'; 4 | 5 | import 'Bloc.dart'; 6 | 7 | mixin StarterBloc implements Bloc { 8 | final _controller = StreamController(); 9 | 10 | @override 11 | Stream get stream => _controller.stream; 12 | 13 | @override 14 | void emit(Output value) => _controller.sink.add(value); 15 | 16 | @override 17 | void dispose() { 18 | _controller.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /gen/todo_api/test/priority_test.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | import 'package:todo_api/api.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | // tests for Priority 15 | void main() { 16 | 17 | group('test Priority', () { 18 | 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/Result.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'NetworkIssue.dart'; 4 | 5 | sealed class Result {} 6 | 7 | class success extends Result { 8 | final Entity data; 9 | success(this.data); 10 | } 11 | 12 | class failure extends Result { 13 | final String description; 14 | failure(this.description); 15 | } 16 | 17 | class networkIssue extends Result { 18 | final NetworkIssue issue; 19 | networkIssue(this.issue); 20 | } 21 | -------------------------------------------------------------------------------- /gen/todo_api/test/priority_enum_test.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | import 'package:todo_api/api.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | // tests for PriorityEnum 15 | void main() { 16 | 17 | group('test PriorityEnum', () { 18 | 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/Assembly/Assembly.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | class Assembly { 6 | 7 | final Scene scene; 8 | 9 | Assembly._(this.scene); 10 | 11 | factory Assembly(Router router) { 12 | final useCase = UseCase(TodoAppState.instance.currentTodoSubject.value!); 13 | final presenter = Presenter(useCase, router); 14 | final scene = Scene(presenter); 15 | 16 | return Assembly._(scene); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/UseCase/UseCaseOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | sealed class _UseCaseOutput {} 6 | 7 | @visibleForTesting 8 | class presentEditView extends _UseCaseOutput {} 9 | 10 | @visibleForTesting 11 | class presentLoading extends _UseCaseOutput {} 12 | 13 | @visibleForTesting 14 | class presentDisplayView extends _UseCaseOutput {} 15 | 16 | @visibleForTesting 17 | class presentNotFound extends _UseCaseOutput { 18 | final String id; 19 | presentNotFound(this.id); 20 | } 21 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/TodoManager.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Entities/Todo.dart'; 4 | 5 | import 'Result.dart'; 6 | import 'TodoValues.dart'; 7 | 8 | abstract class TodoManager { 9 | 10 | Future>> all(); 11 | Future> completed(String id, bool completed); 12 | Future> create(TodoValues values); 13 | Future> update(String id, TodoValues values); 14 | Future> fetch(String id); 15 | Future> delete(String id); 16 | } 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/Assembly/Assembly.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | class Assembly { 6 | final Scene scene; 7 | Assembly._(this.scene); 8 | 9 | factory Assembly(Router router) { 10 | final useCase = UseCase( 11 | EntityGateway.entityGateway, 12 | TodoAppState.instance.toDoSceneRefreshSubject, 13 | TodoAppState.instance.itemStartModeSubject); 14 | final presenter = Presenter(useCase, router); 15 | final scene = Scene(presenter); 16 | 17 | return Assembly._(scene); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gen/todo_api/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | README.md 4 | analysis_options.yaml 5 | doc/PriorityEnum.md 6 | doc/TodoApi.md 7 | doc/TodoParams.md 8 | doc/TodoResponse.md 9 | git_push.sh 10 | lib/api.dart 11 | lib/api/todo_api.dart 12 | lib/api_client.dart 13 | lib/api_exception.dart 14 | lib/api_helper.dart 15 | lib/auth/api_key_auth.dart 16 | lib/auth/authentication.dart 17 | lib/auth/http_basic_auth.dart 18 | lib/auth/http_bearer_auth.dart 19 | lib/auth/oauth.dart 20 | lib/model/priority_enum.dart 21 | lib/model/todo_params.dart 22 | lib/model/todo_response.dart 23 | pubspec.yaml 24 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/Assembly/Assembly.dart: -------------------------------------------------------------------------------- 1 | part of '../TodoItemEdit.dart'; 2 | 3 | class Assembly { 4 | final Scene scene; 5 | Assembly._(this.scene); 6 | 7 | factory Assembly(Router router) { 8 | final useCase = UseCase( 9 | EntityGateway.entityGateway, 10 | TodoAppState.instance.toDoSceneRefreshSubject, 11 | TodoAppState.instance.currentTodoSubject, 12 | TodoAppState.instance.itemStartModeSubject); 13 | final presenter = Presenter(useCase, router); 14 | final scene = Scene(presenter); 15 | 16 | return Assembly._(scene); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/UseCase/UseCaseOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | sealed class _UseCaseOutput {} 6 | 7 | @visibleForTesting 8 | class presentModel extends _UseCaseOutput { 9 | final PresentationModel model; 10 | presentModel(this.model); 11 | } 12 | 13 | @visibleForTesting 14 | class presentSaveCompleted extends _UseCaseOutput {} 15 | @visibleForTesting 16 | class presentEditingCancelled extends _UseCaseOutput {} 17 | @visibleForTesting 18 | class presentCreateCancelled extends _UseCaseOutput {} 19 | 20 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/Presenter/PresenterOutput.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | sealed class _PresenterOutput {} 6 | 7 | @visibleForTesting 8 | class showEditView extends _PresenterOutput {} 9 | 10 | @visibleForTesting 11 | class showLoading extends _PresenterOutput {} 12 | 13 | @visibleForTesting 14 | class showDisplayView extends _PresenterOutput {} 15 | 16 | @visibleForTesting 17 | class showMessageView extends _PresenterOutput { 18 | final String message; 19 | showMessageView(this.message); 20 | } 21 | -------------------------------------------------------------------------------- /gen/todo_api/lib/auth/authentication.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | // ignore: one_member_abstracts 14 | abstract class Authentication { 15 | /// Apply authentication settings to header and query params. 16 | Future applyToParams(List queryParams, Map headerParams); 17 | } 18 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/Assembly/Assembly.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | class Assembly { 6 | final Scene scene; 7 | Assembly._(this.scene); 8 | 9 | factory Assembly(Router router) { 10 | final useCase = UseCase( 11 | EntityGateway.entityGateway, 12 | TodoAppState.instance.itemStartModeSubject.value, 13 | TodoAppState.instance.currentTodoSubject); 14 | final presenter = Presenter(useCase, router); 15 | final scene = Scene(presenter); 16 | 17 | return Assembly._(scene); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Scenes/Common/ErrorScene.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorScene extends StatelessWidget { 4 | final String text; 5 | const ErrorScene({ 6 | required this.text, 7 | }); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text('Error'), 14 | ), 15 | body: Container( 16 | height: double.infinity, 17 | width: double.infinity, 18 | color: Colors.red, 19 | child: Text(text, style: TextStyle(color: Colors.white, fontSize: 28)))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/Scenes/TodoRootRouter/Presenter/Presenter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoRootRouter.dart'; 4 | 5 | @visibleForTesting 6 | class Presenter with StarterBloc 7 | implements TodoItemRouter.Router, TodoList.Router { 8 | 9 | // TodoItemRouterRouter 10 | 11 | void routeCreateItemCancelled() { 12 | emit(PresenterOutput.showPop); 13 | } 14 | 15 | // TodoListRouter 16 | 17 | @override 18 | void routeShowItemDetail() { 19 | emit(PresenterOutput.showRowDetail); 20 | } 21 | 22 | @override 23 | void dispose() { 24 | super.dispose(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/Scenes/Common/BaseBlocBuilder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'Bloc.dart'; 6 | import 'BlocProvider.dart'; 7 | 8 | class BaseBlocBuilder, Output> 9 | extends StatelessWidget { 10 | final SomeBloc? bloc; 11 | final Widget Function(BuildContext, AsyncSnapshot) builder; 12 | BaseBlocBuilder({this.bloc, required this.builder}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return StreamBuilder(stream: bloc?.stream ?? BlocProvider.of(context)?.stream, builder: builder); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /gen/todo_api/lib/auth/oauth.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class OAuth implements Authentication { 14 | OAuth({this.accessToken = ''}); 15 | 16 | String accessToken; 17 | 18 | @override 19 | Future applyToParams(List queryParams, Map headerParams,) async { 20 | if (accessToken.isNotEmpty) { 21 | headerParams['Authorization'] = 'Bearer $accessToken'; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /gen/todo_api/doc/TodoParams.md: -------------------------------------------------------------------------------- 1 | # todo_api.model.TodoParams 2 | 3 | ## Load the model package 4 | ```dart 5 | import 'package:todo_api/api.dart'; 6 | ``` 7 | 8 | ## Properties 9 | Name | Type | Description | Notes 10 | ------------ | ------------- | ------------- | ------------- 11 | **title** | **String** | | 12 | **note** | **String** | multiline note | [optional] 13 | **priority** | [**PriorityEnum**](PriorityEnum.md) | | 14 | **completeBy** | [**DateTime**](DateTime.md) | todo must be completed by this date | [optional] 15 | **completed** | **bool** | todo is completed | 16 | 17 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 18 | 19 | 20 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - FMDB (2.7.5): 4 | - FMDB/standard (= 2.7.5) 5 | - FMDB/standard (2.7.5) 6 | - sqflite (0.0.3): 7 | - Flutter 8 | - FMDB (>= 2.7.5) 9 | 10 | DEPENDENCIES: 11 | - Flutter (from `Flutter`) 12 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 13 | 14 | SPEC REPOS: 15 | trunk: 16 | - FMDB 17 | 18 | EXTERNAL SOURCES: 19 | Flutter: 20 | :path: Flutter 21 | sqflite: 22 | :path: ".symlinks/plugins/sqflite/ios" 23 | 24 | SPEC CHECKSUMS: 25 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 26 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 27 | sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a 28 | 29 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 30 | 31 | COCOAPODS: 1.15.2 32 | -------------------------------------------------------------------------------- /gen/todo_api/doc/TodoResponse.md: -------------------------------------------------------------------------------- 1 | # todo_api.model.TodoResponse 2 | 3 | ## Load the model package 4 | ```dart 5 | import 'package:todo_api/api.dart'; 6 | ``` 7 | 8 | ## Properties 9 | Name | Type | Description | Notes 10 | ------------ | ------------- | ------------- | ------------- 11 | **id** | **String** | | 12 | **title** | **String** | | 13 | **note** | **String** | multiline note | [optional] 14 | **priority** | [**PriorityEnum**](PriorityEnum.md) | | 15 | **completeBy** | [**DateTime**](DateTime.md) | todo must be completed by this date | [optional] 16 | **completed** | **bool** | todo is completed | 17 | 18 | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/LongResult.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'NetworkIssue.dart'; 4 | 5 | sealed class Result {} 6 | 7 | class success extends Result { 8 | final Entity data; 9 | success(this.data); 10 | } 11 | 12 | class failure extends Result { 13 | final String description; 14 | failure(this.description); 15 | } 16 | 17 | class domainIssue extends Result { 18 | final DomainIssue issue; 19 | domainIssue(this.issue); 20 | } 21 | 22 | class networkIssue extends Result { 23 | final NetworkIssue issue; 24 | networkIssue(this.issue); 25 | } 26 | -------------------------------------------------------------------------------- /lib/Repository/Entities/Todo.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Abstraction/TodoValues.dart'; 4 | import 'Priority.dart'; 5 | 6 | class Todo { 7 | final String id; 8 | final String title; 9 | final String note; 10 | final DateTime? completeBy; 11 | final Priority priority; 12 | final bool completed; 13 | 14 | Todo( 15 | {required this.id, 16 | required this.title, 17 | this.note = "", 18 | this.completeBy, 19 | this.priority = Priority.none, 20 | this.completed = false}); 21 | 22 | TodoValues get todoValues => TodoValues( 23 | title: this.title, 24 | note: this.note, 25 | completeBy: this.completeBy, 26 | priority: this.priority, 27 | completed: this.completed); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/Scenes/Common/BlocBuilder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | import 'BaseBlocBuilder.dart'; 6 | import 'Bloc.dart'; 7 | 8 | class BlocBuilder, Output> 9 | extends StatelessWidget { 10 | final SomeBloc? bloc; 11 | final Widget Function(BuildContext, Output) builder; 12 | BlocBuilder({required this.bloc, required this.builder}); 13 | @override 14 | Widget build(BuildContext context) { 15 | return BaseBlocBuilder( 16 | bloc: bloc, 17 | builder: (context, snapshot) { 18 | if (!snapshot.hasData) { 19 | return SizedBox(); 20 | } 21 | final data = snapshot.data!; 22 | return builder(context, data); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/Repository/Network/RealNetworkClient.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:todo_api/api.dart'; 4 | 5 | import 'NetworkClient.dart'; 6 | 7 | class RealNetworkClient implements NetworkClient { 8 | final TodoApi _todoApi; 9 | 10 | RealNetworkClient(): _todoApi = TodoApi(ApiClient()); 11 | 12 | @override 13 | Future createTodo(TodoParams params) => _todoApi.createTodo(params); 14 | 15 | @override 16 | Future deleteTodo(String id) => _todoApi.deleteTodo(id); 17 | 18 | @override 19 | Future?> getAllTodos() => _todoApi.getAllTodos(); 20 | 21 | @override 22 | Future getTodo(String id) => _todoApi.getTodoById(id); 23 | 24 | @override 25 | Future updateTodo(String id, TodoParams params) => _todoApi.updateTodo(id, params); 26 | } 27 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'A UI toolkit for beautiful and fast apps.' 10 | s.homepage = 'https://flutter.dev' 11 | s.license = { :type => 'BSD' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '12.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /gen/todo_api/lib/auth/http_basic_auth.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class HttpBasicAuth implements Authentication { 14 | HttpBasicAuth({this.username = '', this.password = ''}); 15 | 16 | String username; 17 | String password; 18 | 19 | @override 20 | Future applyToParams(List queryParams, Map headerParams,) async { 21 | if (username.isNotEmpty && password.isNotEmpty) { 22 | final credentials = '$username:$password'; 23 | headerParams['Authorization'] = 'Basic ${base64.encode(utf8.encode(credentials))}'; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/Scenes/Common/Waiting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Waiting extends StatelessWidget { 4 | final Widget child; 5 | final bool isWaiting; 6 | 7 | const Waiting({required this.child, required this.isWaiting}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Align( 12 | alignment: Alignment.topCenter, 13 | child: Stack( 14 | children: [ 15 | SafeArea(child: child), 16 | Visibility( 17 | visible: isWaiting, 18 | child: Container( 19 | height: double.infinity, 20 | width: double.infinity, 21 | color: Colors.white, 22 | child: Center( 23 | child: CircularProgressIndicator(color: Colors.lightGreen,), 24 | ), 25 | ), 26 | ) 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/Scenes/Common/BlocProvider.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'Bloc.dart'; 5 | 6 | 7 | class BlocProvider extends StatefulWidget { 8 | final Widget child; 9 | final T bloc; 10 | 11 | const BlocProvider({Key? key, required this.bloc, required this.child}) 12 | : super(key: key); 13 | 14 | static T? of(BuildContext context) { 15 | final BlocProvider? provider = context.findAncestorWidgetOfExactType(); 16 | return provider?.bloc; 17 | } 18 | 19 | @override 20 | State createState() => _BlocProviderState(); 21 | } 22 | 23 | class _BlocProviderState extends State { 24 | @override 25 | Widget build(BuildContext context) => widget.child; 26 | 27 | @override 28 | void dispose() { 29 | widget.bloc.dispose(); 30 | super.dispose(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Repository/Abstraction/EntityGateway.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Db/SqlLiteEntityGateway.dart'; 4 | import '../Ephemeral/EphemeralEntityGateway.dart'; 5 | import '../Network/NetworkEntityGateway.dart'; 6 | import 'TodoManager.dart'; 7 | 8 | enum _Implementation { 9 | test, 10 | db, 11 | network 12 | } 13 | 14 | abstract class EntityGateway { 15 | TodoManager get todoManager; 16 | 17 | static final gatewayImplementation = _Implementation.network; 18 | 19 | static EntityGateway get entityGateway { 20 | switch(gatewayImplementation) { 21 | case _Implementation.test: 22 | return EphemeralEntityGateway(); 23 | case _Implementation.db: 24 | return SqlLiteEntityGateway(); 25 | case _Implementation.network: 26 | return NetworkEntityGateway(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/TodoItemDisplay.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | import 'package:flutter_todo/Repository/Entities/Priority.dart'; 9 | import 'package:flutter_todo/Repository/Entities/Todo.dart'; 10 | import '../../AppState/TodoAppState.dart'; 11 | import '../../Common/ActionDecoratedScene.dart'; 12 | import '../../Common/BlocBuilder.dart'; 13 | import '../../Common/Localize.dart'; 14 | import '../../Common/StarterBloc.dart'; 15 | 16 | part 'Assembly/Assembly.dart'; 17 | part 'View/Scene.dart'; 18 | part 'Router/Router.dart'; 19 | part 'Presenter/Presenter.dart'; 20 | part 'Presenter/PresenterOutput.dart'; 21 | part 'Presenter/ViewModel.dart'; 22 | part 'UseCase/PresentationModel.dart'; 23 | part 'UseCase/UseCase.dart'; 24 | part 'UseCase/UseCaseOutput.dart'; 25 | -------------------------------------------------------------------------------- /lib/Scenes/Common/BlocConsumer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | import 'BaseBlocConsumer.dart'; 6 | import 'Bloc.dart'; 7 | 8 | class BlocConsumer, Output> 9 | extends StatelessWidget { 10 | final SomeBloc? bloc; 11 | final Widget Function(BuildContext, Output) builder; 12 | final void Function(Output)listener; 13 | 14 | BlocConsumer({required this.bloc, required this.builder, required this.listener}); 15 | @override 16 | Widget build(BuildContext context) { 17 | return BaseBlocConsumer( 18 | bloc: bloc, 19 | builder: (context, snapshot) { 20 | if (!snapshot.hasData) { 21 | return SizedBox(); 22 | } 23 | final data = snapshot.data!; 24 | return builder(context, data); 25 | }, 26 | listener: listener, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/UseCase/PresentationModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | enum FieldName { title, note, completeBy, priority, completed } 6 | 7 | sealed class _RowPresentationModel { 8 | final FieldName field; 9 | _RowPresentationModel(this.field); 10 | 11 | } 12 | 13 | @visibleForTesting 14 | class stringRow extends _RowPresentationModel { 15 | final String value; 16 | stringRow(super.field, this.value); 17 | } 18 | 19 | @visibleForTesting 20 | class dateRow extends _RowPresentationModel { 21 | final DateTime value; 22 | dateRow(super.field, this.value); 23 | } 24 | 25 | @visibleForTesting 26 | class boolRow extends _RowPresentationModel { 27 | final bool value; 28 | boolRow(super.field, this.value); 29 | } 30 | 31 | @visibleForTesting 32 | class priorityRow extends _RowPresentationModel { 33 | final Priority value; 34 | priorityRow(super.field, this.value); 35 | } 36 | -------------------------------------------------------------------------------- /gen/todo_api/lib/api_exception.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class ApiException implements Exception { 14 | ApiException(this.code, this.message); 15 | 16 | ApiException.withInner(this.code, this.message, this.innerException, this.stackTrace); 17 | 18 | int code = 0; 19 | String? message; 20 | Exception? innerException; 21 | StackTrace? stackTrace; 22 | 23 | @override 24 | String toString() { 25 | if (message == null) { 26 | return 'ApiException'; 27 | } 28 | if (innerException == null) { 29 | return 'ApiException $code: $message'; 30 | } 31 | return 'ApiException $code: $message (Inner exception: $innerException)\n\n$stackTrace'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/UseCase/UseCase.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | @visibleForTesting 6 | class UseCase with StarterBloc<_UseCaseOutput> { 7 | List<_RowPresentationModel> rowList = []; 8 | 9 | final Todo _todo; 10 | 11 | UseCase(this._todo) { 12 | 13 | rowList.add(stringRow(FieldName.title, _todo.title)); 14 | if (_todo.note != "") { 15 | rowList.add(stringRow(FieldName.note, _todo.note)); 16 | } 17 | final completeBy = _todo.completeBy; 18 | if (completeBy != null) { 19 | rowList.add(dateRow(FieldName.completeBy, completeBy)); 20 | } 21 | switch (_todo.priority) { 22 | case Priority.none: 23 | break; 24 | default: 25 | rowList.add(priorityRow(FieldName.priority, _todo.priority)); 26 | } 27 | rowList.add(boolRow(FieldName.completed, _todo.completed)); 28 | emit(presentModel(rowList)); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | super.dispose(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/Presenter/ViewModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | @visibleForTesting 6 | class ViewModel { 7 | final List rows; 8 | 9 | ViewModel.fromPresentation(PresentationModel model) 10 | : rows = model.rows 11 | .map((row) => RowViewModel.fromPresentation(row)) 12 | .toList(); 13 | } 14 | 15 | @visibleForTesting 16 | class RowViewModel { 17 | final int index; 18 | final String id; 19 | final String title; 20 | final String completeBy; 21 | final String priority; 22 | final bool completed; 23 | 24 | RowViewModel.fromPresentation(PresentationRowModel model) 25 | : index = model.index, 26 | id = model.id, 27 | title = model.title, 28 | completeBy = 29 | (model.completeBy != null) ? localizedDate(model.completeBy!) : "", 30 | priority = 31 | List.generate(model.priority.bangs + 1, (index) => " ") 32 | .reduce((value, element) => "!$value"), 33 | completed = model.completed; 34 | } 35 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/TodoList.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | 9 | import '../../Repository/Abstraction/EntityGateway.dart'; 10 | import '../../Repository/Abstraction/Result.dart'; 11 | import '../../Repository/Entities/Priority.dart'; 12 | import '../../Repository/Entities/Todo.dart'; 13 | import '../AppState/TodoAppState.dart'; 14 | import '../AppState/TodoItemStartMode.dart'; 15 | import '../Common/BlocBuilder.dart'; 16 | import '../Common/FullScreenLoadingIndicator.dart'; 17 | import '../Common/Localize.dart'; 18 | import '../Common/StarterBloc.dart'; 19 | 20 | part 'Assembly/Assembly.dart'; 21 | part 'View/Scene.dart'; 22 | part 'View/Cell.dart'; 23 | part 'View/CheckBox.dart'; 24 | part 'Router/Router.dart'; 25 | part 'Presenter/Presenter.dart'; 26 | part 'Presenter/PresenterOutput.dart'; 27 | part 'Presenter/ViewModel.dart'; 28 | part 'UseCase/UseCase.dart'; 29 | part 'UseCase/UseCaseOutput.dart'; 30 | part 'UseCase/PresentationModel.dart'; 31 | -------------------------------------------------------------------------------- /gen/todo_api/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/TodoItemRouter.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_todo/Scenes/Common/FullScreenLoadingIndicator.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | import 'package:flutter_todo/Repository/Entities/Todo.dart'; 8 | import 'package:flutter_todo/Repository/Abstraction/EntityGateway.dart'; 9 | import 'package:flutter_todo/Repository/Abstraction/Result.dart'; 10 | import '../../AppState/TodoAppState.dart'; 11 | import '../../AppState/TodoItemStartMode.dart'; 12 | import '../../Common/ActionDecoratedScene.dart'; 13 | import '../../Common/BlocBuilder.dart'; 14 | import '../../Common/Localize.dart'; 15 | import '../../Common/StarterBloc.dart'; 16 | import '../TodoItemDisplay/TodoItemDisplay.dart' as TodoItemDisplay; 17 | import '../TodoItemEdit/TodoItemEdit.dart' as TodoItemEdit; 18 | 19 | part 'Assembly/Assembly.dart'; 20 | part 'View/Scene.dart'; 21 | part 'Router/Router.dart'; 22 | part 'Presenter/Presenter.dart'; 23 | part 'Presenter/PresenterOutput.dart'; 24 | part 'UseCase/UseCase.dart'; 25 | part 'UseCase/UseCaseOutput.dart'; 26 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/Presenter/ViewModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | @visibleForTesting 6 | class ViewModel { 7 | final String title; 8 | final String note; 9 | final DateTime? completeBy; 10 | final bool completeBySwitchIsOn; 11 | final String completeByString; 12 | final int priority; 13 | final bool completed; 14 | final String modeTitle; 15 | final ErrorMessage? errorMessage; 16 | final bool showEditCompleteBy; 17 | final bool isWaiting; 18 | 19 | ViewModel.fromModel(PresentationModel model) 20 | : title = model.title, 21 | note = model.note, 22 | completeBy = model.completeBy, 23 | completeByString = 24 | (model.completeBy != null) ? localizedDate(model.completeBy!) : "", 25 | completeBySwitchIsOn = (model.completeBy != null), 26 | priority = model.priority.bangs, 27 | completed = model.completed, 28 | modeTitle = model.modeTitle, 29 | errorMessage = model.errorMessage, 30 | showEditCompleteBy = model.showEditCompleteBy, 31 | isWaiting = model.isWaiting; 32 | } 33 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_localizations/flutter_localizations.dart'; 5 | import 'package:flutter_todo/Scenes/TodoRootRouter/TodoRootRouter.dart' as TodoRootRouter; 6 | import 'Scenes/Common/Localization/TodoLocalizationsDelegate.dart'; 7 | import 'Scenes/Common/Localization/FrenchCupertinoLocalizations.dart'; 8 | 9 | void main() => runApp(MyApp()); 10 | 11 | class MyApp extends StatelessWidget { 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return MaterialApp( 16 | title: 'Demo of Todo using Clean Architecture', 17 | theme: ThemeData( 18 | primarySwatch: Colors.blue, 19 | ), 20 | home: TodoRootRouter.Assembly().scene, 21 | localizationsDelegates: [ 22 | const TodoLocalizationsDelegate(), 23 | GlobalMaterialLocalizations.delegate, 24 | GlobalWidgetsLocalizations.delegate, 25 | FrenchCupertinoLocalizations.delegate, 26 | ], 27 | supportedLocales: [ 28 | const Locale('en'), // English 29 | const Locale('fr'), // French 30 | ], 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 17 | base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 18 | - platform: android 19 | create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 20 | base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 21 | - platform: ios 22 | create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 23 | base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /lib/Repository/Db/SqlLiteManager.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:sqflite/sqflite.dart'; 4 | import 'package:path/path.dart'; 5 | 6 | class SqlLiteManager { 7 | 8 | static final _instance = SqlLiteManager._private(); 9 | SqlLiteManager._private(); 10 | factory SqlLiteManager() => _instance; 11 | 12 | Database? _database; 13 | Future get database async { 14 | 15 | if(_database == null) { 16 | final databasesPath = await getDatabasesPath(); 17 | _database = await openDatabase( 18 | join(databasesPath, 'Todo.db'), 19 | onCreate: (db, version) { 20 | return db.execute( 21 | "create table todo(" 22 | "id text primary key," 23 | "title text, " 24 | "note text, " 25 | "completeBy integer, " 26 | "priority text, " 27 | "completed integer) without rowid", 28 | ); 29 | }, 30 | version: 1, 31 | ); 32 | } 33 | return _database!; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/Presenter/Presenter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | @visibleForTesting 6 | class Presenter with StarterBloc<_PresenterOutput> { 7 | final UseCase _useCase; 8 | final Router _router; 9 | 10 | Presenter(this._useCase, this._router) { 11 | _useCase.stream.listen((event) { 12 | switch (event) { 13 | case presentLoading(): 14 | emit(showLoading()); 15 | case presentModel(:final model): 16 | emit(showModel(ViewModel.fromPresentation(model))); 17 | case presentItemDetail(): 18 | _router.routeShowItemDetail(); 19 | } 20 | }); 21 | } 22 | 23 | void eventShowCompleted(bool completed) { 24 | _useCase.eventShowCompleted(completed); 25 | } 26 | 27 | void eventCompleted(bool completed, int index) { 28 | _useCase.eventCompleted(completed, index); 29 | } 30 | 31 | void eventDelete(int index) { 32 | _useCase.eventDelete(index); 33 | } 34 | 35 | void eventCreate() { 36 | _useCase.eventCreate(); 37 | } 38 | 39 | void eventItemSelected(int index) { 40 | _useCase.eventItemSelected(index); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _useCase.dispose(); 46 | super.dispose(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gen/todo_api/test/todo_params_test.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | import 'package:todo_api/api.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | // tests for TodoParams 15 | void main() { 16 | // final instance = TodoParams(); 17 | 18 | group('test TodoParams', () { 19 | // String title 20 | test('to test the property `title`', () async { 21 | // TODO 22 | }); 23 | 24 | // multiline note 25 | // String note 26 | test('to test the property `note`', () async { 27 | // TODO 28 | }); 29 | 30 | // high, medium, low or none 31 | // String priority 32 | test('to test the property `priority`', () async { 33 | // TODO 34 | }); 35 | 36 | // todo must be completed by this date 37 | // DateTime completeBy 38 | test('to test the property `completeBy`', () async { 39 | // TODO 40 | }); 41 | 42 | // todo is completed 43 | // bool completed 44 | test('to test the property `completed`', () async { 45 | // TODO 46 | }); 47 | 48 | 49 | }); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /gen/todo_api/lib/auth/api_key_auth.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class ApiKeyAuth implements Authentication { 14 | ApiKeyAuth(this.location, this.paramName); 15 | 16 | final String location; 17 | final String paramName; 18 | 19 | String apiKeyPrefix = ''; 20 | String apiKey = ''; 21 | 22 | @override 23 | Future applyToParams(List queryParams, Map headerParams,) async { 24 | final paramValue = apiKeyPrefix.isEmpty ? apiKey : '$apiKeyPrefix $apiKey'; 25 | 26 | if (paramValue.isNotEmpty) { 27 | if (location == 'query') { 28 | queryParams.add(QueryParam(paramName, paramValue)); 29 | } else if (location == 'header') { 30 | headerParams[paramName] = paramValue; 31 | } else if (location == 'cookie') { 32 | headerParams.update( 33 | 'Cookie', 34 | (existingCookie) => '$existingCookie; $paramName=$paramValue', 35 | ifAbsent: () => '$paramName=$paramValue', 36 | ); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/Repository/Entities/Priority.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | enum Priority { high, medium, low, none } 4 | 5 | extension PriorityExt on Priority { 6 | int get bangs { 7 | switch (this) { 8 | case Priority.high: 9 | return 3; 10 | case Priority.medium: 11 | return 2; 12 | case Priority.low: 13 | return 1; 14 | case Priority.none: 15 | return 0; 16 | } 17 | } 18 | 19 | static Priority fromBangs(int bangs) { 20 | switch (bangs) { 21 | case 3: 22 | return Priority.high; 23 | case 2: 24 | return Priority.medium; 25 | case 1: 26 | return Priority.low; 27 | case 0: 28 | return Priority.none; 29 | default: 30 | assert(false, "bangs must be 0, 1, 2 or 3"); 31 | return Priority.none; 32 | } 33 | } 34 | 35 | static Priority fromString(String rawValue) { 36 | switch (rawValue) { 37 | case "high": 38 | return Priority.high; 39 | case "medium": 40 | return Priority.medium; 41 | case "low": 42 | return Priority.low; 43 | case "none": 44 | return Priority.none; 45 | default: 46 | assert(false, "rawValue must be high, medium, low or none"); 47 | return Priority.none; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/UseCase/PresentationModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | @visibleForTesting 6 | class PresentationModel { 7 | final List rows; 8 | PresentationModel(this.rows); 9 | 10 | PresentationModel.fromEntities(List entities, bool showCompleted) 11 | : rows = entities.indexed 12 | .map((indexedPair) => 13 | PresentationRowModel(indexedPair.$2, indexedPair.$1)) 14 | .where((model) => showCompleted || model.completed == false) 15 | .toList() { 16 | rows.sort((a, b) { 17 | return switch ((a.completeBy, b.completeBy)) { 18 | (null, null) => 0, 19 | (null, _) => 1, 20 | (_, null) => -1, 21 | _ => a.completeBy!.compareTo(b.completeBy!) 22 | }; 23 | }); 24 | } 25 | } 26 | 27 | @visibleForTesting 28 | class PresentationRowModel { 29 | final int index; 30 | final String id; 31 | final String title; 32 | final DateTime? completeBy; 33 | final Priority priority; 34 | final bool completed; 35 | 36 | PresentationRowModel(Todo entity, int index) 37 | : index = index, 38 | id = entity.id, 39 | title = entity.title, 40 | completeBy = entity.completeBy, 41 | priority = entity.priority, 42 | completed = entity.completed; 43 | } 44 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/UseCase/UseCase.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | class UseCase with StarterBloc<_UseCaseOutput> { 6 | final EntityGateway _entityGateway; 7 | 8 | final TodoItemStartMode _itemStartMode; 9 | final BehaviorSubject _currentTodoSubject; 10 | 11 | UseCase(this._entityGateway, this._itemStartMode, this._currentTodoSubject) { 12 | switch (_itemStartMode) { 13 | case TodoItemStartModeCreate(): 14 | _startCreate(); 15 | case TodoItemStartModeUpdate(:final itemId): 16 | _startUpdate(itemId); 17 | } 18 | } 19 | 20 | void _startCreate() { 21 | _currentTodoSubject.value = null; 22 | emit(presentEditView()); 23 | } 24 | 25 | Future _startUpdate(String itemId) async { 26 | emit(presentLoading()); 27 | final result = await _entityGateway.todoManager.fetch(itemId); 28 | switch (result) { 29 | case success(:final data): 30 | _currentTodoSubject.value = data; 31 | emit(presentDisplayView()); 32 | case failure(:final description): 33 | assert(false, "Unexpected error: $description"); 34 | case networkIssue(:final issue): 35 | assert(false, "Unexpected Network issue: reason $issue"); 36 | } 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/Repository/Ephemeral/EphemeralData.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Entities/Priority.dart'; 4 | import 'EphemeralTodo.dart'; 5 | 6 | class TodoEphemeralData { 7 | 8 | static final data = [ 9 | EphemeralTodo( 10 | id: "1", 11 | title: "Get Milk", 12 | note: "lots", 13 | priority: Priority.low, 14 | completed: true), 15 | EphemeralTodo( 16 | id: "2", 17 | title: "Get Going", 18 | note: "The sdFDS sd fdsfFSD DSFds\nsdf sdf sd fsd f\nf sdf sd f", 19 | completeBy: DateTime.parse("2018-12-12"), 20 | priority: Priority.high, 21 | completed: false), 22 | EphemeralTodo( 23 | id: "3", 24 | title: "Farm Tools", 25 | note: 26 | "hammer, nails, plow\nThis is something else. This is something else2. This is something else3.\nLet's do it again: hammer, nails, plow\nThis is something else. This is something else2. This is something else3.", 27 | priority: Priority.medium, 28 | completed: false), 29 | EphemeralTodo(id: "4", title: "Get Juice", note: "lots", completed: true), 30 | EphemeralTodo( 31 | id: "5", 32 | title: "Charlie Brown", 33 | note: "Get the album", 34 | completeBy: DateTime.parse("2019-02-12"), 35 | priority: Priority.high, 36 | completed: false), 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /lib/Scenes/Common/TodoOkDialog.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class TodoOkDialog { 9 | static void show(BuildContext context, String alertTitle, String message) { 10 | final platform = Theme.of(context).platform; 11 | if (platform == TargetPlatform.iOS) 12 | unawaited(showCupertinoDialog( 13 | context: context, 14 | builder: (context) => CupertinoAlertDialog( 15 | title: Text(alertTitle), 16 | content: Text(message), 17 | actions: [ 18 | CupertinoButton( 19 | child: Text("OK"), 20 | onPressed: () => Navigator.of(context).pop(), 21 | ) 22 | ]))); 23 | else 24 | unawaited(showDialog( 25 | context: context, 26 | builder: (context) => AlertDialog( 27 | title: Text(alertTitle), 28 | content: Text(message), 29 | actions: [ 30 | TextButton( 31 | child: Text("OK"), 32 | onPressed: () => Navigator.of(context).pop(), 33 | ) 34 | ]))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Scenes/Common/TodoSwitch.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | 6 | class TodoSwitch extends StatefulWidget { 7 | 8 | final bool _state; 9 | final void Function(bool) _onChanged; 10 | 11 | TodoSwitch({required bool state, required Function(bool) onChanged}) 12 | : _state = state, _onChanged = onChanged; 13 | 14 | @override 15 | State createState() => TodoSwitchState(); 16 | 17 | } 18 | 19 | class TodoSwitchState extends State { 20 | 21 | late bool _state; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _state = widget._state; 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | 32 | final platform = Theme.of(context).platform; 33 | 34 | void onChanged(bool state) { 35 | setState(() { 36 | _state = state; 37 | }); 38 | widget._onChanged(state); 39 | 40 | } 41 | 42 | return (platform == TargetPlatform.iOS) 43 | ? CupertinoSwitch( 44 | value: _state, 45 | onChanged: onChanged 46 | ) 47 | : Switch( 48 | value: _state, 49 | onChanged: onChanged, 50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /gen/todo_api/test/todo_response_test.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | import 'package:todo_api/api.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | // tests for TodoResponse 15 | void main() { 16 | // final instance = TodoResponse(); 17 | 18 | group('test TodoResponse', () { 19 | // String id 20 | test('to test the property `id`', () async { 21 | // TODO 22 | }); 23 | 24 | // String title 25 | test('to test the property `title`', () async { 26 | // TODO 27 | }); 28 | 29 | // multiline note 30 | // String note 31 | test('to test the property `note`', () async { 32 | // TODO 33 | }); 34 | 35 | // high, medium, low or none 36 | // String priority 37 | test('to test the property `priority`', () async { 38 | // TODO 39 | }); 40 | 41 | // todo must be completed by this date 42 | // DateTime completeBy 43 | test('to test the property `completeBy`', () async { 44 | // TODO 45 | }); 46 | 47 | // todo is completed 48 | // bool completed 49 | test('to test the property `completed`', () async { 50 | // TODO 51 | }); 52 | 53 | 54 | }); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /gen/todo_api/lib/auth/http_bearer_auth.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | typedef HttpBearerAuthProvider = String Function(); 14 | 15 | class HttpBearerAuth implements Authentication { 16 | HttpBearerAuth(); 17 | 18 | dynamic _accessToken; 19 | 20 | dynamic get accessToken => _accessToken; 21 | 22 | set accessToken(dynamic accessToken) { 23 | if (accessToken is! String && accessToken is! HttpBearerAuthProvider) { 24 | throw ArgumentError('accessToken value must be either a String or a String Function().'); 25 | } 26 | _accessToken = accessToken; 27 | } 28 | 29 | @override 30 | Future applyToParams(List queryParams, Map headerParams,) async { 31 | if (_accessToken == null) { 32 | return; 33 | } 34 | 35 | String accessToken; 36 | 37 | if (_accessToken is String) { 38 | accessToken = _accessToken; 39 | } else if (_accessToken is HttpBearerAuthProvider) { 40 | accessToken = _accessToken!(); 41 | } else { 42 | return; 43 | } 44 | 45 | if (accessToken.isNotEmpty) { 46 | headerParams['Authorization'] = 'Bearer $accessToken'; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/Presenter/Presenter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | @visibleForTesting 6 | class Presenter 7 | with StarterBloc<_PresenterOutput> 8 | implements TodoItemDisplay.Router, TodoItemEdit.Router { 9 | final UseCase _useCase; 10 | final Router router; 11 | 12 | Presenter(this._useCase, this.router) { 13 | _useCase.stream.listen((event) { 14 | switch(event) { 15 | case presentLoading(): 16 | emit(showLoading()); 17 | case presentDisplayView(): 18 | emit(showDisplayView()); 19 | case presentEditView(): 20 | emit(showEditView()); 21 | case presentNotFound(:final id): 22 | final message = localizedString("todoNotFound"); 23 | //final message = String(format: messageFormat, id) 24 | emit(showMessageView(message + ' ' + id)); 25 | } 26 | }); 27 | } 28 | 29 | // TodoItemDisplayRouter 30 | 31 | @override 32 | void routeEditView() { 33 | emit(showEditView()); 34 | } 35 | 36 | // TodoItemEditRouter 37 | 38 | @override 39 | void routeSaveCompleted() { 40 | emit(showDisplayView()); 41 | } 42 | 43 | @override 44 | void routeEditingCancelled() { 45 | emit(showDisplayView()); 46 | } 47 | 48 | @override 49 | void routeCreateCancelled() { 50 | router.routeCreateItemCancelled(); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _useCase.dispose(); 56 | super.dispose(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/View/CheckBox.dart: -------------------------------------------------------------------------------- 1 | part of '../TodoList.dart'; 2 | 3 | typedef void _CheckBoxOnPressed(bool checked); 4 | class _CheckBox extends StatefulWidget { 5 | 6 | final bool _checked; 7 | final _CheckBoxOnPressed _onPressed; 8 | 9 | _CheckBox({bool checked = false, required _CheckBoxOnPressed onPressed}) 10 | : _checked = checked, _onPressed = onPressed; 11 | 12 | @override 13 | State createState() => _CheckBoxState(); 14 | 15 | } 16 | 17 | class _CheckBoxState extends State<_CheckBox> { 18 | 19 | late bool _checked; 20 | 21 | @override 22 | void initState() { 23 | _checked = widget._checked; 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | final hitSpace = 44.0; 30 | return SizedBox( 31 | width: hitSpace, 32 | height: hitSpace, 33 | child: new TextButton( 34 | onPressed: _onClicked, 35 | child: Image( 36 | width: hitSpace, 37 | height: hitSpace, 38 | image: AssetImage( 39 | _checked 40 | ? 'images/checkbox_checked.png' 41 | : 'images/checkbox_unchecked.png', 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | 48 | void _onClicked() { 49 | setState(() { 50 | _checked = !_checked; 51 | }); 52 | widget._onPressed(_checked); 53 | } 54 | } 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/TodoItemEdit.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | 9 | import 'package:flutter_todo/Repository/Entities/Priority.dart'; 10 | import 'package:flutter_todo/Repository/Entities/Todo.dart'; 11 | import 'package:flutter_todo/Repository/Abstraction/EntityGateway.dart'; 12 | import 'package:flutter_todo/Repository/Abstraction/Result.dart'; 13 | import 'package:flutter_todo/Repository/Abstraction/TodoValues.dart'; 14 | import '../../AppState/TodoAppState.dart'; 15 | import '../../AppState/TodoItemStartMode.dart'; 16 | import '../../Common/ActionDecoratedScene.dart'; 17 | import '../../Common/BlocConsumer.dart'; 18 | import '../../Common/CupertinoPopoverDatePicker.dart'; 19 | import '../../Common/ErrorMessages.dart'; 20 | import '../../Common/Localize.dart'; 21 | import '../../Common/StarterBloc.dart'; 22 | import '../../Common/TodoExclusive.dart'; 23 | import '../../Common/TodoOkDialog.dart'; 24 | import '../../Common/TodoSwitch.dart'; 25 | import '../../Common/TodoTextField.dart'; 26 | import '../../Common/Waiting.dart'; 27 | 28 | part 'Assembly/Assembly.dart'; 29 | part 'View/Scene.dart'; 30 | part 'Router/Router.dart'; 31 | part 'Presenter/Presenter.dart'; 32 | part 'Presenter/PresenterOutput.dart'; 33 | part 'Presenter/ViewModel.dart'; 34 | part 'UseCase/UseCase.dart'; 35 | part 'UseCase/UseCaseOutput.dart'; 36 | part 'UseCase/PresentationModel.dart'; 37 | 38 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/UseCase/PresentationModel.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | @visibleForTesting 6 | class PresentationModel { 7 | final String title; 8 | final String note; 9 | final DateTime? _completeBy; 10 | final Priority priority; 11 | final bool completed; 12 | final String modeTitle; 13 | final ErrorMessage? errorMessage; 14 | final bool showEditCompleteBy; 15 | final bool isWaiting; 16 | 17 | PresentationModel({ 18 | required this.title, 19 | required this.note, 20 | DateTime? completeBy, 21 | required this.priority, 22 | required this.completed, 23 | required this.modeTitle, 24 | this.errorMessage, 25 | this.showEditCompleteBy = false, 26 | this.isWaiting = false, 27 | }) : _completeBy = completeBy; 28 | 29 | DateTime? get completeBy => _completeBy; 30 | 31 | factory PresentationModel.fromEditingTodo(EditingTodo editingTodo, 32 | {required String modeTitle, 33 | ErrorMessage? errorMessage, 34 | bool showEditCompleteBy = false, 35 | bool isWaiting = false}) { 36 | return PresentationModel( 37 | title: editingTodo.title, 38 | note: editingTodo.note, 39 | completeBy: editingTodo.completeBy, 40 | priority: editingTodo.priority, 41 | completed: editingTodo.completed, 42 | modeTitle: modeTitle, 43 | errorMessage: errorMessage, 44 | isWaiting: isWaiting, 45 | showEditCompleteBy: showEditCompleteBy); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/Scenes/Common/BaseBlocConsumer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'Bloc.dart'; 8 | import 'BlocProvider.dart'; 9 | 10 | class BaseBlocConsumer, Output> 11 | extends StatefulWidget { 12 | final SomeBloc? bloc; 13 | final Widget Function(BuildContext, AsyncSnapshot) builder; 14 | final void Function(Output)listener; 15 | 16 | BaseBlocConsumer({this.bloc, required this.builder, required this.listener}); 17 | 18 | @override 19 | State> createState() => _BaseBlocConsumerState(); 20 | } 21 | 22 | class _BaseBlocConsumerState, Output> extends State> { 23 | var downStreamController = StreamController(); 24 | late final Stream? originStream; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | originStream = (widget.bloc?.stream ?? BlocProvider.of(context)?.stream); 30 | originStream?.listen(_originListener); 31 | } 32 | 33 | void _originListener(Output event) { 34 | downStreamController.sink.add(event); 35 | widget.listener(event); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return StreamBuilder(stream: downStreamController.stream, builder: widget.builder); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | downStreamController.close(); 46 | super.dispose(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Repository/Ephemeral/EphemeralTodo.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import '../Abstraction/TodoValues.dart'; 4 | import '../Entities/Priority.dart'; 5 | import '../Entities/Todo.dart'; 6 | 7 | class EphemeralTodo { 8 | 9 | String id; 10 | String title; 11 | String note; 12 | DateTime? completeBy; 13 | Priority priority; 14 | bool completed; 15 | 16 | EphemeralTodo({ required this.id, required this.title, this.note = "", this.completeBy, this.priority = Priority.none, this.completed = false}); 17 | 18 | factory EphemeralTodo.fromTodo(todo) { 19 | return EphemeralTodo( 20 | id: todo.id, 21 | title: todo.title, 22 | note: todo.note, 23 | completeBy: todo.completeBy, 24 | priority: todo.priority, 25 | completed: todo.completed 26 | ); 27 | } 28 | 29 | factory EphemeralTodo.fromTodoValues(String id, TodoValues values) { 30 | return EphemeralTodo( 31 | id: id, 32 | title: values.title, 33 | note: values.note, 34 | completeBy: values.completeBy, 35 | priority: values.priority, 36 | completed: values.completed 37 | ); 38 | } 39 | 40 | void fromValues(TodoValues values) { 41 | title = values.title; 42 | note = values.note; 43 | completeBy = values.completeBy; 44 | priority = values.priority; 45 | completed = values.completed; 46 | } 47 | 48 | Todo get toTodo => Todo( 49 | id: this.id, 50 | title: this.title, 51 | note: this.note, 52 | completeBy: this.completeBy, 53 | priority: this.priority, 54 | completed: this.completed 55 | ); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /gen/todo_api/test/todo_api_test.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | import 'package:todo_api/api.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | 15 | /// tests for TodoApi 16 | void main() { 17 | // final instance = TodoApi(); 18 | 19 | group('tests for TodoApi', () { 20 | // Add a new todo 21 | // 22 | // Create a new todo 23 | // 24 | //Future createTodo(TodoResponse todoResponse) async 25 | test('test createTodo', () async { 26 | // TODO 27 | }); 28 | 29 | // Deletes a todo 30 | // 31 | // delete a todo 32 | // 33 | //Future deleteTodo(String todoId) async 34 | test('test deleteTodo', () async { 35 | // TODO 36 | }); 37 | 38 | // get all todos 39 | // 40 | // get all todos 41 | // 42 | //Future> getAllTodos() async 43 | test('test getAllTodos', () async { 44 | // TODO 45 | }); 46 | 47 | // Find todo by ID 48 | // 49 | // Returns a single todo by id 50 | // 51 | //Future getTodoById(String todoId) async 52 | test('test getTodoById', () async { 53 | // TODO 54 | }); 55 | 56 | // Updates a todo 57 | // 58 | // 59 | // 60 | //Future updateTodo(String todoId, TodoResponse todoResponse) async 61 | test('test updateTodo', () async { 62 | // TODO 63 | }); 64 | 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /gen/todo_api/lib/api.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | library openapi.api; 12 | 13 | import 'dart:async'; 14 | import 'dart:convert'; 15 | import 'dart:io'; 16 | 17 | import 'package:collection/collection.dart'; 18 | import 'package:http/http.dart'; 19 | import 'package:intl/intl.dart'; 20 | import 'package:meta/meta.dart'; 21 | 22 | part 'api_client.dart'; 23 | part 'api_helper.dart'; 24 | part 'api_exception.dart'; 25 | part 'auth/authentication.dart'; 26 | part 'auth/api_key_auth.dart'; 27 | part 'auth/oauth.dart'; 28 | part 'auth/http_basic_auth.dart'; 29 | part 'auth/http_bearer_auth.dart'; 30 | 31 | part 'api/todo_api.dart'; 32 | 33 | part 'model/priority_enum.dart'; 34 | part 'model/todo_params.dart'; 35 | part 'model/todo_response.dart'; 36 | 37 | 38 | /// An [ApiClient] instance that uses the default values obtained from 39 | /// the OpenAPI specification file. 40 | var defaultApiClient = ApiClient(); 41 | 42 | const _delimiters = {'csv': ',', 'ssv': ' ', 'tsv': '\t', 'pipes': '|'}; 43 | const _dateEpochMarker = 'epoch'; 44 | const _deepEquality = DeepCollectionEquality(); 45 | final _dateFormatter = DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 46 | final _regList = RegExp(r'^List<(.*)>$'); 47 | final _regSet = RegExp(r'^Set<(.*)>$'); 48 | final _regMap = RegExp(r'^Map$'); 49 | 50 | bool _isEpochMarker(String? pattern) => pattern == _dateEpochMarker || pattern == '/$_dateEpochMarker/'; 51 | -------------------------------------------------------------------------------- /lib/Scenes/Common/TodoTextField.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | class TodoTextField extends StatelessWidget { 6 | 7 | final String value; 8 | final void Function(String) onChanged; 9 | final String? placeholder; 10 | final int minLines; 11 | final int maxLines; 12 | final _textEditingController = TextEditingController(); 13 | 14 | TodoTextField({ 15 | required this.value, 16 | required this.onChanged, 17 | this.placeholder, 18 | this.minLines = 1, 19 | this.maxLines = 1 }) { 20 | _textEditingController.text = value; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | 26 | final platform = Theme.of(context).platform; 27 | 28 | final onChanged = (value) => this.onChanged(value); 29 | if(platform == TargetPlatform.iOS) { 30 | return CupertinoTextField( 31 | placeholder: placeholder, 32 | minLines: minLines, 33 | maxLines: maxLines, 34 | controller: _textEditingController, 35 | onChanged: onChanged 36 | ); 37 | } 38 | else { 39 | return TextField( 40 | decoration: InputDecoration( 41 | border: UnderlineInputBorder(), 42 | hintText: placeholder 43 | ), 44 | minLines: minLines, 45 | maxLines: maxLines, 46 | controller: _textEditingController, 47 | onChanged: onChanged, 48 | ); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | flutter_export_environment.sh 72 | .flutter-plugins-dependencies 73 | .last_build_id 74 | ios/build/Pods.build/ 75 | .fvm/flutter_sdk 76 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/Presenter/Presenter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | @visibleForTesting 6 | class Presenter with StarterBloc<_PresenterOutput> { 7 | final UseCase _useCase; 8 | final Router _router; 9 | 10 | Presenter(this._useCase, this._router) { 11 | _useCase.stream.listen((event) { 12 | switch(event) { 13 | case presentModel(:final model): 14 | emit(showModel(ViewModel.fromModel(model))); 15 | case presentSaveCompleted(): 16 | _router.routeSaveCompleted(); 17 | case presentCreateCancelled(): 18 | _router.routeCreateCancelled(); 19 | case presentEditingCancelled(): 20 | _router.routeEditingCancelled(); 21 | } 22 | }); 23 | } 24 | 25 | void eventEditedTitle(String title) { 26 | _useCase.eventEditedTitle(title); 27 | } 28 | 29 | void eventEditedNote(String note) { 30 | _useCase.eventEditedNote(note); 31 | } 32 | 33 | void eventCompleteBy(bool isOn) { 34 | _useCase.eventCompleteBy(isOn); 35 | } 36 | 37 | void eventEnableEditCompleteBy() { 38 | _useCase.eventEnableEditCompleteBy(); 39 | } 40 | 41 | void eventEditedCompleteBy(DateTime completeBy) { 42 | _useCase.eventEditedCompleteBy(completeBy); 43 | } 44 | 45 | void eventCompleted(bool completed) { 46 | _useCase.eventCompleted(completed); 47 | } 48 | 49 | void eventEditedPriority(int? index) { 50 | final priority = PriorityExt.fromBangs(index!); 51 | _useCase.eventEditedPriority(priority); 52 | } 53 | 54 | void eventSave() { 55 | _useCase.eventSave(); 56 | } 57 | 58 | void eventCancel() { 59 | _useCase.eventCancel(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | _useCase.dispose(); 65 | super.dispose(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todo in Flutter - Clean Architecture with BLoCs 2 | 3 | This Todo app is a full reference implementation for a Flutter App based on Mobile Clean Architecture. It demonstrates many tasks you might do on a daily basis during a project, such as: 4 | - routing, 5 | - data validation and capture, 6 | - saving of captured data, 7 | - localization, 8 | - the use of view and presentation models 9 | - parameter passing between Scenes, 10 | - output sub-protocols organized by event, 11 | - sharing state between sibling UseCases 12 | - separation of Entity Domain Objects from Data Transfer Objects (repository pattern) 13 | 14 | The are two Routers: 15 | * A Navigator acts as a root router managing a list and a detail scene. 16 | * A custom router manages the Editing and Display modes of the detail scene. 17 | 18 | No attempt has been made to make the scenes aesthetically pleasing - its all about the code. 19 | 20 | The app UI displays iOS native controls on an iOS Device and Android native controls on an Android Device. 21 | This was done to explore the possibility of having native controls with one code set. 22 | It is more likely that you or your designer will choose a brand specific UI that will look the same on both devices. This is commonly done in commercial apps. 23 | 24 | The app is based on the BLoC/BLoc Provider pattern. Each module's Presenter and UseCase is implemented as a BLoC. A BLoC uses a stream to return asynchronous events. Each Scene view uses a StreamBuilder to drive the display from events emitted by the Presenter. The Presenter processes the events emitted by the UseCase. 25 | 26 | ## Demo Modes 27 | 28 | The demo has two modes: *test* and *db*. `test` mode uses an in-memory datastore. `db` mode uses a database! You can flip between the two modes by changing the value of the `gatewayImplementation` in the `EntityGatewayFactory` . 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Todo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_todo 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/Scenes/TodoRootRouter/View/Scene.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoRootRouter.dart'; 4 | 5 | @visibleForTesting 6 | class Scene extends StatefulWidget { 7 | final Presenter _presenter; 8 | 9 | Scene(this._presenter); 10 | 11 | @override 12 | State createState() => _SceneState(); 13 | } 14 | 15 | class _SceneState extends State { 16 | late final Presenter _presenter; 17 | final _navKey = GlobalKey(); 18 | var _pages = []; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | this._presenter = widget._presenter; 24 | _pages = [ 25 | MaterialPage( 26 | child: TodoList.Assembly(_presenter).scene), 27 | ]; 28 | 29 | _presenter.stream.listen((event) { 30 | switch (event) { 31 | case PresenterOutput.showPop: 32 | _navKey.currentState!.pop(); 33 | default: 34 | _pages.add(event._page(_presenter)); 35 | setState(() { 36 | _pages = _pages.toList(); 37 | }); 38 | } 39 | }); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Navigator( 45 | key: _navKey, 46 | pages: _pages, 47 | onPopPage: (route, result) { 48 | if (!route.didPop(result)) { 49 | return false; 50 | } 51 | _pages.removeLast(); 52 | setState(() { 53 | _pages = _pages.toList(); 54 | }); 55 | return true; 56 | }, 57 | ); 58 | } 59 | 60 | @override 61 | void dispose() { 62 | _presenter.dispose(); 63 | super.dispose(); 64 | } 65 | } 66 | 67 | extension on PresenterOutput { 68 | MaterialPage _page(Presenter presenter) { 69 | return switch (this) { 70 | PresenterOutput.showRowDetail => MaterialPage( 71 | child: TodoItemRouter.Assembly(presenter).scene), 72 | _ => MaterialPage( 73 | child: ErrorScene(text: "Output not handled: '$this'")) 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gen/todo_api/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/Presenter/Presenter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | @visibleForTesting 6 | class Presenter with StarterBloc<_PresenterOutput> { 7 | final UseCase _useCase; 8 | final Router _router; 9 | late List _viewModelList; 10 | 11 | Presenter(this._useCase, this._router) { 12 | _useCase.stream.listen((event) { 13 | switch (event) { 14 | case presentModel(:final modelList): 15 | _viewModelList = []; 16 | for (final model in modelList) { 17 | switch (model) { 18 | case stringRow(:final field, :final value): 19 | final fieldName = localizedString(field.name); 20 | _viewModelList.add(RowViewModel(fieldName, value)); 21 | case boolRow(:final field, :final value): 22 | final fieldName = localizedString(field.name); 23 | _viewModelList.add(RowViewModel( 24 | fieldName, localizedString(value == true ? "yes" : "no"))); 25 | case priorityRow(:final field, :final value): 26 | final fieldName = localizedString(field.name); 27 | _viewModelList 28 | .add(RowViewModel(fieldName, localizedString(value.name))); 29 | case dateRow(:final field, :final value): 30 | final fieldName = localizedString(field.name); 31 | _viewModelList 32 | .add(RowViewModel(fieldName, localizedDate(value))); 33 | } 34 | } 35 | emit(showFieldList(_viewModelList)); 36 | break; 37 | } 38 | }); 39 | } 40 | 41 | void eventModeEdit() { 42 | _router.routeEditView(); 43 | } 44 | 45 | RowViewModel row(int index) { 46 | return _viewModelList[index]; 47 | } 48 | 49 | int get rowCount { 50 | return _viewModelList.length; 51 | } 52 | 53 | String get editLabel => localizedString("edit"); 54 | 55 | @override 56 | void dispose() { 57 | _useCase.dispose(); 58 | super.dispose(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/Repository/Network/NetworkExceptionGuard.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:todo_api/api.dart'; 4 | 5 | import '../Abstraction/NetworkIssue.dart'; 6 | import '../Abstraction/LongResult.dart' as Long; 7 | import '../Abstraction/Result.dart'; 8 | 9 | mixin ExceptionGuard { 10 | Future> exceptionGuard(Future func()) async { 11 | try { 12 | final result = await func(); 13 | return success(result); 14 | } on ApiException catch (ex) { 15 | if (ex.code == 400 && 16 | ex.message != null && 17 | ex.message!.startsWith('Socket operation failed:')) 18 | return networkIssue(NetworkIssue.noNetwork); 19 | return failure("${ex.code} ${ex.message}"); 20 | } catch (ex) { 21 | return failure(ex.toString()); 22 | } 23 | } 24 | 25 | Future> 26 | longExceptionGuard(Future func(), 27 | {required DomainIssue? domainFilter(Exception ex)}) async { 28 | try { 29 | final result = await func(); 30 | return Long.success(result); 31 | } on ApiException catch (ex) { 32 | if (ex.code == 400 && 33 | ex.message != null && 34 | ex.message!.startsWith('Socket operation failed:')) 35 | return Long.networkIssue(NetworkIssue.noNetwork); 36 | final domainIssue = domainFilter(ex); 37 | if (domainIssue != null) return Long.domainIssue(domainIssue); 38 | return Long.failure("${ex.code} ${ex.message}"); 39 | } on DomainIssueException catch (ex) { 40 | final domainIssue = domainFilter(ex); 41 | if (domainIssue != null) return Long.domainIssue(domainIssue); 42 | return Long.failure( 43 | "Domain issue processing for ${ex.domainIssue} was not defined"); 44 | } catch (ex) { 45 | return Long.failure(ex.toString()); 46 | } 47 | } 48 | } 49 | 50 | class DomainIssueException implements Exception { 51 | final DomainIssue domainIssue; 52 | DomainIssueException(this.domainIssue); 53 | } 54 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemRouter/View/Scene.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemRouter.dart'; 4 | 5 | @visibleForTesting 6 | class Scene extends StatefulWidget { 7 | final Presenter _presenter; 8 | 9 | Scene(this._presenter); 10 | 11 | @override 12 | State createState() => _SceneState(); 13 | } 14 | 15 | class _SceneState extends State { 16 | late final Presenter _presenter; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | this._presenter = widget._presenter; 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final platform = Theme.of(context).platform; 27 | return BlocBuilder( 28 | bloc: _presenter, 29 | builder: (context, data) { 30 | final body = switch (data) { 31 | showLoading() => FullScreenLoadingIndicator(), 32 | showDisplayView() => TodoItemDisplay.Assembly(_presenter).scene, 33 | showEditView() => TodoItemEdit.Assembly(_presenter).scene, 34 | showMessageView(:final message) => Center(child: Text(message)) 35 | }; 36 | 37 | final decoratedScene = (body is ActionDecoratedScene) 38 | ? body as ActionDecoratedScene 39 | : null; 40 | return Scaffold( 41 | appBar: AppBar( 42 | iconTheme: IconThemeData(color: Colors.white), 43 | title: decoratedScene?.title ?? 44 | Text( 45 | localizedString('todo'), 46 | style: TextStyle(color: Colors.white), 47 | ), 48 | backgroundColor: Colors.lightGreen, 49 | elevation: platform == TargetPlatform.iOS ? 0.0 : 4.0, 50 | actions: decoratedScene?.actions, 51 | leading: decoratedScene?.leading, 52 | ), 53 | body: AnimatedSwitcher( 54 | duration: Duration( 55 | milliseconds: 750), 56 | child: body), 57 | ); 58 | }); 59 | } 60 | 61 | @override 62 | void dispose() { 63 | _presenter.dispose(); 64 | super.dispose(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gen/todo_api/README.md: -------------------------------------------------------------------------------- 1 | # todo_api 2 | This is the Todo Server based on the OpenAPI 3.0 specification. 3 | 4 | This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: 5 | 6 | - API version: 1.0.11 7 | - Build package: org.openapitools.codegen.languages.DartClientCodegen 8 | 9 | ## Requirements 10 | 11 | Dart 2.12 or later 12 | 13 | ## Installation & Usage 14 | 15 | ### Github 16 | If this Dart package is published to Github, add the following dependency to your pubspec.yaml 17 | ``` 18 | dependencies: 19 | todo_api: 20 | git: https://github.com/GIT_USER_ID/GIT_REPO_ID.git 21 | ``` 22 | 23 | ### Local 24 | To use the package in your local drive, add the following dependency to your pubspec.yaml 25 | ``` 26 | dependencies: 27 | todo_api: 28 | path: /path/to/todo_api 29 | ``` 30 | 31 | ## Tests 32 | 33 | TODO 34 | 35 | ## Getting Started 36 | 37 | Please follow the [installation procedure](#installation--usage) and then run the following: 38 | 39 | ```dart 40 | import 'package:todo_api/api.dart'; 41 | 42 | 43 | final api_instance = TodoApi(); 44 | final todoParams = TodoParams(); // TodoParams | Create a new todo 45 | 46 | try { 47 | final result = api_instance.createTodo(todoParams); 48 | print(result); 49 | } catch (e) { 50 | print('Exception when calling TodoApi->createTodo: $e\n'); 51 | } 52 | 53 | ``` 54 | 55 | ## Documentation for API Endpoints 56 | 57 | All URIs are relative to *https://todo-backend-lyle.fly.dev/api* 58 | 59 | Class | Method | HTTP request | Description 60 | ------------ | ------------- | ------------- | ------------- 61 | *TodoApi* | [**createTodo**](doc//TodoApi.md#createtodo) | **POST** /todo | Add a new todo 62 | *TodoApi* | [**deleteTodo**](doc//TodoApi.md#deletetodo) | **DELETE** /todo/{todoId} | Deletes a todo 63 | *TodoApi* | [**getAllTodos**](doc//TodoApi.md#getalltodos) | **GET** /todo | get all todos 64 | *TodoApi* | [**getTodoById**](doc//TodoApi.md#gettodobyid) | **GET** /todo/{todoId} | Find todo by ID 65 | *TodoApi* | [**updateTodo**](doc//TodoApi.md#updatetodo) | **PUT** /todo/{todoId} | Updates a todo 66 | 67 | 68 | ## Documentation For Models 69 | 70 | - [PriorityEnum](doc//PriorityEnum.md) 71 | - [TodoParams](doc//TodoParams.md) 72 | - [TodoResponse](doc//TodoResponse.md) 73 | 74 | 75 | ## Documentation For Authorization 76 | 77 | Endpoints do not require authorization. 78 | 79 | 80 | ## Author 81 | 82 | lyle@cellarpoint.com 83 | 84 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.example.flutter_todo" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.flutter_todo" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/UseCase/UseCase.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | @visibleForTesting 6 | class UseCase with StarterBloc<_UseCaseOutput> { 7 | final EntityGateway _entityGateway; 8 | 9 | List _todoList = []; 10 | bool _showCompleted = false; 11 | final PublishSubject _toDoSceneRefreshSubject; 12 | final BehaviorSubject _itemStartModeSubject; 13 | 14 | UseCase(this._entityGateway, this._toDoSceneRefreshSubject, 15 | this._itemStartModeSubject) { 16 | _toDoSceneRefreshSubject.listen((_) async => await _refresh()); 17 | unawaited(_refresh()); 18 | } 19 | 20 | Future _refresh() async { 21 | emit(presentLoading()); 22 | final result = await _entityGateway.todoManager.all(); 23 | switch (result) { 24 | case success(:final data): 25 | _todoList = data; 26 | _refreshPresentation(); 27 | case failure(:final description): 28 | assert(false, "Unexpected error: $description"); 29 | case networkIssue(:final issue): 30 | assert(false, "Unexpected Network issue: reason $issue"); 31 | } 32 | } 33 | 34 | void _refreshPresentation() { 35 | emit(presentModel(PresentationModel.fromEntities(_todoList, _showCompleted))); 36 | } 37 | 38 | Future eventCompleted(bool completed, int index) async { 39 | final result = await _entityGateway.todoManager 40 | .completed(_todoList[index].id, completed); 41 | switch (result) { 42 | case success(:final data): 43 | _todoList[index] = data; 44 | _refreshPresentation(); 45 | case failure(:final description): 46 | assert(false, "Unexpected error: $description"); 47 | case networkIssue(:final issue): 48 | assert(false, "Unexpected Network issue: reason $issue"); 49 | } 50 | } 51 | 52 | Future eventDelete(int index) async { 53 | final result = await _entityGateway.todoManager.delete(_todoList[index].id); 54 | switch (result) { 55 | case success(): 56 | _todoList.removeAt(index); 57 | _refreshPresentation(); 58 | case failure(:final description): 59 | assert(false, "Unexpected error: $description"); 60 | case networkIssue(:final issue): 61 | assert(false, "Unexpected Network issue: reason $issue"); 62 | } 63 | } 64 | 65 | void eventItemSelected(int index) { 66 | final id = _todoList[index].id; 67 | _itemStartModeSubject.value = TodoItemStartModeUpdate(id); 68 | emit(presentItemDetail()); 69 | } 70 | 71 | void eventCreate() { 72 | _itemStartModeSubject.value = TodoItemStartModeCreate(); 73 | emit(presentItemDetail()); 74 | } 75 | 76 | void eventShowCompleted(bool completed) { 77 | _showCompleted = completed; 78 | _refreshPresentation(); 79 | } 80 | 81 | @override 82 | void dispose() { 83 | super.dispose(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/Repository/Ephemeral/EphemeralTodoManager.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:uuid/uuid.dart'; 4 | 5 | import '../Abstraction/TodoManager.dart'; 6 | import '../Abstraction/Result.dart'; 7 | import '../Abstraction/TodoValues.dart'; 8 | import '../Entities/Todo.dart'; 9 | import 'EphemeralData.dart'; 10 | import 'EphemeralTodo.dart'; 11 | 12 | 13 | class EphemeralTodoManager extends TodoManager { 14 | 15 | Future>> all() => Future.value(success(TodoEphemeralData.data.map((testTodo) => testTodo.toTodo).toList())); 16 | 17 | Future> completed(String id, bool completed) { 18 | try { 19 | final todo = _findTodo(id); 20 | todo.completed = completed; 21 | return Future.value(success(todo.toTodo)); 22 | } 23 | catch (reason) { 24 | return Future.value(failure(reason.toString())); 25 | } 26 | } 27 | 28 | Future> create(TodoValues values) { 29 | final todo = EphemeralTodo.fromTodoValues(Uuid().v1(), values); 30 | TodoEphemeralData.data.add(todo); 31 | return Future.value(success(todo.toTodo)); 32 | } 33 | 34 | Future> update(String id, TodoValues values) { 35 | 36 | try { 37 | final testTodo = _findTodo(id); 38 | testTodo.fromValues(values); 39 | return Future.value(success(testTodo.toTodo)); 40 | } 41 | catch (reason) { 42 | return Future.value(failure(reason.toString())); 43 | } 44 | } 45 | 46 | Future> fetch(String id) { 47 | try { 48 | final testTodo = _findTodo(id); 49 | return Future.value(success(testTodo.toTodo)); 50 | } 51 | catch (reason) { 52 | return Future.value(failure(reason.toString())); 53 | } 54 | } 55 | 56 | Future> delete(String id) { 57 | 58 | try { 59 | final index = _findTodoIndex(id); 60 | final testTodo = TodoEphemeralData.data[index]; 61 | TodoEphemeralData.data.remove(index); 62 | return Future.value(success(testTodo.toTodo)); 63 | } 64 | catch (reason) { 65 | return Future.value(failure(reason.toString())); 66 | } 67 | } 68 | 69 | 70 | EphemeralTodo _findTodo(String id) { 71 | for(final entity in TodoEphemeralData.data) { 72 | if(entity.id == id) { 73 | return entity; 74 | } 75 | } 76 | throw "id $id not found"; 77 | } 78 | 79 | int _findTodoIndex(String id) { 80 | int index = 0; 81 | for (final entity in TodoEphemeralData.data) { 82 | if(entity.id == id) { 83 | return index; 84 | } 85 | index++; 86 | } 87 | throw "id $id not found"; 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /lib/Scenes/Common/TodoExclusive.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | 6 | class TodoExclusive extends StatefulWidget { 7 | 8 | final int value; 9 | final void Function(int?) onValueChanged; 10 | final List itemNames; 11 | 12 | TodoExclusive({required this.value, required this.itemNames, required this.onValueChanged}); 13 | 14 | @override 15 | State createState() => TodoExclusiveState(); 16 | 17 | } 18 | 19 | class TodoExclusiveState extends State { 20 | 21 | int? _value; 22 | late Map _cupertinoItems; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _value = widget.value; 28 | _cupertinoItems = _makeSegmentedControlChildren(widget.itemNames); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | 34 | final platform = Theme.of(context).platform; 35 | 36 | void onChanged(int? value) { 37 | setState(() { 38 | _value = value; 39 | }); 40 | widget.onValueChanged(value); 41 | } 42 | 43 | return (platform == TargetPlatform.iOS) 44 | ? ConstrainedBox( 45 | constraints: BoxConstraints.tightFor(height: 44), 46 | child: CupertinoSegmentedControl( 47 | groupValue: _value, 48 | // selectedColor: Colors.green, 49 | // pressedColor: Colors.red, 50 | // unselectedColor: Colors.white, 51 | // borderColor: Colors.green, 52 | 53 | children: _cupertinoItems, 54 | onValueChanged: onChanged, 55 | ), 56 | ) 57 | : Column( 58 | children: _makeRadioTiles(_value, widget.itemNames, onChanged) 59 | ); 60 | } 61 | 62 | 63 | Map _makeSegmentedControlChildren(List titles) { 64 | 65 | final children = Map(); 66 | var index = 0; 67 | for(var title in titles) { 68 | children[index] = Text(title, style: TextStyle(fontSize: 13)); 69 | index++; 70 | } 71 | return children; 72 | } 73 | 74 | List _makeRadioTiles(int? groupValue, List titles, void Function(int?) onChanged) { 75 | 76 | final List children = []; 77 | var index = 0; 78 | for(var title in titles) { 79 | final tile = RadioListTile( 80 | value: index, 81 | groupValue: groupValue, 82 | title: Text(title, 83 | style: TextStyle(fontSize: 15) 84 | ), 85 | onChanged: onChanged, 86 | ); 87 | children.add(tile); 88 | index++; 89 | } 90 | return children; 91 | 92 | } 93 | 94 | 95 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_todo 2 | description: Todo in Clean Style 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | publish_to: none 16 | 17 | environment: 18 | sdk: ">=3.0.0 <4.0.0" 19 | 20 | dependencies: 21 | flutter: 22 | sdk: flutter 23 | path: ^1.8.3 24 | rxdart: ^0.27.7 25 | sqflite: ^2.3.0 26 | uuid: ^3.0.7 27 | 28 | todo_api: 29 | path: gen/todo_api 30 | 31 | 32 | flutter_localizations: 33 | sdk: flutter 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.5 38 | 39 | dev_dependencies: 40 | flutter_test: 41 | sdk: flutter 42 | 43 | # For information on the generic Dart part of this file, see the 44 | # following page: https://www.dartlang.org/tools/pub/pubspec 45 | 46 | # The following section is specific to Flutter. 47 | flutter: 48 | 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add assets to your application, add an assets section, like this: 55 | assets: 56 | - images/checkbox_checked.png 57 | - images/checkbox_unchecked.png 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.io/assets-and-images/#resolution-aware. 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.io/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.io/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /gen/todo_api/lib/model/priority_enum.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | /// high, medium, low or none 14 | class PriorityEnum { 15 | /// Instantiate a new enum with the provided [value]. 16 | const PriorityEnum._(this.value); 17 | 18 | /// The underlying value of this enum member. 19 | final String value; 20 | 21 | @override 22 | String toString() => value; 23 | 24 | String toJson() => value; 25 | 26 | static const high = PriorityEnum._(r'high'); 27 | static const medium = PriorityEnum._(r'medium'); 28 | static const low = PriorityEnum._(r'low'); 29 | static const none = PriorityEnum._(r'none'); 30 | 31 | /// List of all possible values in this [enum][PriorityEnum]. 32 | static const values = [ 33 | high, 34 | medium, 35 | low, 36 | none, 37 | ]; 38 | 39 | static PriorityEnum? fromJson(dynamic value) => PriorityEnumTypeTransformer().decode(value); 40 | 41 | static List listFromJson(dynamic json, {bool growable = false,}) { 42 | final result = []; 43 | if (json is List && json.isNotEmpty) { 44 | for (final row in json) { 45 | final value = PriorityEnum.fromJson(row); 46 | if (value != null) { 47 | result.add(value); 48 | } 49 | } 50 | } 51 | return result.toList(growable: growable); 52 | } 53 | } 54 | 55 | /// Transformation class that can [encode] an instance of [PriorityEnum] to String, 56 | /// and [decode] dynamic data back to [PriorityEnum]. 57 | class PriorityEnumTypeTransformer { 58 | factory PriorityEnumTypeTransformer() => _instance ??= const PriorityEnumTypeTransformer._(); 59 | 60 | const PriorityEnumTypeTransformer._(); 61 | 62 | String encode(PriorityEnum data) => data.value; 63 | 64 | /// Decodes a [dynamic value][data] to a PriorityEnum. 65 | /// 66 | /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, 67 | /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] 68 | /// cannot be decoded successfully, then an [UnimplementedError] is thrown. 69 | /// 70 | /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, 71 | /// and users are still using an old app with the old code. 72 | PriorityEnum? decode(dynamic data, {bool allowNull = true}) { 73 | if (data != null) { 74 | switch (data) { 75 | case r'high': return PriorityEnum.high; 76 | case r'medium': return PriorityEnum.medium; 77 | case r'low': return PriorityEnum.low; 78 | case r'none': return PriorityEnum.none; 79 | default: 80 | if (!allowNull) { 81 | throw ArgumentError('Unknown enum value to decode: $data'); 82 | } 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | /// Singleton [PriorityEnumTypeTransformer] instance. 89 | static PriorityEnumTypeTransformer? _instance; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /lib/Scenes/Common/CupertinoPopoverDatePicker.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class CupertinoPopoverDatePicker { 7 | 8 | late DateTime _currentPickerValue; 9 | 10 | void show(DateTime time, BuildContext context, Function(DateTime) onSet, String setLabel) { 11 | _currentPickerValue = time; 12 | showCupertinoModalPopup( 13 | context: context, 14 | builder: (BuildContext context) { 15 | return _bottomPicker( 16 | height: 217.0 + 44, 17 | picker: Column( 18 | children: [ 19 | Container( 20 | height: 44, 21 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.end, 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | CupertinoButton( 27 | padding: EdgeInsets.all(0), //fix weird bug 28 | child: Text(setLabel), 29 | onPressed: () { 30 | onSet(_currentPickerValue); 31 | Navigator.of(context).pop(); 32 | }, 33 | ), 34 | ] 35 | ), 36 | ), 37 | Divider(height: 1,), 38 | DefaultTextStyle( 39 | style: const TextStyle( 40 | color: CupertinoColors.black, 41 | fontSize: 22.0, 42 | ), 43 | child: Expanded( 44 | child: CupertinoDatePicker( 45 | mode: CupertinoDatePickerMode.date, 46 | minimumYear: DateTime.now().isBefore(time) ? DateTime.now().year : time.year, 47 | initialDateTime: time, 48 | onDateTimeChanged: (DateTime newDateTime) { 49 | _currentPickerValue = newDateTime; 50 | }, 51 | ), 52 | ), 53 | ), 54 | ] 55 | ), 56 | ); 57 | }, 58 | ); 59 | } 60 | 61 | Widget _bottomPicker({required double height, required Widget picker}) { 62 | return Container( 63 | height: height, 64 | padding: const EdgeInsets.only(top: 6.0), 65 | color: CupertinoColors.white, 66 | child: GestureDetector( 67 | // Blocks taps from propagating to the modal sheet and popping. 68 | onTap: () {}, 69 | child: SafeArea( 70 | top: false, 71 | child: picker, 72 | ), 73 | ), 74 | ); 75 | } 76 | 77 | 78 | 79 | } -------------------------------------------------------------------------------- /lib/Scenes/Common/Localization/TodoLocalizationsDelegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | class TodoLocalizations { 6 | TodoLocalizations(this.locale); 7 | 8 | final Locale locale; 9 | 10 | static TodoLocalizations? of(BuildContext context) { 11 | return Localizations.of(context, TodoLocalizations); 12 | } 13 | 14 | static Map> _localizedValues = { 15 | 'en': { 16 | 'enterATitle': 'Enter a Title', 17 | 'title': 'Title', 18 | 'note': 'Note', 19 | 'completeBy': 'Complete By', 20 | 'priority': 'Priority', 21 | 'completed': 'Completed', 22 | 23 | 'yes': 'Yes', 24 | 'no': 'No', 25 | 26 | 'high': 'High', 27 | 'medium': 'Medium', 28 | 'low': 'Low', 29 | 'none': 'None', 30 | 31 | 'save': 'Save', 32 | 'edit': 'Edit', 33 | 'set': 'Set', 34 | 'create': 'Create', 35 | 36 | 'todo': 'To Do', 37 | 'todoList': 'To Do List', 38 | 39 | 'todoNotFound': "Todo with Id: '%@' was not found", 40 | 'titleRequiredTitle': 'Title is Empty', 41 | 'titleRequiredMessage': 'Enter a Title', 42 | }, 43 | 'fr': { 44 | 'enterATitle': 'Entrez un titre', 45 | 'title': 'Titre', 46 | 'note': 'Remarque', 47 | 'completeBy': 'Compléter par', 48 | 'priority': 'Priorité', 49 | 'completed': 'Terminé', 50 | 51 | 'yes': 'Oui', 52 | 'no': 'Non', 53 | 54 | 'high': 'Haute', 55 | 'medium': 'Moyen', 56 | 'low': 'Faible', 57 | 'none': 'Aucun', 58 | 59 | 'save': 'Enregistrer', 60 | 'edit': 'Modifier', 61 | 'set': 'Ensemble', 62 | 'create': 'Créer', 63 | 64 | 'todo': 'Faire', 65 | 'todoList': 'Liste de choses à faire', 66 | 67 | 'todoNotFound': "Todo avec Id: '%@' n'a pas été trouvé", 68 | 'titleRequiredTitle': 'Le Titre est Vide', 69 | 'itleRequiredMessage': 'Entrez un Titre', 70 | }, 71 | }; 72 | 73 | String localize(String string) { 74 | final localized = _localizedValues[locale.languageCode]?[string]; 75 | if (localized == null) 76 | throw Exception("localize: no entry for language code '${locale.languageCode}', key: '$string'"); 77 | return localized; 78 | } 79 | } 80 | 81 | class TodoLocalizationsDelegate extends LocalizationsDelegate { 82 | const TodoLocalizationsDelegate(); 83 | 84 | static late String _localeString; 85 | static late TodoLocalizations _todoLocalizations; 86 | 87 | @override 88 | bool isSupported(Locale locale) => ['en', 'fr'].contains(locale.languageCode); 89 | 90 | @override 91 | Future load(Locale locale) async { 92 | 93 | _localeString = locale.languageCode + "_" + (locale.countryCode ?? 'US'); 94 | _todoLocalizations = TodoLocalizations(locale); 95 | return SynchronousFuture(_todoLocalizations); 96 | } 97 | 98 | @override 99 | bool shouldReload(TodoLocalizationsDelegate old) => false; 100 | 101 | static DateFormat get outboundDateFormatter => DateFormat.yMMMd(_localeString); 102 | static String localize(String string) { 103 | return _todoLocalizations.localize(string); 104 | } 105 | } -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemDisplay/View/Scene.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemDisplay.dart'; 4 | 5 | @visibleForTesting 6 | class Scene extends StatefulWidget implements ActionDecoratedScene { 7 | final Presenter _presenter; 8 | 9 | Scene(this._presenter) : super(key: Key("Display")); 10 | 11 | @override 12 | State createState() => _SceneState(); 13 | 14 | @override 15 | Widget? get leading { 16 | return null; 17 | } 18 | 19 | @override 20 | Widget get title { 21 | return Text(localizedString('todo'), style: TextStyle(color: Colors.white)); 22 | } 23 | 24 | @override 25 | List get actions { 26 | return [ 27 | _EditButton( 28 | label: _presenter.editLabel, onPressed: _presenter.eventModeEdit) 29 | ]; 30 | } 31 | } 32 | 33 | class _SceneState extends State { 34 | late final Presenter _presenter; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | this._presenter = widget._presenter; 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return BlocBuilder( 45 | bloc: _presenter, 46 | builder: (context, output) { 47 | return switch (output) { 48 | showFieldList(:final model) => ListView.builder( 49 | itemCount: model.length, 50 | itemBuilder: (BuildContext context, int i) { 51 | return _Row(row: model[i]); 52 | }, 53 | ) 54 | }; 55 | }); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | _presenter.dispose(); 61 | super.dispose(); 62 | } 63 | } 64 | 65 | class _Row extends StatelessWidget { 66 | const _Row({ 67 | Key? key, 68 | required this.row, 69 | }) : super(key: key); 70 | 71 | final RowViewModel row; 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | return Column( 76 | children: [ 77 | Padding( 78 | padding: const EdgeInsets.all(12.0), 79 | child: Row( 80 | crossAxisAlignment: CrossAxisAlignment.start, 81 | children: [ 82 | ConstrainedBox( 83 | constraints: BoxConstraints(minWidth: 130), 84 | child: Text( 85 | "${row.fieldName}:", 86 | style: TextStyle(fontSize: 17), 87 | ), 88 | ), 89 | Expanded( 90 | child: Text( 91 | row.value, 92 | softWrap: true, 93 | style: TextStyle(fontSize: 17), 94 | ), 95 | ), 96 | ], 97 | ), 98 | ), 99 | Divider( 100 | height: 1, 101 | color: Colors.blue, 102 | ), 103 | ], 104 | ); 105 | } 106 | } 107 | 108 | class _EditButton extends StatelessWidget { 109 | final String label; 110 | final void Function() onPressed; 111 | const _EditButton({required this.label, required this.onPressed}); 112 | 113 | @override 114 | Widget build(BuildContext context) { 115 | return (Platform.isIOS) 116 | ? CupertinoButton( 117 | child: Text( 118 | label, 119 | style: TextStyle(fontSize: 18, color: Colors.white), 120 | ), 121 | onPressed: onPressed, 122 | ) 123 | : IconButton( 124 | icon: Icon(Icons.edit), 125 | color: Colors.white, 126 | onPressed: onPressed, 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gen/todo_api/lib/api_helper.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class QueryParam { 14 | const QueryParam(this.name, this.value); 15 | 16 | final String name; 17 | final String value; 18 | 19 | @override 20 | String toString() => '${Uri.encodeQueryComponent(name)}=${Uri.encodeQueryComponent(value)}'; 21 | } 22 | 23 | // Ported from the Java version. 24 | Iterable _queryParams(String collectionFormat, String name, dynamic value,) { 25 | // Assertions to run in debug mode only. 26 | assert(name.isNotEmpty, 'Parameter cannot be an empty string.'); 27 | 28 | final params = []; 29 | 30 | if (value is List) { 31 | if (collectionFormat == 'multi') { 32 | return value.map((dynamic v) => QueryParam(name, parameterToString(v)),); 33 | } 34 | 35 | // Default collection format is 'csv'. 36 | if (collectionFormat.isEmpty) { 37 | collectionFormat = 'csv'; // ignore: parameter_assignments 38 | } 39 | 40 | final delimiter = _delimiters[collectionFormat] ?? ','; 41 | 42 | params.add(QueryParam(name, value.map(parameterToString).join(delimiter),)); 43 | } else if (value != null) { 44 | params.add(QueryParam(name, parameterToString(value))); 45 | } 46 | 47 | return params; 48 | } 49 | 50 | /// Format the given parameter object into a [String]. 51 | String parameterToString(dynamic value) { 52 | if (value == null) { 53 | return ''; 54 | } 55 | if (value is DateTime) { 56 | return value.toUtc().toIso8601String(); 57 | } 58 | if (value is PriorityEnum) { 59 | return PriorityEnumTypeTransformer().encode(value).toString(); 60 | } 61 | return value.toString(); 62 | } 63 | 64 | /// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json' 65 | /// content type. Otherwise, returns the decoded body as decoded by dart:http package. 66 | Future _decodeBodyBytes(Response response) async { 67 | final contentType = response.headers['content-type']; 68 | return contentType != null && contentType.toLowerCase().startsWith('application/json') 69 | ? response.bodyBytes.isEmpty ? '' : utf8.decode(response.bodyBytes) 70 | : response.body; 71 | } 72 | 73 | /// Returns a valid [T] value found at the specified Map [key], null otherwise. 74 | T? mapValueOfType(dynamic map, String key) { 75 | final dynamic value = map is Map ? map[key] : null; 76 | return value is T ? value : null; 77 | } 78 | 79 | /// Returns a valid Map found at the specified Map [key], null otherwise. 80 | Map? mapCastOfType(dynamic map, String key) { 81 | final dynamic value = map is Map ? map[key] : null; 82 | return value is Map ? value.cast() : null; 83 | } 84 | 85 | /// Returns a valid [DateTime] found at the specified Map [key], null otherwise. 86 | DateTime? mapDateTime(dynamic map, String key, [String? pattern]) { 87 | final dynamic value = map is Map ? map[key] : null; 88 | if (value != null) { 89 | int? millis; 90 | if (value is int) { 91 | millis = value; 92 | } else if (value is String) { 93 | if (_isEpochMarker(pattern)) { 94 | millis = int.tryParse(value); 95 | } else { 96 | return DateTime.tryParse(value); 97 | } 98 | } 99 | if (millis != null) { 100 | return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); 101 | } 102 | } 103 | return null; 104 | } 105 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/View/Cell.dart: -------------------------------------------------------------------------------- 1 | part of '../TodoList.dart'; 2 | 3 | class _Cell extends StatelessWidget { 4 | 5 | final RowViewModel row; 6 | final Presenter presenter; 7 | 8 | _Cell({required this.row, required this.presenter }); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | 13 | return Dismissible( 14 | key: UniqueKey(), 15 | child: GestureDetector( 16 | onTap: () { 17 | presenter.eventItemSelected(row.index); 18 | }, 19 | child: Container( 20 | height: 58, 21 | decoration: BoxDecoration( 22 | color: Colors.white, 23 | border: Border(bottom: BorderSide(color: Colors.grey[300]!)) 24 | ), 25 | child: Container( 26 | padding: EdgeInsets.symmetric(vertical: 6), 27 | child: Row( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Container( 31 | padding: EdgeInsets.all(4), 32 | child: _CheckBox( 33 | checked: row.completed, 34 | onPressed: (checked) { 35 | presenter.eventCompleted(checked, row.index); 36 | }, 37 | ), 38 | ), 39 | Container( 40 | padding: EdgeInsets.only(right: 4), 41 | width: 30, 42 | child: Text(row.priority, 43 | style: _textStylePriority(), 44 | textAlign: TextAlign.end, 45 | ) 46 | ), 47 | Column( 48 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: [ 51 | Text(row.title, style: _textStyleTitle()), 52 | Text(row.completeBy, style: _textStyleDetail()), 53 | ], 54 | ), 55 | ] 56 | ), 57 | ) 58 | ), 59 | ), 60 | onDismissed: (direction) { 61 | presenter.eventDelete(row.index); 62 | }, 63 | background: _dismissReveal(context), 64 | ); 65 | } 66 | 67 | TextStyle _textStylePriority() { 68 | return TextStyle( 69 | color: Colors.redAccent, 70 | fontSize: 17, 71 | ); 72 | } 73 | 74 | TextStyle _textStyleTitle() { 75 | return TextStyle( 76 | color: Colors.black, 77 | fontSize: 17, 78 | ); 79 | } 80 | 81 | TextStyle _textStyleDetail() { 82 | return TextStyle( 83 | color: Colors.black, 84 | fontSize: 15, 85 | ); 86 | } 87 | 88 | Widget _dismissReveal(BuildContext context) { 89 | return Container( 90 | alignment: Alignment.centerRight, 91 | padding: EdgeInsets.only(right: 16.0), 92 | color: Colors.red, 93 | child: Theme.of(context).platform == TargetPlatform.iOS 94 | ? Text("Delete", 95 | style: TextStyle( 96 | color: Colors.white, 97 | fontSize: 15) 98 | ) 99 | : Icon( 100 | Icons.delete, 101 | color: Colors.white, 102 | ), 103 | ); 104 | } 105 | 106 | 107 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/Repository/Network/NetworkTodoManager.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:todo_api/api.dart'; 4 | import '../Abstraction/TodoManager.dart'; 5 | import '../Abstraction/TodoValues.dart'; 6 | import '../Abstraction/Result.dart'; 7 | import '../Entities/Todo.dart'; 8 | import '../Entities/Priority.dart'; 9 | import 'NetworkClient.dart'; 10 | import 'NetworkExceptionGuard.dart'; 11 | 12 | class NetworkTodoManager extends TodoManager with ExceptionGuard { 13 | final NetworkClient apiClient; 14 | NetworkTodoManager(this.apiClient); 15 | 16 | @override 17 | Future>> all() => exceptionGuard(() async { 18 | final response = await apiClient.getAllTodos(); 19 | if (response == null) throw "No Content"; 20 | return response.map((todoResponse) => todoResponse._todo).toList(); 21 | }); 22 | 23 | @override 24 | Future> completed(String id, bool completed) => 25 | exceptionGuard(() async { 26 | final getResponse = await apiClient.getTodo(id); 27 | if (getResponse == null) throw "No Content"; 28 | final values = getResponse._todo.todoValues; 29 | final newValues = TodoValues( 30 | title: values.title, 31 | note: values.note, 32 | completeBy: values.completeBy, 33 | priority: values.priority, 34 | completed: completed, 35 | ); 36 | final updateResponse = 37 | await apiClient.updateTodo(id, newValues._todoParams); 38 | if (updateResponse == null) throw "No Content"; 39 | return updateResponse._todo; 40 | }); 41 | 42 | @override 43 | Future> create(TodoValues values) => exceptionGuard(() async { 44 | final response = 45 | await apiClient.createTodo(values._todoParams); 46 | if (response == null) throw "No Content"; 47 | return response._todo; 48 | }); 49 | 50 | @override 51 | Future> delete(String id) => exceptionGuard(() async { 52 | final getResponse = await apiClient.getTodo(id); 53 | if (getResponse == null) throw "No Content"; 54 | await apiClient.deleteTodo(id); 55 | return getResponse._todo; 56 | }); 57 | 58 | @override 59 | Future> fetch(String id) => exceptionGuard(() async { 60 | final response = await apiClient.getTodo(id); 61 | if (response == null) throw "No Content"; 62 | return response._todo; 63 | }); 64 | 65 | @override 66 | Future> update(String id, TodoValues values) => 67 | exceptionGuard(() async { 68 | final response = 69 | await apiClient.updateTodo(id, values._todoParams); 70 | if (response == null) throw "No Content"; 71 | return response._todo; 72 | }); 73 | } 74 | 75 | extension on TodoValues { 76 | TodoParams get _todoParams { 77 | return TodoParams( 78 | title: this.title, 79 | note: this.note, 80 | priority: this.priority._priorityEnum, 81 | completeBy: this.completeBy, 82 | completed: this.completed); 83 | } 84 | } 85 | 86 | extension on Priority { 87 | PriorityEnum get _priorityEnum { 88 | return switch (this) { 89 | Priority.high => PriorityEnum.high, 90 | Priority.medium => PriorityEnum.medium, 91 | Priority.low => PriorityEnum.low, 92 | Priority.none => PriorityEnum.none 93 | }; 94 | } 95 | } 96 | 97 | extension on TodoResponse { 98 | Todo get _todo { 99 | return Todo( 100 | id: this.id, 101 | title: this.title, 102 | note: this.note ?? "", 103 | priority: this.priority._priority, 104 | completeBy: this.completeBy, 105 | completed: this.completed); 106 | } 107 | } 108 | 109 | extension on PriorityEnum { 110 | Priority get _priority { 111 | return switch (this) { 112 | PriorityEnum.high => Priority.high, 113 | PriorityEnum.medium => Priority.medium, 114 | PriorityEnum.low => Priority.low, 115 | PriorityEnum.none => Priority.none, 116 | _ => throw ("PriorityEnum must be high, medium, low or none") 117 | }; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/Scenes/TodoList/View/Scene.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoList.dart'; 4 | 5 | @visibleForTesting 6 | class Scene extends StatefulWidget { 7 | final Presenter _presenter; 8 | Scene(this._presenter); 9 | 10 | @override 11 | State createState() => _SceneState(); 12 | } 13 | 14 | class _SceneState extends State { 15 | late final Presenter _presenter; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | this._presenter = widget._presenter; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final platform = Theme.of(context).platform; 26 | return BlocBuilder( 27 | bloc: _presenter, 28 | builder: (context, output) { 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: Text(localizedString("todoList"), 32 | style: TextStyle(color: Colors.white)), 33 | centerTitle: platform == TargetPlatform.iOS, 34 | backgroundColor: Colors.lightGreen, 35 | elevation: platform == TargetPlatform.iOS ? 0.0 : 4.0, 36 | actions: [ 37 | _CheckedButton( 38 | enabled: output == showLoading() ? false : true, 39 | onPressed: _presenter.eventShowCompleted), 40 | _AddTodoButton( 41 | enabled: output == showLoading() ? false : true, 42 | onPressed: _presenter.eventCreate, 43 | ), 44 | ], 45 | ), 46 | body: SafeArea( 47 | child: switch (output) { 48 | showLoading() => FullScreenLoadingIndicator(), 49 | showModel(:final model) => ListView.builder( 50 | itemCount: model.rows.length, 51 | itemBuilder: (context, index) => _Cell( 52 | row: model.rows[index], 53 | presenter: _presenter, 54 | ) 55 | ) 56 | } 57 | ), 58 | ); 59 | } 60 | ); 61 | } 62 | 63 | @override 64 | void dispose() { 65 | _presenter.dispose(); 66 | super.dispose(); 67 | } 68 | } 69 | 70 | class _AddTodoButton extends StatelessWidget { 71 | final VoidCallback onPressed; 72 | final bool enabled; 73 | 74 | const _AddTodoButton({ 75 | required this.enabled, 76 | required this.onPressed, 77 | }); 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | final onPressed = (enabled) ? this.onPressed : null; 82 | final disabledColor = Colors.white60; 83 | double? iconSize; 84 | if (Theme.of(context).platform == TargetPlatform.iOS) { 85 | iconSize = 34; 86 | } 87 | return IconButton( 88 | iconSize: iconSize, 89 | icon: Icon(CupertinoIcons.add), 90 | color: Colors.white, 91 | disabledColor: disabledColor, 92 | onPressed: onPressed, 93 | ); 94 | } 95 | } 96 | 97 | class _CheckedButton extends StatefulWidget { 98 | final void Function(bool) onPressed; 99 | final bool enabled; 100 | 101 | const _CheckedButton({ 102 | required this.enabled, 103 | required this.onPressed, 104 | }); 105 | 106 | @override 107 | State<_CheckedButton> createState() => _CheckedButtonState(); 108 | } 109 | 110 | class _CheckedButtonState extends State<_CheckedButton> { 111 | bool isSelected = false; 112 | 113 | @override 114 | Widget build(BuildContext context) { 115 | final platform = Theme.of(context).platform; 116 | final icon = (platform == TargetPlatform.iOS) 117 | ? Icon(CupertinoIcons.checkmark_alt) 118 | : Icon(Icons.check); 119 | final selectedIcon = (platform == TargetPlatform.iOS) 120 | ? Icon(CupertinoIcons.checkmark_rectangle_fill) 121 | : Icon(Icons.check_box); 122 | return IconButton( 123 | icon: isSelected ? selectedIcon : icon, 124 | color: Colors.white, 125 | disabledColor: Colors.white60, 126 | onPressed: (widget.enabled) 127 | ? () { 128 | setState(() { 129 | isSelected = !isSelected; 130 | }); 131 | widget.onPressed(isSelected); 132 | } 133 | : null, 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /gen/todo_api/lib/model/todo_params.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class TodoParams { 14 | /// Returns a new [TodoParams] instance. 15 | TodoParams({ 16 | required this.title, 17 | this.note, 18 | required this.priority, 19 | this.completeBy, 20 | required this.completed, 21 | }); 22 | 23 | String title; 24 | 25 | /// multiline note 26 | String? note; 27 | 28 | PriorityEnum priority; 29 | 30 | /// todo must be completed by this date 31 | DateTime? completeBy; 32 | 33 | /// todo is completed 34 | bool completed; 35 | 36 | @override 37 | bool operator ==(Object other) => identical(this, other) || other is TodoParams && 38 | other.title == title && 39 | other.note == note && 40 | other.priority == priority && 41 | other.completeBy == completeBy && 42 | other.completed == completed; 43 | 44 | @override 45 | int get hashCode => 46 | // ignore: unnecessary_parenthesis 47 | (title.hashCode) + 48 | (note == null ? 0 : note!.hashCode) + 49 | (priority.hashCode) + 50 | (completeBy == null ? 0 : completeBy!.hashCode) + 51 | (completed.hashCode); 52 | 53 | @override 54 | String toString() => 'TodoParams[title=$title, note=$note, priority=$priority, completeBy=$completeBy, completed=$completed]'; 55 | 56 | Map toJson() { 57 | final json = {}; 58 | json[r'title'] = this.title; 59 | if (this.note != null) { 60 | json[r'note'] = this.note; 61 | } else { 62 | json[r'note'] = null; 63 | } 64 | json[r'priority'] = this.priority; 65 | if (this.completeBy != null) { 66 | json[r'completeBy'] = _dateFormatter.format(this.completeBy!.toUtc()); 67 | } else { 68 | json[r'completeBy'] = null; 69 | } 70 | json[r'completed'] = this.completed; 71 | return json; 72 | } 73 | 74 | /// Returns a new [TodoParams] instance and imports its values from 75 | /// [value] if it's a [Map], null otherwise. 76 | // ignore: prefer_constructors_over_static_methods 77 | static TodoParams? fromJson(dynamic value) { 78 | if (value is Map) { 79 | final json = value.cast(); 80 | 81 | // Ensure that the map contains the required keys. 82 | // Note 1: the values aren't checked for validity beyond being non-null. 83 | // Note 2: this code is stripped in release mode! 84 | assert(() { 85 | requiredKeys.forEach((key) { 86 | assert(json.containsKey(key), 'Required key "TodoParams[$key]" is missing from JSON.'); 87 | assert(json[key] != null, 'Required key "TodoParams[$key]" has a null value in JSON.'); 88 | }); 89 | return true; 90 | }()); 91 | 92 | return TodoParams( 93 | title: mapValueOfType(json, r'title')!, 94 | note: mapValueOfType(json, r'note'), 95 | priority: PriorityEnum.fromJson(json[r'priority'])!, 96 | completeBy: mapDateTime(json, r'completeBy', r''), 97 | completed: mapValueOfType(json, r'completed')!, 98 | ); 99 | } 100 | return null; 101 | } 102 | 103 | static List listFromJson(dynamic json, {bool growable = false,}) { 104 | final result = []; 105 | if (json is List && json.isNotEmpty) { 106 | for (final row in json) { 107 | final value = TodoParams.fromJson(row); 108 | if (value != null) { 109 | result.add(value); 110 | } 111 | } 112 | } 113 | return result.toList(growable: growable); 114 | } 115 | 116 | static Map mapFromJson(dynamic json) { 117 | final map = {}; 118 | if (json is Map && json.isNotEmpty) { 119 | json = json.cast(); // ignore: parameter_assignments 120 | for (final entry in json.entries) { 121 | final value = TodoParams.fromJson(entry.value); 122 | if (value != null) { 123 | map[entry.key] = value; 124 | } 125 | } 126 | } 127 | return map; 128 | } 129 | 130 | // maps a json object with a list of TodoParams-objects as value to a dart map 131 | static Map> mapListFromJson(dynamic json, {bool growable = false,}) { 132 | final map = >{}; 133 | if (json is Map && json.isNotEmpty) { 134 | // ignore: parameter_assignments 135 | json = json.cast(); 136 | for (final entry in json.entries) { 137 | map[entry.key] = TodoParams.listFromJson(entry.value, growable: growable,); 138 | } 139 | } 140 | return map; 141 | } 142 | 143 | /// The list of required keys that must be present in a JSON. 144 | static const requiredKeys = { 145 | 'title', 146 | 'priority', 147 | 'completed', 148 | }; 149 | } 150 | 151 | -------------------------------------------------------------------------------- /gen/todo_api/lib/model/todo_response.dart: -------------------------------------------------------------------------------- 1 | // 2 | // AUTO-GENERATED FILE, DO NOT MODIFY! 3 | // 4 | // @dart=2.12 5 | 6 | // ignore_for_file: unused_element, unused_import 7 | // ignore_for_file: always_put_required_named_parameters_first 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: lines_longer_than_80_chars 10 | 11 | part of openapi.api; 12 | 13 | class TodoResponse { 14 | /// Returns a new [TodoResponse] instance. 15 | TodoResponse({ 16 | required this.id, 17 | required this.title, 18 | this.note, 19 | required this.priority, 20 | this.completeBy, 21 | required this.completed, 22 | }); 23 | 24 | String id; 25 | 26 | String title; 27 | 28 | /// multiline note 29 | String? note; 30 | 31 | PriorityEnum priority; 32 | 33 | /// todo must be completed by this date 34 | DateTime? completeBy; 35 | 36 | /// todo is completed 37 | bool completed; 38 | 39 | @override 40 | bool operator ==(Object other) => identical(this, other) || other is TodoResponse && 41 | other.id == id && 42 | other.title == title && 43 | other.note == note && 44 | other.priority == priority && 45 | other.completeBy == completeBy && 46 | other.completed == completed; 47 | 48 | @override 49 | int get hashCode => 50 | // ignore: unnecessary_parenthesis 51 | (id.hashCode) + 52 | (title.hashCode) + 53 | (note == null ? 0 : note!.hashCode) + 54 | (priority.hashCode) + 55 | (completeBy == null ? 0 : completeBy!.hashCode) + 56 | (completed.hashCode); 57 | 58 | @override 59 | String toString() => 'TodoResponse[id=$id, title=$title, note=$note, priority=$priority, completeBy=$completeBy, completed=$completed]'; 60 | 61 | Map toJson() { 62 | final json = {}; 63 | json[r'id'] = this.id; 64 | json[r'title'] = this.title; 65 | if (this.note != null) { 66 | json[r'note'] = this.note; 67 | } else { 68 | json[r'note'] = null; 69 | } 70 | json[r'priority'] = this.priority; 71 | if (this.completeBy != null) { 72 | json[r'completeBy'] = _dateFormatter.format(this.completeBy!.toUtc()); 73 | } else { 74 | json[r'completeBy'] = null; 75 | } 76 | json[r'completed'] = this.completed; 77 | return json; 78 | } 79 | 80 | /// Returns a new [TodoResponse] instance and imports its values from 81 | /// [value] if it's a [Map], null otherwise. 82 | // ignore: prefer_constructors_over_static_methods 83 | static TodoResponse? fromJson(dynamic value) { 84 | if (value is Map) { 85 | final json = value.cast(); 86 | 87 | // Ensure that the map contains the required keys. 88 | // Note 1: the values aren't checked for validity beyond being non-null. 89 | // Note 2: this code is stripped in release mode! 90 | assert(() { 91 | requiredKeys.forEach((key) { 92 | assert(json.containsKey(key), 'Required key "TodoResponse[$key]" is missing from JSON.'); 93 | assert(json[key] != null, 'Required key "TodoResponse[$key]" has a null value in JSON.'); 94 | }); 95 | return true; 96 | }()); 97 | 98 | return TodoResponse( 99 | id: mapValueOfType(json, r'id')!, 100 | title: mapValueOfType(json, r'title')!, 101 | note: mapValueOfType(json, r'note'), 102 | priority: PriorityEnum.fromJson(json[r'priority'])!, 103 | completeBy: mapDateTime(json, r'completeBy', r''), 104 | completed: mapValueOfType(json, r'completed')!, 105 | ); 106 | } 107 | return null; 108 | } 109 | 110 | static List listFromJson(dynamic json, {bool growable = false,}) { 111 | final result = []; 112 | if (json is List && json.isNotEmpty) { 113 | for (final row in json) { 114 | final value = TodoResponse.fromJson(row); 115 | if (value != null) { 116 | result.add(value); 117 | } 118 | } 119 | } 120 | return result.toList(growable: growable); 121 | } 122 | 123 | static Map mapFromJson(dynamic json) { 124 | final map = {}; 125 | if (json is Map && json.isNotEmpty) { 126 | json = json.cast(); // ignore: parameter_assignments 127 | for (final entry in json.entries) { 128 | final value = TodoResponse.fromJson(entry.value); 129 | if (value != null) { 130 | map[entry.key] = value; 131 | } 132 | } 133 | } 134 | return map; 135 | } 136 | 137 | // maps a json object with a list of TodoResponse-objects as value to a dart map 138 | static Map> mapListFromJson(dynamic json, {bool growable = false,}) { 139 | final map = >{}; 140 | if (json is Map && json.isNotEmpty) { 141 | // ignore: parameter_assignments 142 | json = json.cast(); 143 | for (final entry in json.entries) { 144 | map[entry.key] = TodoResponse.listFromJson(entry.value, growable: growable,); 145 | } 146 | } 147 | return map; 148 | } 149 | 150 | /// The list of required keys that must be present in a JSON. 151 | static const requiredKeys = { 152 | 'id', 153 | 'title', 154 | 'priority', 155 | 'completed', 156 | }; 157 | } 158 | 159 | -------------------------------------------------------------------------------- /lib/Repository/Db/SqlLiteTodoManager.dart: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Lyle Resnick. All rights reserved. 2 | 3 | import 'package:sqflite/sqflite.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | import '../Entities/Entity.dart'; 6 | import '../Entities/Priority.dart'; 7 | import '../Entities/Todo.dart'; 8 | import '../Abstraction/Result.dart'; 9 | import '../Abstraction/TodoManager.dart'; 10 | import '../Abstraction/TodoValues.dart'; 11 | import 'SqlLiteManager.dart'; 12 | 13 | class SqlLiteTodoManager implements TodoManager { 14 | final SqlLiteManager db; 15 | 16 | SqlLiteTodoManager(this.db); 17 | 18 | @override 19 | Future>> all() async { 20 | final database = await db.database; 21 | final todos = await database.query('todo'); 22 | 23 | final all = 24 | todos.map((todoResponse) => todoResponse._todo).toList(); 25 | return success(all); 26 | } 27 | 28 | @override 29 | Future> completed(String id, bool completed) async { 30 | try { 31 | final database = await db.database; 32 | final count = await database.update( 33 | 'todo', 34 | { 35 | 'completed': completed ? 1 : 0, 36 | }, 37 | where: "id = ?", 38 | whereArgs: [id], 39 | ); 40 | if (count != 1) return failure("id $id notFound"); 41 | final todo = await _fetchBody(id); 42 | return success(todo); 43 | } catch (reason) { 44 | return failure(reason.toString()); 45 | } 46 | } 47 | 48 | @override 49 | Future> create(TodoValues values) async { 50 | try { 51 | final database = await db.database; 52 | final todo = values._toTodo(id: Uuid().v1()); 53 | await database.insert( 54 | 'todo', 55 | todo._dynamicValueMap, 56 | conflictAlgorithm: ConflictAlgorithm.replace, 57 | ); 58 | return success(todo); 59 | } catch (reason) { 60 | return failure(reason.toString()); 61 | } 62 | } 63 | 64 | @override 65 | Future> delete(String id) async { 66 | try { 67 | final todo = await _fetchBody(id); 68 | final database = await db.database; 69 | final count = await database.delete( 70 | 'todo', 71 | where: "id = ?", 72 | whereArgs: [id], 73 | ); 74 | if (count != 1) return failure("id $id notFound"); 75 | return success(todo); 76 | } catch (reason) { 77 | return failure(reason.toString()); 78 | } 79 | } 80 | 81 | @override 82 | Future> fetch(String id) async { 83 | try { 84 | final todo = await _fetchBody(id); 85 | return success(todo); 86 | } catch (reason) { 87 | return failure(reason.toString()); 88 | } 89 | } 90 | 91 | Future _fetchBody(String id) async { 92 | final database = await db.database; 93 | final todoMap = await database.query( 94 | 'todo', 95 | distinct: true, 96 | where: "id = ?", 97 | whereArgs: [id], 98 | ); 99 | if (todoMap.length == 1) 100 | return todoMap[0]._todo; 101 | else 102 | throw "id $id notFound"; 103 | } 104 | 105 | @override 106 | Future> update(String id, TodoValues values) async { 107 | var todo = Todo( 108 | id: id, 109 | title: values.title, 110 | note: values.note, 111 | completeBy: values.completeBy, 112 | priority: values.priority, 113 | completed: values.completed, 114 | ); 115 | 116 | try { 117 | final database = await db.database; 118 | final count = await database.update( 119 | 'todo', 120 | todo._dynamicValueMap, 121 | where: "id = ?", 122 | whereArgs: [id], 123 | ); 124 | if (count != 1) return failure("id $id notFound"); 125 | return success(todo); 126 | } catch (reason) { 127 | return failure(reason.toString()); 128 | } 129 | } 130 | } 131 | 132 | extension on Map { 133 | static const _idProp = "id"; 134 | static const _titleProp = "title"; 135 | static const _noteProp = "note"; 136 | static const _completeByProp = "completeBy"; 137 | static const _priorityProp = "priority"; 138 | static const _completedProp = "completed"; 139 | 140 | Todo get _todo { 141 | jsonNullCheck(this, [_idProp, _titleProp, _noteProp, _completedProp], 142 | "Todo.fromStringValueDictionary"); 143 | final id = this[_idProp] as String; 144 | final title = this[_titleProp] as String; 145 | final note = this[_noteProp] as String; 146 | final completeBy = this[_completeByProp] as int?; 147 | final priority = this[_priorityProp] ?? "none"; 148 | final completed = this[_completedProp] as int; 149 | 150 | return Todo( 151 | id: id, 152 | title: title, 153 | note: note, 154 | completeBy: (completeBy != null) 155 | ? DateTime.fromMillisecondsSinceEpoch(completeBy) 156 | : null, 157 | priority: PriorityExt.fromString(priority), 158 | completed: (completed == 1)); 159 | } 160 | } 161 | 162 | extension on TodoValues { 163 | Todo _toTodo({required String id}) { 164 | return Todo( 165 | id: id, 166 | title: this.title, 167 | note: this.note, 168 | completeBy: this.completeBy, 169 | priority: this.priority, 170 | completed: this.completed); 171 | } 172 | } 173 | 174 | extension on Todo { 175 | Map get _dynamicValueMap { 176 | return { 177 | 'id': id, 178 | 'title': title, 179 | 'note': note, 180 | 'completeBy': completeBy?.millisecondsSinceEpoch, 181 | 'priority': (priority == Priority.none) ? null : priority.name, 182 | 'completed': completed ? 1 : 0, 183 | }; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/Scenes/TodoItem/TodoItemEdit/UseCase/UseCase.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Lyle Resnick. All rights reserved. 2 | 3 | part of '../TodoItemEdit.dart'; 4 | 5 | class EditingTodo { 6 | String? id; 7 | String title; 8 | String note; 9 | DateTime? completeBy; 10 | Priority priority; 11 | bool completed; 12 | 13 | EditingTodo( 14 | {this.id, 15 | this.title = "", 16 | this.note = "", 17 | this.completeBy, 18 | this.priority = Priority.none, 19 | this.completed = false}); 20 | 21 | factory EditingTodo.fromTodo(Todo todo) { 22 | return EditingTodo( 23 | id: todo.id, 24 | title: todo.title, 25 | note: todo.note, 26 | completeBy: todo.completeBy, 27 | priority: todo.priority, 28 | completed: todo.completed); 29 | } 30 | 31 | TodoValues toTodoValues() { 32 | return TodoValues( 33 | title: this.title, 34 | note: this.note, 35 | completeBy: this.completeBy, 36 | priority: this.priority, 37 | completed: this.completed, 38 | ); 39 | } 40 | } 41 | 42 | @visibleForTesting 43 | class UseCase with StarterBloc<_UseCaseOutput> { 44 | late EditingTodo _editingTodo; 45 | 46 | final PublishSubject _toDoSceneRefreshSubject; 47 | final BehaviorSubject _currentTodoSubject; 48 | final BehaviorSubject _itemStartModeSubject; 49 | late final _UseCaseDelegate _useCaseDelegate; 50 | 51 | UseCase(EntityGateway entityGateway, this._toDoSceneRefreshSubject, this._currentTodoSubject, 52 | this._itemStartModeSubject) { 53 | switch (TodoAppState.instance.itemStartModeSubject.value) { 54 | case TodoItemStartModeCreate(): 55 | _useCaseDelegate = _CreateUseCaseDelegate(this, entityGateway); 56 | case TodoItemStartModeUpdate(): 57 | _useCaseDelegate = _UpdateUseCaseDelegate(this, entityGateway, _currentTodoSubject); 58 | } 59 | 60 | _editingTodo = _useCaseDelegate.initialEditingTodo; 61 | _refreshPresentation(); 62 | } 63 | 64 | void _refreshPresentation( 65 | {ErrorMessage? errorMessage, bool showEditCompleteBy = false, bool isWaiting = false}) { 66 | emit(presentModel(PresentationModel.fromEditingTodo(_editingTodo, 67 | modeTitle: switch (_itemStartModeSubject.value) { 68 | TodoItemStartModeCreate() => 'create', 69 | TodoItemStartModeUpdate() => 'edit', 70 | }, 71 | errorMessage: errorMessage, 72 | showEditCompleteBy: showEditCompleteBy, 73 | isWaiting: isWaiting))); 74 | } 75 | 76 | void eventEditedTitle(String title) { 77 | _editingTodo.title = title; 78 | } 79 | 80 | void eventEditedNote(String note) { 81 | _editingTodo.note = note; 82 | } 83 | 84 | void eventCompleteBy(bool isOn) { 85 | if (isOn) 86 | _editingTodo.completeBy = DateTime.now(); 87 | else 88 | _editingTodo.completeBy = null; 89 | _refreshPresentation(); 90 | } 91 | 92 | void eventEnableEditCompleteBy() { 93 | _refreshPresentation(showEditCompleteBy: true); 94 | } 95 | 96 | void eventEditedCompleteBy(DateTime completeBy) { 97 | _editingTodo.completeBy = completeBy; 98 | _refreshPresentation(); 99 | } 100 | 101 | void eventCompleted(bool completed) { 102 | _editingTodo.completed = completed; 103 | } 104 | 105 | void eventEditedPriority(Priority priority) { 106 | _editingTodo.priority = priority; 107 | } 108 | 109 | Future eventSave() async { 110 | if (_editingTodo.title == "") { 111 | _refreshPresentation(errorMessage: ErrorMessage.titleIsEmpty); 112 | return; 113 | } 114 | _refreshPresentation(isWaiting: true); 115 | await Future.delayed(Duration(milliseconds: 500)); 116 | final result = await _useCaseDelegate.save(_editingTodo); 117 | switch (result) { 118 | case success(:final data): 119 | _currentTodoSubject.value = data; 120 | _toDoSceneRefreshSubject.add(null); 121 | _itemStartModeSubject.value = TodoItemStartModeUpdate(data.id); 122 | emit(presentSaveCompleted()); 123 | case failure(:final description): 124 | assert(false, "Unexpected error: $description"); 125 | case networkIssue(:final issue): 126 | assert(false, "Unexpected Network issue: reason $issue"); 127 | } 128 | } 129 | 130 | void eventCancel() { 131 | _useCaseDelegate.cancel(); 132 | } 133 | 134 | @override 135 | void dispose() { 136 | super.dispose(); 137 | } 138 | } 139 | 140 | abstract interface class _UseCaseDelegate { 141 | final EntityGateway _entityGateway; 142 | final UseCase _useCase; 143 | _UseCaseDelegate(this._useCase, this._entityGateway); 144 | EditingTodo get initialEditingTodo; 145 | Future> save(EditingTodo editingTodo); 146 | void cancel(); 147 | } 148 | 149 | class _CreateUseCaseDelegate extends _UseCaseDelegate { 150 | _CreateUseCaseDelegate(super._useCase, super._entityGateway); 151 | 152 | @override 153 | EditingTodo get initialEditingTodo => EditingTodo(); 154 | 155 | @override 156 | Future> save(EditingTodo editingTodo) { 157 | return _entityGateway.todoManager.create(editingTodo.toTodoValues()); 158 | } 159 | 160 | @override 161 | void cancel() { 162 | _useCase.emit(presentCreateCancelled()); 163 | } 164 | } 165 | 166 | class _UpdateUseCaseDelegate extends _UseCaseDelegate { 167 | final BehaviorSubject _currentTodoSubject; 168 | 169 | _UpdateUseCaseDelegate(super._useCase, super._entityGateway, this._currentTodoSubject); 170 | 171 | @override 172 | EditingTodo get initialEditingTodo => EditingTodo.fromTodo(_currentTodoSubject.value!); 173 | 174 | @override 175 | Future> save(EditingTodo editingTodo) { 176 | return _entityGateway.todoManager.update(editingTodo.id!, editingTodo.toTodoValues()); 177 | } 178 | 179 | @override 180 | void cancel() { 181 | _useCase.emit(presentEditingCancelled()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /api_generator/todo_api.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Swagger Todo - OpenAPI 3.0 4 | description: |- 5 | This is the Todo Server based on the OpenAPI 3.0 specification. 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | email: lyle@cellarpoint.com 9 | license: 10 | name: Apache 2.0 11 | url: http://www.apache.org/licenses/LICENSE-2.0.html 12 | version: 1.0.11 13 | externalDocs: 14 | description: Find out more about Clean Architecture 15 | url: http://lyleresnick.com 16 | servers: 17 | - url: https://todo-backend-lyle.fly.dev/api 18 | tags: 19 | - name: todo 20 | description: Basic Todo 21 | externalDocs: 22 | description: Find out more 23 | url: https://github.com/lyleresnick/FlutterViperTodo 24 | paths: 25 | /todo: 26 | get: 27 | tags: 28 | - todo 29 | summary: get all todos 30 | description: get all todos 31 | operationId: getAllTodos 32 | responses: 33 | '200': 34 | description: Successful operation 35 | content: 36 | application/json: 37 | schema: 38 | type: array 39 | items: 40 | $ref: '#/components/schemas/TodoResponse' 41 | post: 42 | tags: 43 | - todo 44 | summary: Add a new todo 45 | description: Create a new todo 46 | operationId: createTodo 47 | requestBody: 48 | description: Create a new todo 49 | content: 50 | application/json: 51 | schema: 52 | $ref: '#/components/schemas/TodoParams' 53 | required: true 54 | responses: 55 | '200': 56 | description: Successful operation 57 | content: 58 | application/json: 59 | schema: 60 | $ref: '#/components/schemas/TodoResponse' 61 | '405': 62 | description: Invalid input 63 | 64 | /todo/{todoId}: 65 | get: 66 | tags: 67 | - todo 68 | summary: Find todo by ID 69 | description: Returns a single todo by id 70 | operationId: getTodoById 71 | parameters: 72 | - name: todoId 73 | in: path 74 | description: ID of todo to return 75 | required: true 76 | schema: 77 | type: string 78 | responses: 79 | '200': 80 | description: successful operation 81 | content: 82 | application/json: 83 | schema: 84 | $ref: '#/components/schemas/TodoResponse' 85 | '404': 86 | description: Todo with todoId not found 87 | put: 88 | tags: 89 | - todo 90 | summary: Updates a todo 91 | description: '' 92 | operationId: updateTodo 93 | parameters: 94 | - name: todoId 95 | in: path 96 | description: ID of todo 97 | required: true 98 | schema: 99 | type: string 100 | requestBody: 101 | description: Create a new todo 102 | content: 103 | application/json: 104 | schema: 105 | $ref: '#/components/schemas/TodoParams' 106 | required: true 107 | responses: 108 | '200': 109 | description: Successful operation 110 | content: 111 | application/json: 112 | schema: 113 | $ref: '#/components/schemas/TodoResponse' 114 | '405': 115 | description: Invalid input 116 | '400': 117 | description: Todo with todoId not found 118 | 119 | delete: 120 | tags: 121 | - todo 122 | summary: Deletes a todo 123 | description: delete a todo 124 | operationId: deleteTodo 125 | parameters: 126 | - name: todoId 127 | in: path 128 | description: Todo id to delete 129 | required: true 130 | schema: 131 | type: string 132 | format: uid 133 | responses: 134 | '400': 135 | description: Todo with todoId not found 136 | components: 137 | schemas: 138 | TodoResponse: 139 | required: 140 | - id 141 | - title 142 | - priority 143 | - completed 144 | type: object 145 | properties: 146 | id: 147 | type: string 148 | format: uid 149 | example: 123d12d31-123d123d123d-012d12d 150 | title: 151 | type: string 152 | example: doggie 153 | note: 154 | type: string 155 | nullable: true 156 | example: This is a note 157 | description: multiline note 158 | priority: 159 | $ref: '#/components/schemas/PriorityEnum' 160 | completeBy: 161 | type: string 162 | format: date 163 | nullable: true 164 | description: todo must be completed by this date 165 | completed: 166 | type: boolean 167 | description: todo is completed 168 | TodoParams: 169 | required: 170 | - title 171 | - priority 172 | - completed 173 | type: object 174 | properties: 175 | title: 176 | type: string 177 | example: doggie 178 | note: 179 | type: string 180 | nullable: true 181 | example: This is a note 182 | description: multiline note 183 | priority: 184 | $ref: '#/components/schemas/PriorityEnum' 185 | completeBy: 186 | type: string 187 | format: date 188 | nullable: true 189 | description: todo must be completed by this date 190 | completed: 191 | type: boolean 192 | description: todo is completed 193 | PriorityEnum: 194 | type: string 195 | description: high, medium, low or none 196 | enum: 197 | - high 198 | - medium 199 | - low 200 | - none 201 | 202 | requestBodies: 203 | Todo: 204 | description: Todo object that needs to be added 205 | content: 206 | application/json: 207 | schema: 208 | $ref: '#/components/schemas/TodoParams' 209 | -------------------------------------------------------------------------------- /gen/todo_api/doc/TodoApi.md: -------------------------------------------------------------------------------- 1 | # todo_api.api.TodoApi 2 | 3 | ## Load the API package 4 | ```dart 5 | import 'package:todo_api/api.dart'; 6 | ``` 7 | 8 | All URIs are relative to *https://todo-backend-lyle.fly.dev/api* 9 | 10 | Method | HTTP request | Description 11 | ------------- | ------------- | ------------- 12 | [**createTodo**](TodoApi.md#createtodo) | **POST** /todo | Add a new todo 13 | [**deleteTodo**](TodoApi.md#deletetodo) | **DELETE** /todo/{todoId} | Deletes a todo 14 | [**getAllTodos**](TodoApi.md#getalltodos) | **GET** /todo | get all todos 15 | [**getTodoById**](TodoApi.md#gettodobyid) | **GET** /todo/{todoId} | Find todo by ID 16 | [**updateTodo**](TodoApi.md#updatetodo) | **PUT** /todo/{todoId} | Updates a todo 17 | 18 | 19 | # **createTodo** 20 | > TodoResponse createTodo(todoParams) 21 | 22 | Add a new todo 23 | 24 | Create a new todo 25 | 26 | ### Example 27 | ```dart 28 | import 'package:todo_api/api.dart'; 29 | 30 | final api_instance = TodoApi(); 31 | final todoParams = TodoParams(); // TodoParams | Create a new todo 32 | 33 | try { 34 | final result = api_instance.createTodo(todoParams); 35 | print(result); 36 | } catch (e) { 37 | print('Exception when calling TodoApi->createTodo: $e\n'); 38 | } 39 | ``` 40 | 41 | ### Parameters 42 | 43 | Name | Type | Description | Notes 44 | ------------- | ------------- | ------------- | ------------- 45 | **todoParams** | [**TodoParams**](TodoParams.md)| Create a new todo | 46 | 47 | ### Return type 48 | 49 | [**TodoResponse**](TodoResponse.md) 50 | 51 | ### Authorization 52 | 53 | No authorization required 54 | 55 | ### HTTP request headers 56 | 57 | - **Content-Type**: application/json 58 | - **Accept**: application/json 59 | 60 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) 61 | 62 | # **deleteTodo** 63 | > deleteTodo(todoId) 64 | 65 | Deletes a todo 66 | 67 | delete a todo 68 | 69 | ### Example 70 | ```dart 71 | import 'package:todo_api/api.dart'; 72 | 73 | final api_instance = TodoApi(); 74 | final todoId = todoId_example; // String | Todo id to delete 75 | 76 | try { 77 | api_instance.deleteTodo(todoId); 78 | } catch (e) { 79 | print('Exception when calling TodoApi->deleteTodo: $e\n'); 80 | } 81 | ``` 82 | 83 | ### Parameters 84 | 85 | Name | Type | Description | Notes 86 | ------------- | ------------- | ------------- | ------------- 87 | **todoId** | **String**| Todo id to delete | 88 | 89 | ### Return type 90 | 91 | void (empty response body) 92 | 93 | ### Authorization 94 | 95 | No authorization required 96 | 97 | ### HTTP request headers 98 | 99 | - **Content-Type**: Not defined 100 | - **Accept**: Not defined 101 | 102 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) 103 | 104 | # **getAllTodos** 105 | > List getAllTodos() 106 | 107 | get all todos 108 | 109 | get all todos 110 | 111 | ### Example 112 | ```dart 113 | import 'package:todo_api/api.dart'; 114 | 115 | final api_instance = TodoApi(); 116 | 117 | try { 118 | final result = api_instance.getAllTodos(); 119 | print(result); 120 | } catch (e) { 121 | print('Exception when calling TodoApi->getAllTodos: $e\n'); 122 | } 123 | ``` 124 | 125 | ### Parameters 126 | This endpoint does not need any parameter. 127 | 128 | ### Return type 129 | 130 | [**List**](TodoResponse.md) 131 | 132 | ### Authorization 133 | 134 | No authorization required 135 | 136 | ### HTTP request headers 137 | 138 | - **Content-Type**: Not defined 139 | - **Accept**: application/json 140 | 141 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) 142 | 143 | # **getTodoById** 144 | > TodoResponse getTodoById(todoId) 145 | 146 | Find todo by ID 147 | 148 | Returns a single todo by id 149 | 150 | ### Example 151 | ```dart 152 | import 'package:todo_api/api.dart'; 153 | 154 | final api_instance = TodoApi(); 155 | final todoId = todoId_example; // String | ID of todo to return 156 | 157 | try { 158 | final result = api_instance.getTodoById(todoId); 159 | print(result); 160 | } catch (e) { 161 | print('Exception when calling TodoApi->getTodoById: $e\n'); 162 | } 163 | ``` 164 | 165 | ### Parameters 166 | 167 | Name | Type | Description | Notes 168 | ------------- | ------------- | ------------- | ------------- 169 | **todoId** | **String**| ID of todo to return | 170 | 171 | ### Return type 172 | 173 | [**TodoResponse**](TodoResponse.md) 174 | 175 | ### Authorization 176 | 177 | No authorization required 178 | 179 | ### HTTP request headers 180 | 181 | - **Content-Type**: Not defined 182 | - **Accept**: application/json 183 | 184 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) 185 | 186 | # **updateTodo** 187 | > TodoResponse updateTodo(todoId, todoParams) 188 | 189 | Updates a todo 190 | 191 | 192 | 193 | ### Example 194 | ```dart 195 | import 'package:todo_api/api.dart'; 196 | 197 | final api_instance = TodoApi(); 198 | final todoId = todoId_example; // String | ID of todo 199 | final todoParams = TodoParams(); // TodoParams | Create a new todo 200 | 201 | try { 202 | final result = api_instance.updateTodo(todoId, todoParams); 203 | print(result); 204 | } catch (e) { 205 | print('Exception when calling TodoApi->updateTodo: $e\n'); 206 | } 207 | ``` 208 | 209 | ### Parameters 210 | 211 | Name | Type | Description | Notes 212 | ------------- | ------------- | ------------- | ------------- 213 | **todoId** | **String**| ID of todo | 214 | **todoParams** | [**TodoParams**](TodoParams.md)| Create a new todo | 215 | 216 | ### Return type 217 | 218 | [**TodoResponse**](TodoResponse.md) 219 | 220 | ### Authorization 221 | 222 | No authorization required 223 | 224 | ### HTTP request headers 225 | 226 | - **Content-Type**: application/json 227 | - **Accept**: application/json 228 | 229 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) 230 | 231 | --------------------------------------------------------------------------------