├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── flutter-ci.yml ├── .gitignore ├── .metadata ├── .process ├── README.md └── sprints_ptBR │ ├── 00_Sprint.pdf │ ├── 01_Sprint.pdf │ ├── 02_Sprint.pdf │ └── 03_Sprint.pdf ├── .resources ├── 00arch_overview_simple.png └── 01arch_overview_complex.png ├── .vscode ├── launch.json └── settings.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_ptbr.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── memo │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── 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 │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── 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-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── application │ ├── app.dart │ ├── constants │ │ └── .gitkeep │ ├── coordinator │ │ ├── coordinator_information_parser.dart │ │ ├── coordinator_router_delegate.dart │ │ ├── routes.dart │ │ └── routes_coordinator.dart │ ├── layout_provider.dart │ ├── pages │ │ ├── home │ │ │ └── home_page.dart │ │ ├── settings │ │ │ └── settings_page.dart │ │ └── splash_page.dart │ ├── utils │ │ └── .gitkeep │ ├── view-models │ │ └── app_vm.dart │ └── widgets │ │ └── .gitkeep ├── core │ └── faults │ │ ├── errors │ │ ├── base_error.dart │ │ ├── inconsistent_state_error.dart │ │ └── serialization_error.dart │ │ └── exceptions │ │ └── base_exception.dart ├── data │ ├── gateways │ │ ├── document_database_gateway.dart │ │ └── sembast_database.dart │ ├── repositories │ │ └── deck_repository.dart │ └── serializers │ │ ├── card_block_serializer.dart │ │ ├── card_execution_serializer.dart │ │ ├── card_serializer.dart │ │ ├── deck_serializer.dart │ │ ├── resource_serializer.dart │ │ └── serializer.dart ├── domain │ ├── enums │ │ ├── card_block_type.dart │ │ ├── card_difficulty.dart │ │ └── resource_type.dart │ ├── models │ │ ├── card.dart │ │ ├── card_block.dart │ │ ├── card_execution.dart │ │ ├── deck.dart │ │ └── resource.dart │ └── services │ │ └── deck_services.dart └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test ├── data │ ├── database_repository_test.dart │ ├── sembast_database_test.dart │ └── serializers │ │ ├── card_block_serializer_test.dart │ │ ├── card_execution_serializer_test.dart │ │ ├── card_serializer_test.dart │ │ ├── deck_serializer_test.dart │ │ └── resource_serializer_test.dart ├── domain │ └── models │ │ ├── card_block_test.dart │ │ ├── card_execution_test.dart │ │ ├── card_test.dart │ │ ├── deck_test.dart │ │ └── resource_test.dart ├── fixtures │ ├── card.json │ ├── card_block.json │ ├── card_execution.json │ ├── deck.json │ ├── fixtures.dart │ └── resource.json └── utils │ └── .gitkeep └── web ├── favicon.png ├── icons ├── Icon-192.png └── Icon-512.png ├── index.html └── manifest.json /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Algo não está funcionando/Something is not working 3 | about: Eu encontrei algum bug na aplicação/I found a bug in the app 4 | title: '' 5 | labels: 'needs triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Descreva o Bug/Describe the bug:** 11 | 12 | <-- Faça uma clara e breve descrição do problema / Provide us a brief and clear description of the problem --> 13 | 14 | **Passos para Reproduzir/Steps to Reproduce:** 15 | 16 | <-- Quais os passos necessários para que o bug seja reproduzido? / What are the necessary steps to reproduce this bug? 17 | Exemplo/Example: 18 | 1. Abra o app/Open the app 19 | 2. Entre na tela X/Navigate to X Screen 20 | 3. Clique no componente Y/Tap in the component Y --> 21 | 22 | **Resultado Esperado/Expected Result:** 23 | 24 | <-- Qual resultado você esperava? / What did you expect to achieve? --> 25 | 26 | **Resultado Obtido/Actual Result:** 27 | 28 | <-- Qual resultado você obteve? / What happened instead? --> 29 | 30 | **Screenshots:** 31 | 32 | <-- Se possível, adicione Screenshots para ilustrar o problema. / If applicable, adds some Screenshots to illustrate 33 | the problem. --> 34 | 35 | **Versão do app/App Version:** 36 | 37 | <-- Versão do app na qual o problema foi reproduzido / App version in use when the problem was reproduced --> -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Eu quero entrar em contato com os desenvolvedores 4 | url: https://olmps.co 5 | about: Entre em contato com o time de desenvolvimento através do site da empresa Olympus -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Nova Funcionalidade/New Feature 3 | about: Eu quero sugerir uma nova funcionalidade para o app/I want to suggest a new feature 4 | title: '' 5 | labels: 'needs triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Caso de Uso/Use Case:** 11 | 12 | <-- Quais casos de uso são impactados por essa nova funcionalidade? Quais problemas ela resolve? / What use cases would 13 | have a direct impact on this new feature? What problems does it solve? --> 14 | 15 | **Proposta/Proposal:** 16 | 17 | <-- Faça uma breve porém informativa descrição da sua proposta. Se possível, ilustre ela com imagens / Do a brief and 18 | complete description of your proposal. If applicable, illustrate it with images. --> -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Faça aqui uma breve descrição das alterações que esse PR faz e o porquê / Describe the changes proposed in this PR and 2 | their motivation:* 3 | 4 | *Liste pelo menos um issue relacionado à este PR. / List at least one open issue related to this PR:* 5 | 6 | ## Pre-launch Checklist 7 | 8 | - [ ] Eu li o [CONTRIBUTING.md](../CONTRIBUTING.md) e segui as sugestões descritas ali sobre boas práticas de código / I 9 | read [CONTRIBUTING.md](../CONTRIBUTING.md) and followed the suggestions proposed there about writing good code; 10 | - [ ] Eu listei pelo menos um issue relacionado à este PR / I listed at least one issue related to this PR; 11 | - [ ] Eu adicionei novos testes para cobrir as mudanças que eu fiz neste PR, ou este PR é uma exceção / I added new 12 | tests that cover the changes made in this PR or this PR is a test-exception; 13 | - [ ] Eu atualizei o [CHANGELOG.md](./../CHANGELOG.md) com as mudanças feitas neste PR / I updated 14 | [CHANGELOG.md](./../CHANGELOG.md) with the changes made in this PR; 15 | - [ ] Eu atualizei/adicionei comentários relevantes nas modificações efetuadas / I updated or added relevant docs in the 16 | modifications I've made; 17 | - [ ] Todos os testes previamente existentes e os novos criados por mim estão finalizando com sucesso / All the existing 18 | tests - the old and the new ones - are finishing with success. -------------------------------------------------------------------------------- /.github/workflows/flutter-ci.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v1 11 | 12 | - name: Setup Java 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: '12.x' 16 | 17 | - name: Setup Flutter 18 | uses: subosito/flutter-action@v1 19 | with: 20 | channel: 'stable' 21 | 22 | - name: Installing dependences 23 | run: flutter pub get 24 | 25 | - name: Analysing code 26 | run: flutter analyze . 27 | 28 | - name: Running tests 29 | run: flutter test 30 | 31 | - name: Building for android 32 | run: flutter build appbundle --dart-define=ENV=${{env.FLUTTER_ENV}} 33 | env: 34 | FLUTTER_ENV: ${{ github.event.pull_request.base.ref == 'main' && 'PROD' || 'DEV' }} 35 | 36 | - name: Building for ios 37 | run: flutter build ios --release --no-codesign --dart-define=ENV=${{env.FLUTTER_ENV}} 38 | env: 39 | FLUTTER_ENV: ${{ github.event.pull_request.base.ref == 'main' && 'PROD' || 'DEV' }} 40 | -------------------------------------------------------------------------------- /.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 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: 4d7946a68d26794349189cf21b3f68cc6fe61dcb 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.process/README.md: -------------------------------------------------------------------------------- 1 | # Processo 2 | 3 | O processo definido para executar este projeto se fundamenta na metodologia Agile que, 4 | ao mesmo tempo que nos beneficiamos dos eventos e boas práticas do Scrum, também flexibilizamos 5 | alguns comportamentos que acreditamos fazerem mais sentido para a equipe. 6 | 7 | Este processo não significa que o projeto será executado desta forma **para sempre**. Ele 8 | apenas guiará as primeiras *fundamentações* da aplicação, como se fosse um histórico das decisões iniciais. 9 | Isso significa que o objetivo final - de expor este processo - é apenas e unicamente dar uma opinião (para 10 | à comunidade que esteja interessada) de como é possível estruturar um processo leve e enxuto, para construir 11 | (o ínicio, meio e fim?) uma aplicação. 12 | 13 | ### Documentos 14 | 15 | - [Vídeos do background do projeto](https://www.youtube.com/watch?v=HgOtgacKSNY&list=PLXA_TifFgaBAu0l39GWyJVVr0azXpV9wz&ab_channel=LucasMontano): Série (playlist no YT) 16 | contando sobre todo o processo e execução do projeto; 17 | - [Sprints](/.process/sprints_ptBR/): Entra em detalhes sobre todos os *major* eventos de cada sprint, seus respectivos 18 | resultados e planejamentos. -------------------------------------------------------------------------------- /.process/sprints_ptBR/00_Sprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.process/sprints_ptBR/00_Sprint.pdf -------------------------------------------------------------------------------- /.process/sprints_ptBR/01_Sprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.process/sprints_ptBR/01_Sprint.pdf -------------------------------------------------------------------------------- /.process/sprints_ptBR/02_Sprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.process/sprints_ptBR/02_Sprint.pdf -------------------------------------------------------------------------------- /.process/sprints_ptBR/03_Sprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.process/sprints_ptBR/03_Sprint.pdf -------------------------------------------------------------------------------- /.resources/00arch_overview_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.resources/00arch_overview_simple.png -------------------------------------------------------------------------------- /.resources/01arch_overview_complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/.resources/01arch_overview_complex.png -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.0", 3 | "configurations": [ 4 | { 5 | "name": "DEV - App", 6 | "type": "dart", 7 | "request": "launch", 8 | "program": "lib/main.dart", 9 | "args": [ 10 | "--dart-define=ENV=DEV" 11 | ] 12 | }, 13 | { 14 | "name": "PROFILE - App", 15 | "type": "dart", 16 | "request": "launch", 17 | "flutterMode": "profile", 18 | "program": "lib/main.dart", 19 | "args": [ 20 | "--dart-define=ENV=DEV" 21 | ] 22 | }, 23 | { 24 | "name": "PROD - App", 25 | "type": "dart", 26 | "request": "launch", 27 | "program": "lib/main.dart", 28 | "args": [ 29 | "--dart-define=ENV=PROD" 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "dart.lineLength": 120, 4 | "editor.rulers": [120] 5 | } 6 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Table of contents 4 | - [Architecture](#architecture) 5 | - [`.vscode/`](#vscode) 6 | - [Useful vscode extensions](#useful-vscode-extensions) 7 | - [`android/` - Android required files](#android---android-required-files) 8 | - [`ios/` - iOS required files](#ios---ios-required-files) 9 | - [`lib/` - Flutter application](#lib---flutter-application) 10 | - [Why](#why) 11 | - [Overview](#overview) 12 | - [`application/`](#application) 13 | - [`constants/`](#constants) 14 | - [`coordinator/`](#coordinator) 15 | - [`pages/` (views)](#pages-views) 16 | - [`utils/`](#utils) 17 | - [`widgets/`](#widgets) 18 | - [`view_models/`](#view_models) 19 | - [`domain/`](#domain) 20 | - [`enums/`](#enums) 21 | - [`models/`](#models) 22 | - [`services/`](#services) 23 | - [`data/`](#data) 24 | - [`gateways/`](#gateways) 25 | - [`repositories/`](#repositories) 26 | - [`serializers/`](#serializers) 27 | - [`core/`](#core) 28 | - [`faults/`](#faults) 29 | - [`test/` - Unit and UI testing](#test---unit-and-ui-testing) 30 | - [`utils/`](#utils-1) 31 | - [`fixtures/`](#fixtures) 32 | - [`web/`](#web) 33 | - [Extra](#extra) 34 | - [Why `river_pod` and not "x" state management library?](#why-river_pod-and-not-x-state-management-library) 35 | - [Why `sembast` and not "x" database?](#why-sembast-and-not-x-database) 36 | - [Why `mocktail` and not `mockito`?](#why-mocktail-and-not-mockito) 37 | - [`CoordinatorRouter` and `Router` (or Navigator 2.0)](#coordinatorrouter-and-router-or-navigator-20) 38 | - [Environment](#environment) 39 | - [Release](#release) 40 | 41 | ## `.vscode/` 42 | 43 | While this project heavily enforces that vscode should be used, IntelliJ is also an alternative, although it won't 44 | provide the best experience with the setup made in this repository. If you still prefer to use it, there should be no 45 | problem at all, just make sure to follow the same guidelines specified in [`settings.json`](.vscode/settings.json). 46 | 47 | All configuration files exist in [`.vscode`](.vscode/) folder and **should be git-tracked**. 48 | 49 | - [`launch.json`](.vscode/launch.json) is where all pre-configured command-line scripts are at, such as running a 50 | debug dev environment; 51 | - [`settings.json`](.vscode/settings.json) is responsible for the editor configurations, such as line-length, rules 52 | and auto-format on save. 53 | 54 | ### Useful vscode extensions 55 | 56 | - Dart (id: dart-code.dart-code); 57 | - Flutter (id: dart-code.flutter); 58 | - Awesome Flutter Snippets (id: nash.awesome-flutter-snippets) - frequently used snippets in any Flutter application; 59 | - Brack Pair Color (id: coenraads.bracket-pair-colorizer) - useful when dealing with nested/verbose widgets. 60 | 61 | It's highly recommended to, at least, add the `Dart` and `Flutter` extension, as they provide an absurd amount of useful 62 | features. 63 | 64 | > You can copy the id and search in the vscode marketplace to find them. 65 | 66 | ## `android/` - Android required files 67 | 68 | Stores all required (and generated) files to output builds for the Android platform. 69 | 70 | This is where native Android (Kotlin) code also lives, if there is a need to implement native-specific features. 71 | 72 | ## `ios/` - iOS required files 73 | 74 | Stores all required (and generated) files to output builds for the iOS/iPadOS platforms. 75 | 76 | This is where native iOS (Swift) code also lives, if there is a need to implement native-specific features. 77 | 78 | ## `lib/` - Flutter application 79 | 80 | Entry point to the Flutter application, where most of the *action* will happen. 81 | 82 | ### Why 83 | 84 | > You can skip this explanation, this is just an overview on the topic of why we have decided to go with this particular 85 | > architectural approach. 86 | 87 | At first glance (looking at the name of the topmost folders), with the objective of defining its layers and the 88 | respective interactions, you may question yourself if this project is using 89 | [clean architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), 90 | [domain driven design (DDD)](https://martinfowler.com/bliki/DomainDrivenDesign.html) or even some pieces of 91 | [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). Now, when you start reading it and finding 92 | out which part depends on what - and what they expect to execute their responsibilities -, you may wonder about things 93 | like "entities are not mapped to models!", "where are the use-cases?" and questions about the fact that this approach 94 | **doesn't follow these architectures principles**. Why is that? 95 | 96 | Well, clean architecture was originally intended for robust/enterprise-like applications that have to deal with a ton of 97 | business-logic complexity and highly-verbose dependencies - such as libraries, frameworks and any external resources. 98 | While this is a frequent scenario in the present state of software applications, **this project is definitely not the 99 | case of a highly-complex scenario** - it may evolve to be complex enough, but it won't exceed the complexity of being a 100 | REST-consuming client that **focuses** much more on the presentation layer than anything else. 101 | 102 | Nowadays, simpler architectural designs like MVC/MVVM/MVP are much more common in client applications due to this exact 103 | fact: a overcomplex and high boilerplate architecture doesn't provide any significant value - they make things harder 104 | and slower with no clear benefit other than separating a bunch of layers **for the sake of separating them**. But they 105 | come with a price: there is no clear distinction in between **Business Logic and Data manipulation** if you don't 106 | enforce such standards. 107 | 108 | No, we won't remove the classic separation of "View <-> Business Logic <-> Data" relationship, it's just that, in this 109 | case, **we think that following every nook and cranny of part of these architectures would be overengineering**, thus 110 | making things slower just to follow some principles that don't necessarily apply to this case. This approach will surely 111 | not make sense (or even be completely dumb) for some, but may be good for others. 112 | [Relevant xkcd](https://xkcd.com/927/). 113 | 114 | One extra thing: this is heavily influenced by a bunch personal opinion and experiences in some production projects 115 | that the team has worked on. This project's external dependencies will keep changing as the time goes on, Flutter will 116 | also keep evolving, and we have to adapt in a way to maintain consistency, integrity and scalability of our solution. 117 | So, it's probable that there is (or will be) better ways to achieve the same goals/objectives, and for this, we look 118 | into your help to make this project's architecture continuously provide a good developer experience to add new features, 119 | update old ones and keep those nasty bugs away. 120 | 121 | ### Overview 122 | 123 | ![Simple Architecture Overview](.resources/00arch_overview_simple.png "Architecture Overview") 124 | 125 | The picture above gives us a really simplified overview of each major layer that gives shape to this application. 126 | 127 | If you don't want to dig in on what each part is responsible of (and why), here is a TLDR: 128 | - `application`: all interface elements alongside its view models (may contain validation and such business logic), 129 | the latter which communicates with the `domain`; 130 | - `domain`: handles most of the business logic and if necessary, make the respective calls to the `data` layer; 131 | - `data`: retrieves and modifies any data, without the knowledge of any other layers whatsoever. This is the 132 | lower-boundary of our application that communicates with external frameworks and libraries; 133 | - `core`: shared functionality to all layers. 134 | 135 | Now, if you want to take a closer inspection on each interaction of each layer, the image below might be more suited to 136 | comprehend exactly how each layer (and its exceptions) interacts/depends on others. 137 | 138 | ![Complex Architecture Overview](.resources/01arch_overview_complex.png "Architecture Overview") 139 | 140 | - The dotted arrow means a direct dependency, such as the connection between *View Models -> Services*. These 141 | connections require that the communication should always be made through an interface and following the 142 | [dependency inversion principle (DiP)](https://en.wikipedia.org/wiki/Dependency_inversion_principle); 143 | - The straight line means a direct association or usage, such as the connection between *View Models -> Models*; 144 | - The smaller straight line also means a direct association or usage, such as the connection between *Pages -> Enums*. 145 | The difference from the bigger ones is that these "cross-boundaries" between layers in a non-traditional way - through 146 | interactors like *View Models* (that connects the `application` with `domain`), *Services* (that connects the `domain` 147 | with `data`) and *Gateways* (that connects `data` with external dependencies). 148 | 149 | All of the interactions above are explained in their respective sections below. 150 | 151 | ### `application/` 152 | 153 | The topmost layer, the entry point of all user interactions, which depends directly on Flutter to function properly. 154 | The `application` should be responsible only for rendering elements and capturing inputs, touches, and any interaction 155 | that comes directly from the user, alongside the interface's capabilities, like scroll, navigation, responsivity, 156 | etcetera. 157 | 158 | Rules about each `application/` structure's responsibilities: 159 | - It should never **interact** with any layer other than its own sub-folders; 160 | - It should never **access** any other layer classes (not even indirectly). 161 | 162 | Two exceptions for the above: 163 | - These structures can **access** the [`domain/enums`](#enums) - while it exposes a piece of the `domain` layer, we 164 | consider this to be an acceptable exception (explained in [`data/`](#data)); 165 | - The **[ViewModels (VMs)](#view_models)** can **interact** with the `domain/` because they are the structure that 166 | allows us to, in only *one-way*, cross boundaries from the `application` to the `domain`. 167 | 168 | #### `constants/` 169 | 170 | Stores any kind of constant, like images, strings, themes, etcetera. 171 | 172 | #### `coordinator/` 173 | 174 | Allows us to take control over our routing and navigation, in close contact with the `Flutter` framework to do so. 175 | 176 | The responsibility of the coordinator is to make all of those pesky deep-linking and navigation stack problems become 177 | easier to deal with. 178 | 179 | #### `pages/` (views) 180 | 181 | Each page is normally associated with a `Scaffold`, that represents all the contents of a single `MaterialPageRoute`, 182 | which is controlled by the `CoordinatorRouter`. 183 | 184 | These `pages` are the only elements that can access the [`view_models/`](#view_models). 185 | 186 | #### `utils/` 187 | 188 | UI-related utilities like formatting, widgets helpers, animations, painters, etcetera. 189 | 190 | #### `widgets/` 191 | 192 | Individual `Widget`s that represent some custom visual element that is reused in multiple different 193 | [`pages`](#pages-views) or even other `application/widgets`. These should not know anything about VMs, pages, or 194 | anything other than `application/utils` and `application/constants`. They should be **pure** and **independent**. 195 | 196 | #### `view_models/` 197 | 198 | The boundary between the [`application/`](#application) and [`domain/`](#domain). The ViewModels, (suffixed with `VM` 199 | in each class), always should be built upon an interface (following the DiP) and should never - ever - know anything 200 | about the UI, meaning the `flutter` framework - but maybe some constant stuff like `Platform` and core 201 | meta-functionality, but never anything related to the layout per-se. 202 | 203 | The `VM`s are the only structures in [`application/`](#application) that communicates with inner layers, more 204 | specifically, with the [`domain/`](#domain). In the process of achieving this, it will inevitably leak some of the core 205 | business logic (things like input validation) that should be mostly contained in the [`domain/`](#domain) layer. 206 | 207 | ### `domain/` 208 | 209 | The intermediate layer. Using the core structures (models, entities and enums), the domain is where all the business logic 210 | should be contained, by accessing the [`repositories`](#repositories) to achieve its goals. 211 | 212 | Rules about each `domain/` structure's responsibilities: 213 | - It should never **interact** with any layer other than its own sub-folders; 214 | - It should never **access** any other layer classes (not even indirectly). 215 | 216 | One exception for the above: 217 | - The **[Services](#services)** can **interact** with the `data/` (through the [`repositories`](#repositories)) because 218 | they are the structure that allows us to, in only *one-way*, cross boundaries from the `domain` to the `data`. 219 | 220 | #### `enums/` 221 | 222 | They are just like our [`models`](#models) - a data structure that represent part of our business, but with the 223 | difference that it can be *described* statically (they are constant). 224 | 225 | > These are the only structures that can be accessed (or leaked) to the views due to its constant nature. It provides a 226 | > type-safety when dealing with these cases and if we don't actually leak it, normally what we have is a duplication of 227 | > this same enumerator behavior in the UI, but less type-safe (or just replicating the exact same behavior). 228 | 229 | #### `models/` 230 | 231 | A domain model - a set of structures that represent a business object. 232 | 233 | #### `services/` 234 | 235 | The boundary between the [`domain/`](#domain) and [`data/`](#data). Each service (suffixed with `Service` in each 236 | class) should always be built upon an interface (following the DiP). 237 | 238 | The `services/` should contain all the heavy business logic associated with each `model` in our project. They are 239 | usually split to represent each [`models/`](#models) related business logic, but this could be split in even smaller 240 | structures (called Use Cases in the clean architecture) if proven necessary. 241 | 242 | They are the only structures in [`domain/`](#domain) that communicates with the [`data/`](#data) layer, more 243 | specifically, through the [`repositories/`](#repositories). 244 | 245 | ### `data/` 246 | 247 | The bottom layer. Communicates with raw libraries and frameworks to consume its raw data and expose it to its consumer. 248 | These libraries and frameworks are abstractions (interfaces, following the DiP) to access things like remote servers, 249 | hardware capabilities (audio, video, geo), databases, etcetera. 250 | 251 | Rules about each `data/` structure's responsibilities: 252 | - It should never **interact** with any layer other than its own sub-folders; 253 | - It should never **access** any other layer classes (not even indirectly). 254 | 255 | One exception for the above: 256 | - Both [`serializers/`](#serializers) and [`repositories/`](#repositories) can **access** the [`enums/`](#enums) and 257 | [`models/`](#models) - while it exposes a piece of the `domain` layer, we consider this to be an acceptable exception 258 | (explained below). 259 | 260 | Just like we have decided to expose the `enums/` to the interface, we also agreed to expose both `enums/` and 261 | `models/` to the [`serializers`](#serializers) and [`repositories`](#repositories). The alternative here was to 262 | [create intermediate data models (DTOs)](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#what-data-crosses-the-boundaries), 263 | but *separating the domain model from this exact copy (DTO) doesn't provide any meaningful solution to this 264 | architecture* (I like [this answer in StackExchange](https://softwareengineering.stackexchange.com/a/388545) about the 265 | same exact issue). 266 | 267 | #### `gateways/` 268 | 269 | Raw access to libraries, databases and all external dependency that crosses the boundary of our application to anything 270 | that lives outside of the project. 271 | 272 | These should be built like any other major structure, through interfaces and following the DiP. 273 | 274 | Also, because gateways are "rare naming" occurrence in most architectures, 275 | [here is the reference of why](https://martinfowler.com/eaaCatalog/gateway.html). 276 | 277 | #### `repositories/` 278 | 279 | Interfaces the implementation of a [`gateway`](#gateways) to not expose the particularities of such external 280 | dependencies to the [`services`](#services). The `repositories` can be considered like *interface adapters*, allowing 281 | an independency when making changes to the implemented technologies, affecting only this layer (and obviously the 282 | technology implementation itself). 283 | 284 | Each of these *adapters* are suffixed with `Repository` and are built upon interfaces (following the DiP). 285 | 286 | #### `serializers/` 287 | 288 | Instead of a codegen approach (due to the drawbacks of being dependent of auto-generating the parsing of our core 289 | models), we decided to go with the manual serialization. The `serializers/` exist with the sole purpose of translating 290 | [`models/`](#models) to/from a raw structure. 291 | 292 | ### `core/` 293 | 294 | Fundamental functionality to all the layers (being accessed by any of them), but doesn't know about their existance. 295 | In terms of knowledge, they are similar to the [`data/`](#data) layer, other than the fact that the `data/` layer itself 296 | can access `core/`. 297 | 298 | The `core/` shares functionality like [`faults/`](#faults), environment management, project-wide constants, etcetera. 299 | 300 | #### `faults/` 301 | 302 | Has all the project's custom `Error`s and `Exception`s classes. 303 | 304 | ## `test/` - Unit and UI testing 305 | 306 | Nothing out of the ordinary here, we simply make a mirror of the `lib/` folder structure within `test/`. I.e. if we have 307 | a file that is `lib/application/widgets/custom_container.dart`, we would have a mirrored 308 | `test/application/widgets/custom_container_test.dart`. 309 | 310 | Due to some [limitations of the dart language](https://github.com/dart-lang/language/issues/1482) and the new 311 | null-safety approach, [`mockito`](https://github.com/dart-lang/mockito) is now using a codegen to mock, which we 312 | honestly think that [`mocktail`](https://github.com/felangel/mocktail) is a better alternative. 313 | 314 | ### `utils/` 315 | 316 | Shared functionality amongst all test cases. 317 | 318 | ### `fixtures/` 319 | 320 | Tests [fixtures](https://en.wikipedia.org/wiki/Test_fixture#Software) - here is a 321 | [good SO answer](https://stackoverflow.com/a/14684400/8558606) explaining what they represent. In our scenario, they 322 | usually represent raw data, models or entities. 323 | 324 | ## `web/` 325 | 326 | Stores all required (and generated) files to output builds for the Web platform. Currently not supported. 327 | 328 | --- 329 | 330 | # Extra 331 | 332 | These are points that aren't directly related to the folder structure and each responsibility, but things that also 333 | permeates the knowledge required to fully understand this architecture. 334 | 335 | ## Why `river_pod` and not "x" state management library? 336 | 337 | With the past experiences with libraries like the native `InheritedWidget`, `Provider` and `Bloc`, we had found that 338 | they tend to be quite verbose (thus bloating the code) and limited in some scenarios. Diving into each of these 339 | particularities would be a long discussion, but 340 | [`river_pod` has a brief explanation](https://github.com/rrousselGit/river_pod#why-another-project-when-provider-already-exists) 341 | on why it solves such problems and why it's better to use it. 342 | 343 | ## Why `sembast` and not "x" database? 344 | 345 | [`sembast`](https://github.com/tekartik/sembast.dart) is one of the few NoSQL databases that are really easy to use, 346 | supports web (in a parallel package) and provides a decent amount of functionality like reactivity and complex queries, 347 | with the addition of built-in support for migration. The library has its limitations due to its inherent nature, but we 348 | don't think that it will be an issue for this project. 349 | 350 | ## Why `mocktail` and not `mockito`? 351 | 352 | After NNBD, `mockito` is using a codegen approach to deal with mocks, which we quite dislike given that there is no 353 | clear benefit when comparing to `mocktail`. There is an [open issue](https://github.com/dart-lang/mockito/issues/347) to 354 | merge mocktail into `mockito`, but until then (assuming it will be merged), we think that the same functionalities will 355 | continue to work using `mocktail` only. 356 | 357 | ## `CoordinatorRouter` and `Router` (or Navigator 2.0) 358 | 359 | This was probably one of the decisions that we still are somewhat unsure about. To not go into a lot of the details, 360 | instead of using external libraries routing libraries (like `vrouter`, `auto_route` and `beamer`), we decided to take a 361 | shot on doing our own implementation of the Navigator 2.0 (or Router), because we believed that it could give us a much 362 | more fine-grained (and less bloated) implementation of what we consider a coordinator pattern (name more frequent in the 363 | iOS development ecosystem, but what we consider our router). 364 | 365 | While we are quite content with the result, that the solution matched our preference (personal opinion) of splitting the 366 | application pages in a complete separate class from the router/coordinator (contrary to what most of the routing 367 | libraries do), and access it call navigatons, we are not so sure about the future of the Navigator 2.0 and how 368 | hard/verbose it can become. We didn't quite like the API and it may be a point of difficult fixes/updates in the future. 369 | 370 | There is an [research](https://github.com/flutter/uxr/wiki/Navigator-2.0-API-Usability-Research) going on to improve 371 | Flutter's Navigator API, so we should keep track of its evolution and how it may improve our solution. 372 | 373 | While the Flutter framework doesn't provide an improved version of the current Navigator 2.0 state **AND IF** the 374 | current coordinator proves to be more of a burden than a help, we should migrate to one of the libraries mentioned 375 | above. 376 | 377 | ## Environment 378 | 379 | For different types of build environments, we don't use the common _flavors_, iOS schemas and all of that painful setup, 380 | due to the fact that, since Flutter `1.17`, we can now use command arguments to inject any variable in our application - 381 | no more multiple `main.dart` files and such stuff. Simply run: 382 | 383 | `flutter run --dart-define=ENV=MY_ENVIRONMENT` 384 | 385 | If you are using `vscode` IDE, there is the [launch configuration files](.vscode/launch.json) for you to auto run and 386 | debug the application. 387 | 388 | And that's it, the currently supported environments are: `DEV` and `PROD`. 389 | 390 | ### Release 391 | 392 | WIP -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Releases here should only be made whenever there is a build available for them in the respective stores (even if it's 9 | a beta or production release, they must be documented here). 10 | 11 | ## [Unreleased] 12 | 13 | > Android build: ? 14 | > iOS build: ? 15 | 16 | ## [0.1.0-dev.1] - 2021-04-01 17 | 18 | Initial release, defines core architecture. 19 | The application is unusable on this version. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | ## Issues 4 | 5 | Whether you find a bug, something that is not clarified or a feature request, feel free to 6 | [open a issue](https://github.com/olmps/memo/issues). 7 | 8 | When creating an issue, please provide as much information as possible in order to help the project maintainers to 9 | understand and track the issue. It may have: 10 | 11 | - Goal 12 | - Expected Results 13 | - Actual Results 14 | - Steps to Reproduce 15 | - Code samples reproducing the issue 16 | - Version of the project which has the bug 17 | 18 | ## Pull Requests 19 | 20 | When creating Pull Requests, follow the most [common good practices](https://gist.github.com/MarcDiethelm/7303312), 21 | respect the swiftlint static checks (modifications/exceptions may be discussed), and create an awesome description so 22 | we can understand it without asking any whys. 23 | 24 | ## Commit Messages 25 | 26 | Commit messages matter. Aim to be succinct and descriptive at the same time. We consider as a good source 27 | [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) from Chris Beams. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Olympus 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [Portuguese](README_ptbr.md) 2 | 3 | # Memo 4 | 5 | Monorepo for Memo. 6 | 7 | Memo is an open-source, programming-oriented [spaced repetition](https://en.wikipedia.org/wiki/Spaced_repetition) 8 | software (SRS) written in Flutter. 9 | 10 | 16 | 17 | > As of now, this project is designed to only output builds for Android and iOS. Even though, given the current 18 | > *stability* of Flutter SDK for desktop (Windows, Linux and macOS) and web, there is a high probability that this 19 | > project will eventually support builds for all platforms. 20 | 21 | This README is meant to guide how this project is structured and should serve as a guide to help the project scale with 22 | the current and future requirements. Think of it as a flexible set of rules that guides the project's decisions. While 23 | they can (and probably will) change over time, discussions must be raised to trigger such changes: this means that 24 | we will think/question ourselves before taking an action that breaks any rational decision taken here. It is also 25 | effective to guide PR discussions. 26 | 27 | - [Setup](#setup): how the configure your local project; 28 | - [Architecture](#architecture): how this application works from inside; 29 | - [Background](#background): some background story about this project; 30 | - [Contributing & Good Practices](#contributing--good-practices): recommendation on how to write good code for this 31 | application; 32 | - [License](#license): how this software is licensed and how you may use it. 33 | 34 | ## Setup 35 | 36 | If you have no idea how to install Flutter and run it locally, check this 37 | [_Get started_](https://flutter.dev/docs/get-started/install). 38 | 39 | If you have Flutter setup locally, on the project's root folder, install pubspec dependencies by running 40 | `flutter pub get`. 41 | 42 | ## Architecture 43 | 44 | How this application works from inside and how it interacts with external dependencies - written in details in 45 | [ARCHITECTURE.md](ARCHITECTURE.md). 46 | 47 | ## Background 48 | 49 | This project was built with the help of the sponsors below: 50 | 51 | WIP(sponsors) 52 | 53 | If you're interested in checking out an overview about how we dealt with this project's software process (inside our team), 54 | check out [.process/](.process/README.md) (sorry, for now only in ptBR). 55 | 56 | ## Contributing & Good Practices 57 | 58 | See [CONTRIBUTING](CONTRIBUTING.md) for details about how to contribute to the project. 59 | 60 | ## License 61 | 62 | Memo is published under [BSD 3-Clause](LICENSE). 63 | -------------------------------------------------------------------------------- /README_ptbr.md: -------------------------------------------------------------------------------- 1 | [Inglês](/README.md) | Português 2 | 3 | # Memo 4 | 5 | Monorepo do Memo. 6 | 7 | Memo é um software de código aberto (escrito em Flutter) de 8 | [repetição espaçada](https://en.wikipedia.org/wiki/Spaced_repetition) (SRS, em inglês) voltado ao tema de programação. 9 | 10 | 16 | 17 | > Atualmente, este projeto está construído apenas para gerar *builds* para Android e iOS. Embora o fato de que, dado a 18 | > estabilidade da SDK do Flutter para desktop (Windows, Linux e macOS) e web, existe uma alta probabilidade que este 19 | > projeto eventualmente suportará *builds* para todas as plataformas. 20 | 21 | Este README e todos os sub-documentos presentes aqui (CONTRIBUTING, ARCHITECTURE & CHANGELOG) tem como objetivo guiar a 22 | estrutura deste projeto e devem auxiliar na escalabilidade das funcionalidades existentes hoje e nas que serão criadas 23 | com o decorrer do andamento do projeto. Estes documentos servem como um conjunto flexível de regras que guiam as 24 | decisões tomadas no andamento do projeto. Embora estas regras possam - e provavelmente irão - mudar, discussões devem 25 | ser levantadas sobre os motivos para tais mudanças, de maneira que essas discussões e decisões sejam transparentes para 26 | todos. 27 | 28 | - [Setup](#setup): como configurar seu projeto localmente; 29 | - [Arquitetura](#arquitetura): como está estruturada a arquitetura da aplicação; 30 | - [Background](#background): um pouco do *background* sobre este projeto; 31 | - [Contribuição & Boas Práticas](#contribuição--boas-práticas): recomendações sobre contribuições; 32 | - [Licença](#licença): como essa aplicação está licenciada e como você pode utilizá-la. 33 | 34 | ## Setup 35 | 36 | Se você não tem ideia de como instalar o Flutter e rodá-lo localmente, dê uma olhada nesse 37 | [_Get started_ (em inglês)](https://flutter.dev/docs/get-started/install) 38 | 39 | Agora, se você já tem o Flutter configurado localmente, na pasta raíz do projeto, instale as dependências através do 40 | comando `flutter pub get`. 41 | 42 | ## Arquitetura 43 | 44 | Como essa aplicação foi estruturada e como ela interage com dependência externas - escrito em detalhes em 45 | [ARCHITECTURE](ARCHITECTURE.md) (em inglês). 46 | 47 | ## Background 48 | 49 | Este projeto foi construído com a ajuda dos patrocinadores abaixo: 50 | 51 | WIP(sponsors) 52 | 53 | Se você está interessado em dar uma olhadinha sobre como acabamos lidando com o processo de software deste projeto (dentro 54 | da nossa equipe), dê uma olhada no [.process/](.process/README.md). 55 | 56 | ## Contribuição & Boas Práticas 57 | 58 | Veja o documento [CONTRIBUTING](CONTRIBUTING.md) para mais detalhes sobre como contribuir com este projeto. 59 | 60 | ## Licença 61 | 62 | Memo está licenciado sobre a licença [BSD 3-Clause](LICENSE). -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:strict/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.memo" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/memo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.memo 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 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 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.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 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | path_provider: 14 | :path: ".symlinks/plugins/path_provider/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 18 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 19 | 20 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 21 | 22 | COCOAPODS: 1.10.0 23 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | B774123EE16171FE45568EC4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F25C95E61793C93B8011FC3 /* Pods_Runner.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 34 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 35 | 2F25C95E61793C93B8011FC3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 37 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 38 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 40 | 807607B4C6BAA717C8C86187 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 41 | 89BDB34036FB33E681229584 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 42 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 43 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 44 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | C37FC59A18B683ACC4771BE5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | B774123EE16171FE45568EC4 /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 9740EEB11CF90186004384FC /* Flutter */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 68 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 69 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 70 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 71 | ); 72 | name = Flutter; 73 | sourceTree = ""; 74 | }; 75 | 97C146E51CF9000F007C117D = { 76 | isa = PBXGroup; 77 | children = ( 78 | 9740EEB11CF90186004384FC /* Flutter */, 79 | 97C146F01CF9000F007C117D /* Runner */, 80 | 97C146EF1CF9000F007C117D /* Products */, 81 | EDCDAFF05F016B19E443A76E /* Pods */, 82 | ED7F8C87ED031021CB17D572 /* Frameworks */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | 97C146EF1CF9000F007C117D /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146EE1CF9000F007C117D /* Runner.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | 97C146F01CF9000F007C117D /* Runner */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 98 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 99 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 100 | 97C147021CF9000F007C117D /* Info.plist */, 101 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 102 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 103 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 104 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 105 | ); 106 | path = Runner; 107 | sourceTree = ""; 108 | }; 109 | ED7F8C87ED031021CB17D572 /* Frameworks */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 2F25C95E61793C93B8011FC3 /* Pods_Runner.framework */, 113 | ); 114 | name = Frameworks; 115 | sourceTree = ""; 116 | }; 117 | EDCDAFF05F016B19E443A76E /* Pods */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | C37FC59A18B683ACC4771BE5 /* Pods-Runner.debug.xcconfig */, 121 | 807607B4C6BAA717C8C86187 /* Pods-Runner.release.xcconfig */, 122 | 89BDB34036FB33E681229584 /* Pods-Runner.profile.xcconfig */, 123 | ); 124 | name = Pods; 125 | path = Pods; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 97C146ED1CF9000F007C117D /* Runner */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 134 | buildPhases = ( 135 | 5F6724F854B3B4530334EE72 /* [CP] Check Pods Manifest.lock */, 136 | 9740EEB61CF901F6004384FC /* Run Script */, 137 | 97C146EA1CF9000F007C117D /* Sources */, 138 | 97C146EB1CF9000F007C117D /* Frameworks */, 139 | 97C146EC1CF9000F007C117D /* Resources */, 140 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 142 | F11B46619634D8B4BE86AF84 /* [CP] Embed Pods Frameworks */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 1020; 160 | ORGANIZATIONNAME = ""; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | LastSwiftMigration = 1100; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 169 | compatibilityVersion = "Xcode 9.3"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 97C146E51CF9000F007C117D; 177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 97C146ED1CF9000F007C117D /* Runner */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 97C146EC1CF9000F007C117D /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | ); 208 | name = "Thin Binary"; 209 | outputPaths = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 214 | }; 215 | 5F6724F854B3B4530334EE72 /* [CP] Check Pods Manifest.lock */ = { 216 | isa = PBXShellScriptBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | inputFileListPaths = ( 221 | ); 222 | inputPaths = ( 223 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 224 | "${PODS_ROOT}/Manifest.lock", 225 | ); 226 | name = "[CP] Check Pods Manifest.lock"; 227 | outputFileListPaths = ( 228 | ); 229 | outputPaths = ( 230 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | shellPath = /bin/sh; 234 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 235 | showEnvVarsInLog = 0; 236 | }; 237 | 9740EEB61CF901F6004384FC /* Run Script */ = { 238 | isa = PBXShellScriptBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | ); 242 | inputPaths = ( 243 | ); 244 | name = "Run Script"; 245 | outputPaths = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | shellPath = /bin/sh; 249 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 250 | }; 251 | F11B46619634D8B4BE86AF84 /* [CP] Embed Pods Frameworks */ = { 252 | isa = PBXShellScriptBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | inputFileListPaths = ( 257 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 258 | ); 259 | name = "[CP] Embed Pods Frameworks"; 260 | outputFileListPaths = ( 261 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | shellPath = /bin/sh; 265 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 266 | showEnvVarsInLog = 0; 267 | }; 268 | /* End PBXShellScriptBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 97C146EA1CF9000F007C117D /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 276 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXSourcesBuildPhase section */ 281 | 282 | /* Begin PBXVariantGroup section */ 283 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 284 | isa = PBXVariantGroup; 285 | children = ( 286 | 97C146FB1CF9000F007C117D /* Base */, 287 | ); 288 | name = Main.storyboard; 289 | sourceTree = ""; 290 | }; 291 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 292 | isa = PBXVariantGroup; 293 | children = ( 294 | 97C147001CF9000F007C117D /* Base */, 295 | ); 296 | name = LaunchScreen.storyboard; 297 | sourceTree = ""; 298 | }; 299 | /* End PBXVariantGroup section */ 300 | 301 | /* Begin XCBuildConfiguration section */ 302 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 333 | ENABLE_NS_ASSERTIONS = NO; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | SDKROOT = iphoneos; 346 | SUPPORTED_PLATFORMS = iphoneos; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Profile; 351 | }; 352 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 353 | isa = XCBuildConfiguration; 354 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | CLANG_ENABLE_MODULES = YES; 358 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 359 | ENABLE_BITCODE = NO; 360 | INFOPLIST_FILE = Runner/Info.plist; 361 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.example.memo; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 365 | SWIFT_VERSION = 5.0; 366 | VERSIONING_SYSTEM = "apple-generic"; 367 | }; 368 | name = Profile; 369 | }; 370 | 97C147031CF9000F007C117D /* Debug */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ALWAYS_SEARCH_USER_PATHS = NO; 374 | CLANG_ANALYZER_NONNULL = YES; 375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 376 | CLANG_CXX_LIBRARY = "libc++"; 377 | CLANG_ENABLE_MODULES = YES; 378 | CLANG_ENABLE_OBJC_ARC = YES; 379 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 380 | CLANG_WARN_BOOL_CONVERSION = YES; 381 | CLANG_WARN_COMMA = YES; 382 | CLANG_WARN_CONSTANT_CONVERSION = YES; 383 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 385 | CLANG_WARN_EMPTY_BODY = YES; 386 | CLANG_WARN_ENUM_CONVERSION = YES; 387 | CLANG_WARN_INFINITE_RECURSION = YES; 388 | CLANG_WARN_INT_CONVERSION = YES; 389 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 391 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 393 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 394 | CLANG_WARN_STRICT_PROTOTYPES = YES; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 399 | COPY_PHASE_STRIP = NO; 400 | DEBUG_INFORMATION_FORMAT = dwarf; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | ENABLE_TESTABILITY = YES; 403 | GCC_C_LANGUAGE_STANDARD = gnu99; 404 | GCC_DYNAMIC_NO_PIC = NO; 405 | GCC_NO_COMMON_BLOCKS = YES; 406 | GCC_OPTIMIZATION_LEVEL = 0; 407 | GCC_PREPROCESSOR_DEFINITIONS = ( 408 | "DEBUG=1", 409 | "$(inherited)", 410 | ); 411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 413 | GCC_WARN_UNDECLARED_SELECTOR = YES; 414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 415 | GCC_WARN_UNUSED_FUNCTION = YES; 416 | GCC_WARN_UNUSED_VARIABLE = YES; 417 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 418 | MTL_ENABLE_DEBUG_INFO = YES; 419 | ONLY_ACTIVE_ARCH = YES; 420 | SDKROOT = iphoneos; 421 | TARGETED_DEVICE_FAMILY = "1,2"; 422 | }; 423 | name = Debug; 424 | }; 425 | 97C147041CF9000F007C117D /* Release */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ALWAYS_SEARCH_USER_PATHS = NO; 429 | CLANG_ANALYZER_NONNULL = YES; 430 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 431 | CLANG_CXX_LIBRARY = "libc++"; 432 | CLANG_ENABLE_MODULES = YES; 433 | CLANG_ENABLE_OBJC_ARC = YES; 434 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 435 | CLANG_WARN_BOOL_CONVERSION = YES; 436 | CLANG_WARN_COMMA = YES; 437 | CLANG_WARN_CONSTANT_CONVERSION = YES; 438 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 439 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 440 | CLANG_WARN_EMPTY_BODY = YES; 441 | CLANG_WARN_ENUM_CONVERSION = YES; 442 | CLANG_WARN_INFINITE_RECURSION = YES; 443 | CLANG_WARN_INT_CONVERSION = YES; 444 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 445 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 446 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 447 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 448 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 449 | CLANG_WARN_STRICT_PROTOTYPES = YES; 450 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 451 | CLANG_WARN_UNREACHABLE_CODE = YES; 452 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 453 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 454 | COPY_PHASE_STRIP = NO; 455 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 456 | ENABLE_NS_ASSERTIONS = NO; 457 | ENABLE_STRICT_OBJC_MSGSEND = YES; 458 | GCC_C_LANGUAGE_STANDARD = gnu99; 459 | GCC_NO_COMMON_BLOCKS = YES; 460 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 461 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 462 | GCC_WARN_UNDECLARED_SELECTOR = YES; 463 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 464 | GCC_WARN_UNUSED_FUNCTION = YES; 465 | GCC_WARN_UNUSED_VARIABLE = YES; 466 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 467 | MTL_ENABLE_DEBUG_INFO = NO; 468 | SDKROOT = iphoneos; 469 | SUPPORTED_PLATFORMS = iphoneos; 470 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 471 | TARGETED_DEVICE_FAMILY = "1,2"; 472 | VALIDATE_PRODUCT = YES; 473 | }; 474 | name = Release; 475 | }; 476 | 97C147061CF9000F007C117D /* Debug */ = { 477 | isa = XCBuildConfiguration; 478 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 479 | buildSettings = { 480 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 481 | CLANG_ENABLE_MODULES = YES; 482 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 483 | ENABLE_BITCODE = NO; 484 | INFOPLIST_FILE = Runner/Info.plist; 485 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 486 | PRODUCT_BUNDLE_IDENTIFIER = com.example.memo; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 489 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 490 | SWIFT_VERSION = 5.0; 491 | VERSIONING_SYSTEM = "apple-generic"; 492 | }; 493 | name = Debug; 494 | }; 495 | 97C147071CF9000F007C117D /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 498 | buildSettings = { 499 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 500 | CLANG_ENABLE_MODULES = YES; 501 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 502 | ENABLE_BITCODE = NO; 503 | INFOPLIST_FILE = Runner/Info.plist; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 505 | PRODUCT_BUNDLE_IDENTIFIER = com.example.memo; 506 | PRODUCT_NAME = "$(TARGET_NAME)"; 507 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 508 | SWIFT_VERSION = 5.0; 509 | VERSIONING_SYSTEM = "apple-generic"; 510 | }; 511 | name = Release; 512 | }; 513 | /* End XCBuildConfiguration section */ 514 | 515 | /* Begin XCConfigurationList section */ 516 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | 97C147031CF9000F007C117D /* Debug */, 520 | 97C147041CF9000F007C117D /* Release */, 521 | 249021D3217E4FDB00AE95B9 /* Profile */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | 97C147061CF9000F007C117D /* Debug */, 530 | 97C147071CF9000F007C117D /* Release */, 531 | 249021D4217E4FDB00AE95B9 /* Profile */, 532 | ); 533 | defaultConfigurationIsVisible = 0; 534 | defaultConfigurationName = Release; 535 | }; 536 | /* End XCConfigurationList section */ 537 | }; 538 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 539 | } 540 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/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/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | memo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/application/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:layoutr/common_layout.dart'; 4 | import 'package:memo/application/coordinator/coordinator_information_parser.dart'; 5 | import 'package:memo/application/coordinator/coordinator_router_delegate.dart'; 6 | import 'package:memo/application/coordinator/routes_coordinator.dart'; 7 | import 'package:memo/application/layout_provider.dart'; 8 | import 'package:memo/application/pages/splash_page.dart'; 9 | import 'package:memo/application/view-models/app_vm.dart'; 10 | 11 | /// "Pre-load" root widget for the application 12 | /// 13 | /// This widget is a wrapper to provide (and load) an instance of [AppState], while showing a splash screen while it's 14 | /// loading for any external/internal dependencies. 15 | class AppRoot extends StatelessWidget { 16 | const AppRoot(this.vm); 17 | final AppVM vm; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ValueListenableBuilder>( 22 | valueListenable: vm, 23 | builder: (context, value, child) { 24 | return value.maybeWhen( 25 | data: (state) { 26 | // Wraps in a LayoutBuilder to override the layout provider accordingly 27 | return LayoutBuilder( 28 | builder: (context, constraints) { 29 | return ProviderScope( 30 | // Override all `Provider` and `ScopedProvider` that are late-initialized 31 | overrides: [ 32 | // exampleServices.overrideWithValue(state.exampleServices), 33 | layoutProvider.overrideWithValue(CommonLayout(constraints.maxWidth)), 34 | ], 35 | child: _LoadedAppRoot(), 36 | ); 37 | }, 38 | ); 39 | }, 40 | orElse: () => const MaterialApp(home: SplashPage()), 41 | ); 42 | }, 43 | ); 44 | } 45 | } 46 | 47 | /// Loaded root widget for the application 48 | /// 49 | /// After [AppRoot] is done with the loading, [_LoadedAppRoot] takes place (of the [SplashPage]) as the root of our 50 | /// application (and have all late-initialized providers available to it). 51 | class _LoadedAppRoot extends StatefulWidget { 52 | @override 53 | _LoadedAppRootState createState() => _LoadedAppRootState(); 54 | } 55 | 56 | class _LoadedAppRootState extends State<_LoadedAppRoot> { 57 | PlatformRouteInformationProvider? _routeInformationParser; 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | final coordinator = context.read(coordinatorProvider); 62 | 63 | // Must keep stored the `PlatformRouteInformationProvider`, otherwise when this widget rebuilds (for any reason), 64 | // the current route will be reset to our "root". Not sure if this is the best approach, but this new Router API 65 | // sure is confusing. 66 | _routeInformationParser ??= PlatformRouteInformationProvider( 67 | initialRouteInformation: RouteInformation(location: coordinator.currentRoute), 68 | ); 69 | 70 | return MaterialApp.router( 71 | title: 'Memo', 72 | debugShowCheckedModeBanner: false, 73 | routerDelegate: CoordinatorRouterDelegate(coordinator), 74 | routeInformationParser: CoordinatorInformationParser(), 75 | routeInformationProvider: _routeInformationParser, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/application/constants/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/lib/application/constants/.gitkeep -------------------------------------------------------------------------------- /lib/application/coordinator/coordinator_information_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:memo/application/coordinator/routes.dart'; 4 | import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; 5 | 6 | /// Custom type-safe layer implementation for [Router] route parsing 7 | /// 8 | /// See also: 9 | /// - `RoutesCoordinator`, where all the heavy navigation management is handled; 10 | /// - `CoordinatorRouterDelegate`, which intermediates the communication between the `RoutesCoordinator` and the OS. 11 | class CoordinatorInformationParser extends RouteInformationParser { 12 | @override 13 | Future parseRouteInformation(RouteInformation routeInformation) async { 14 | final location = routeInformation.location; 15 | if (location != null) { 16 | return SynchronousFuture(parseRoute(location)); 17 | } 18 | 19 | throw InconsistentStateError.coordinator('RouteInformation.location should never be null'); 20 | } 21 | 22 | @override 23 | RouteInformation restoreRouteInformation(AppPath configuration) => 24 | RouteInformation(location: configuration.formattedPath); 25 | } 26 | -------------------------------------------------------------------------------- /lib/application/coordinator/coordinator_router_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:memo/application/coordinator/routes.dart'; 4 | import 'package:memo/application/coordinator/routes_coordinator.dart'; 5 | import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; 6 | 7 | /// Core class to glue our [RoutesCoordinator] to the [Router] ecosystem, in this case, the [RouterDelegate] 8 | /// 9 | /// This delegate essentially connects our [RoutesCoordinator] implementation to the lifecycle of [Router], meaning that 10 | /// we can intermediate all the [Router]/[Navigator] configuration through a single core class, the [RoutesCoordinator]. 11 | /// 12 | /// See also: 13 | /// - [RoutesCoordinator], where all the heavy navigation management is handled; 14 | /// - `CoordinatorInformationParser`, which provides a type-safe way to parse [Router] locations. 15 | class CoordinatorRouterDelegate extends RouterDelegate 16 | with 17 | ChangeNotifier, // ignore: prefer_mixin 18 | PopNavigatorRouterDelegateMixin { 19 | CoordinatorRouterDelegate(RoutesCoordinator coordinator) : _coordinator = coordinator { 20 | // Pass along any updates from the RouterDelegate to our coordinator, so we can keep things synchronized 21 | // 22 | // We also can't use providers (we have to store the coordinator and attach a manual listener) because we must use 23 | // it in methods other than build, like in `currentConfiguration` and `setNewRoutePath` overrides. 24 | _coordinator.addListener(notifyListeners); 25 | } 26 | 27 | final RoutesCoordinator _coordinator; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Navigator( 32 | key: navigatorKey, 33 | onPopPage: _onPopPage, 34 | pages: _coordinator.pages, 35 | ); 36 | } 37 | 38 | @override 39 | Future setInitialRoutePath(AppPath configuration) { 40 | // TODO(matuella): This doesn't seems right, but because we are passing our own `RouteInformationProvider` in the 41 | // root `MaterialApp`, this will be called once again, thus resetting the navigation. 42 | return SynchronousFuture(null); 43 | } 44 | 45 | @override 46 | GlobalKey get navigatorKey => _coordinator.navigatorKey; 47 | 48 | @override 49 | AppPath get currentConfiguration => _coordinator.currentPath; 50 | 51 | @override 52 | Future setNewRoutePath(AppPath configuration) => SynchronousFuture(_coordinator.setNewRoutePath(configuration)); 53 | 54 | // `avoid_annotating_with_dynamic` conflicting with `implicit-dynamic` 55 | // ignore: avoid_annotating_with_dynamic 56 | bool _onPopPage(Route route, dynamic result) { 57 | final didPop = route.didPop(result); 58 | if (!didPop) { 59 | return false; 60 | } 61 | 62 | final routePage = route.settings; 63 | 64 | // Can't forget to notify the RoutesCoordinator that the page was popped 65 | if (routePage is Page) { 66 | _coordinator.didPop(routePage); 67 | } else { 68 | throw InconsistentStateError.coordinator( 69 | 'RouteSettings shoul be a subtype of `Page` - received type: ${routePage.runtimeType}', 70 | ); 71 | } 72 | 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/application/coordinator/routes.dart: -------------------------------------------------------------------------------- 1 | /// Parses a raw [path] into a type-safe [AppPath] 2 | AppPath parseRoute(String path) { 3 | final pathUri = Uri.parse(path); 4 | 5 | // Forwards '/' to our "first home", as we don't have one route for a "base" path 6 | if (pathUri.pathSegments.isEmpty) { 7 | return StudyPath(); 8 | } 9 | 10 | final firstSubPath = pathUri.pathSegments[0]; 11 | 12 | // handle home-related tabs 13 | if (firstSubPath == StudyPath.name) { 14 | return StudyPath(); 15 | } 16 | 17 | if (firstSubPath == ProgressPath.name) { 18 | return ProgressPath(); 19 | } 20 | 21 | // handle '/settings' and related 22 | if (firstSubPath == SettingsPath.name) { 23 | return SettingsPath(); 24 | } 25 | 26 | // Set a fallback to a page because web has this expected behavior of the user actively changing the URL 27 | return StudyPath(); 28 | } 29 | 30 | /// Class responsible for storing typed information about 31 | /// the current navigation path in the app 32 | abstract class AppPath { 33 | String get formattedPath; 34 | } 35 | 36 | // 37 | // Home 38 | // 39 | abstract class HomePath extends AppPath {} 40 | 41 | class StudyPath extends HomePath { 42 | static const name = 'study'; 43 | 44 | @override 45 | String get formattedPath => '/$name'; 46 | } 47 | 48 | class ProgressPath extends HomePath { 49 | static const name = 'progress'; 50 | 51 | @override 52 | String get formattedPath => '/$name'; 53 | } 54 | 55 | // 56 | // Settings 57 | // 58 | class SettingsPath extends AppPath { 59 | static const name = 'settings'; 60 | 61 | @override 62 | String get formattedPath => '/$name'; 63 | } 64 | -------------------------------------------------------------------------------- /lib/application/coordinator/routes_coordinator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:memo/application/coordinator/routes.dart'; 4 | import 'package:memo/application/pages/home/home_page.dart'; 5 | import 'package:memo/application/pages/settings/settings_page.dart'; 6 | import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; 7 | 8 | final coordinatorProvider = Provider( 9 | (ref) => RoutesCoordinator(navigatorKey: GlobalKey()), 10 | ); 11 | 12 | /// Coordinates the logic of the visible [Page]s stack based on locations (or URIs) 13 | /// 14 | /// This coordinator is built to handle one side of the two-way communication between [RoutesCoordinator] (this class, 15 | /// the core logic) and `CoordinatorRouterDelegate` (OS navigation intentions), by providing type-safe locations through 16 | /// `CoordinatorInformationParser` and [AppPath], with the objective to have complete control of Flutter's [Route] and 17 | /// [Navigator]. 18 | class RoutesCoordinator extends ChangeNotifier { 19 | RoutesCoordinator({required this.navigatorKey}) 20 | : _pages = [ 21 | MaterialPage( 22 | child: const HomePage(bottomTab: HomeBottomTab.study), 23 | key: _homeKey, 24 | name: StudyPath().formattedPath, 25 | ), 26 | ]; 27 | 28 | final GlobalKey navigatorKey; 29 | 30 | /// Shared key between the multiple home pages 31 | static const _homeKey = ValueKey('Home'); 32 | 33 | /// Descending ordered (visibles come last) stack of visible/existing pages 34 | List get pages => List.unmodifiable(_pages); 35 | List _pages; 36 | 37 | /// The path respective to the the current visible page 38 | AppPath get currentPath => parseRoute(currentRoute); 39 | 40 | /// Raw value for [currentPath] 41 | String get currentRoute { 42 | final currentRoute = _pages.last.name; 43 | 44 | if (currentRoute == null) { 45 | throw InconsistentStateError.coordinator('RoutesCoordinator list of pages was empty'); 46 | } 47 | 48 | return currentRoute; 49 | } 50 | 51 | /// Notifies this coordinator to pop the [page] 52 | void didPop(Page page) { 53 | _pages.remove(page); 54 | notifyListeners(); 55 | } 56 | 57 | /// Updates the current route to [path], making the required changes to the pages stack 58 | void setNewRoutePath(AppPath path) { 59 | if (path is HomePath) { 60 | // Any path inheriting HomePath should be considered as the root of our application, so when navigating to it, we 61 | // remove any other visible pages 62 | if (currentPath.formattedPath != path.formattedPath) { 63 | _pages = []; 64 | 65 | final HomeBottomTab homeTab; 66 | if (path is StudyPath) { 67 | homeTab = HomeBottomTab.study; 68 | } else if (path is ProgressPath) { 69 | homeTab = HomeBottomTab.progress; 70 | } else { 71 | throw InconsistentStateError.coordinator("Unsupported `HomeBottomTab` (with path '$path' for `HomePage`"); 72 | } 73 | 74 | _addPage(HomePage(bottomTab: homeTab), name: path.formattedPath, customKey: _homeKey); 75 | } else { 76 | // Otherwise we simply remove all pages other than the matched one 77 | _pages.removeRange(1, _pages.length); 78 | } 79 | } 80 | 81 | if (path is SettingsPath) { 82 | _addPage(SettingsPage(), name: path.formattedPath); 83 | } 84 | 85 | notifyListeners(); 86 | } 87 | 88 | /// Adds a new page to the top of the current stack (last in [_pages] list) 89 | /// 90 | /// - [isFullscreen] changes the type of navigation that this page is shown; 91 | /// - [customKey] overrides the custom key for this page (which is creating a `ValueKey` from the [name] argument). 92 | void _addPage(Widget widget, {required String name, bool isFullscreen = true, ValueKey? customKey}) { 93 | final pageKey = customKey ?? ValueKey(name); 94 | 95 | // Usually, there can't be multiple pages with the same key in the pages stack. If this is the case, the usage of 96 | // `Key` to manage the existing pages must be reevaluated 97 | final pagesWithSameKey = _pages.where((page) => page.key == pageKey); 98 | if (pagesWithSameKey.isNotEmpty) { 99 | throw InconsistentStateError.coordinator( 100 | 'No pages with the same keys are allowed. Page with the same key: $pagesWithSameKey', 101 | ); 102 | } 103 | 104 | _pages.add( 105 | MaterialPage(child: widget, key: pageKey, name: name, fullscreenDialog: isFullscreen), 106 | ); 107 | } 108 | 109 | /// Inserts a page in any position of the stack ([_pages] list) 110 | void _insertPage(Widget widget, {required int index, required String name}) { 111 | _pages.insert( 112 | index, 113 | MaterialPage(child: widget, key: ValueKey(name), name: name), 114 | ); 115 | } 116 | 117 | /// Adds a generic path to the current stack 118 | void addRoute(AppPath path) { 119 | setNewRoutePath(path); 120 | } 121 | 122 | void navigateToStudy() { 123 | setNewRoutePath(StudyPath()); 124 | } 125 | 126 | void navigateToProgress() { 127 | setNewRoutePath(ProgressPath()); 128 | } 129 | 130 | void navigateToSettings() { 131 | setNewRoutePath(SettingsPath()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/application/layout_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:layoutr/common_layout.dart'; 3 | 4 | /// Provides utilities to elements that requires a responsive layout 5 | /// 6 | /// Arguments: 7 | /// - `double`: the width of the device. 8 | // This `ScopedProvider` must be overriden in a `ProviderScope.overrides` before used 9 | final layoutProvider = ScopedProvider(null); 10 | -------------------------------------------------------------------------------- /lib/application/pages/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:memo/application/coordinator/routes_coordinator.dart'; 5 | 6 | enum HomeBottomTab { study, progress } 7 | 8 | class HomePage extends StatelessWidget { 9 | const HomePage({required this.bottomTab, Key? key}) : super(key: key); 10 | 11 | final HomeBottomTab bottomTab; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final tabIndex = HomeBottomTab.values.indexOf(bottomTab); 16 | 17 | return Scaffold( 18 | appBar: _AppBar(bottomTab), 19 | // IndexedStack to retain each page state, more specifically, to preserving scrolling 20 | body: IndexedStack( 21 | index: tabIndex, 22 | children: [ 23 | Scaffold( 24 | body: Container(), 25 | ), 26 | Scaffold( 27 | body: Container(), 28 | ) 29 | ], 30 | ), 31 | bottomNavigationBar: _BottomAppBar(bottomTab), 32 | ); 33 | } 34 | } 35 | 36 | // A custom app bar as `PreferredSizeWidget`, to conform to the requirements of a `Scaffold.appBar` 37 | class _AppBar extends HookWidget implements PreferredSizeWidget { 38 | const _AppBar(this._tab); 39 | 40 | @override 41 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 42 | 43 | final HomeBottomTab _tab; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return AppBar( 48 | title: Text(_tab.title), 49 | actions: [ 50 | IconButton( 51 | icon: const Icon(Icons.settings), 52 | onPressed: () { 53 | context.read(coordinatorProvider).navigateToSettings(); 54 | }, 55 | ), 56 | ], 57 | ); 58 | } 59 | } 60 | 61 | class _BottomAppBar extends StatelessWidget { 62 | const _BottomAppBar(this._tab); 63 | 64 | final HomeBottomTab _tab; 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | final tabItems = HomeBottomTab.values 69 | .map( 70 | (tab) => BottomNavigationBarItem(icon: Icon(tab.icon), label: tab.title), 71 | ) 72 | .toList(); 73 | 74 | return BottomAppBar( 75 | child: BottomNavigationBar( 76 | onTap: (index) { 77 | switch (HomeBottomTab.values[index]) { 78 | case HomeBottomTab.study: 79 | context.read(coordinatorProvider).navigateToStudy(); 80 | break; 81 | case HomeBottomTab.progress: 82 | context.read(coordinatorProvider).navigateToProgress(); 83 | break; 84 | } 85 | }, 86 | currentIndex: HomeBottomTab.values.indexOf(_tab), 87 | items: tabItems, 88 | ), 89 | ); 90 | } 91 | } 92 | 93 | extension _TabMetadata on HomeBottomTab { 94 | String get title { 95 | switch (this) { 96 | case HomeBottomTab.study: 97 | return 'Study'; 98 | case HomeBottomTab.progress: 99 | return 'Progress'; 100 | } 101 | } 102 | 103 | IconData get icon { 104 | switch (this) { 105 | case HomeBottomTab.study: 106 | return Icons.folder_open; 107 | case HomeBottomTab.progress: 108 | return Icons.arrow_upward; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/application/pages/settings/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar(title: const Text('Settings')), 8 | body: const Center(child: Text('This is the settings')), 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/application/pages/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SplashPage extends StatelessWidget { 4 | const SplashPage({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Scaffold( 9 | body: Center( 10 | child: Text('MEMO', style: TextStyle(fontSize: 72)), 11 | ), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/application/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/lib/application/utils/.gitkeep -------------------------------------------------------------------------------- /lib/application/view-models/app_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:memo/data/repositories/deck_repository.dart'; 4 | import 'package:memo/domain/services/deck_services.dart'; 5 | import 'package:memo/data/gateways/document_database_gateway.dart'; 6 | import 'package:memo/data/gateways/sembast_database.dart' as sembast_db; 7 | import 'package:sembast/sembast.dart'; 8 | 9 | /// Manages all app asynchronous dependencies 10 | /// 11 | /// This is where we glue all nested interdependencies that will be used through the whole application's lifecycle. 12 | /// 13 | /// Ideally, this should also be a provider (a [FutureProvider]), but because we cannot override providers (non 14 | /// [ScopedProvider]) that aren't in a root [ProviderScope], we fall back to a more vanilla implementation, using the 15 | /// [ValueNotifier], provided by the `flutter/foundation` library. 16 | /// 17 | /// When resolving the future, returns all required dependencies through an [AppState] instance 18 | abstract class AppVM extends ValueNotifier> { 19 | AppVM(AsyncValue value) : super(value); 20 | } 21 | 22 | class AppVMImpl extends AppVM { 23 | AppVMImpl() : super(const AsyncValue.loading()) { 24 | _loadAppVM(); 25 | } 26 | 27 | /// Requests this instance to load all of its dependencies 28 | Future _loadAppVM() async { 29 | const splashMinDuration = Duration(milliseconds: 500); 30 | final dependencies = await Future.wait([ 31 | sembast_db.openDatabase(), 32 | // Set a minimum (reasonable) duration for this first load, as it may simply flick a splash screen if too fast 33 | Future.delayed(splashMinDuration), 34 | ]); 35 | 36 | // Ideally, we shouldn't let a Data layer component be instantiated in an application component (VM), but due to how 37 | // all "state-management" libraries work (in response to Flutter's widget-centric design), it's almost impossible to 38 | // not attach application-wide dependencies into the widget's tree, meaning: at some point, the UI will have to know 39 | // about these classes and, in our case, river_pod is not different. What we are - currently - leaking: 40 | // - Services to UI: we need all services to override the default null value of its `Provider` in the root 41 | // `ProviderScope`. This is because all data-related dependencies are async. While we could make all of our services 42 | // `FutureProvider`, it would generate a significant boilerplate throughout all the application. 43 | // 44 | // All of these needs a late initialization due to runtime dependencies, which we will only know after some async 45 | // initialization. 46 | 47 | final dbRepo = SembastGateway(dependencies[0] as Database); 48 | final decksRepo = DeckRepositoryImpl(dbRepo); 49 | final deckServices = DeckServicesImpl(decksRepo); 50 | 51 | value = AsyncValue.data(AppState(deckServices: deckServices)); 52 | } 53 | } 54 | 55 | class AppState { 56 | const AppState({required this.deckServices}); 57 | final DeckServices deckServices; 58 | } 59 | -------------------------------------------------------------------------------- /lib/application/widgets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/lib/application/widgets/.gitkeep -------------------------------------------------------------------------------- /lib/core/faults/errors/base_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | /// Core [Error] class - every class that represent an [Error] should extend from [BaseError]. 5 | /// 6 | /// The difference between throwing an [Error] and an [Exception] can be found in their respective declarations in 7 | /// `dart:core`. 8 | @immutable 9 | abstract class BaseError extends Error with EquatableMixin { 10 | BaseError({required this.type, required this.message}); 11 | 12 | final ErrorType type; 13 | final String message; 14 | 15 | @override 16 | List get props => [type]; 17 | 18 | @override 19 | String toString() => '$type - $message'; 20 | } 21 | 22 | enum ErrorType { 23 | // InconsistentStateError 24 | inconsistentState, 25 | coordinatorInconsistentState, 26 | serviceInconsistentState, 27 | repositoryInconsistentState, 28 | viewModelInconsistentState, 29 | layoutInconsistentState, 30 | 31 | // SerializationError 32 | serialization, 33 | } 34 | -------------------------------------------------------------------------------- /lib/core/faults/errors/inconsistent_state_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/core/faults/errors/base_error.dart'; 2 | 3 | /// Inconsistent state through the developer's logic 4 | /// 5 | /// Thrown in unexpected scenarios where an arbitrary logic should never fail. 6 | /// 7 | /// A [InconsistentStateError] should never be thrown where any external dependencies fail, like opening a third-party 8 | /// plugin and/or library, making network calls, and so on. 9 | class InconsistentStateError extends BaseError { 10 | /// Creates a new generic inconsistency error that doesn't address a major application layer 11 | /// 12 | /// See also: 13 | /// - [InconsistentStateError.coordinator] - for inconsistent states in the coordinator; 14 | /// - [InconsistentStateError.service] - for inconsistent states in services; 15 | /// - [InconsistentStateError.repository] - for inconsistent states in repositories; 16 | /// - [InconsistentStateError.viewModel] - for inconsistent states in VMs; 17 | /// - [InconsistentStateError.layout] - for inconsistent states in the layout. 18 | InconsistentStateError(String message) : super(type: ErrorType.inconsistentState, message: message); 19 | 20 | /// Creates a new error to represent the coordinator's inconsistent state 21 | InconsistentStateError.coordinator(String message) 22 | : super(type: ErrorType.coordinatorInconsistentState, message: message); 23 | 24 | /// Creates a new error to represent a service's inconsistent state 25 | InconsistentStateError.service(String message) : super(type: ErrorType.serviceInconsistentState, message: message); 26 | 27 | /// Creates a new error to represent a repository's inconsistent state 28 | InconsistentStateError.repository(String message) 29 | : super(type: ErrorType.repositoryInconsistentState, message: message); 30 | 31 | /// Creates a new error to represent a viewModel's inconsistent state 32 | InconsistentStateError.viewModel(String message) 33 | : super(type: ErrorType.viewModelInconsistentState, message: message); 34 | 35 | /// Creates a new error to represent a layout inconsistent state 36 | InconsistentStateError.layout(String message) : super(type: ErrorType.layoutInconsistentState, message: message); 37 | } 38 | -------------------------------------------------------------------------------- /lib/core/faults/errors/serialization_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/core/faults/errors/base_error.dart'; 2 | 3 | /// Failed to parse an object instance to/from a raw value 4 | class SerializationError extends BaseError { 5 | SerializationError(String message) : super(type: ErrorType.serialization, message: message); 6 | } 7 | -------------------------------------------------------------------------------- /lib/core/faults/exceptions/base_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | typedef ExceptionObserver = void Function(BaseException exception); 5 | 6 | /// The core [Exception] class - every class that represent an [Exception] should extend from [BaseException]. 7 | /// 8 | /// The difference between throwing an [Error] and an [Exception] can be found in their respective declarations in 9 | /// `dart.core`. 10 | @immutable 11 | abstract class BaseException extends Equatable implements Exception { 12 | BaseException({required this.type, this.debugInfo, this.debugData}) { 13 | observer?.call(this); 14 | } 15 | 16 | final ExceptionType type; 17 | 18 | final String? debugInfo; 19 | final dynamic? debugData; 20 | 21 | /// Unique instance to observe all [BaseException] instances 22 | /// 23 | /// This observer is called whenever the constructor body - of a new [BaseException] instance - runs. 24 | static ExceptionObserver? observer; 25 | 26 | @override 27 | List get props => [type]; 28 | 29 | @override 30 | String toString() => ''' 31 | [BaseException - $type] $debugInfo. 32 | Debug Data: $debugData 33 | '''; 34 | } 35 | 36 | enum ExceptionType { 37 | placeholderType, 38 | } 39 | -------------------------------------------------------------------------------- /lib/data/gateways/document_database_gateway.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:sembast/sembast.dart'; 3 | 4 | /// Handles the local persistence to the database 5 | abstract class DocumentDatabaseGateway { 6 | /// Adds an [object] to the [store], using a [id] 7 | /// 8 | /// If there is already an object with the same [id], the default behavior is to merge all of its fields. 9 | /// [shouldMerge] should be `false` if pre-existing fields should not be merged. 10 | Future put({ 11 | required String id, 12 | required Map object, 13 | required String store, 14 | bool shouldMerge = true, 15 | }); 16 | 17 | /// Deletes the value with [id] from the [store] 18 | Future remove({required String id, required String store}); 19 | 20 | /// Retrieves an object with [id] from the [store] 21 | /// 22 | /// Returns `null` if the key doesn't exist 23 | Future?> get({required String id, required String store}); 24 | 25 | /// Retrieves all objects within [store] 26 | Future>> getAll({required String store}); 27 | 28 | /// Retrieves a stream of all the [store] objects, triggered whenever any update occurs to this [store] 29 | Future>>> listenAll({required String store}); 30 | } 31 | 32 | // 33 | // DocumentDatabaseGateway implementation using `sembast` 34 | // 35 | class SembastGateway implements DocumentDatabaseGateway { 36 | SembastGateway(this._db); 37 | 38 | // `sembast` database instance 39 | final Database _db; 40 | 41 | @override 42 | Future put({ 43 | required String id, 44 | required Map object, 45 | required String store, 46 | bool shouldMerge = true, 47 | }) async { 48 | final storeMap = stringMapStoreFactory.store(store); 49 | 50 | await storeMap.record(id).put(_db, object, merge: shouldMerge); 51 | } 52 | 53 | @override 54 | Future remove({required String id, required String store}) async { 55 | final storeMap = stringMapStoreFactory.store(store); 56 | await storeMap.record(id).delete(_db); 57 | } 58 | 59 | @override 60 | Future?> get({required String id, required String store}) async { 61 | final storeMap = stringMapStoreFactory.store(store); 62 | return storeMap.record(id).get(_db); 63 | } 64 | 65 | @override 66 | Future>> getAll({ 67 | required String store, 68 | }) async { 69 | final storeMap = stringMapStoreFactory.store(store); 70 | 71 | final allRecords = await storeMap.find(_db); 72 | return allRecords.map((record) => record.value).toList(); 73 | } 74 | 75 | @override 76 | Future>>> listenAll({required String store}) async { 77 | final storeMap = stringMapStoreFactory.store(store); 78 | 79 | return storeMap.query().onSnapshots(_db).transform(snapshotTransformer); 80 | } 81 | 82 | /// Transforms a list of `sembast` snapshot records into a list of objects 83 | final snapshotTransformer = 84 | StreamTransformer>>, List>>.fromHandlers( 85 | handleData: (snapshots, sink) { 86 | final transformedRecords = snapshots.map((record) => record.value).toList(); 87 | sink.add(transformedRecords); 88 | }, 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /lib/data/gateways/sembast_database.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:sembast/sembast.dart'; 7 | import 'package:sembast/sembast_io.dart'; 8 | 9 | const _schemaVersion = 1; 10 | const _dbName = 'memo_sembast.db'; 11 | 12 | /// Opens this application's [Database], creating a new one if nonexistent 13 | Future openDatabase() async { 14 | final dir = await getApplicationDocumentsDirectory(); 15 | // Make sure that the application documents directory exists 16 | await dir.create(recursive: true); 17 | 18 | final dbPath = path.join(dir.path, _dbName); 19 | 20 | return databaseFactoryIo.openDatabase(dbPath, version: _schemaVersion, onVersionChanged: applyMigrations); 21 | } 22 | 23 | @visibleForTesting 24 | Future applyMigrations(Database db, int oldVersion, int newVersion) async { 25 | // Call the necessary migrations in order 26 | } 27 | 28 | // 29 | // Migrations 30 | // 31 | 32 | // Example: 33 | // Future migrateToVersion2(Database db) async { 34 | // final store = stringMapStoreFactory.store('storeThatNeedsMigration'); 35 | // final updatableItemsFinder = Finder(filter: Filter.equals('myUpdatedField', 1)); 36 | // await store.update(db, { 'myUpdatedField': 2 }, finder: updatableItemsFinder); 37 | // } 38 | -------------------------------------------------------------------------------- /lib/data/repositories/deck_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/domain/models/deck.dart'; 2 | import 'package:memo/data/serializers/deck_serializer.dart'; 3 | import 'package:memo/data/gateways/document_database_gateway.dart'; 4 | 5 | abstract class DeckRepository { 6 | Future> getAllDecks(); 7 | } 8 | 9 | class DeckRepositoryImpl implements DeckRepository { 10 | DeckRepositoryImpl(this._db); 11 | 12 | final DocumentDatabaseGateway _db; 13 | final _deckSerializer = DeckSerializer(); 14 | final _deckStore = 'decks'; 15 | 16 | @override 17 | Future> getAllDecks() async { 18 | final rawDecks = await _db.getAll(store: _deckStore); 19 | return rawDecks.map(_deckSerializer.from).toList(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/serializers/card_block_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/core/faults/errors/serialization_error.dart'; 2 | import 'package:memo/data/serializers/serializer.dart'; 3 | import 'package:memo/domain/enums/card_block_type.dart'; 4 | import 'package:memo/domain/models/card_block.dart'; 5 | 6 | class CardBlockSerializer implements Serializer> { 7 | @override 8 | CardBlock from(Map json) { 9 | final rawType = json['type'] as String; 10 | final type = _typeFromRaw(rawType); 11 | 12 | final rawContents = json['rawContents'] as String; 13 | 14 | return CardBlock(type: type, rawContents: rawContents); 15 | } 16 | 17 | @override 18 | Map to(CardBlock block) => { 19 | 'type': block.type.raw, 20 | 'rawContents': block.rawContents, 21 | }; 22 | 23 | CardBlockType _typeFromRaw(String raw) => CardBlockType.values.firstWhere( 24 | (type) => type.raw == raw, 25 | orElse: () { 26 | throw SerializationError("Failed to find a CardBlockType with the raw value of '$raw'"); 27 | }, 28 | ); 29 | } 30 | 31 | extension on CardBlockType { 32 | String get raw { 33 | switch (this) { 34 | case CardBlockType.text: 35 | return 'text'; 36 | case CardBlockType.htmlText: 37 | return 'htmlText'; 38 | case CardBlockType.image: 39 | return 'image'; 40 | case CardBlockType.code: 41 | return 'code'; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/data/serializers/card_execution_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/core/faults/errors/serialization_error.dart'; 2 | import 'package:memo/data/serializers/card_block_serializer.dart'; 3 | import 'package:memo/data/serializers/serializer.dart'; 4 | import 'package:memo/domain/enums/card_difficulty.dart'; 5 | import 'package:memo/domain/models/card_execution.dart'; 6 | 7 | class CardExecutionSerializer implements Serializer> { 8 | final blockSerializer = CardBlockSerializer(); 9 | 10 | @override 11 | CardExecution from(Map json) { 12 | final rawStarted = json['started'] as int; 13 | final started = DateTime.fromMillisecondsSinceEpoch(rawStarted, isUtc: true); 14 | 15 | final rawFinished = json['finished'] as int; 16 | final finished = DateTime.fromMillisecondsSinceEpoch(rawFinished, isUtc: true); 17 | 18 | final rawAnswer = json['answer'] as List; 19 | final rawQuestion = json['question'] as List; 20 | 21 | // Casting just to make sure, because sembast returns an ImmutableList 22 | final answer = rawAnswer.cast>().map(blockSerializer.from).toList(); 23 | final question = rawQuestion.cast>().map(blockSerializer.from).toList(); 24 | 25 | final rawDifficulty = json['answeredDifficulty'] as int; 26 | final answeredDifficulty = _typeFromRaw(rawDifficulty); 27 | 28 | return CardExecution( 29 | started: started, 30 | finished: finished, 31 | question: question, 32 | answer: answer, 33 | answeredDifficulty: answeredDifficulty, 34 | ); 35 | } 36 | 37 | @override 38 | Map to(CardExecution execution) => { 39 | 'started': execution.started.toUtc().millisecondsSinceEpoch, 40 | 'finished': execution.finished.toUtc().millisecondsSinceEpoch, 41 | 'answer': execution.answer.map(blockSerializer.to), 42 | 'question': execution.question.map(blockSerializer.to), 43 | 'answeredDifficulty': execution.answeredDifficulty.raw, 44 | }; 45 | 46 | CardDifficulty _typeFromRaw(int raw) => CardDifficulty.values.firstWhere( 47 | (type) => type.raw == raw, 48 | orElse: () { 49 | throw SerializationError("Failed to find a CardDifficulty with the raw value of '$raw'"); 50 | }, 51 | ); 52 | } 53 | 54 | extension on CardDifficulty { 55 | int get raw { 56 | switch (this) { 57 | case CardDifficulty.easy: 58 | return 1; 59 | case CardDifficulty.medium: 60 | return 2; 61 | case CardDifficulty.hard: 62 | return 3; 63 | } 64 | } 65 | } 66 | 67 | class CardExecutionsSerializer implements Serializer> { 68 | final executionSerializer = CardExecutionSerializer(); 69 | 70 | @override 71 | CardExecutions from(Map json) { 72 | final cardId = json['cardId'] as String; 73 | final deckId = json['deckId'] as String; 74 | 75 | final rawExecutions = json['executions'] as List; 76 | // Casting just to make sure, because sembast returns an ImmutableList 77 | final executions = rawExecutions.cast>().map(executionSerializer.from).toList(); 78 | 79 | return CardExecutions(cardId: cardId, deckId: deckId, executions: executions); 80 | } 81 | 82 | @override 83 | Map to(CardExecutions cardExecutions) => { 84 | 'cardId': cardExecutions.cardId, 85 | 'deckId': cardExecutions.deckId, 86 | 'executions': cardExecutions.executions.map(executionSerializer.to).toList(), 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /lib/data/serializers/card_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/data/serializers/card_block_serializer.dart'; 2 | import 'package:memo/data/serializers/card_execution_serializer.dart'; 3 | import 'package:memo/data/serializers/serializer.dart'; 4 | import 'package:memo/domain/models/card.dart'; 5 | import 'package:memo/domain/models/card_execution.dart'; 6 | 7 | class CardSerializer implements Serializer> { 8 | final blockSerializer = CardBlockSerializer(); 9 | final executionSerializer = CardExecutionSerializer(); 10 | 11 | @override 12 | Card from(Map json) { 13 | final id = json['id'] as String; 14 | final deckId = json['deckId'] as String; 15 | 16 | final rawAnswer = json['answer'] as List; 17 | final rawQuestion = json['question'] as List; 18 | 19 | // Casting just to make sure, because sembast returns an ImmutableList 20 | final answer = rawAnswer.cast>().map(blockSerializer.from).toList(); 21 | final question = rawQuestion.cast>().map(blockSerializer.from).toList(); 22 | 23 | final executionsAmount = json['executionsAmount'] as int; 24 | 25 | CardExecution? lastExecution; 26 | if (json.containsKey('lastExecution')) { 27 | final rawLastExecution = json['lastExecution'] as Map; 28 | lastExecution = executionSerializer.from(rawLastExecution); 29 | } 30 | 31 | DateTime? dueDate; 32 | if (json.containsKey('dueDate')) { 33 | final rawDueDate = json['dueDate'] as int; 34 | dueDate = DateTime.fromMillisecondsSinceEpoch(rawDueDate, isUtc: true); 35 | } 36 | 37 | return Card( 38 | id: id, 39 | deckId: deckId, 40 | question: question, 41 | answer: answer, 42 | executionsAmount: executionsAmount, 43 | lastExecution: lastExecution, 44 | dueDate: dueDate, 45 | ); 46 | } 47 | 48 | @override 49 | Map to(Card card) => { 50 | 'id': card.id, 51 | 'deckId': card.deckId, 52 | 'answer': card.answer.map(blockSerializer.to), 53 | 'question': card.question.map(blockSerializer.to), 54 | 'executionsAmount': card.executionsAmount, 55 | if (card.lastExecution != null) 'lastExecution': executionSerializer.to(card.lastExecution!), 56 | if (card.dueDate != null) 'dueDate': card.dueDate!.toUtc().millisecondsSinceEpoch, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /lib/data/serializers/deck_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/data/serializers/serializer.dart'; 2 | import 'package:memo/domain/models/deck.dart'; 3 | 4 | class DeckSerializer implements Serializer> { 5 | @override 6 | Deck from(Map json) { 7 | final id = json['id'] as String; 8 | final name = json['name'] as String; 9 | final description = json['description'] as String; 10 | final category = json['category'] as String; 11 | 12 | final rawTags = json['tags'] as List; 13 | // Casting just to make sure, because sembast returns an ImmutableList 14 | final tags = rawTags.cast(); 15 | 16 | final timeSpentInMillis = json['timeSpentInMillis'] as int; 17 | final easyCardsAmount = json['easyCardsAmount'] as int; 18 | final mediumCardsAmount = json['mediumCardsAmount'] as int; 19 | final hardCardsAmount = json['hardCardsAmount'] as int; 20 | 21 | return Deck( 22 | id: id, 23 | name: name, 24 | description: description, 25 | category: category, 26 | tags: tags, 27 | timeSpentInMillis: timeSpentInMillis, 28 | easyCardsAmount: easyCardsAmount, 29 | mediumCardsAmount: mediumCardsAmount, 30 | hardCardsAmount: hardCardsAmount, 31 | ); 32 | } 33 | 34 | @override 35 | Map to(Deck deck) => { 36 | 'id': deck.id, 37 | 'name': deck.name, 38 | 'description': deck.description, 39 | 'category': deck.category, 40 | 'tags': deck.tags, 41 | 'timeSpentInMillis': deck.timeSpentInMillis, 42 | 'easyCardsAmount': deck.easyCardsAmount, 43 | 'mediumCardsAmount': deck.mediumCardsAmount, 44 | 'hardCardsAmount': deck.hardCardsAmount, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/data/serializers/resource_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/data/serializers/serializer.dart'; 2 | import 'package:memo/domain/enums/resource_type.dart'; 3 | import 'package:memo/domain/models/resource.dart'; 4 | 5 | class ResourceSerializer implements Serializer> { 6 | @override 7 | Resource from(Map json) { 8 | final id = json['id'] as String; 9 | final description = json['description'] as String; 10 | final rawType = json['type'] as String; 11 | final type = _typeFromRaw(rawType); 12 | 13 | final url = json['url'] as String; 14 | 15 | final rawTags = json['tags'] as List; 16 | // Casting just to make sure, because sembast returns an ImmutableList 17 | final tags = rawTags.cast(); 18 | 19 | return Resource( 20 | id: id, 21 | description: description, 22 | tags: tags, 23 | type: type, 24 | url: url, 25 | ); 26 | } 27 | 28 | @override 29 | Map to(Resource resource) => { 30 | 'id': resource.id, 31 | 'description': resource.description, 32 | 'tags': resource.tags, 33 | 'type': resource.type.raw, 34 | 'url': resource.url, 35 | }; 36 | 37 | ResourceType _typeFromRaw(String raw) => ResourceType.values.firstWhere( 38 | (type) => type.raw == raw, 39 | orElse: () => ResourceType.unknown, 40 | ); 41 | } 42 | 43 | extension on ResourceType { 44 | String get raw { 45 | switch (this) { 46 | case ResourceType.article: 47 | return 'article'; 48 | case ResourceType.book: 49 | return 'book'; 50 | case ResourceType.video: 51 | return 'video'; 52 | case ResourceType.unknown: 53 | return 'unknown'; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/data/serializers/serializer.dart: -------------------------------------------------------------------------------- 1 | /// Middleware that should be responsible of parsing a type [T] to/from a JSON representation 2 | abstract class Serializer { 3 | T from(U json); 4 | U to(T object); 5 | } 6 | -------------------------------------------------------------------------------- /lib/domain/enums/card_block_type.dart: -------------------------------------------------------------------------------- 1 | enum CardBlockType { text, htmlText, image, code } 2 | -------------------------------------------------------------------------------- /lib/domain/enums/card_difficulty.dart: -------------------------------------------------------------------------------- 1 | enum CardDifficulty { easy, medium, hard } 2 | -------------------------------------------------------------------------------- /lib/domain/enums/resource_type.dart: -------------------------------------------------------------------------------- 1 | enum ResourceType { article, book, video, unknown } 2 | -------------------------------------------------------------------------------- /lib/domain/models/card.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:memo/domain/models/card_block.dart'; 3 | import 'package:memo/domain/models/card_execution.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | @immutable 7 | class Card extends Equatable { 8 | Card({ 9 | required this.id, 10 | required this.deckId, 11 | required this.question, 12 | required this.answer, 13 | this.executionsAmount = 0, 14 | this.lastExecution, 15 | this.dueDate, 16 | }) : assert(executionsAmount >= 0, 'executionsAmount must be a positive (or zero) integer'), 17 | assert(question.isNotEmpty), 18 | assert(answer.isNotEmpty), 19 | assert( 20 | (lastExecution == null && executionsAmount == 0) || (lastExecution != null && executionsAmount > 0), 21 | 'If lastExecution is provided, the executionsAmount must be greater than 0, and vice versa', 22 | ), 23 | assert( 24 | (lastExecution == null && dueDate == null) || (lastExecution != null && dueDate != null), 25 | 'Both lastExecution and dueDate must be simultaneously null or not null', 26 | ); 27 | 28 | final String id; 29 | 30 | final String deckId; 31 | 32 | /// Ordered blocks to represent this card's question and provide the necessary metadata 33 | final List question; 34 | 35 | /// Ordered blocks to represent this card's answer and provide the necessary metadata 36 | final List answer; 37 | 38 | /// The amount of times which this [Card] has been executed 39 | final int executionsAmount; 40 | 41 | final CardExecution? lastExecution; 42 | DateTime? get lastExecuted => lastExecution?.finished; 43 | 44 | /// The date which this [Card] is requires to be reviewed 45 | final DateTime? dueDate; 46 | 47 | /// `true` if this [Card] was never executed 48 | bool get isPristine => lastExecution == null; 49 | 50 | @override 51 | List get props => [id, deckId, question, answer, executionsAmount, lastExecuted, lastExecution, dueDate]; 52 | } 53 | -------------------------------------------------------------------------------- /lib/domain/models/card_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:memo/domain/enums/card_block_type.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | /// Wraps the [rawContents] of a "segment" of the respective `Card` answer/question 6 | /// 7 | /// This is just a single piece of a `Card`'s question or answer, which can be composed of multiple [CardBlock]s. 8 | @immutable 9 | class CardBlock extends Equatable { 10 | CardBlock({required this.type, required this.rawContents}) : assert(rawContents.isNotEmpty); 11 | 12 | final CardBlockType type; 13 | final String rawContents; 14 | 15 | @override 16 | List get props => [type, rawContents]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/models/card_execution.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:memo/domain/enums/card_difficulty.dart'; 3 | import 'package:memo/domain/models/card_block.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | /// Representation of the exact history (immutable) of a `Card` execution 7 | @immutable 8 | class CardExecution extends Equatable { 9 | CardExecution({ 10 | required this.started, 11 | required this.finished, 12 | required this.question, 13 | required this.answer, 14 | required this.answeredDifficulty, 15 | }) : assert(started.isBefore(finished)), 16 | assert(question.isNotEmpty), 17 | assert(answer.isNotEmpty); 18 | 19 | final DateTime started; 20 | final DateTime finished; 21 | int get timeSpentInMillis => started.difference(finished).inMilliseconds; 22 | 23 | final List question; 24 | final List answer; 25 | 26 | final CardDifficulty answeredDifficulty; 27 | 28 | @override 29 | List get props => [started, finished, question, answer, answeredDifficulty]; 30 | } 31 | 32 | /// Associates a `Deck.id` and all executions for a card with its particular `cardId` 33 | @immutable 34 | class CardExecutions extends Equatable { 35 | CardExecutions({required this.cardId, required this.deckId, required this.executions}) 36 | : assert(executions.isNotEmpty); 37 | 38 | final String cardId; 39 | final String deckId; 40 | final List executions; 41 | 42 | @override 43 | List get props => [cardId, deckId, executions]; 44 | } 45 | -------------------------------------------------------------------------------- /lib/domain/models/deck.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @immutable 5 | class Deck extends Equatable { 6 | const Deck({ 7 | required this.id, 8 | required this.name, 9 | required this.description, 10 | required this.category, 11 | required this.tags, 12 | this.timeSpentInMillis = 0, 13 | this.easyCardsAmount = 0, 14 | this.mediumCardsAmount = 0, 15 | this.hardCardsAmount = 0, 16 | }) : assert(timeSpentInMillis >= 0, 'timeSpentInMillis must be a positive (or zero) integer'), 17 | assert(easyCardsAmount >= 0, 'easyCardsAmount must be a positive (or zero) integer'), 18 | assert(mediumCardsAmount >= 0, 'mediumCardsAmount must be a positive (or zero) integer'), 19 | assert(hardCardsAmount >= 0, 'hardCardsAmount must be a positive (or zero) integer'); 20 | 21 | final String id; 22 | final String name; 23 | final String description; 24 | final String category; 25 | 26 | /// List of tags that can associate with this `Resource` 27 | /// 28 | /// This is useful in cases where we must match a `Deck.tags` with each available resources 29 | final List tags; 30 | 31 | /// The total amount of time spent executing `Card`s for this deck (in milliseconds) 32 | final int timeSpentInMillis; 33 | 34 | /// The total amount of easy answers (`CardDifficulty`) for this deck 35 | final int easyCardsAmount; 36 | 37 | /// The total amount of medium answers (`CardDifficulty`) for this deck 38 | final int mediumCardsAmount; 39 | 40 | /// The total amount of hard answers (`CardDifficulty`) for this deck 41 | final int hardCardsAmount; 42 | 43 | /// `true` if this [Deck] has never executed any `Card` 44 | bool get isPristine => timeSpentInMillis == 0; 45 | 46 | @override 47 | List get props => [ 48 | id, 49 | name, 50 | description, 51 | category, 52 | tags, 53 | timeSpentInMillis, 54 | easyCardsAmount, 55 | mediumCardsAmount, 56 | hardCardsAmount, 57 | ]; 58 | } 59 | -------------------------------------------------------------------------------- /lib/domain/models/resource.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:memo/domain/enums/resource_type.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | class Resource extends Equatable { 7 | Resource({required this.id, required this.description, required this.tags, required this.type, required this.url}) 8 | : assert(tags.isNotEmpty, 'tags must have at least one element'); 9 | 10 | final String id; 11 | 12 | final String description; 13 | 14 | /// List of tags that can associate with this [Resource] 15 | /// 16 | /// This is useful in cases where we must match a `Deck.tags` with each available resources 17 | final List tags; 18 | 19 | /// Metadata that describes which type of [url] this resource refers to 20 | /// 21 | /// Example types: "video", "article", etctera. 22 | final ResourceType type; 23 | 24 | /// URL that links to this particular [Resource] 25 | final String url; 26 | 27 | @override 28 | List get props => [id, description, tags, url]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/domain/services/deck_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:memo/data/repositories/deck_repository.dart'; 2 | import 'package:memo/domain/models/deck.dart'; 3 | 4 | abstract class DeckServices { 5 | Future> getAllDecks(); 6 | } 7 | 8 | class DeckServicesImpl implements DeckServices { 9 | DeckServicesImpl(this.repo); 10 | 11 | final DeckRepository repo; 12 | 13 | @override 14 | Future> getAllDecks() => repo.getAllDecks(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:memo/application/app.dart'; 3 | import 'package:memo/application/view-models/app_vm.dart'; 4 | 5 | void main() { 6 | WidgetsFlutterBinding.ensureInitialized(); 7 | 8 | final appVM = AppVMImpl(); 9 | runApp(AppRoot(appVM)); 10 | } 11 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "19.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.3.0" 18 | args: 19 | dependency: transitive 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.0" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.5.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.1.0" 39 | characters: 40 | dependency: transitive 41 | description: 42 | name: characters 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.0" 46 | charcode: 47 | dependency: transitive 48 | description: 49 | name: charcode 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | cli_util: 54 | dependency: transitive 55 | description: 56 | name: cli_util 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "0.3.0" 60 | clock: 61 | dependency: transitive 62 | description: 63 | name: clock 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.1.0" 67 | collection: 68 | dependency: transitive 69 | description: 70 | name: collection 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.15.0" 74 | convert: 75 | dependency: transitive 76 | description: 77 | name: convert 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "3.0.0" 81 | coverage: 82 | dependency: transitive 83 | description: 84 | name: coverage 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.0.2" 88 | crypto: 89 | dependency: transitive 90 | description: 91 | name: crypto 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "3.0.0" 95 | equatable: 96 | dependency: "direct main" 97 | description: 98 | name: equatable 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "2.0.0" 102 | fake_async: 103 | dependency: transitive 104 | description: 105 | name: fake_async 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.2.0" 109 | ffi: 110 | dependency: transitive 111 | description: 112 | name: ffi 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "1.0.0" 116 | file: 117 | dependency: transitive 118 | description: 119 | name: file 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "6.1.0" 123 | flutter: 124 | dependency: "direct main" 125 | description: flutter 126 | source: sdk 127 | version: "0.0.0" 128 | flutter_hooks: 129 | dependency: "direct main" 130 | description: 131 | name: flutter_hooks 132 | url: "https://pub.dartlang.org" 133 | source: hosted 134 | version: "0.16.0" 135 | flutter_riverpod: 136 | dependency: "direct main" 137 | description: 138 | name: flutter_riverpod 139 | url: "https://pub.dartlang.org" 140 | source: hosted 141 | version: "0.13.1+1" 142 | flutter_test: 143 | dependency: "direct dev" 144 | description: flutter 145 | source: sdk 146 | version: "0.0.0" 147 | freezed_annotation: 148 | dependency: transitive 149 | description: 150 | name: freezed_annotation 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "0.14.1" 154 | glob: 155 | dependency: transitive 156 | description: 157 | name: glob 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "2.0.1" 161 | hooks_riverpod: 162 | dependency: "direct main" 163 | description: 164 | name: hooks_riverpod 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "0.13.1+1" 168 | http_multi_server: 169 | dependency: transitive 170 | description: 171 | name: http_multi_server 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "3.0.0" 175 | http_parser: 176 | dependency: transitive 177 | description: 178 | name: http_parser 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "4.0.0" 182 | io: 183 | dependency: transitive 184 | description: 185 | name: io 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "1.0.0" 189 | js: 190 | dependency: transitive 191 | description: 192 | name: js 193 | url: "https://pub.dartlang.org" 194 | source: hosted 195 | version: "0.6.3" 196 | json_annotation: 197 | dependency: transitive 198 | description: 199 | name: json_annotation 200 | url: "https://pub.dartlang.org" 201 | source: hosted 202 | version: "4.0.1" 203 | layoutr: 204 | dependency: "direct main" 205 | description: 206 | name: layoutr 207 | url: "https://pub.dartlang.org" 208 | source: hosted 209 | version: "1.0.0" 210 | logging: 211 | dependency: transitive 212 | description: 213 | name: logging 214 | url: "https://pub.dartlang.org" 215 | source: hosted 216 | version: "1.0.1" 217 | matcher: 218 | dependency: transitive 219 | description: 220 | name: matcher 221 | url: "https://pub.dartlang.org" 222 | source: hosted 223 | version: "0.12.10" 224 | meta: 225 | dependency: "direct main" 226 | description: 227 | name: meta 228 | url: "https://pub.dartlang.org" 229 | source: hosted 230 | version: "1.3.0" 231 | mime: 232 | dependency: transitive 233 | description: 234 | name: mime 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "1.0.0" 238 | mocktail: 239 | dependency: "direct dev" 240 | description: 241 | name: mocktail 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "0.1.1" 245 | node_preamble: 246 | dependency: transitive 247 | description: 248 | name: node_preamble 249 | url: "https://pub.dartlang.org" 250 | source: hosted 251 | version: "1.4.13" 252 | package_config: 253 | dependency: transitive 254 | description: 255 | name: package_config 256 | url: "https://pub.dartlang.org" 257 | source: hosted 258 | version: "2.0.0" 259 | path: 260 | dependency: "direct main" 261 | description: 262 | name: path 263 | url: "https://pub.dartlang.org" 264 | source: hosted 265 | version: "1.8.0" 266 | path_provider: 267 | dependency: "direct main" 268 | description: 269 | name: path_provider 270 | url: "https://pub.dartlang.org" 271 | source: hosted 272 | version: "2.0.1" 273 | path_provider_linux: 274 | dependency: transitive 275 | description: 276 | name: path_provider_linux 277 | url: "https://pub.dartlang.org" 278 | source: hosted 279 | version: "2.0.0" 280 | path_provider_macos: 281 | dependency: transitive 282 | description: 283 | name: path_provider_macos 284 | url: "https://pub.dartlang.org" 285 | source: hosted 286 | version: "2.0.0" 287 | path_provider_platform_interface: 288 | dependency: transitive 289 | description: 290 | name: path_provider_platform_interface 291 | url: "https://pub.dartlang.org" 292 | source: hosted 293 | version: "2.0.1" 294 | path_provider_windows: 295 | dependency: transitive 296 | description: 297 | name: path_provider_windows 298 | url: "https://pub.dartlang.org" 299 | source: hosted 300 | version: "2.0.0" 301 | pedantic: 302 | dependency: transitive 303 | description: 304 | name: pedantic 305 | url: "https://pub.dartlang.org" 306 | source: hosted 307 | version: "1.11.0" 308 | platform: 309 | dependency: transitive 310 | description: 311 | name: platform 312 | url: "https://pub.dartlang.org" 313 | source: hosted 314 | version: "3.0.0" 315 | plugin_platform_interface: 316 | dependency: transitive 317 | description: 318 | name: plugin_platform_interface 319 | url: "https://pub.dartlang.org" 320 | source: hosted 321 | version: "2.0.0" 322 | pool: 323 | dependency: transitive 324 | description: 325 | name: pool 326 | url: "https://pub.dartlang.org" 327 | source: hosted 328 | version: "1.5.0" 329 | process: 330 | dependency: transitive 331 | description: 332 | name: process 333 | url: "https://pub.dartlang.org" 334 | source: hosted 335 | version: "4.2.1" 336 | pub_semver: 337 | dependency: transitive 338 | description: 339 | name: pub_semver 340 | url: "https://pub.dartlang.org" 341 | source: hosted 342 | version: "2.0.0" 343 | riverpod: 344 | dependency: transitive 345 | description: 346 | name: riverpod 347 | url: "https://pub.dartlang.org" 348 | source: hosted 349 | version: "0.13.1" 350 | sembast: 351 | dependency: "direct main" 352 | description: 353 | name: sembast 354 | url: "https://pub.dartlang.org" 355 | source: hosted 356 | version: "3.0.0+4" 357 | shelf: 358 | dependency: transitive 359 | description: 360 | name: shelf 361 | url: "https://pub.dartlang.org" 362 | source: hosted 363 | version: "1.1.0" 364 | shelf_packages_handler: 365 | dependency: transitive 366 | description: 367 | name: shelf_packages_handler 368 | url: "https://pub.dartlang.org" 369 | source: hosted 370 | version: "3.0.0" 371 | shelf_static: 372 | dependency: transitive 373 | description: 374 | name: shelf_static 375 | url: "https://pub.dartlang.org" 376 | source: hosted 377 | version: "1.0.0" 378 | shelf_web_socket: 379 | dependency: transitive 380 | description: 381 | name: shelf_web_socket 382 | url: "https://pub.dartlang.org" 383 | source: hosted 384 | version: "1.0.1" 385 | sky_engine: 386 | dependency: transitive 387 | description: flutter 388 | source: sdk 389 | version: "0.0.99" 390 | source_map_stack_trace: 391 | dependency: transitive 392 | description: 393 | name: source_map_stack_trace 394 | url: "https://pub.dartlang.org" 395 | source: hosted 396 | version: "2.1.0" 397 | source_maps: 398 | dependency: transitive 399 | description: 400 | name: source_maps 401 | url: "https://pub.dartlang.org" 402 | source: hosted 403 | version: "0.10.10" 404 | source_span: 405 | dependency: transitive 406 | description: 407 | name: source_span 408 | url: "https://pub.dartlang.org" 409 | source: hosted 410 | version: "1.8.0" 411 | stack_trace: 412 | dependency: transitive 413 | description: 414 | name: stack_trace 415 | url: "https://pub.dartlang.org" 416 | source: hosted 417 | version: "1.10.0" 418 | state_notifier: 419 | dependency: transitive 420 | description: 421 | name: state_notifier 422 | url: "https://pub.dartlang.org" 423 | source: hosted 424 | version: "0.7.0" 425 | stream_channel: 426 | dependency: transitive 427 | description: 428 | name: stream_channel 429 | url: "https://pub.dartlang.org" 430 | source: hosted 431 | version: "2.1.0" 432 | strict: 433 | dependency: "direct dev" 434 | description: 435 | name: strict 436 | url: "https://pub.dartlang.org" 437 | source: hosted 438 | version: "1.0.0" 439 | string_scanner: 440 | dependency: transitive 441 | description: 442 | name: string_scanner 443 | url: "https://pub.dartlang.org" 444 | source: hosted 445 | version: "1.1.0" 446 | synchronized: 447 | dependency: transitive 448 | description: 449 | name: synchronized 450 | url: "https://pub.dartlang.org" 451 | source: hosted 452 | version: "3.0.0" 453 | term_glyph: 454 | dependency: transitive 455 | description: 456 | name: term_glyph 457 | url: "https://pub.dartlang.org" 458 | source: hosted 459 | version: "1.2.0" 460 | test: 461 | dependency: transitive 462 | description: 463 | name: test 464 | url: "https://pub.dartlang.org" 465 | source: hosted 466 | version: "1.16.5" 467 | test_api: 468 | dependency: transitive 469 | description: 470 | name: test_api 471 | url: "https://pub.dartlang.org" 472 | source: hosted 473 | version: "0.2.19" 474 | test_core: 475 | dependency: transitive 476 | description: 477 | name: test_core 478 | url: "https://pub.dartlang.org" 479 | source: hosted 480 | version: "0.3.15" 481 | typed_data: 482 | dependency: transitive 483 | description: 484 | name: typed_data 485 | url: "https://pub.dartlang.org" 486 | source: hosted 487 | version: "1.3.0" 488 | uuid: 489 | dependency: "direct main" 490 | description: 491 | name: uuid 492 | url: "https://pub.dartlang.org" 493 | source: hosted 494 | version: "3.0.2" 495 | vector_math: 496 | dependency: transitive 497 | description: 498 | name: vector_math 499 | url: "https://pub.dartlang.org" 500 | source: hosted 501 | version: "2.1.0" 502 | vm_service: 503 | dependency: transitive 504 | description: 505 | name: vm_service 506 | url: "https://pub.dartlang.org" 507 | source: hosted 508 | version: "6.1.0+1" 509 | watcher: 510 | dependency: transitive 511 | description: 512 | name: watcher 513 | url: "https://pub.dartlang.org" 514 | source: hosted 515 | version: "1.0.0" 516 | web_socket_channel: 517 | dependency: transitive 518 | description: 519 | name: web_socket_channel 520 | url: "https://pub.dartlang.org" 521 | source: hosted 522 | version: "2.0.0" 523 | webkit_inspection_protocol: 524 | dependency: transitive 525 | description: 526 | name: webkit_inspection_protocol 527 | url: "https://pub.dartlang.org" 528 | source: hosted 529 | version: "1.0.0" 530 | win32: 531 | dependency: transitive 532 | description: 533 | name: win32 534 | url: "https://pub.dartlang.org" 535 | source: hosted 536 | version: "2.0.4" 537 | xdg_directories: 538 | dependency: transitive 539 | description: 540 | name: xdg_directories 541 | url: "https://pub.dartlang.org" 542 | source: hosted 543 | version: "0.2.0" 544 | yaml: 545 | dependency: transitive 546 | description: 547 | name: yaml 548 | url: "https://pub.dartlang.org" 549 | source: hosted 550 | version: "3.1.0" 551 | sdks: 552 | dart: ">=2.12.0 <3.0.0" 553 | flutter: ">=1.20.0" 554 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: memo 2 | description: An open-source, programming-oriented spaced repetition application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" 7 | 8 | version: 0.1.0+0 9 | 10 | environment: 11 | sdk: ">=2.12.0 <3.0.0" 12 | 13 | dependencies: 14 | flutter: 15 | sdk: flutter 16 | 17 | ### 18 | # Core 19 | ### 20 | meta: ^1.3.0 21 | equatable: ^2.0.0 22 | path: ^1.8.0 23 | path_provider: ^2.0.0 24 | 25 | ### 26 | # Database & Storage 27 | ### 28 | sembast: ^3.0.0 29 | uuid: ^3.0.0 30 | 31 | ### 32 | # State Management 33 | ### 34 | flutter_riverpod: ^0.13.0 35 | 36 | ### 37 | # UI 38 | ### 39 | layoutr: ^1.0.0 40 | 41 | flutter_hooks: ^0.16.0 42 | hooks_riverpod: ^0.13.0 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | ### 49 | # Lint 50 | ### 51 | strict: ^1.0.0 52 | 53 | ### 54 | # Testing 55 | ### 56 | mocktail: ^0.1.0 57 | 58 | flutter: 59 | # The following line ensures that the Material Icons font is 60 | # included with your application, so that you can use the icons in 61 | # the material Icons class. 62 | uses-material-design: true 63 | -------------------------------------------------------------------------------- /test/data/database_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/data/gateways/document_database_gateway.dart'; 3 | import 'package:sembast/sembast.dart'; 4 | import 'package:sembast/sembast_memory.dart'; 5 | 6 | void main() { 7 | final fakeObject = {'fake': 'fake'}; 8 | const fakeRecordId = 'a-fake-id'; 9 | const fakeStore = 'fake_store'; 10 | final fakeRecord = stringMapStoreFactory.store(fakeStore).record(fakeRecordId); 11 | 12 | late Database memorySembast; 13 | late DocumentDatabaseGateway db; 14 | 15 | setUp(() async { 16 | await databaseFactoryMemory.deleteDatabase('test.db'); 17 | memorySembast = await databaseFactoryMemory.openDatabase('test.db'); 18 | db = SembastGateway(memorySembast); 19 | }); 20 | 21 | test('DatabaseRepositoryImpl should put a new object', () async { 22 | expect(await fakeRecord.get(memorySembast), null); 23 | 24 | await db.put(id: fakeRecordId, object: fakeObject, store: fakeStore); 25 | 26 | expect(await fakeRecord.get(memorySembast), fakeObject); 27 | }); 28 | 29 | test('DatabaseRepositoryImpl should update an existing object', () async { 30 | await fakeRecord.put(memorySembast, fakeObject); 31 | 32 | final fakeUpdatedObject = {'fake': 'fakeUpdated', 'newFake': 'fake'}; 33 | 34 | await db.put(id: fakeRecordId, object: fakeUpdatedObject, store: fakeStore); 35 | 36 | expect(await fakeRecord.get(memorySembast), fakeUpdatedObject); 37 | }); 38 | 39 | test('DatabaseRepositoryImpl should remove pre-existing fields in an update without merge', () async { 40 | await fakeRecord.put(memorySembast, fakeObject); 41 | 42 | final fakeUpdatedObject = {'newFake': 'fake'}; 43 | 44 | await db.put(id: fakeRecordId, object: fakeUpdatedObject, store: fakeStore, shouldMerge: false); 45 | 46 | expect(await fakeRecord.get(memorySembast), fakeUpdatedObject); 47 | }); 48 | 49 | test('DatabaseRepositoryImpl should maintain pre-existing fields in an update with merge', () async { 50 | await fakeRecord.put(memorySembast, fakeObject); 51 | 52 | final fakeUpdatedObject = {'newFake': 'fake'}; 53 | 54 | await db.put(id: fakeRecordId, object: fakeUpdatedObject, store: fakeStore); 55 | 56 | fakeUpdatedObject.addAll(fakeObject); 57 | expect(await fakeRecord.get(memorySembast), fakeUpdatedObject); 58 | }); 59 | 60 | test('DatabaseRepositoryImpl should remove an existing object', () async { 61 | await fakeRecord.put(memorySembast, fakeObject); 62 | 63 | expect(await fakeRecord.get(memorySembast), fakeObject); 64 | await db.remove(id: fakeRecordId, store: fakeStore); 65 | expect(await fakeRecord.get(memorySembast), null); 66 | }); 67 | 68 | test('DatabaseRepositoryImpl should do nothing when removing a nonexistent object', () async { 69 | await fakeRecord.put(memorySembast, fakeObject); 70 | 71 | await db.remove(id: fakeRecordId, store: fakeStore); 72 | expect(await fakeRecord.get(memorySembast), null); 73 | }); 74 | 75 | test('DatabaseRepositoryImpl should retrieve a single existing object', () async { 76 | await fakeRecord.put(memorySembast, fakeObject); 77 | 78 | final object = await db.get(id: fakeRecordId, store: fakeStore); 79 | expect(object, isNotNull); 80 | }); 81 | 82 | test('DatabaseRepositoryImpl should get null when retrieving a single nonexistent object', () async { 83 | final object = await db.get(id: fakeRecordId, store: fakeStore); 84 | expect(object, null); 85 | }); 86 | 87 | test('DatabaseRepositoryImpl should retrieve multiple objects', () async { 88 | await fakeRecord.put(memorySembast, fakeObject); 89 | await stringMapStoreFactory.store(fakeStore).record('2').put(memorySembast, fakeObject); 90 | 91 | final objects = await db.getAll(store: fakeStore); 92 | expect(objects.length, 2); 93 | }); 94 | 95 | test('DatabaseRepositoryImpl should retrieve an empty list if there is no objects in the store', () async { 96 | final objects = await db.getAll(store: fakeStore); 97 | expect(objects.isEmpty, true); 98 | }); 99 | 100 | // TODO(matuella): Find a way to test the listenAll 101 | } 102 | -------------------------------------------------------------------------------- /test/data/sembast_database_test.dart: -------------------------------------------------------------------------------- 1 | void main() { 2 | // `applyMigrations` tests should go here when new versions are added 3 | } 4 | -------------------------------------------------------------------------------- /test/data/serializers/card_block_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/enums/card_block_type.dart'; 3 | import 'package:memo/domain/models/card_block.dart'; 4 | import 'package:memo/data/serializers/card_block_serializer.dart'; 5 | 6 | import '../../fixtures/fixtures.dart' as fixtures; 7 | 8 | void main() { 9 | final serializer = CardBlockSerializer(); 10 | 11 | final testBlock = CardBlock(type: CardBlockType.text, rawContents: 'Raw text'); 12 | 13 | test('CardBlockSerializer should correctly encode/decode a CardBlock', () { 14 | final rawBlock = fixtures.cardBlock(); 15 | 16 | final decodedBlock = serializer.from(rawBlock); 17 | expect(decodedBlock, testBlock); 18 | 19 | final encodedBlock = serializer.to(decodedBlock); 20 | expect(encodedBlock, rawBlock); 21 | }); 22 | 23 | test('CardBlockSerializer should fail to decode without required properties', () { 24 | expect(() { 25 | final rawBlock = fixtures.cardBlock()..remove('type'); 26 | serializer.from(rawBlock); 27 | }, throwsA(isA())); 28 | 29 | expect(() { 30 | final rawBlock = fixtures.cardBlock()..remove('rawContents'); 31 | serializer.from(rawBlock); 32 | }, throwsA(isA())); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/data/serializers/card_execution_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/data/serializers/card_execution_serializer.dart'; 3 | import 'package:memo/domain/enums/card_block_type.dart'; 4 | import 'package:memo/domain/enums/card_difficulty.dart'; 5 | import 'package:memo/domain/models/card_block.dart'; 6 | import 'package:memo/domain/models/card_execution.dart'; 7 | 8 | import '../../fixtures/fixtures.dart' as fixtures; 9 | 10 | void main() { 11 | final testExecution = CardExecution( 12 | started: DateTime.fromMillisecondsSinceEpoch(1616747007347, isUtc: true), 13 | finished: DateTime.fromMillisecondsSinceEpoch(1616747027347, isUtc: true), 14 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 15 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string answer')], 16 | answeredDifficulty: CardDifficulty.medium, 17 | ); 18 | 19 | group('CardExecutionSerializer -', () { 20 | final serializer = CardExecutionSerializer(); 21 | 22 | test('CardExecutionSerializer should correctly encode/decode a CardExecution', () { 23 | final rawExecution = fixtures.cardExecution(); 24 | 25 | final decodedExecution = serializer.from(rawExecution); 26 | expect(decodedExecution, testExecution); 27 | 28 | final encodedExecution = serializer.to(decodedExecution); 29 | expect(encodedExecution, rawExecution); 30 | }); 31 | 32 | test('CardExecutionSerializer should fail to decode without required properties', () { 33 | expect(() { 34 | final rawExecution = fixtures.cardExecution()..remove('started'); 35 | serializer.from(rawExecution); 36 | }, throwsA(isA())); 37 | expect(() { 38 | final rawExecution = fixtures.cardExecution()..remove('finished'); 39 | serializer.from(rawExecution); 40 | }, throwsA(isA())); 41 | expect(() { 42 | final rawExecution = fixtures.cardExecution()..remove('question'); 43 | serializer.from(rawExecution); 44 | }, throwsA(isA())); 45 | expect(() { 46 | final rawExecution = fixtures.cardExecution()..remove('answer'); 47 | serializer.from(rawExecution); 48 | }, throwsA(isA())); 49 | expect(() { 50 | final rawExecution = fixtures.cardExecution()..remove('answeredDifficulty'); 51 | serializer.from(rawExecution); 52 | }, throwsA(isA())); 53 | }); 54 | }); 55 | 56 | group('CardExecutionsSerializer -', () { 57 | final serializer = CardExecutionsSerializer(); 58 | Map createRawExecutions() => { 59 | 'cardId': '1', 60 | 'deckId': '1', 61 | 'executions': [fixtures.cardExecution()], 62 | }; 63 | 64 | final testExecutions = CardExecutions(cardId: '1', deckId: '1', executions: [testExecution]); 65 | 66 | test('CardExecutionsSerializer should correctly encode/decode a CardExecutions', () { 67 | final rawExecutions = createRawExecutions(); 68 | 69 | final decodedExecutions = serializer.from(rawExecutions); 70 | expect(decodedExecutions, testExecutions); 71 | 72 | final encodedExecution = serializer.to(decodedExecutions); 73 | expect(encodedExecution, rawExecutions); 74 | }); 75 | 76 | test('CardExecutionsSerializer should fail to decode without required properties', () { 77 | expect(() { 78 | final rawExecutions = createRawExecutions()..remove('cardId'); 79 | serializer.from(rawExecutions); 80 | }, throwsA(isA())); 81 | expect(() { 82 | final rawExecutions = createRawExecutions()..remove('deckId'); 83 | serializer.from(rawExecutions); 84 | }, throwsA(isA())); 85 | expect(() { 86 | final rawExecutions = createRawExecutions()..remove('executions'); 87 | serializer.from(rawExecutions); 88 | }, throwsA(isA())); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /test/data/serializers/card_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/data/serializers/card_serializer.dart'; 3 | import 'package:memo/domain/enums/card_block_type.dart'; 4 | import 'package:memo/domain/enums/card_difficulty.dart'; 5 | import 'package:memo/domain/models/card.dart'; 6 | import 'package:memo/domain/models/card_block.dart'; 7 | import 'package:memo/domain/models/card_execution.dart'; 8 | 9 | import '../../fixtures/fixtures.dart' as fixtures; 10 | 11 | void main() { 12 | final serializer = CardSerializer(); 13 | final testCard = Card( 14 | id: '1', 15 | deckId: '1', 16 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 17 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string answer')], 18 | ); 19 | 20 | test('CardSerializer should correctly encode/decode a Card', () { 21 | final rawCard = fixtures.card(); 22 | 23 | final decodedCard = serializer.from(rawCard); 24 | expect(decodedCard, testCard); 25 | 26 | final encodedCard = serializer.to(decodedCard); 27 | expect(encodedCard, rawCard); 28 | }); 29 | 30 | test('CardSerializer should fail to decode without required properties', () { 31 | expect(() { 32 | final rawCard = fixtures.card()..remove('id'); 33 | serializer.from(rawCard); 34 | }, throwsA(isA())); 35 | 36 | expect(() { 37 | final rawCard = fixtures.card()..remove('deckId'); 38 | serializer.from(rawCard); 39 | }, throwsA(isA())); 40 | 41 | expect(() { 42 | final rawCard = fixtures.card()..remove('question'); 43 | serializer.from(rawCard); 44 | }, throwsA(isA())); 45 | 46 | expect(() { 47 | final rawCard = fixtures.card()..remove('answer'); 48 | serializer.from(rawCard); 49 | }, throwsA(isA())); 50 | 51 | expect(() { 52 | final rawCard = fixtures.card()..remove('executionsAmount'); 53 | serializer.from(rawCard); 54 | }, throwsA(isA())); 55 | }); 56 | 57 | test('CardSerializer should decode with optional properties', () { 58 | final rawCard = fixtures.card() 59 | ..['executionsAmount'] = 5 60 | ..['lastExecution'] = fixtures.cardExecution() // Use the existing `CardExecution` fixture 61 | ..['dueDate'] = 1616757292509; // Fake date in millis 62 | 63 | final decodedCard = serializer.from(rawCard); 64 | 65 | final allPropsCard = Card( 66 | id: testCard.id, 67 | deckId: testCard.deckId, 68 | question: testCard.question, 69 | answer: testCard.answer, 70 | executionsAmount: 5, 71 | lastExecution: CardExecution( 72 | started: DateTime.fromMillisecondsSinceEpoch(1616747007347, isUtc: true), 73 | finished: DateTime.fromMillisecondsSinceEpoch(1616747027347, isUtc: true), 74 | question: testCard.question, 75 | answer: testCard.answer, 76 | answeredDifficulty: CardDifficulty.medium, 77 | ), 78 | dueDate: DateTime.fromMillisecondsSinceEpoch(1616757292509, isUtc: true), 79 | ); 80 | 81 | expect(decodedCard, allPropsCard); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /test/data/serializers/deck_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/data/serializers/deck_serializer.dart'; 3 | import 'package:memo/domain/models/deck.dart'; 4 | 5 | import '../../fixtures/fixtures.dart' as fixtures; 6 | 7 | void main() { 8 | final serializer = DeckSerializer(); 9 | const testDeck = Deck( 10 | id: '1', 11 | name: 'My Deck', 12 | description: 'This deck represents a deck.', 13 | category: 'Category', 14 | tags: ['Tag 1', 'Tag 2'], 15 | ); 16 | 17 | test('DeckSerializer should correctly encode/decode a Deck', () { 18 | final rawDeck = fixtures.deck(); 19 | 20 | final decodedDeck = serializer.from(rawDeck); 21 | expect(decodedDeck, testDeck); 22 | 23 | final encodedDeck = serializer.to(decodedDeck); 24 | expect(encodedDeck, rawDeck); 25 | }); 26 | 27 | test('DeckSerializer should fail to decode without required properties', () { 28 | expect(() { 29 | final rawDeck = fixtures.deck()..remove('id'); 30 | serializer.from(rawDeck); 31 | }, throwsA(isA())); 32 | expect(() { 33 | final rawDeck = fixtures.deck()..remove('name'); 34 | serializer.from(rawDeck); 35 | }, throwsA(isA())); 36 | expect(() { 37 | final rawDeck = fixtures.deck()..remove('description'); 38 | serializer.from(rawDeck); 39 | }, throwsA(isA())); 40 | expect(() { 41 | final rawDeck = fixtures.deck()..remove('category'); 42 | serializer.from(rawDeck); 43 | }, throwsA(isA())); 44 | expect(() { 45 | final rawDeck = fixtures.deck()..remove('tags'); 46 | serializer.from(rawDeck); 47 | }, throwsA(isA())); 48 | expect(() { 49 | final rawDeck = fixtures.deck()..remove('timeSpentInMillis'); 50 | serializer.from(rawDeck); 51 | }, throwsA(isA())); 52 | expect(() { 53 | final rawDeck = fixtures.deck()..remove('easyCardsAmount'); 54 | serializer.from(rawDeck); 55 | }, throwsA(isA())); 56 | expect(() { 57 | final rawDeck = fixtures.deck()..remove('mediumCardsAmount'); 58 | serializer.from(rawDeck); 59 | }, throwsA(isA())); 60 | expect(() { 61 | final rawDeck = fixtures.deck()..remove('hardCardsAmount'); 62 | serializer.from(rawDeck); 63 | }, throwsA(isA())); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/data/serializers/resource_serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/data/serializers/resource_serializer.dart'; 3 | import 'package:memo/domain/enums/resource_type.dart'; 4 | import 'package:memo/domain/models/resource.dart'; 5 | 6 | import '../../fixtures/fixtures.dart' as fixtures; 7 | 8 | void main() { 9 | final serializer = ResourceSerializer(); 10 | 11 | final testResource = Resource( 12 | id: '1', 13 | description: 'This is a good article!', 14 | tags: const ['Tag 1', 'Tag 2'], 15 | type: ResourceType.article, 16 | url: 'https://google.com/', 17 | ); 18 | 19 | test('ResourceSerializer should correctly encode/decode a Resource', () { 20 | final rawResource = fixtures.resource(); 21 | 22 | final decodedResource = serializer.from(rawResource); 23 | expect(decodedResource, testResource); 24 | 25 | final encodedResource = serializer.to(decodedResource); 26 | expect(encodedResource, rawResource); 27 | }); 28 | 29 | test('ResourceSerializer should fail to decode without required properties', () { 30 | expect(() { 31 | final rawBlock = fixtures.resource()..remove('id'); 32 | serializer.from(rawBlock); 33 | }, throwsA(isA())); 34 | expect(() { 35 | final rawBlock = fixtures.resource()..remove('description'); 36 | serializer.from(rawBlock); 37 | }, throwsA(isA())); 38 | expect(() { 39 | final rawBlock = fixtures.resource()..remove('url'); 40 | serializer.from(rawBlock); 41 | }, throwsA(isA())); 42 | expect(() { 43 | final rawBlock = fixtures.resource()..remove('tags'); 44 | serializer.from(rawBlock); 45 | }, throwsA(isA())); 46 | expect(() { 47 | final rawBlock = fixtures.resource()..remove('type'); 48 | serializer.from(rawBlock); 49 | }, throwsA(isA())); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/domain/models/card_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/enums/card_block_type.dart'; 3 | import 'package:memo/domain/models/card_block.dart'; 4 | 5 | void main() { 6 | test('CardBlock should not allow empty contents', () { 7 | expect( 8 | () { 9 | CardBlock(type: CardBlockType.text, rawContents: ''); 10 | }, 11 | throwsA(isA()), 12 | ); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/domain/models/card_execution_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/enums/card_block_type.dart'; 3 | import 'package:memo/domain/enums/card_difficulty.dart'; 4 | import 'package:memo/domain/models/card_block.dart'; 5 | import 'package:memo/domain/models/card_execution.dart'; 6 | 7 | void main() { 8 | final started = DateTime.now(); 9 | final question = [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')]; 10 | final answer = [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string answer')]; 11 | 12 | test('CardExecution should not allow empty question/answer', () { 13 | expect( 14 | () { 15 | CardExecution( 16 | started: started, 17 | finished: started.add(const Duration(seconds: 1)), 18 | question: question, 19 | answer: const [], 20 | answeredDifficulty: CardDifficulty.easy, 21 | ); 22 | }, 23 | throwsA(isA()), 24 | ); 25 | 26 | expect( 27 | () { 28 | CardExecution( 29 | started: started, 30 | finished: started.add(const Duration(seconds: 1)), 31 | question: const [], 32 | answer: answer, 33 | answeredDifficulty: CardDifficulty.easy, 34 | ); 35 | }, 36 | throwsA(isA()), 37 | ); 38 | }); 39 | 40 | test('CardExecution should not allow finished to be before started', () { 41 | expect( 42 | () { 43 | CardExecution( 44 | started: started.add(const Duration(seconds: 1)), 45 | finished: started, 46 | question: question, 47 | answer: answer, 48 | answeredDifficulty: CardDifficulty.easy, 49 | ); 50 | }, 51 | throwsA(isA()), 52 | ); 53 | }); 54 | 55 | test('CardExecutions should not allow empty executions', () { 56 | expect( 57 | () { 58 | CardExecutions(cardId: 'cardId', deckId: 'deckId', executions: const []); 59 | }, 60 | throwsA(isA()), 61 | ); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/domain/models/card_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/enums/card_block_type.dart'; 3 | import 'package:memo/domain/enums/card_difficulty.dart'; 4 | import 'package:memo/domain/models/card.dart'; 5 | import 'package:memo/domain/models/card_block.dart'; 6 | import 'package:memo/domain/models/card_execution.dart'; 7 | 8 | void main() { 9 | test('Card should fail assert when executionsAmount is not a valid integer', () { 10 | expect( 11 | () { 12 | Card( 13 | id: '1', 14 | deckId: '1', 15 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 16 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string answer')], 17 | executionsAmount: -1, 18 | ); 19 | }, 20 | throwsA(isA()), 21 | ); 22 | }); 23 | 24 | test('Card should fail assert when question/answer are not present', () { 25 | expect( 26 | () { 27 | Card( 28 | id: '1', 29 | deckId: '1', 30 | question: const [], 31 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string answer')], 32 | executionsAmount: -1, 33 | ); 34 | }, 35 | throwsA(isA()), 36 | ); 37 | 38 | expect( 39 | () { 40 | Card( 41 | id: '1', 42 | deckId: '1', 43 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 44 | answer: const [], 45 | executionsAmount: -1, 46 | ); 47 | }, 48 | throwsA(isA()), 49 | ); 50 | }); 51 | 52 | test('Card should fail assert when lastExecution and executionsAmount are not in sync', () { 53 | expect( 54 | () { 55 | Card( 56 | id: '1', 57 | deckId: '1', 58 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 59 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 60 | executionsAmount: 1, 61 | ); 62 | }, 63 | throwsA(isA()), 64 | ); 65 | 66 | expect( 67 | () { 68 | Card( 69 | id: '1', 70 | deckId: '1', 71 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 72 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 73 | lastExecution: CardExecution( 74 | started: DateTime.fromMillisecondsSinceEpoch(1616747007347, isUtc: true), 75 | finished: DateTime.fromMillisecondsSinceEpoch(1616747027347, isUtc: true), 76 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 77 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 78 | answeredDifficulty: CardDifficulty.medium, 79 | ), 80 | ); 81 | }, 82 | throwsA(isA()), 83 | ); 84 | }); 85 | 86 | test('Card should fail assert when lastExecution and dueDate are not in sync', () { 87 | expect( 88 | () { 89 | Card( 90 | id: '1', 91 | deckId: '1', 92 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 93 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 94 | executionsAmount: 1, 95 | lastExecution: CardExecution( 96 | started: DateTime.fromMillisecondsSinceEpoch(1616747007347, isUtc: true), 97 | finished: DateTime.fromMillisecondsSinceEpoch(1616747027347, isUtc: true), 98 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 99 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 100 | answeredDifficulty: CardDifficulty.medium, 101 | ), 102 | ); 103 | }, 104 | throwsA(isA()), 105 | ); 106 | 107 | expect( 108 | () { 109 | Card( 110 | id: '1', 111 | deckId: '1', 112 | question: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 113 | answer: [CardBlock(type: CardBlockType.text, rawContents: 'This is my simple string question')], 114 | dueDate: DateTime.now(), 115 | ); 116 | }, 117 | throwsA(isA()), 118 | ); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /test/domain/models/deck_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/models/deck.dart'; 3 | 4 | void main() { 5 | test('Deck should not allow invalid integers for its properties', () { 6 | expect( 7 | () { 8 | Deck( 9 | id: 'id', 10 | name: 'name', 11 | description: 'description', 12 | category: 'category', 13 | tags: const [], 14 | timeSpentInMillis: -1, 15 | ); 16 | }, 17 | throwsA(isA()), 18 | ); 19 | 20 | expect( 21 | () { 22 | Deck( 23 | id: 'id', 24 | name: 'name', 25 | description: 'description', 26 | category: 'category', 27 | tags: const [], 28 | easyCardsAmount: -1, 29 | ); 30 | }, 31 | throwsA(isA()), 32 | ); 33 | 34 | expect( 35 | () { 36 | Deck( 37 | id: 'id', 38 | name: 'name', 39 | description: 'description', 40 | category: 'category', 41 | tags: const [], 42 | mediumCardsAmount: -1, 43 | ); 44 | }, 45 | throwsA(isA()), 46 | ); 47 | 48 | expect( 49 | () { 50 | Deck( 51 | id: 'id', 52 | name: 'name', 53 | description: 'description', 54 | category: 'category', 55 | tags: const [], 56 | hardCardsAmount: -1, 57 | ); 58 | }, 59 | throwsA(isA()), 60 | ); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /test/domain/models/resource_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:memo/domain/enums/resource_type.dart'; 3 | import 'package:memo/domain/models/resource.dart'; 4 | 5 | void main() { 6 | test('Deck should not allow empty tags', () { 7 | expect( 8 | () { 9 | Resource(id: 'id', description: 'description', tags: const [], type: ResourceType.article, url: 'url'); 10 | }, 11 | throwsA(isA()), 12 | ); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "deckId": "1", 4 | "question": [ 5 | { 6 | "type": "text", 7 | "rawContents": "This is my simple string question" 8 | } 9 | ], 10 | "answer": [ 11 | { 12 | "type": "text", 13 | "rawContents": "This is my simple string answer" 14 | } 15 | ], 16 | "executionsAmount": 0 17 | } -------------------------------------------------------------------------------- /test/fixtures/card_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "text", 3 | "rawContents": "Raw text" 4 | } -------------------------------------------------------------------------------- /test/fixtures/card_execution.json: -------------------------------------------------------------------------------- 1 | { 2 | "started": 1616747007347, 3 | "finished": 1616747027347, 4 | "question": [ 5 | { 6 | "type": "text", 7 | "rawContents": "This is my simple string question" 8 | } 9 | ], 10 | "answer": [ 11 | { 12 | "type": "text", 13 | "rawContents": "This is my simple string answer" 14 | } 15 | ], 16 | "answeredDifficulty": 2 17 | } -------------------------------------------------------------------------------- /test/fixtures/deck.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "name": "My Deck", 4 | "description": "This deck represents a deck.", 5 | "category": "Category", 6 | "tags": ["Tag 1", "Tag 2"], 7 | "timeSpentInMillis": 0, 8 | "easyCardsAmount": 0, 9 | "mediumCardsAmount": 0, 10 | "hardCardsAmount": 0 11 | } -------------------------------------------------------------------------------- /test/fixtures/fixtures.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | const _path = 'test/fixtures'; 5 | 6 | Map _readFixture(String name) => 7 | jsonDecode(File('$_path/$name').readAsStringSync()) as Map; 8 | 9 | Map cardExecution() => _readFixture('card_execution.json'); 10 | Map cardBlock() => _readFixture('card_block.json'); 11 | Map card() => _readFixture('card.json'); 12 | Map deck() => _readFixture('deck.json'); 13 | Map resource() => _readFixture('resource.json'); 14 | -------------------------------------------------------------------------------- /test/fixtures/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "description": "This is a good article!", 4 | "tags": ["Tag 1", "Tag 2"], 5 | "type": "article", 6 | "url": "https://google.com/" 7 | } -------------------------------------------------------------------------------- /test/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/test/utils/.gitkeep -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasmontano/memo/818d0f0bbb4ea8285d0536e0092d1b61ecb66d19/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | memo 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memo", 3 | "short_name": "memo", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | --------------------------------------------------------------------------------