From 813c2bdbf5580c355c7b04b4504ad52a733dcb02 Mon Sep 17 00:00:00 2001 From: Philip Johnson Date: Tue, 5 Nov 2024 10:51:17 -1000 Subject: [PATCH] Deploy website - based on 4a90ad52d851276221ead73dac0a476c4d0b0b59 --- 404.html | 4 +-- assets/js/0058b4c6.6e93fa71.js | 1 + assets/js/0058b4c6.8e9fe34f.js | 1 - assets/js/1e5c498d.2d5ae52d.js | 1 - assets/js/1e5c498d.b0d3812b.js | 1 + ...aa863.a5c0076e.js => 31caa863.98841c3b.js} | 2 +- assets/js/39838d4e.295e1e77.js | 1 + assets/js/39838d4e.76946f78.js | 1 - assets/js/505d7517.829b9dc4.js | 1 + assets/js/505d7517.e701aaa5.js | 1 - assets/js/6b1fc3de.240ddb3d.js | 1 - assets/js/6b1fc3de.f192a887.js | 1 + assets/js/7d1225b6.82c0a513.js | 1 - assets/js/7d1225b6.a1eabaf6.js | 1 + assets/js/7d56ced7.3bfef52c.js | 1 - assets/js/7d56ced7.e01aba21.js | 1 + assets/js/9140a56a.63431e4b.js | 1 + assets/js/a5b8d3e9.07f3c936.js | 1 - assets/js/a5b8d3e9.e7cd2989.js | 1 + assets/js/c7c467a1.79a7edaa.js | 1 - assets/js/c7c467a1.f201450f.js | 1 + assets/js/f3759001.356c3aa2.js | 1 + assets/js/f3759001.ecb01d32.js | 1 - assets/js/main.3278aba6.js | 2 ++ ...CENSE.txt => main.3278aba6.js.LICENSE.txt} | 0 assets/js/main.4b7e3713.js | 2 -- assets/js/runtime~main.1bfa5458.js | 1 - assets/js/runtime~main.819bba13.js | 1 + blog.html | 4 +-- blog/2023/02/10/welcome.html | 4 +-- blog/archive.html | 4 +-- docs/business.html | 4 +-- docs/business/market-size.html | 4 +-- docs/business/milestones.html | 4 +-- docs/business/roadmap.html | 4 +-- docs/develop.html | 6 ++-- docs/develop/architecture.html | 29 ++++++++++++------ docs/develop/backups.html | 8 ++--- docs/develop/coding-standards.html | 8 ++--- docs/develop/dart-analyze.html | 22 +++++++++++++ docs/develop/deployment.html | 8 ++--- docs/develop/design/badges.html | 6 ++-- docs/develop/design/data-model-old.html | 4 +-- docs/develop/design/data-model.html | 6 ++-- docs/develop/design/data-mutation.html | 8 ++--- docs/develop/design/input-fields.html | 6 ++-- docs/develop/design/with-widgets.html | 6 ++-- docs/develop/installation.html | 16 +++++----- docs/develop/onboarding.html | 22 +++++++------ .../release-0.0/chatgpt-feedback.html | 6 ++-- .../release-0.0/customer-feedback.html | 6 ++-- docs/develop/releases/release-0.0/design.html | 6 ++-- .../release-0.0/entrepreneur-feedback.html | 6 ++-- docs/develop/releases/release-1.0/cvp.html | 6 ++-- .../release-1.0/end-of-season-feedback.html | 6 ++-- docs/develop/releases/release-1.0/goals.html | 8 ++--- .../release-1.0/onboarding-feedback.html | 6 ++-- docs/develop/scripts.html | 8 ++--- docs/develop/testing.html | 12 +++++--- docs/home/food-security.html | 4 +-- docs/home/innovations.html | 4 +-- docs/home/related-work.html | 4 +-- docs/home/serious-gardeners.html | 4 +-- docs/home/sneak-peek.html | 4 +-- docs/home/team.html | 4 +-- docs/home/welcome.html | 4 +-- docs/user-guide/adding-plantings.html | 4 +-- .../adding-vendors-crops-varieties.html | 4 +-- docs/user-guide/badges.html | 4 +-- docs/user-guide/chat-rooms.html | 4 +-- docs/user-guide/define-a-garden.html | 4 +-- docs/user-guide/downloading.html | 4 +-- docs/user-guide/explore-a-chapter.html | 4 +-- docs/user-guide/explore-a-garden.html | 4 +-- docs/user-guide/geobot.html | 4 +-- docs/user-guide/guided-tour.html | 4 +-- docs/user-guide/observations.html | 4 +-- docs/user-guide/outcomes.html | 4 +-- docs/user-guide/overview.html | 4 +-- docs/user-guide/privacy.html | 4 +-- docs/user-guide/registration.html | 4 +-- docs/user-guide/scenarios.html | 4 +-- docs/user-guide/seeds.html | 4 +-- docs/user-guide/tasks.html | 4 +-- docs/user-guide/terms-and-conditions.html | 4 +-- img/develop/release-1.0/dart-analyze.png | Bin 0 -> 1251028 bytes img/develop/release-1.0/ggc-architecture.png | Bin 33889 -> 34355 bytes img/develop/release-1.0/ggc-architecture.pptx | Bin 47862 -> 49038 bytes .../release-1.0/ggc-dataflow-diagram.png | Bin 0 -> 39499 bytes index.html | 4 +-- markdown-page.html | 4 +-- sitemap.xml | 2 +- 92 files changed, 219 insertions(+), 181 deletions(-) create mode 100644 assets/js/0058b4c6.6e93fa71.js delete mode 100644 assets/js/0058b4c6.8e9fe34f.js delete mode 100644 assets/js/1e5c498d.2d5ae52d.js create mode 100644 assets/js/1e5c498d.b0d3812b.js rename assets/js/{31caa863.a5c0076e.js => 31caa863.98841c3b.js} (99%) create mode 100644 assets/js/39838d4e.295e1e77.js delete mode 100644 assets/js/39838d4e.76946f78.js create mode 100644 assets/js/505d7517.829b9dc4.js delete mode 100644 assets/js/505d7517.e701aaa5.js delete mode 100644 assets/js/6b1fc3de.240ddb3d.js create mode 100644 assets/js/6b1fc3de.f192a887.js delete mode 100644 assets/js/7d1225b6.82c0a513.js create mode 100644 assets/js/7d1225b6.a1eabaf6.js delete mode 100644 assets/js/7d56ced7.3bfef52c.js create mode 100644 assets/js/7d56ced7.e01aba21.js create mode 100644 assets/js/9140a56a.63431e4b.js delete mode 100644 assets/js/a5b8d3e9.07f3c936.js create mode 100644 assets/js/a5b8d3e9.e7cd2989.js delete mode 100644 assets/js/c7c467a1.79a7edaa.js create mode 100644 assets/js/c7c467a1.f201450f.js create mode 100644 assets/js/f3759001.356c3aa2.js delete mode 100644 assets/js/f3759001.ecb01d32.js create mode 100644 assets/js/main.3278aba6.js rename assets/js/{main.4b7e3713.js.LICENSE.txt => main.3278aba6.js.LICENSE.txt} (100%) delete mode 100644 assets/js/main.4b7e3713.js delete mode 100644 assets/js/runtime~main.1bfa5458.js create mode 100644 assets/js/runtime~main.819bba13.js create mode 100644 docs/develop/dart-analyze.html create mode 100644 img/develop/release-1.0/dart-analyze.png create mode 100644 img/develop/release-1.0/ggc-dataflow-diagram.png diff --git a/404.html b/404.html index b5e43f2f5..58fc2fc38 100644 --- a/404.html +++ b/404.html @@ -5,8 +5,8 @@ Page Not Found | Geo Garden Club - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

diff --git a/assets/js/0058b4c6.6e93fa71.js b/assets/js/0058b4c6.6e93fa71.js new file mode 100644 index 000000000..b2b2a1829 --- /dev/null +++ b/assets/js/0058b4c6.6e93fa71.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4088],{6462:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"homeSidebar":[{"type":"category","collapsed":false,"label":"About GeoGardenClub","items":[{"type":"link","label":"Welcome","href":"/docs/home/welcome","docId":"home/welcome","unlisted":false},{"type":"link","label":"\\"Serious\\" Gardeners","href":"/docs/home/serious-gardeners","docId":"home/serious-gardeners","unlisted":false},{"type":"link","label":"Design Innovations","href":"/docs/home/innovations","docId":"home/innovations","unlisted":false},{"type":"link","label":"Garden Planning Tools","href":"/docs/home/related-work","docId":"home/related-work","unlisted":false},{"type":"link","label":"Food Security","href":"/docs/home/food-security","docId":"home/food-security","unlisted":false},{"type":"link","label":"The Team","href":"/docs/home/team","docId":"home/team","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"User Guide","items":[{"type":"link","label":"Overview","href":"/docs/user-guide/overview","docId":"user-guide/overview","unlisted":false},{"type":"link","label":"Downloading","href":"/docs/user-guide/downloading","docId":"user-guide/downloading","unlisted":false},{"type":"link","label":"Registration","href":"/docs/user-guide/registration","docId":"user-guide/registration","unlisted":false},{"type":"link","label":"Define a Garden","href":"/docs/user-guide/define-a-garden","docId":"user-guide/define-a-garden","unlisted":false},{"type":"link","label":"Add Plantings to Beds","href":"/docs/user-guide/adding-plantings","docId":"user-guide/adding-plantings","unlisted":false},{"type":"link","label":"Explore a Garden","href":"/docs/user-guide/explore-a-garden","docId":"user-guide/explore-a-garden","unlisted":false},{"type":"link","label":"Explore a Chapter","href":"/docs/user-guide/explore-a-chapter","docId":"user-guide/explore-a-chapter","unlisted":false},{"type":"link","label":"Add Crops, Varieties, Vendors to the Chapter Database","href":"/docs/user-guide/adding-vendors-crops-varieties","docId":"user-guide/adding-vendors-crops-varieties","unlisted":false},{"type":"link","label":"Planting Scenarios","href":"/docs/user-guide/scenarios","docId":"user-guide/scenarios","unlisted":false},{"type":"link","label":"Observations","href":"/docs/user-guide/observations","docId":"user-guide/observations","unlisted":false},{"type":"link","label":"GeoBot","href":"/docs/user-guide/geobot","docId":"user-guide/geobot","unlisted":false},{"type":"link","label":"Badges","href":"/docs/user-guide/badges","docId":"user-guide/badges","unlisted":false},{"type":"link","label":"outcomes","href":"/docs/user-guide/outcomes","docId":"user-guide/outcomes","unlisted":false},{"type":"link","label":"Tasks","href":"/docs/user-guide/tasks","docId":"user-guide/tasks","unlisted":false},{"type":"link","label":"Seeds","href":"/docs/user-guide/seeds","docId":"user-guide/seeds","unlisted":false},{"type":"link","label":"Chat Rooms","href":"/docs/user-guide/chat-rooms","docId":"user-guide/chat-rooms","unlisted":false},{"type":"link","label":"Frequently Asked (Gardening) Questions","href":"/docs/user-guide/guided-tour","docId":"user-guide/guided-tour","unlisted":false},{"type":"link","label":"Terms and Conditions","href":"/docs/user-guide/terms-and-conditions","docId":"user-guide/terms-and-conditions","unlisted":false},{"type":"link","label":"Privacy Policy","href":"/docs/user-guide/privacy","docId":"user-guide/privacy","unlisted":false}],"collapsible":true}],"businessSidebar":[{"type":"link","label":"Welcome","href":"/docs/business/","docId":"business/index","unlisted":false},{"type":"link","label":"Roadmap","href":"/docs/business/roadmap","docId":"business/roadmap","unlisted":false},{"type":"link","label":"Milestones","href":"/docs/business/milestones","docId":"business/milestones","unlisted":false},{"type":"link","label":"Market Size","href":"/docs/business/market-size","docId":"business/market-size","unlisted":false},{"type":"link","label":"Lean Canvas","href":"https://docs.google.com/presentation/d/1oUzy1zeraTf6PgWlk2R3Ea7Iw2Ju24Dds5mffq2o5Wg/edit#slide=id.p1"},{"type":"link","label":"Gardening vs. Farming","href":"https://docs.google.com/presentation/d/1rMu7DWJblHvVJt6CGmR8eyCN7uBMXPfBpc1rdhanxjQ/edit#slide=id.p"},{"type":"link","label":"Home Gardening Pain Points","href":"https://docs.google.com/presentation/d/1TKDWQI60PxRhBpMGW0tvMyXX0nmgEBz71-DmoT2LxfU/edit#slide=id.g11d82564388_0_187"},{"type":"link","label":"BAI 2024 Pitch Deck","href":"https://geogardenclub.com/pdf/bai-pitch-2024.pdf"},{"type":"link","label":"Business Documents Repo","href":"https://github.com/geogardenclub/documents"},{"type":"link","label":"Developer Guide","href":"/docs/develop/"}],"developSidebar":[{"type":"link","label":"Welcome","href":"/docs/develop/","docId":"develop/index","unlisted":false},{"type":"link","label":"Onboarding","href":"/docs/develop/onboarding","docId":"develop/onboarding","unlisted":false},{"type":"link","label":"Installation","href":"/docs/develop/installation","docId":"develop/installation","unlisted":false},{"type":"link","label":"Scripts","href":"/docs/develop/scripts","docId":"develop/scripts","unlisted":false},{"type":"link","label":"Coding Standards","href":"/docs/develop/coding-standards","docId":"develop/coding-standards","unlisted":false},{"type":"link","label":"Architecture","href":"/docs/develop/architecture","docId":"develop/architecture","unlisted":false},{"type":"link","label":"Deployment","href":"/docs/develop/deployment","docId":"develop/deployment","unlisted":false},{"type":"link","label":"Backups","href":"/docs/develop/backups","docId":"develop/backups","unlisted":false},{"type":"category","collapsed":true,"label":"Design","items":[{"type":"link","label":"Data Model","href":"/docs/develop/design/data-model","docId":"develop/design/data-model","unlisted":false},{"type":"link","label":"Badges","href":"/docs/develop/design/badges","docId":"develop/design/badges","unlisted":false},{"type":"link","label":"GGC Input Fields","href":"/docs/develop/design/input-fields","docId":"develop/design/input-fields","unlisted":false},{"type":"link","label":"\\"With\\" widgets","href":"/docs/develop/design/with-widgets","docId":"develop/design/with-widgets","unlisted":false},{"type":"link","label":"Data Mutation","href":"/docs/develop/design/data-mutation","docId":"develop/design/data-mutation","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"Quality Assurance","items":[{"type":"link","label":"Dart analyze","href":"/docs/develop/dart-analyze","docId":"develop/dart-analyze","unlisted":false},{"type":"link","label":"Testing","href":"/docs/develop/testing","docId":"develop/testing","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Prior Releases","items":[{"type":"category","label":"Release 1.0 (Technology Evaluation)","collapsed":true,"items":[{"type":"link","label":"Technology Goals","href":"/docs/develop/releases/release-1.0/goals","docId":"develop/releases/release-1.0/goals","unlisted":false},{"type":"link","label":"Core Value Propositions","href":"/docs/develop/releases/release-1.0/cvp","docId":"develop/releases/release-1.0/cvp","unlisted":false},{"type":"link","label":"Onboarding Feedback","href":"/docs/develop/releases/release-1.0/onboarding-feedback","docId":"develop/releases/release-1.0/onboarding-feedback","unlisted":false},{"type":"link","label":"End of Season Feedback","href":"/docs/develop/releases/release-1.0/end-of-season-feedback","docId":"develop/releases/release-1.0/end-of-season-feedback","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Release 0.0 (Mockup)","items":[{"type":"link","label":"Design and implementation","href":"/docs/develop/releases/release-0.0/design","docId":"develop/releases/release-0.0/design","unlisted":false},{"type":"link","label":"Customer feedback","href":"/docs/develop/releases/release-0.0/customer-feedback","docId":"develop/releases/release-0.0/customer-feedback","unlisted":false},{"type":"link","label":"Entrepreneur feedback","href":"/docs/develop/releases/release-0.0/entrepreneur-feedback","docId":"develop/releases/release-0.0/entrepreneur-feedback","unlisted":false},{"type":"link","label":"ChatGPT feedback","href":"/docs/develop/releases/release-0.0/chatgpt-feedback","docId":"develop/releases/release-0.0/chatgpt-feedback","unlisted":false}],"collapsible":true}],"collapsible":true},{"type":"link","label":"Business Development Guide","href":"/docs/business/"}]},"docs":{"business/index":{"id":"business/index","title":"Welcome to the GGC Business Development Guide","description":"Welcome to the Business Development Guide for the Geo Garden Club project.","sidebar":"businessSidebar"},"business/market-size":{"id":"business/market-size","title":"Market Size Estimation (USA)","description":"Here is some data that can help provide a sense for the potential market size for GGC in the United States.","sidebar":"businessSidebar"},"business/milestones":{"id":"business/milestones","title":"Milestones","description":"| Year | Date | Milestone |","sidebar":"businessSidebar"},"business/roadmap":{"id":"business/roadmap","title":"Roadmap","description":"This roadmap documents our approach incremental development and release of our technology.","sidebar":"businessSidebar"},"develop/architecture":{"id":"develop/architecture","title":"Architecture","description":"The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the \\"Riverpod Architecture\\". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.","sidebar":"developSidebar"},"develop/backups":{"id":"develop/backups","title":"Backups","description":"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn\'t lose too much work.","sidebar":"developSidebar"},"develop/coding-standards":{"id":"develop/coding-standards","title":"Coding Standards","description":"In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid \\"technical debt\\".","sidebar":"developSidebar"},"develop/dart-analyze":{"id":"develop/dart-analyze","title":"Dart analyze","description":"One form of quality assurance in GGC is the use of Dart Analyze. This is a static analysis tool that looks for common code problems.","sidebar":"developSidebar"},"develop/deployment":{"id":"develop/deployment","title":"Deployment","description":"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.","sidebar":"developSidebar"},"develop/design/badges":{"id":"develop/design/badges","title":"Badges","description":"Goals","sidebar":"developSidebar"},"develop/design/data-model":{"id":"develop/design/data-model","title":"Data Model","description":"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we\'ve made along the way.","sidebar":"developSidebar"},"develop/design/data-model-old":{"id":"develop/design/data-model-old","title":"Data Model","description":"This page documents the data model intended to satisfy the 1.0 release requirements."},"develop/design/data-mutation":{"id":"develop/design/data-mutation","title":"Data Mutation","description":"Prelude: AsyncValue","sidebar":"developSidebar"},"develop/design/input-fields":{"id":"develop/design/input-fields","title":"GGC Input Fields","description":"Motivation","sidebar":"developSidebar"},"develop/design/with-widgets":{"id":"develop/design/with-widgets","title":"\\"With\\" widgets","description":"Why \\"with\\"?","sidebar":"developSidebar"},"develop/index":{"id":"develop/index","title":"Welcome to the GGC Developers Guide","description":"Welcome to the Developer\'s Guide for the Geo Garden Club project.","sidebar":"developSidebar"},"develop/installation":{"id":"develop/installation","title":"Installation","description":"Flutter","sidebar":"developSidebar"},"develop/onboarding":{"id":"develop/onboarding","title":"Onboarding","description":"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.","sidebar":"developSidebar"},"develop/releases/release-0.0/chatgpt-feedback":{"id":"develop/releases/release-0.0/chatgpt-feedback","title":"ChatGPT feedback","description":"Just for fun, I had a conversation with ChatGPT in April 2023 about how to design a gardening app. Much of its responses were uninsightful, but it did come up with some banging ideas for badges.","sidebar":"developSidebar"},"develop/releases/release-0.0/customer-feedback":{"id":"develop/releases/release-0.0/customer-feedback","title":"Customer feedback","description":"Following the Lean Startup principle of \\"validated learning\\", we performed an evaluation study of the technology innovations present in the mockup with 24 experienced gardeners during the summer and fall of 2022. We report on the results of that study in the following 15 minute video (or 7 minutes, if you run the video at 2x speed):","sidebar":"developSidebar"},"develop/releases/release-0.0/design":{"id":"develop/releases/release-0.0/design","title":"Design and implementation","description":"In 2022, we built a simple single page web application using React. The goal of the system was to provide an executable mockup to perform \\"customer discovery\\". The mockup showed potential users our vision of some of the features that would be made available in our production technology, and enabled us to gather feedback on the potential utility and value of our vision.","sidebar":"developSidebar"},"develop/releases/release-0.0/entrepreneur-feedback":{"id":"develop/releases/release-0.0/entrepreneur-feedback","title":"Entrepreneur feedback","description":"The following sections document feedback received from entrepreneurs and high tech professionals (who are not necessarily gardeners, and thus not in our customer demographics). Our request to these folks was to evaluate the viability of Agile/Geo Garden Club as a commercial technology venture, and to help us identify opportunities for improvement. We got a lot of useful feedback.","sidebar":"developSidebar"},"develop/releases/release-1.0/cvp":{"id":"develop/releases/release-1.0/cvp","title":"Core Value Propositions","description":"The goal of the 1.0 (Technology Evaluation) release is to provide an app to a small group that will use the app and provide us with feedback. The 1.0 release will partially test our business model by helping us evaluate its success at implementing the \\"core value propositions\\" (CVPs) for GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/end-of-season-feedback":{"id":"develop/releases/release-1.0/end-of-season-feedback","title":"End of Season Feedback","description":"In September 2024, Jenna sent out an 8 question survey to users to request feedback. We received five responses. Here is a summary of the feedback:","sidebar":"developSidebar"},"develop/releases/release-1.0/goals":{"id":"develop/releases/release-1.0/goals","title":"Technology Goals","description":"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/onboarding-feedback":{"id":"develop/releases/release-1.0/onboarding-feedback","title":"Onboarding Feedback","description":"Jenna personally onboarded six users to Release 1.0 and gathered the following feedback from them either during the onboarding session or shortly thereafter.","sidebar":"developSidebar"},"develop/scripts":{"id":"develop/scripts","title":"Scripts","description":"GGC development is supported by a number of Unix shell scripts. All scripts are named starting with \\"run\\" and use snake case to separate words in the script name.","sidebar":"developSidebar"},"develop/testing":{"id":"develop/testing","title":"Testing","description":"Another form of quality assurance in GGC is testing.","sidebar":"developSidebar"},"home/food-security":{"id":"home/food-security","title":"Food Security","description":"Food security is the elephant in the room, and a major motivation for founding GeoGardenClub. Here is a brief introduction to the relationship between food security and home gardens.","sidebar":"homeSidebar"},"home/innovations":{"id":"home/innovations","title":"Design Innovations","description":"What should an app provide if it is intended to support the needs of \\"serious\\" gardeners? Here is GeoGardenClub\'s answer:","sidebar":"homeSidebar"},"home/related-work":{"id":"home/related-work","title":"Garden Planning Tools","description":"If you search for \\"garden planning tools\\" on the Internet, you\'ll find dozens of applications. Most of those are essentially \\"landscape architecture\\" tools for people who want to design the visual look of their (flower) gardens. This is an interesting design problem, but not the problem addressed by Geo Garden Club.","sidebar":"homeSidebar"},"home/serious-gardeners":{"id":"home/serious-gardeners","title":"\\"Serious\\" Gardeners","description":"The founders of GeoGardenClub view food production as a vast continuum of activities and levels of commitment, as shown in the following diagram:","sidebar":"homeSidebar"},"home/sneak-peek":{"id":"home/sneak-peek","title":"Mobile App Sneak Peek","description":"We are working on the alpha release of the GeoGardenClub mobile app, with an expected release date of early 2024. While the app is not yet ready for prime time, we thought it would be fun to show you some selected screen shots so you can get an idea of where we\'re heading."},"home/team":{"id":"home/team","title":"The Team","description":"Jenna Deane","sidebar":"homeSidebar"},"home/welcome":{"id":"home/welcome","title":"Welcome","description":"GGC started in 2021 when a small group of gardeners in Bellingham, Washington realized that, despite the availability of dozens of home gardening apps, we were planning and managing our gardens using spreadsheets, Word docs, and even paper and pencil!","sidebar":"homeSidebar"},"user-guide/adding-plantings":{"id":"user-guide/adding-plantings","title":"Add Plantings to Beds","description":"Add plantings","sidebar":"homeSidebar"},"user-guide/adding-vendors-crops-varieties":{"id":"user-guide/adding-vendors-crops-varieties","title":"Add Crops, Varieties, Vendors to the Chapter Database","description":"The crops, varieties, and vendors in each chapter\'s database are crowdsourced from the chapter\'s members. This ensures that the database is exclusive to what is grown in the area. To add a crop, variety, or vendor navigate to the appropriate index screen from the side navigation menu.","sidebar":"homeSidebar"},"user-guide/badges":{"id":"user-guide/badges","title":"Badges","description":"Why Badges?","sidebar":"homeSidebar"},"user-guide/chat-rooms":{"id":"user-guide/chat-rooms","title":"Chat Rooms","description":"GGC allows gardeners to communicate with each other through chat rooms. Chat rooms are a great way to ask questions, share knowledge, and build community. Chat rooms are organized by chapter or garden.","sidebar":"homeSidebar"},"user-guide/define-a-garden":{"id":"user-guide/define-a-garden","title":"Define a Garden","description":"Defining a garden in GeoGardenClub requires you to create three kinds of entities: a \\"Garden\\", one or more \\"Beds\\", and one or more \\"Plantings\\". (You can also define a fourth type of entity, \\"Outcomes\\", but we\'ll get to that later).","sidebar":"homeSidebar"},"user-guide/downloading":{"id":"user-guide/downloading","title":"Downloading","description":"Enroll as a GGC Beta Tester","sidebar":"homeSidebar"},"user-guide/explore-a-chapter":{"id":"user-guide/explore-a-chapter","title":"Explore a Chapter","description":"Find the Chapter Index Screens","sidebar":"homeSidebar"},"user-guide/explore-a-garden":{"id":"user-guide/explore-a-garden","title":"Explore a Garden","description":"The summary card for every Garden in a Chapter contains a button labeled \\"Details\\". Pressing that button takes you to a screen with a bottom nav bar containing icons taking you to four screens: Timeline, Filter, Outcomes, and Tasks. Each view presents a useful perspective on that Garden.","sidebar":"homeSidebar"},"user-guide/geobot":{"id":"user-guide/geobot","title":"GeoBot","description":"Why GeoBot?","sidebar":"homeSidebar"},"user-guide/guided-tour":{"id":"user-guide/guided-tour","title":"Frequently Asked (Gardening) Questions","description":"GeoGardenClub was created to increase the performance of home, community, and school gardens and efficiency of the gardener by answering questions a gardener might have as they plan or work through the gardening season. Here are some of the questions that GeoGardenClub can help answer:","sidebar":"homeSidebar"},"user-guide/observations":{"id":"user-guide/observations","title":"Observations","description":"GGC allows gardeners to make \\"observations\\" regarding a planting of a plant variety on a specific day. Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests, document outcomes like yields, or simply record the progress of a planting.","sidebar":"homeSidebar"},"user-guide/outcomes":{"id":"user-guide/outcomes","title":"outcomes","description":"300---","sidebar":"homeSidebar"},"user-guide/overview":{"id":"user-guide/overview","title":"Overview","description":"Welcome to the User Guide for the GeoGardenClub app.","sidebar":"homeSidebar"},"user-guide/privacy":{"id":"user-guide/privacy","title":"Privacy Policy","description":"Last updated June 20, 2024","sidebar":"homeSidebar"},"user-guide/registration":{"id":"user-guide/registration","title":"Registration","description":"After downloading the app, open it to see the login screen. Tap Register to create a new account.","sidebar":"homeSidebar"},"user-guide/scenarios":{"id":"user-guide/scenarios","title":"Planting Scenarios","description":"Growing plants in a greenhouse or other climate controlled environment","sidebar":"homeSidebar"},"user-guide/seeds":{"id":"user-guide/seeds","title":"Seeds","description":"Geo Garden Club believes that seed saving and sharing is a critical component of building resilient gardening communities. By saving seeds, gardeners can ensure that they have access to the varieties that grow best in their area. By sharing seeds, gardeners can help others in their community grow the same varieties.","sidebar":"homeSidebar"},"user-guide/tasks":{"id":"user-guide/tasks","title":"Tasks","description":"What are Tasks?","sidebar":"homeSidebar"},"user-guide/terms-and-conditions":{"id":"user-guide/terms-and-conditions","title":"Terms and Conditions","description":"Last updated February 10, 2024","sidebar":"homeSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/0058b4c6.8e9fe34f.js b/assets/js/0058b4c6.8e9fe34f.js deleted file mode 100644 index 5c6b6c55c..000000000 --- a/assets/js/0058b4c6.8e9fe34f.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4088],{6462:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"homeSidebar":[{"type":"category","collapsed":false,"label":"About GeoGardenClub","items":[{"type":"link","label":"Welcome","href":"/docs/home/welcome","docId":"home/welcome","unlisted":false},{"type":"link","label":"\\"Serious\\" Gardeners","href":"/docs/home/serious-gardeners","docId":"home/serious-gardeners","unlisted":false},{"type":"link","label":"Design Innovations","href":"/docs/home/innovations","docId":"home/innovations","unlisted":false},{"type":"link","label":"Garden Planning Tools","href":"/docs/home/related-work","docId":"home/related-work","unlisted":false},{"type":"link","label":"Food Security","href":"/docs/home/food-security","docId":"home/food-security","unlisted":false},{"type":"link","label":"The Team","href":"/docs/home/team","docId":"home/team","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"User Guide","items":[{"type":"link","label":"Overview","href":"/docs/user-guide/overview","docId":"user-guide/overview","unlisted":false},{"type":"link","label":"Downloading","href":"/docs/user-guide/downloading","docId":"user-guide/downloading","unlisted":false},{"type":"link","label":"Registration","href":"/docs/user-guide/registration","docId":"user-guide/registration","unlisted":false},{"type":"link","label":"Define a Garden","href":"/docs/user-guide/define-a-garden","docId":"user-guide/define-a-garden","unlisted":false},{"type":"link","label":"Add Plantings to Beds","href":"/docs/user-guide/adding-plantings","docId":"user-guide/adding-plantings","unlisted":false},{"type":"link","label":"Explore a Garden","href":"/docs/user-guide/explore-a-garden","docId":"user-guide/explore-a-garden","unlisted":false},{"type":"link","label":"Explore a Chapter","href":"/docs/user-guide/explore-a-chapter","docId":"user-guide/explore-a-chapter","unlisted":false},{"type":"link","label":"Add Crops, Varieties, Vendors to the Chapter Database","href":"/docs/user-guide/adding-vendors-crops-varieties","docId":"user-guide/adding-vendors-crops-varieties","unlisted":false},{"type":"link","label":"Planting Scenarios","href":"/docs/user-guide/scenarios","docId":"user-guide/scenarios","unlisted":false},{"type":"link","label":"Observations","href":"/docs/user-guide/observations","docId":"user-guide/observations","unlisted":false},{"type":"link","label":"GeoBot","href":"/docs/user-guide/geobot","docId":"user-guide/geobot","unlisted":false},{"type":"link","label":"Badges","href":"/docs/user-guide/badges","docId":"user-guide/badges","unlisted":false},{"type":"link","label":"outcomes","href":"/docs/user-guide/outcomes","docId":"user-guide/outcomes","unlisted":false},{"type":"link","label":"Tasks","href":"/docs/user-guide/tasks","docId":"user-guide/tasks","unlisted":false},{"type":"link","label":"Seeds","href":"/docs/user-guide/seeds","docId":"user-guide/seeds","unlisted":false},{"type":"link","label":"Chat Rooms","href":"/docs/user-guide/chat-rooms","docId":"user-guide/chat-rooms","unlisted":false},{"type":"link","label":"Frequently Asked (Gardening) Questions","href":"/docs/user-guide/guided-tour","docId":"user-guide/guided-tour","unlisted":false},{"type":"link","label":"Terms and Conditions","href":"/docs/user-guide/terms-and-conditions","docId":"user-guide/terms-and-conditions","unlisted":false},{"type":"link","label":"Privacy Policy","href":"/docs/user-guide/privacy","docId":"user-guide/privacy","unlisted":false}],"collapsible":true}],"businessSidebar":[{"type":"link","label":"Welcome","href":"/docs/business/","docId":"business/index","unlisted":false},{"type":"link","label":"Roadmap","href":"/docs/business/roadmap","docId":"business/roadmap","unlisted":false},{"type":"link","label":"Milestones","href":"/docs/business/milestones","docId":"business/milestones","unlisted":false},{"type":"link","label":"Market Size","href":"/docs/business/market-size","docId":"business/market-size","unlisted":false},{"type":"link","label":"Lean Canvas","href":"https://docs.google.com/presentation/d/1oUzy1zeraTf6PgWlk2R3Ea7Iw2Ju24Dds5mffq2o5Wg/edit#slide=id.p1"},{"type":"link","label":"Gardening vs. Farming","href":"https://docs.google.com/presentation/d/1rMu7DWJblHvVJt6CGmR8eyCN7uBMXPfBpc1rdhanxjQ/edit#slide=id.p"},{"type":"link","label":"Home Gardening Pain Points","href":"https://docs.google.com/presentation/d/1TKDWQI60PxRhBpMGW0tvMyXX0nmgEBz71-DmoT2LxfU/edit#slide=id.g11d82564388_0_187"},{"type":"link","label":"BAI 2024 Pitch Deck","href":"https://geogardenclub.com/pdf/bai-pitch-2024.pdf"},{"type":"link","label":"Business Documents Repo","href":"https://github.com/geogardenclub/documents"},{"type":"link","label":"Developer Guide","href":"/docs/develop/"}],"developSidebar":[{"type":"link","label":"Welcome","href":"/docs/develop/","docId":"develop/index","unlisted":false},{"type":"link","label":"Onboarding","href":"/docs/develop/onboarding","docId":"develop/onboarding","unlisted":false},{"type":"link","label":"Installation","href":"/docs/develop/installation","docId":"develop/installation","unlisted":false},{"type":"link","label":"Scripts","href":"/docs/develop/scripts","docId":"develop/scripts","unlisted":false},{"type":"link","label":"Coding Standards","href":"/docs/develop/coding-standards","docId":"develop/coding-standards","unlisted":false},{"type":"link","label":"Architecture","href":"/docs/develop/architecture","docId":"develop/architecture","unlisted":false},{"type":"link","label":"Deployment","href":"/docs/develop/deployment","docId":"develop/deployment","unlisted":false},{"type":"link","label":"Testing","href":"/docs/develop/testing","docId":"develop/testing","unlisted":false},{"type":"link","label":"Backups","href":"/docs/develop/backups","docId":"develop/backups","unlisted":false},{"type":"category","collapsed":true,"label":"Design","items":[{"type":"link","label":"Data Model","href":"/docs/develop/design/data-model","docId":"develop/design/data-model","unlisted":false},{"type":"link","label":"Badges","href":"/docs/develop/design/badges","docId":"develop/design/badges","unlisted":false},{"type":"link","label":"GGC Input Fields","href":"/docs/develop/design/input-fields","docId":"develop/design/input-fields","unlisted":false},{"type":"link","label":"\\"With\\" widgets","href":"/docs/develop/design/with-widgets","docId":"develop/design/with-widgets","unlisted":false},{"type":"link","label":"Data Mutation","href":"/docs/develop/design/data-mutation","docId":"develop/design/data-mutation","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Prior Releases","items":[{"type":"category","label":"Release 1.0 (Technology Evaluation)","collapsed":true,"items":[{"type":"link","label":"Technology Goals","href":"/docs/develop/releases/release-1.0/goals","docId":"develop/releases/release-1.0/goals","unlisted":false},{"type":"link","label":"Core Value Propositions","href":"/docs/develop/releases/release-1.0/cvp","docId":"develop/releases/release-1.0/cvp","unlisted":false},{"type":"link","label":"Onboarding Feedback","href":"/docs/develop/releases/release-1.0/onboarding-feedback","docId":"develop/releases/release-1.0/onboarding-feedback","unlisted":false},{"type":"link","label":"End of Season Feedback","href":"/docs/develop/releases/release-1.0/end-of-season-feedback","docId":"develop/releases/release-1.0/end-of-season-feedback","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Release 0.0 (Mockup)","items":[{"type":"link","label":"Design and implementation","href":"/docs/develop/releases/release-0.0/design","docId":"develop/releases/release-0.0/design","unlisted":false},{"type":"link","label":"Customer feedback","href":"/docs/develop/releases/release-0.0/customer-feedback","docId":"develop/releases/release-0.0/customer-feedback","unlisted":false},{"type":"link","label":"Entrepreneur feedback","href":"/docs/develop/releases/release-0.0/entrepreneur-feedback","docId":"develop/releases/release-0.0/entrepreneur-feedback","unlisted":false},{"type":"link","label":"ChatGPT feedback","href":"/docs/develop/releases/release-0.0/chatgpt-feedback","docId":"develop/releases/release-0.0/chatgpt-feedback","unlisted":false}],"collapsible":true}],"collapsible":true},{"type":"link","label":"Business Development Guide","href":"/docs/business/"}]},"docs":{"business/index":{"id":"business/index","title":"Welcome to the GGC Business Development Guide","description":"Welcome to the Business Development Guide for the Geo Garden Club project.","sidebar":"businessSidebar"},"business/market-size":{"id":"business/market-size","title":"Market Size Estimation (USA)","description":"Here is some data that can help provide a sense for the potential market size for GGC in the United States.","sidebar":"businessSidebar"},"business/milestones":{"id":"business/milestones","title":"Milestones","description":"| Year | Date | Milestone |","sidebar":"businessSidebar"},"business/roadmap":{"id":"business/roadmap","title":"Roadmap","description":"This roadmap documents our approach incremental development and release of our technology.","sidebar":"businessSidebar"},"develop/architecture":{"id":"develop/architecture","title":"Architecture","description":"The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the \\"Riverpod Architecture\\". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.","sidebar":"developSidebar"},"develop/backups":{"id":"develop/backups","title":"Backups","description":"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn\'t lose too much work.","sidebar":"developSidebar"},"develop/coding-standards":{"id":"develop/coding-standards","title":"Coding Standards","description":"In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid \\"technical debt\\".","sidebar":"developSidebar"},"develop/deployment":{"id":"develop/deployment","title":"Deployment","description":"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.","sidebar":"developSidebar"},"develop/design/badges":{"id":"develop/design/badges","title":"Badges","description":"Goals","sidebar":"developSidebar"},"develop/design/data-model":{"id":"develop/design/data-model","title":"Data Model","description":"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we\'ve made along the way.","sidebar":"developSidebar"},"develop/design/data-model-old":{"id":"develop/design/data-model-old","title":"Data Model","description":"This page documents the data model intended to satisfy the 1.0 release requirements."},"develop/design/data-mutation":{"id":"develop/design/data-mutation","title":"Data Mutation","description":"Prelude: AsyncValue","sidebar":"developSidebar"},"develop/design/input-fields":{"id":"develop/design/input-fields","title":"GGC Input Fields","description":"Motivation","sidebar":"developSidebar"},"develop/design/with-widgets":{"id":"develop/design/with-widgets","title":"\\"With\\" widgets","description":"Why \\"with\\"?","sidebar":"developSidebar"},"develop/index":{"id":"develop/index","title":"Welcome to the GGC Developers Guide","description":"Welcome to the Developer\'s Guide for the Geo Garden Club project.","sidebar":"developSidebar"},"develop/installation":{"id":"develop/installation","title":"Installation","description":"Flutter","sidebar":"developSidebar"},"develop/onboarding":{"id":"develop/onboarding","title":"Onboarding","description":"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.","sidebar":"developSidebar"},"develop/releases/release-0.0/chatgpt-feedback":{"id":"develop/releases/release-0.0/chatgpt-feedback","title":"ChatGPT feedback","description":"Just for fun, I had a conversation with ChatGPT in April 2023 about how to design a gardening app. Much of its responses were uninsightful, but it did come up with some banging ideas for badges.","sidebar":"developSidebar"},"develop/releases/release-0.0/customer-feedback":{"id":"develop/releases/release-0.0/customer-feedback","title":"Customer feedback","description":"Following the Lean Startup principle of \\"validated learning\\", we performed an evaluation study of the technology innovations present in the mockup with 24 experienced gardeners during the summer and fall of 2022. We report on the results of that study in the following 15 minute video (or 7 minutes, if you run the video at 2x speed):","sidebar":"developSidebar"},"develop/releases/release-0.0/design":{"id":"develop/releases/release-0.0/design","title":"Design and implementation","description":"In 2022, we built a simple single page web application using React. The goal of the system was to provide an executable mockup to perform \\"customer discovery\\". The mockup showed potential users our vision of some of the features that would be made available in our production technology, and enabled us to gather feedback on the potential utility and value of our vision.","sidebar":"developSidebar"},"develop/releases/release-0.0/entrepreneur-feedback":{"id":"develop/releases/release-0.0/entrepreneur-feedback","title":"Entrepreneur feedback","description":"The following sections document feedback received from entrepreneurs and high tech professionals (who are not necessarily gardeners, and thus not in our customer demographics). Our request to these folks was to evaluate the viability of Agile/Geo Garden Club as a commercial technology venture, and to help us identify opportunities for improvement. We got a lot of useful feedback.","sidebar":"developSidebar"},"develop/releases/release-1.0/cvp":{"id":"develop/releases/release-1.0/cvp","title":"Core Value Propositions","description":"The goal of the 1.0 (Technology Evaluation) release is to provide an app to a small group that will use the app and provide us with feedback. The 1.0 release will partially test our business model by helping us evaluate its success at implementing the \\"core value propositions\\" (CVPs) for GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/end-of-season-feedback":{"id":"develop/releases/release-1.0/end-of-season-feedback","title":"End of Season Feedback","description":"In September 2024, Jenna sent out an 8 question survey to users to request feedback. We received five responses. Here is a summary of the feedback:","sidebar":"developSidebar"},"develop/releases/release-1.0/goals":{"id":"develop/releases/release-1.0/goals","title":"Technology Goals","description":"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/onboarding-feedback":{"id":"develop/releases/release-1.0/onboarding-feedback","title":"Onboarding Feedback","description":"Jenna personally onboarded six users to Release 1.0 and gathered the following feedback from them either during the onboarding session or shortly thereafter.","sidebar":"developSidebar"},"develop/scripts":{"id":"develop/scripts","title":"Scripts","description":"GGC development is supported by a number of Unix shell scripts. All scripts are named starting with \\"run\\" and use snake case to separate words in the script name.","sidebar":"developSidebar"},"develop/testing":{"id":"develop/testing","title":"Testing","description":"The current goal of testing in GeoGardenClub is to minimize the risk of catastrophic regression from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:","sidebar":"developSidebar"},"home/food-security":{"id":"home/food-security","title":"Food Security","description":"Food security is the elephant in the room, and a major motivation for founding GeoGardenClub. Here is a brief introduction to the relationship between food security and home gardens.","sidebar":"homeSidebar"},"home/innovations":{"id":"home/innovations","title":"Design Innovations","description":"What should an app provide if it is intended to support the needs of \\"serious\\" gardeners? Here is GeoGardenClub\'s answer:","sidebar":"homeSidebar"},"home/related-work":{"id":"home/related-work","title":"Garden Planning Tools","description":"If you search for \\"garden planning tools\\" on the Internet, you\'ll find dozens of applications. Most of those are essentially \\"landscape architecture\\" tools for people who want to design the visual look of their (flower) gardens. This is an interesting design problem, but not the problem addressed by Geo Garden Club.","sidebar":"homeSidebar"},"home/serious-gardeners":{"id":"home/serious-gardeners","title":"\\"Serious\\" Gardeners","description":"The founders of GeoGardenClub view food production as a vast continuum of activities and levels of commitment, as shown in the following diagram:","sidebar":"homeSidebar"},"home/sneak-peek":{"id":"home/sneak-peek","title":"Mobile App Sneak Peek","description":"We are working on the alpha release of the GeoGardenClub mobile app, with an expected release date of early 2024. While the app is not yet ready for prime time, we thought it would be fun to show you some selected screen shots so you can get an idea of where we\'re heading."},"home/team":{"id":"home/team","title":"The Team","description":"Jenna Deane","sidebar":"homeSidebar"},"home/welcome":{"id":"home/welcome","title":"Welcome","description":"GGC started in 2021 when a small group of gardeners in Bellingham, Washington realized that, despite the availability of dozens of home gardening apps, we were planning and managing our gardens using spreadsheets, Word docs, and even paper and pencil!","sidebar":"homeSidebar"},"user-guide/adding-plantings":{"id":"user-guide/adding-plantings","title":"Add Plantings to Beds","description":"Add plantings","sidebar":"homeSidebar"},"user-guide/adding-vendors-crops-varieties":{"id":"user-guide/adding-vendors-crops-varieties","title":"Add Crops, Varieties, Vendors to the Chapter Database","description":"The crops, varieties, and vendors in each chapter\'s database are crowdsourced from the chapter\'s members. This ensures that the database is exclusive to what is grown in the area. To add a crop, variety, or vendor navigate to the appropriate index screen from the side navigation menu.","sidebar":"homeSidebar"},"user-guide/badges":{"id":"user-guide/badges","title":"Badges","description":"Why Badges?","sidebar":"homeSidebar"},"user-guide/chat-rooms":{"id":"user-guide/chat-rooms","title":"Chat Rooms","description":"GGC allows gardeners to communicate with each other through chat rooms. Chat rooms are a great way to ask questions, share knowledge, and build community. Chat rooms are organized by chapter or garden.","sidebar":"homeSidebar"},"user-guide/define-a-garden":{"id":"user-guide/define-a-garden","title":"Define a Garden","description":"Defining a garden in GeoGardenClub requires you to create three kinds of entities: a \\"Garden\\", one or more \\"Beds\\", and one or more \\"Plantings\\". (You can also define a fourth type of entity, \\"Outcomes\\", but we\'ll get to that later).","sidebar":"homeSidebar"},"user-guide/downloading":{"id":"user-guide/downloading","title":"Downloading","description":"Enroll as a GGC Beta Tester","sidebar":"homeSidebar"},"user-guide/explore-a-chapter":{"id":"user-guide/explore-a-chapter","title":"Explore a Chapter","description":"Find the Chapter Index Screens","sidebar":"homeSidebar"},"user-guide/explore-a-garden":{"id":"user-guide/explore-a-garden","title":"Explore a Garden","description":"The summary card for every Garden in a Chapter contains a button labeled \\"Details\\". Pressing that button takes you to a screen with a bottom nav bar containing icons taking you to four screens: Timeline, Filter, Outcomes, and Tasks. Each view presents a useful perspective on that Garden.","sidebar":"homeSidebar"},"user-guide/geobot":{"id":"user-guide/geobot","title":"GeoBot","description":"Why GeoBot?","sidebar":"homeSidebar"},"user-guide/guided-tour":{"id":"user-guide/guided-tour","title":"Frequently Asked (Gardening) Questions","description":"GeoGardenClub was created to increase the performance of home, community, and school gardens and efficiency of the gardener by answering questions a gardener might have as they plan or work through the gardening season. Here are some of the questions that GeoGardenClub can help answer:","sidebar":"homeSidebar"},"user-guide/observations":{"id":"user-guide/observations","title":"Observations","description":"GGC allows gardeners to make \\"observations\\" regarding a planting of a plant variety on a specific day. Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests, document outcomes like yields, or simply record the progress of a planting.","sidebar":"homeSidebar"},"user-guide/outcomes":{"id":"user-guide/outcomes","title":"outcomes","description":"300---","sidebar":"homeSidebar"},"user-guide/overview":{"id":"user-guide/overview","title":"Overview","description":"Welcome to the User Guide for the GeoGardenClub app.","sidebar":"homeSidebar"},"user-guide/privacy":{"id":"user-guide/privacy","title":"Privacy Policy","description":"Last updated June 20, 2024","sidebar":"homeSidebar"},"user-guide/registration":{"id":"user-guide/registration","title":"Registration","description":"After downloading the app, open it to see the login screen. Tap Register to create a new account.","sidebar":"homeSidebar"},"user-guide/scenarios":{"id":"user-guide/scenarios","title":"Planting Scenarios","description":"Growing plants in a greenhouse or other climate controlled environment","sidebar":"homeSidebar"},"user-guide/seeds":{"id":"user-guide/seeds","title":"Seeds","description":"Geo Garden Club believes that seed saving and sharing is a critical component of building resilient gardening communities. By saving seeds, gardeners can ensure that they have access to the varieties that grow best in their area. By sharing seeds, gardeners can help others in their community grow the same varieties.","sidebar":"homeSidebar"},"user-guide/tasks":{"id":"user-guide/tasks","title":"Tasks","description":"What are Tasks?","sidebar":"homeSidebar"},"user-guide/terms-and-conditions":{"id":"user-guide/terms-and-conditions","title":"Terms and Conditions","description":"Last updated February 10, 2024","sidebar":"homeSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/1e5c498d.2d5ae52d.js b/assets/js/1e5c498d.2d5ae52d.js deleted file mode 100644 index 1f631afc3..000000000 --- a/assets/js/1e5c498d.2d5ae52d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1217],{798:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var i=n(5893),a=n(1151);const s={hide_table_of_contents:!1},r="Testing",o={id:"develop/testing",title:"Testing",description:"The current goal of testing in GeoGardenClub is to minimize the risk of catastrophic regression from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:",source:"@site/docs/develop/testing.md",sourceDirName:"develop",slug:"/develop/testing",permalink:"/docs/develop/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Backups",permalink:"/docs/develop/backups"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"Run the tests",id:"run-the-tests",level:2},{value:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",level:2},{value:"About app_test.dart",id:"about-app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"About run_tests_single.sh and app_test_single.dart",id:"about-run_tests_singlesh-and-app_test_singledart",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing excludes many important issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"External service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experience using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by running the tests. A decent test suite should enable us to incrementally improve the quality of the code over time as well as the feature set."}),"\n",(0,i.jsxs)(t.admonition,{title:"When should you run the tests?",type:"info",children:[(0,i.jsx)(t.p,{children:"Ideally, we want the main branch to always be able to run the tests without error."}),(0,i.jsx)(t.p,{children:"To achieve that, the best times to run the tests are:"}),(0,i.jsxs)(t.ol,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Whenever you update the code you're working on from the main branch."})," This ensures that the update from main has not introduced code that breaks the tests in your branch. If the tests fail, address that problem before proceeding."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Before updating the main branch with your code."})," This ensures that you are not introducing code into the main branch that breaks the tests."]}),"\n"]}),(0,i.jsx)(t.p,{children:"As you will learn below, we have not figured out a reasonable way to perform continuous integration (i.e. run the tests automatically in the cloud upon any commit to the main branch.) As a result, it is up to us to manually verify that the main branch can execute the test suite without error."})]}),"\n",(0,i.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.p,{children:"There are a few things you need to do to set up your machine to run the test suite locally."}),"\n",(0,i.jsx)(t.p,{children:"Note that at the current time, we only support testing on macOS."}),"\n",(0,i.jsx)(t.p,{children:"First, bring up the iOS simulator and verify that you can bring up GGC on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.)"}),"\n",(0,i.jsx)(t.p,{children:"Second, install lcov on your machine by invoking:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"brew install lcov\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Third, activate the ",(0,i.jsx)(t.code,{children:"remove_from_coverage"})," Dart package so that the coverage report can be customized. Do this by invoking:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-agsl",children:"dart pub global activate remove_from_coverage\n"})}),"\n",(0,i.jsx)(t.h2,{id:"run-the-tests",children:"Run the tests"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". It should take around 5 minutes to run, and should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app git:[main]\n./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:30 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:37 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s\nXcode build done. 32.5s\n00:44 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\nTesting settings feature\nTesting task feature\nTesting user feature\nTesting variety feature\n04:46 +1: All tests passed! \n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'repositories\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'data\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'domain\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'authentication\\/.*$'\n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 320\n lines.......: 72.9% (6028 of 8264 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:'Note the line "All tests passed" after the sequence of lines documenting the feature under test.'}),"\n",(0,i.jsxs)(t.admonition,{title:"Uh oh...",type:"info",children:[(0,i.jsx)(t.p,{children:'If the tests do not run successfully, there won\'t be the line "All tests passed", and the output will instead look similar to this:'}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:33 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:40 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s\nXcode build done. 35.9s\n00:46 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\n\u2550\u2550\u2561 EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following TestFailure was thrown running a test:\nExpected: \n Actual: \n\nWhen the exception was thrown, this was the stack:\n#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)\n\n#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)\n\n#6 main.. (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)\n\n#7 patrolWidgetTest. (package:patrol_finders/src/common.dart:50:7)\n\n#8 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15)\n\n#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24\nThe test description was:\n Fixture 1 Tests\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] \n Test failed. See exception logs above.\n The test description was: Fixture 1 Tests\n \n\nTo run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'\n03:16 +0 -1: Some tests failed. \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 54.8% (7029 of 12823 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),(0,i.jsx)(t.p,{children:"You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart."}),(0,i.jsx)(t.p,{children:"If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5."})]}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways from this test execution output:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code. Currently, the lib/ directory contains around 44K lines of code, and the integration_test/ directory contains around 1200 lines of code. So, the test code only accounts for around 2% of the total code base."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:'The "Rate Limiter" might be triggered. You can ignore this warning.'}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below). We also remove several directories from the coverage report (i.e. data/, domain/, and repositories/) so that the coverage report does not report on code that is never executed due to mocking (i.e. code in the data/ and repositories/ directories) and also focuses more specifically on UI code."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"While testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to allow pasting in order to let the tests proceed. Otherwise, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"about-app_testdart",children:"About app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testBed($);\n await checkIntegrity($, reason: 'bed feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"about-run_tests_singlesh-and-app_test_singledart",children:"About run_tests_single.sh and app_test_single.dart"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can be useful to see the coverage of our test cases. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),". Here's what it looks like after clicking the button to sort the rows in order of increasing coverage."]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"Note that this report elides the data/, domain/, and repositories/ directories so that the focus is on UI code. Also, the use of mocks means that the code in the data/ and repositories/ directories will never be executed by testing, so reporting coverage for that code is not useful."}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'The goal of the coverage report is to simplify identification of "forgotten" areas of the UI for which we have not created any test cases.'}),"\n",(0,i.jsx)(t.admonition,{title:"forewarned is forarmed",type:"warning",children:(0,i.jsx)(t.p,{children:"Beware that a high level of coverage does not, by itself, indicate that the test suite is high quality. This is because the app code might work correctly for the given test fixture in use, but fail under other circumstances. We must remain vigilent as we develop the app to identify areas of brittleness in the code base, and respond in an appropriate way."})}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Document the test's navigation path with ",(0,i.jsx)(t.code,{children:"expect ($().visible, equals(true))"}),"."]})," It is possible to write a test with mostly ",(0,i.jsx)(t.code,{children:"await"})," statements such as the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\nawait $('Details').tap();\nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["That code is hard to follow (and potentially harder to debug and maintain) because it does not indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts ",(0,i.jsx)(t.code,{children:"expect"})," statements each time the test reaches a new page. This makes it easier to understand the test process:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\n\n// Now at HomeScreenGardensView\nexpect($(HomeScreenGardensView).visible, equals(true)); \nawait $('Details').tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\n\n// Now at CopyPlanting Screen\nexpect($(CopyPlantingScreen).visible, equals(true)); \nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \n"})}),"\n",(0,i.jsx)(t.p,{children:"You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires macOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const a={},s=i.createContext(a);function r(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1e5c498d.b0d3812b.js b/assets/js/1e5c498d.b0d3812b.js new file mode 100644 index 000000000..2b16d122b --- /dev/null +++ b/assets/js/1e5c498d.b0d3812b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1217],{798:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var i=n(5893),a=n(1151);const s={hide_table_of_contents:!1},r="Testing",o={id:"develop/testing",title:"Testing",description:"Another form of quality assurance in GGC is testing.",source:"@site/docs/develop/testing.md",sourceDirName:"develop",slug:"/develop/testing",permalink:"/docs/develop/testing",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Dart analyze",permalink:"/docs/develop/dart-analyze"},next:{title:"Technology Goals",permalink:"/docs/develop/releases/release-1.0/goals"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"Run the tests",id:"run-the-tests",level:2},{value:"Always monitor the iOS simulator!",id:"always-monitor-the-ios-simulator",level:2},{value:"About app_test.dart",id:"about-app_testdart",level:2},{value:"Testing a feature",id:"testing-a-feature",level:2},{value:"About run_tests_single.sh and app_test_single.dart",id:"about-run_tests_singlesh-and-app_test_singledart",level:2},{value:"Coverage",id:"coverage",level:2},{value:"Test Design Hints",id:"test-design-hints",level:2},{value:"Continuous integration",id:"continuous-integration",level:2},{value:"Test fixture design",id:"test-fixture-design",level:2},{value:"Fixture Paths",id:"fixture-paths",level:3},{value:"AssetCollectionBuilder",id:"assetcollectionbuilder",level:3},{value:"TestFixture singleton",id:"testfixture-singleton",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"testing",children:"Testing"})}),"\n",(0,i.jsx)(t.p,{children:"Another form of quality assurance in GGC is testing."}),"\n",(0,i.jsx)(t.p,{children:"In GGC, we want the main branch to always be free of any errors raised by our tests."}),"\n",(0,i.jsxs)(t.p,{children:["The current goal of testing in GeoGardenClub is to minimize the risk of ",(0,i.jsx)(t.em,{children:"catastrophic regression"})," from changes to the UI or business logic. In other words, we want our tests to ensure that changes to non-low level code do not result in an app where important features no longer work. This means that our test suite should ensure that:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'All commonly accessed screens display without error. (The tests might not check screens that are displayed "rarely", such as those resulting from anomalous conditions like network instability.)'}),"\n",(0,i.jsx)(t.li,{children:"CRUD operations on entities can be performed successfully when available."}),"\n",(0,i.jsx)(t.li,{children:"Buttons on all commonly accessed screens, when tapped, do not generate an error, and the resulting screen is checked to see that at least some of the intended results are displayed."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Currently, our approach to testing excludes many important issues:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Load testing."}),' We do not test that the system performs well under "load", where load can mean a large number of concurrent users and/or a large amount of stored data.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"External service testing."}),' We do not test "low-level" code, specifically external services such as database, photo storage, and authentication. This is because we mock external services in our test code.']}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"Matrix (platform/device) testing."})," GGC is intended to be used on three platforms: iOS, Android, and Web. Each of these platforms supports many different devices. We only test on one platform (iOS) and one device (typically iPhone 17)."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.em,{children:"UX testing."})," Our tests do not ensure that user needs are met and that they have a positive experience using the app."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Despite these limitations, our tests should help improve developer courage. In other words, the presence of a test suite that exercises most of the UI can give developers the confidence to attempt improvements to the code base because unintended ripple effects will often be caught by running the tests. A decent test suite should enable us to incrementally improve the quality of the code over time as well as the feature set."}),"\n",(0,i.jsxs)(t.admonition,{title:"When should you run the tests?",type:"info",children:[(0,i.jsx)(t.p,{children:"Ideally, we want the main branch to always be able to run the tests without error."}),(0,i.jsx)(t.p,{children:"To achieve that, the best times to run the tests are:"}),(0,i.jsxs)(t.ol,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Whenever you update the code you're working on from the main branch."})," This ensures that the update from main has not introduced code that breaks the tests in your branch. If the tests fail, address that problem before proceeding."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Before updating the main branch with your code."})," This ensures that you are not introducing code into the main branch that breaks the tests."]}),"\n"]}),(0,i.jsx)(t.p,{children:"As you will learn below, we have not figured out a reasonable way to perform continuous integration (i.e. run the tests automatically in the cloud upon any commit to the main branch.) As a result, it is up to us to manually verify that the main branch can execute the test suite without error."})]}),"\n",(0,i.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.p,{children:"There are a few things you need to do to set up your machine to run the test suite locally."}),"\n",(0,i.jsx)(t.p,{children:"Note that at the current time, we only support testing on macOS."}),"\n",(0,i.jsx)(t.p,{children:"First, bring up the iOS simulator and verify that you can bring up GGC on it. (The test suite assumes that the iOS simulator is available and that the GGC source code can be loaded.)"}),"\n",(0,i.jsx)(t.p,{children:"Second, install lcov on your machine by invoking:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"brew install lcov\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Third, activate the ",(0,i.jsx)(t.code,{children:"remove_from_coverage"})," Dart package so that the coverage report can be customized. Do this by invoking:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-agsl",children:"dart pub global activate remove_from_coverage\n"})}),"\n",(0,i.jsx)(t.h2,{id:"run-the-tests",children:"Run the tests"}),"\n",(0,i.jsxs)(t.p,{children:["To run the test suite, invoke ",(0,i.jsx)(t.code,{children:"./run_tests.sh"}),". It should take around 5 minutes to run, and should produce output similar to the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app git:[main]\n./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:30 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:37 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s\nXcode build done. 32.5s\n00:44 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting bed feature\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\n#0 WriteRateLimiter.rateLimit (package:ggc_app/features/common/rate-limit/write_rate_limiter.dart:36:16)\nRate limiting enabled.\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\nTesting settings feature\nTesting task feature\nTesting user feature\nTesting variety feature\n04:46 +1: All tests passed! \n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'repositories\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'data\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'domain\\/.*$'\n+ flutter pub global run remove_from_coverage:remove_from_coverage -f coverage/lcov.info -r 'authentication\\/.*$'\n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 320\n lines.......: 72.9% (6028 of 8264 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),"\n",(0,i.jsx)(t.p,{children:'Note the line "All tests passed" after the sequence of lines documenting the feature under test.'}),"\n",(0,i.jsxs)(t.admonition,{title:"Uh oh...",type:"info",children:[(0,i.jsx)(t.p,{children:'If the tests do not run successfully, there won\'t be the line "All tests passed", and the output will instead look similar to this:'}),(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"./run_tests.sh\n+ flutter test integration_test/app_test.dart --coverage\n00:04 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart Ru00:33 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart \n00:40 +0: loading /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart 6.8s\nXcode build done. 35.9s\n00:46 +0: GGC Integration Test (All) Fixture 1 Tests \nTesting admin feature\nTesting badge feature\nTesting chapter feature\nTesting chat feature\nTesting crop feature\nTesting garden feature\nTesting gardener feature\nTesting geobot feature\nTesting home feature\nTesting observation feature\nTesting outcome feature\nTesting planting feature\n\u2550\u2550\u2561 EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK \u255e\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nThe following TestFailure was thrown running a test:\nExpected: \n Actual: \n\nWhen the exception was thrown, this was the stack:\n#4 testPlantingCopyPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart:24:3)\n\n#5 testPlanting (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting.dart:13:3)\n\n#6 main.. (file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart:153:7)\n\n#7 patrolWidgetTest. (package:patrol_finders/src/common.dart:50:7)\n\n#8 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15)\n\n#9 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/features/planting/test_planting_copy_planting.dart line 24\nThe test description was:\n Fixture 1 Tests\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n03:15 +0 -1: GGC Integration Test (All) Fixture 1 Tests [E] \n Test failed. See exception logs above.\n The test description was: Fixture 1 Tests\n \n\nTo run this test again: /Users/philipjohnson/Flutter/bin/cache/dart-sdk/bin/dart test /Users/philipjohnson/GitHub/geogardenclub/ggc_app/integration_test/app_test.dart -p vm --plain-name 'GGC Integration Test (All) Fixture 1 Tests'\n03:16 +0 -1: Some tests failed. \n+ genhtml -q coverage/lcov.info -o coverage/html\nOverall coverage rate:\n source files: 472\n lines.......: 54.8% (7029 of 12823 lines)\n functions...: no data found\nMessage summary:\n no messages were reported\n"})}),(0,i.jsx)(t.p,{children:"You can see from this output that the failure occurred during the test of the planting feature. The stack trace indicates the failure occurred on line 24 of testPlantingCopyPlanting.dart."}),(0,i.jsx)(t.p,{children:"If you cannot get the test code to execute successfully even though other developers can, it might be because the tests don't work on the device you've chosen. At the time of writing, the tests run successfully using the iPhone 15 simulator under iOS 17.5."})]}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways from this test execution output:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We only write integration tests; no unit or widget tests. This maximizes the ratio of application code exercised per line of test code. Currently, the lib/ directory contains around 44K lines of code, and the integration_test/ directory contains around 1200 lines of code. So, the test code only accounts for around 2% of the total code base."}),"\n",(0,i.jsxs)(t.li,{children:['Our tests run with a specific "test fixture" (currently we\'re using one called Test Fixture 1). This is a sample dataset containing test values for most or all of the entities in our system (i.e. chapters, beds, gardens, gardeners, etc.). This sample dataset is stored in ',(0,i.jsx)(t.code,{children:"assets/test/fixture1"}),". In the future, we might write tests that require a different fixture."]}),"\n",(0,i.jsx)(t.li,{children:"Our test architecture is organized around features."}),"\n",(0,i.jsx)(t.li,{children:'The "Rate Limiter" might be triggered. You can ignore this warning.'}),"\n",(0,i.jsx)(t.li,{children:"We compute coverage to provide an efficient way to find important areas of the app code that have not yet been tested, not to verify that the tests achieve 100% coverage (more on this below). We also remove several directories from the coverage report (i.e. data/, domain/, and repositories/) so that the coverage report does not report on code that is never executed due to mocking (i.e. code in the data/ and repositories/ directories) and also focuses more specifically on UI code."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"always-monitor-the-ios-simulator",children:"Always monitor the iOS simulator!"}),"\n",(0,i.jsxs)(t.admonition,{type:"warning",children:[(0,i.jsx)(t.mdxAdmonitionTitle,{}),(0,i.jsx)(t.p,{children:"While testing with the iOS simulator, the testing process will occasionally (and unpredictably) pause waiting for you to click on a button to allow pasting:"}),(0,i.jsx)("img",{src:"/img/develop/testing/core-simulator-bridge.png"}),(0,i.jsx)(t.p,{children:"For this reason, it's important to always monitor the simulator at least until the tests start, because you might need to click a button to allow pasting in order to let the tests proceed. Otherwise, the test process will hang indefinitely."}),(0,i.jsx)(t.p,{children:"This is a security feature in the iOS operating system. There is apparently no way to disable it at the current time."})]}),"\n",(0,i.jsx)(t.h2,{id:"about-app_testdart",children:"About app_test.dart"}),"\n",(0,i.jsxs)(t.p,{children:["To further understand the test process, it's helpful to review the code that is run by the ",(0,i.jsx)(t.code,{children:"./run_tests.sh"})," command:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (All)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n badgeInstancesProvider.overrideWith((_) => testFixture.getBadgeInstancesStream()),\n badgeInstanceDatabaseProvider.overrideWith((_) => testFixture.getBadgeInstanceDatabase()),\n bedsProvider.overrideWith((_) => testFixture.getBedsStream()),\n bedDatabaseProvider.overrideWith((ref) => testFixture.getBedDatabase()),\n chaptersProvider.overrideWith((_) => testFixture.getChaptersStream()),\n chapterDatabaseProvider.overrideWith((_) => testFixture.getChapterDatabase()),\n chatRoomDatabaseProvider.overrideWith((_) => testFixture.getChatRoomDatabase()),\n chatUserDatabaseProvider.overrideWith((_) => testFixture.getChatUserDatabase()),\n cropsProvider.overrideWith((_) => testFixture.getCropsStream()),\n cropDatabaseProvider.overrideWith((_) => testFixture.getCropDatabase()),\n editorsProvider.overrideWith((_) => testFixture.getEditorsStream()),\n editorDatabaseProvider.overrideWith((_) => testFixture.getEditorDatabase()),\n familiesProvider.overrideWith((_) => testFixture.getFamiliesStream()),\n familyDatabaseProvider.overrideWith((_) => testFixture.getFamilyDatabase()),\n gardensProvider.overrideWith((_) => testFixture.getGardensStream()),\n gardenDatabaseProvider.overrideWith((_) => testFixture.getGardenDatabase()),\n gardenersProvider.overrideWith((_) => testFixture.getGardenersStream()),\n gardenerDatabaseProvider.overrideWith((_) => testFixture.getGardenerDatabase()),\n observationsProvider.overrideWith((_) => testFixture.getObservationsStream()),\n observationDatabaseProvider.overrideWith((_) => testFixture.getObservationDatabase()),\n outcomesProvider.overrideWith((_) => testFixture.getOutcomesStream()),\n outcomeDatabaseProvider.overrideWith((_) => testFixture.getOutcomeDatabase()),\n plantingsProvider.overrideWith((_) => testFixture.getPlantingsStream()),\n plantingDatabaseProvider.overrideWith((_) => testFixture.getPlantingDatabase()),\n rolesProvider.overrideWith((_) => testFixture.getRolesStream()),\n roleDatabaseProvider.overrideWith((_) => testFixture.getRoleDatabase()),\n tagsProvider.overrideWith((_) => testFixture.getTagsStream()),\n tagDatabaseProvider.overrideWith((_) => testFixture.getTagDatabase()),\n tasksProvider.overrideWith((_) => testFixture.getTasksStream()),\n taskDatabaseProvider.overrideWith((_) => testFixture.getTaskDatabase()),\n usersProvider.overrideWith((_) => testFixture.getUsersStream()),\n userDatabaseProvider.overrideWith((_) => testFixture.getUserDatabase()),\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await checkIntegrity($, reason: 'startup');\n await testAdmin($);\n await checkIntegrity($, reason: 'admin feature');\n await testBadge($);\n await checkIntegrity($, reason: 'badge feature');\n await testBed($);\n await checkIntegrity($, reason: 'bed feature');\n await testChapter($);\n await checkIntegrity($, reason: 'chapter feature');\n await testChat($);\n await checkIntegrity($, reason: 'chat feature');\n await testCrop($);\n await checkIntegrity($, reason: 'crop feature');\n await testGarden($);\n await checkIntegrity($, reason: 'garden feature');\n await testGardener($);\n await checkIntegrity($, reason: 'gardener feature');\n await testGeoBot($);\n await checkIntegrity($, reason: 'geobot feature');\n await testHome($);\n await checkIntegrity($, reason: 'home feature');\n await testObservation($);\n await checkIntegrity($, reason: 'observation feature');\n await testOutcome($);\n await checkIntegrity($, reason: 'outcome feature');\n await testPlanting($);\n await checkIntegrity($, reason: 'planting feature');\n await testSettings($);\n await checkIntegrity($, reason: 'settings feature');\n await testTask($);\n await checkIntegrity($, reason: 'task feature');\n await testVariety($);\n await checkIntegrity($, reason: 'variety feature');\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are the important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["We use the ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/finders/overview",children:"Patrol Finders"})," package, which provides a very helpful syntactic sugar over the built-in Flutter testing package. We do not use the full Patrol package, just their Patrol Finder package."]}),"\n",(0,i.jsx)(t.li,{children:"We use the Riverpod overrides feature so that during testing, our code manipulates the test fixture data rather than the data in the Firebase database."}),"\n",(0,i.jsxs)(t.li,{children:["We simulate Firebase authentication (using firebase_auth_mocks) and the app starts up with the (admin) user ",(0,i.jsx)(t.a,{href:"mailto:jennacorindeane@gmail.com",children:"jennacorindeane@gmail.com"}),' already logged in. So, we don\'t currently test the registration or signin workflows. The app "starts" by displaying the Home screen for Jenna.']}),"\n",(0,i.jsx)(t.li,{children:'We test each feature by calling a "test" function (i.e. testChapter, testCrop, etc.).'}),"\n",(0,i.jsx)(t.li,{children:"After testing each feature, the test code runs the Check Integrity admin function to ensure that the test of the previous feature did not introduce a database inconsistency."}),"\n",(0,i.jsx)(t.li,{children:'Our integration testing approach is "big bang": we run the entire integration test suite in a single function. This means tests are not independent of each other, which can make individual test case design more difficult. We chose this design for pragmatic reasons: setting up the runtime environment for testing takes around 50 seconds (on my late model MacBook Pro). If we ran each of the 15 feature tests independently, that would add on an additional 12 minutes (15 features * 50 seconds) to test suite execution time. No bueno.'}),"\n",(0,i.jsxs)(t.li,{children:["You should rarely need to edit this ",(0,i.jsx)(t.code,{children:"app_test.dart"}),' file. Instead, you will usually edit one of the top-level "test" feature files (i.e. testChapter.dart, testCrop.dart, etc.) You will normally need to edit app_test.dart only when you want to introduce the testing of a new feature.']}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"testing-a-feature",children:"Testing a feature"}),"\n",(0,i.jsx)(t.p,{children:'Let\'s now look at how the "Crop" feature is currently tested. Here is the top-level feature test function for Crops:'}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop.dart\nFuture testCrop(PatrolTester $) async {\n // ignore: avoid_print\n print('Testing crop feature');\n await testCropIndexScreen($);\n await testCropCRUD($);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Each top-level feature test function starts by printing a line of output indicating that the test of this feature is starting. That makes it easier to see how far testing has gotten and helps pinpoint the location of problems when testing fails."}),"\n",(0,i.jsx)(t.li,{children:"A top-level feature test function is typically implemented by calling multiple functions, each of which tests a different aspect of the feature."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Here is testCropIndexScreen:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_index_screen.dart\nFuture testCropIndexScreen(PatrolTester $) async {\n String testCrop = 'Amaranth';\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n await $(testCrop).tap();\n expect($(CropDropdown).$(testCrop).visible, equals(true));\n expect($(CropView).$(testCrop).visible, equals(true));\n // Refresh CropIndexScreen so it displays all crops.\n await gotoDrawerScreen($, ChapterIndexScreen);\n await gotoDrawerScreen($, CropIndexScreen);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"We use Patrol Finder syntax to locate widgets and manipulate them through searching for widgets of a particular type and/or containing a particular text string. Please avoid creating Keys for testing. Patrol Finders make it possible to test the source code without introducing new lines of code purely for the purpose of test support."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Let's now look at the test for create, read, update, and delete of a Crop:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/features/crop/test_crop_crud.dart\nFuture testCropCRUD(PatrolTester $) async {\n String testCropName = 'AAATestCrop';\n String updatedTestCropName = 'AAAATestCrop';\n // Test Create.\n await gotoDrawerScreen($, CropIndexScreen);\n await $(GgcFAB).$('Crop').tap();\n expect($(CreateCropScreen).visible, equals(true));\n await $(CropNameField).enterText(testCropName);\n await $(FamilyDropdown).tap();\n await $('Allium').tap();\n await $(FormButtons).$('Submit').tap();\n expect($(CropIndexScreen).visible, equals(true));\n // Verify Create.\n await $(testCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Create crop');\n // Test Read and Update\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(testCropName).tap();\n await $('Update').tap();\n await $(CropNameField).enterText(updatedTestCropName);\n await $('Submit').tap();\n await $(BackButton).tap();\n await $(BackButton).tap();\n await gotoDrawerScreen($, CropIndexScreen);\n // Verify Update\n await $(updatedTestCropName).waitUntilVisible();\n await checkIntegrity($, reason: 'Update crop');\n // Test Delete\n await gotoDrawerScreen($, AdminScreen);\n await $(SelectScreenTile).$('Entity Management').tap();\n await $(SelectScreenTile).$('Manage Crops').tap();\n await $(CropDropdown).tap();\n await $(updatedTestCropName).tap();\n await $('Update').tap();\n await $(Icons.delete).tap();\n await $('Delete').tap();\n await gotoDrawerScreen($, CropIndexScreen);\n await $(CropDropdown).tap();\n // Verify delete\n expect($(updatedTestCropName).exists, equals(false));\n await checkIntegrity($, reason: 'Delete crop');\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Testing a behavior can require a relatively long sequence of UI interactions. Getting the sequence correct is way easier if you first step through the behavior manually. To make this easier, follow the instructions in the section below on "Run the simulator with test data".'}),"\n",(0,i.jsx)(t.li,{children:"It's fine to test multiple behaviors in a single function. In this case, since we are creating an object, then manipulating it, it seems reasonable to group it all in one function."}),"\n",(0,i.jsx)(t.li,{children:"The function performs a behavior (i.e. create, read, update, or delete), and then verifies that the behavior succeeded. In the case of CRUD operations, it is helpful to run an integrity check after any mutation (create, update, delete) to ensure that the database was not corrupted and to immediately throw an error if it was corrupted by the mutation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"about-run_tests_singlesh-and-app_test_singledart",children:"About run_tests_single.sh and app_test_single.dart"}),"\n",(0,i.jsx)(t.p,{children:"While developing the test for a feature, it is humbug to have to run the entire test suite each time you want to run your newly developed test code."}),"\n",(0,i.jsxs)(t.p,{children:["To speed up testing, you can use the command ",(0,i.jsx)(t.code,{children:"./run_tests_single.sh"}),". This runs the ",(0,i.jsx)(t.code,{children:"app_test_single.dart"})," file, which looks similar to this:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"// integration_test/app_test_single.dart\nvoid main() {\n IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n group('GGC Integration Test (Single)', () {\n patrolWidgetTest('Fixture 1 Tests', (PatrolTester $) async {\n await Firebase.initializeApp();\n setFirebaseUiIsTestMode(true);\n FirebaseAuth mockAuth = MockFirebaseAuth();\n String email = 'jennacorindeane@gmail.com';\n mockAuth.createUserWithEmailAndPassword(email: email, password: '');\n TestFixture testFixture = await TestFixture.getInstance(testFixture1Path);\n await $.pumpWidgetAndSettle(ProviderScope(\n overrides: [\n firebaseAuthProvider.overrideWithValue(mockAuth),\n badgesProvider.overrideWith((_) => testFixture.getBadgesStream()),\n badgeDatabaseProvider.overrideWith((_) => testFixture.getBadgeDatabase()),\n :\n :\n varietiesProvider.overrideWith((_) => testFixture.getVarietiesStream()),\n varietyDatabaseProvider.overrideWith((_) => testFixture.getVarietyDatabase()),\n ],\n child: const MyApp(),\n ));\n expect($(HomeScreen).visible, equals(true), reason: 'Login fails');\n await testCrop($);\n });\n });\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"Here are some important takeaways:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"You can freely edit this file in your branch to focus on the specific feature of interest."}),"\n",(0,i.jsxs)(t.li,{children:['Sometimes you might want to check multiple features at once, that\'s fine. You do you. The idea is that this is a kind of "sandbox" for you to develop tests so that you are not wishing to edit the global ',(0,i.jsx)(t.code,{children:"./run_tests.sh"})," and ",(0,i.jsx)(t.code,{children:"app_test.dart"})," files to speed up testing."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"coverage",children:"Coverage"}),"\n",(0,i.jsxs)(t.p,{children:["It can be useful to see the coverage of our test cases. After running the test suite, you can open the file ",(0,i.jsx)(t.code,{children:"coverage/html/index.html"}),". Here's what it looks like after clicking the button to sort the rows in order of increasing coverage."]}),"\n",(0,i.jsx)("img",{src:"/img/develop/testing/coverage.png"}),"\n",(0,i.jsx)(t.p,{children:"Note that this report elides the data/, domain/, and repositories/ directories so that the focus is on UI code. Also, the use of mocks means that the code in the data/ and repositories/ directories will never be executed by testing, so reporting coverage for that code is not useful."}),"\n",(0,i.jsx)(t.p,{children:"There are clickable links that you can use to drill down to see which statements have been executed and which have not been."}),"\n",(0,i.jsx)(t.p,{children:'The goal of the coverage report is to simplify identification of "forgotten" areas of the UI for which we have not created any test cases.'}),"\n",(0,i.jsx)(t.admonition,{title:"forewarned is forarmed",type:"warning",children:(0,i.jsx)(t.p,{children:"Beware that a high level of coverage does not, by itself, indicate that the test suite is high quality. This is because the app code might work correctly for the given test fixture in use, but fail under other circumstances. We must remain vigilent as we develop the app to identify areas of brittleness in the code base, and respond in an appropriate way."})}),"\n",(0,i.jsx)(t.h2,{id:"test-design-hints",children:"Test Design Hints"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Set up a "Run Configuration" to simplify testing.'})," In IntelliJ, make a Run configuration that invokes ",(0,i.jsx)(t.code,{children:"lib/main_test_fixture.dart"})," so that you can push the green arrow to easily bring up the simulator with the test data loaded into it."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use the Testing Run Configuration to guide the writing of test code steps."}),' To implement a new test, start by using the above Run Configuration to manually walk through the sequence of screens, button taps, and input controller interactions necessary for the test. You can even bring up the simulator and "translate" each of your interactions with the simulator into a line of test code as you single step through the behavior. In many cases, it\'s a one-to-one relationship.']}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Make sure that you include at least one "expect" statement to verify the results of a behavior.'})," So, for example, if you are creating an entity, include an expect statement that checks to see that the entity exists somehow."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Document the test's navigation path with ",(0,i.jsx)(t.code,{children:"expect ($().visible, equals(true))"}),"."]})," It is possible to write a test with mostly ",(0,i.jsx)(t.code,{children:"await"})," statements such as the following:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\nawait $('Details').tap();\nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n"})}),"\n",(0,i.jsxs)(t.p,{children:["That code is hard to follow (and potentially harder to debug and maintain) because it does not indicate which screen the test code driver is manipulating. While this test does accomplish the goal of exercising the app code, a more understandable version inserts ",(0,i.jsx)(t.code,{children:"expect"})," statements each time the test reaches a new page. This makes it easier to understand the test process:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-dart",children:"String testPlanting = 'Raspberry (Golden)';\nawait gotoDrawerScreen($, HomeScreen);\nawait $(BottomNavigationBar).$('Gardens').tap();\n\n// Now at HomeScreenGardensView\nexpect($(HomeScreenGardensView).visible, equals(true)); \nawait $('Details').tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \nexpect($(testPlanting).visible, equals(true));\nawait $(testPlanting).tap();\nawait $(PlantingDetailsCopyButton).tap();\n\n// Now at CopyPlanting Screen\nexpect($(CopyPlantingScreen).visible, equals(true)); \nawait $(GardenDropdown).tap();\nawait $('Alderwood').tap();\nawait $(BedDropdown).tap();\nawait $('02').tap();\nawait $('Submit').scrollTo().tap();\n\n// Now at GardenDetailsScreen\nexpect($(GardenDetailsScreen).visible, equals(true)); \n"})}),"\n",(0,i.jsx)(t.p,{children:"You don't need to put those comments (or the newlines) into your test code; I add them here just to highlight the added lines. But hopefully you can see how these expect statements make the flow of the test easier to understand. It also means the test will fail with a more helpful error message if the test ends up on an unexpected screen."}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:'Don\'t use an absolute "count" of items to do verification.'})," For example, don't think that if the test fixture defines two gardens, your test case can assume it will see exactly two gardens. It could be that in the future, a test case gets added before yours that results in more gardens in the fixture by the time your test code runs. Find some other way to do verification."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't delete or modify any entities in the test fixture."})," If you want to test some sort of mutation, then please consider creating a new entity to mutate (or at the very least, make sure you restore the test fixture entity to its original condition). While other tests shouldn't assume there won't be ",(0,i.jsx)(t.em,{children:"new"})," entities added, all tests can assume that the entities in the test fixture will be there exactly as defined."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Don't write too much test code."}),' Remember that the test code becomes code that needs to be maintained just like the app code. Also remember that time spent on writing test code is time you can\'t spend implementing new features in the app. So, try to design your tests with the goal of writing the minimal amount of test code required to exercise the maximum amount of app code. The prime directive is to reduce the risk of "catastrophic regression"---i.e. changes to the codebase that results in a runtime exception that crashes the app someplace in the UI. So, to start, if your test code exercises a feature\'s UI under "normal" conditions, and you verify that none of those interactions produces a runtime exception that crashes the app, then you\'ve written a ',(0,i.jsx)(t.em,{children:"very"})," helpful test. Of course, checking that the UI actually displays what it should display adds even more value, but if you only have time at the moment to invoke the behavior and ensure that things don't go haywire, that's still something."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Flutter DevTools can be helpful."})," Sometimes I get confused about what widgets are actually displayed on screen, and as a result have problems writing the correct Patrol Finder code. It can be helpful to run ",(0,i.jsx)(t.a,{href:"https://docs.flutter.dev/tools/devtools/android-studio",children:"Flutter DevTools"}),", then run the simulator manually. This enables you to navigate to a page in the simulator and use a browser window to inspect the widget hierarchy to see what type of widgets are visible."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Authentication state weirdness."})," I have discovered that if you perform the logout action during testing, it leaves the simulator in a weird, persistent state where you are navigated to the SignIn page, but the signin form (with fields for email and password) are not displayed. I am not sure why this happens, but to fix it, you can run the simulator normally (i.e. running main.dart) which will display the signin form. Login as any user, quit, and now you can run the tests and mocked authentication will work correctly."]}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"After fixing a bug in the app, consider writing a test to verify the correct behavior."})," Weirdly, bugs tend to congregate in certain areas, and even reappear after you thought you squashed them. It's a good idea after fixing a bug to see if you can quickly write a test that verifies the absence of that bug. It might feel like closing the barn door after the horse is gone, but it's a way of incrementally deepening the test quality."]}),"\n",(0,i.jsx)(t.h2,{id:"continuous-integration",children:"Continuous integration"}),"\n",(0,i.jsx)(t.p,{children:"It would be sweet to run the integration tests each time there is a commit to main. The simplest way to accomplish this would be via a GitHub Action that runs the integration tests. You would think this would be straight forward. Unfortunately, it is not:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'If you run the integration tests using a GitHub action that requires macOS, there is a "minutes multiplier" of 10, which means we will quickly run out of free minutes each month.'}),"\n",(0,i.jsx)(t.li,{children:"I have tried and failed to create a working GitHub action for integration testing under Linux. I have found that even running our integration tests locally under Android is unreliable: sometimes they will fail at the authentication step."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["The Patrol documentation has a section on ",(0,i.jsx)(t.a,{href:"https://patrol.leancode.co/ci/platforms",children:"Continuous Integration Platforms"})," which provides interesting insights into the problems of Flutter integration testing under CI. This is a good place to start if you wish to look into it more."]}),"\n",(0,i.jsxs)(t.p,{children:["You can look at the ",(0,i.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/tree/main/.github/workflows",children:".github/workflows directory"}),' for the current situation. Notice that the integration testing workflows are set up to run when a commit is made to a branch called "never", meaning they are never actually invoked.']}),"\n",(0,i.jsx)(t.h2,{id:"test-fixture-design",children:"Test fixture design"}),"\n",(0,i.jsxs)(t.p,{children:["We use JSON files to create the test data for the tests. The test files are located in one of (potentially many) directories named ",(0,i.jsx)(t.code,{children:"assets\\test\\fixtureN"}),', when "N" is a number uniquely identifying the fixture. Currently, we only have one fixture directory.']}),"\n",(0,i.jsx)(t.p,{children:"Each fixture directory must contain the following files:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"badgeInstanceData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"bedData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"chapterData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"cropData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"editorData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"familyData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"gardenerData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"observationData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"outcomeData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"plantingData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"roleData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"tagData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"taskData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"userData.json"})}),"\n",(0,i.jsx)(t.li,{children:(0,i.jsx)(t.code,{children:"varietyData.json"})}),"\n"]}),"\n",(0,i.jsxs)(t.admonition,{type:"info",children:[(0,i.jsx)(t.p,{children:"The JSON files need to have integrity, so their ids must align. Since many of the GGC IDs end with a four digit millis field we've assigned each type a unique millis field."}),(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"bedIDs end with 3456."}),"\n",(0,i.jsx)(t.li,{children:"cropIDs end with 5678."}),"\n",(0,i.jsx)(t.li,{children:"gardenIDs end with 7890."}),"\n",(0,i.jsx)(t.li,{children:"observationIDs end with 4567."}),"\n",(0,i.jsx)(t.li,{children:"outcomeIDs end with 2345."}),"\n",(0,i.jsx)(t.li,{children:"plantingIDs end with 1234."}),"\n",(0,i.jsx)(t.li,{children:"seedIDs end with 6789."}),"\n",(0,i.jsx)(t.li,{children:"taskIDs end with 8901."}),"\n",(0,i.jsx)(t.li,{children:"varietyIDs end with 9012."}),"\n",(0,i.jsx)(t.li,{children:"badgeInstances end with 9876."}),"\n"]})]}),"\n",(0,i.jsx)(t.h3,{id:"fixture-paths",children:"Fixture Paths"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"lib/features/fixture_paths.dart"})," file defines two constants:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"testFixturePath"})," - the path to the test fixture directory. This constant is used to load the test data in the tests."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"monarchFixturePath"})," - the path to the Monarch fixture directory used by ",(0,i.jsx)(t.code,{children:"WithMonarchData"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"assetcollectionbuilder",children:"AssetCollectionBuilder"}),"\n",(0,i.jsxs)(t.p,{children:["To facilitate the loading of the fixture files, we have created the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class. This class has three static methods to produce each of the collections from a fixture path. The three methods are as follows:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future> getTypes(String assetPath)"})," - loads the data from the fixture file and returns a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future>> getTypesStream(String assetPath)"})," - loads the data from the fixture file and returns a stream of a list of the type."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"Future getTypeCollection(String assetPath)"})," - loads the data from the fixture file and returns a collection of the type."]}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["For example, to create a ",(0,i.jsx)(t.code,{children:"BedCollection"})," from the fixture path, use the following code:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"final bedCollection = await AssetCollectionBuilder.getBedCollection(testFixturePath);\n"})}),"\n",(0,i.jsxs)(t.p,{children:["In addition, the ",(0,i.jsx)(t.code,{children:"AssetCollectionBuilder"})," class has three build methods that build the collections with all the data like the ",(0,i.jsx)(t.code,{children:"WithAllData"})," classes. The methods are as follows"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildChapterCollection(String assetPath, String chapterId)"})," - builds a ",(0,i.jsx)(t.code,{children:"ChapterCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildGardenCollection(String assetPath, String gardenId)"})," - builds a ",(0,i.jsx)(t.code,{children:"GardenCollection"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"buildUserCollection(String assetPath, String currentUserID, String currentUserUID)"})," - builds a ",(0,i.jsx)(t.code,{children:"UserCollection"}),"."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"testfixture-singleton",children:"TestFixture singleton"}),"\n",(0,i.jsxs)(t.p,{children:["The ",(0,i.jsx)(t.code,{children:"TestFixture"})," singleton is used to load the test fixture data. The singleton has the following methods:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getInstance(String assetPath)"})," - returns a Future with the singleton instance. The first time it is called, it will load the test fixture data."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"setup()"})," - initializes the singleton by loading the test fixture data."]}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"There are two methods for each entity in the test fixture:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getStream()"})," - returns a Stream of the List of the entities from the test fixture."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"getDatabase()"})," - returns The ",(0,i.jsx)(t.code,{children:"FixtureDatabase"})," from the test fixture."]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>r});var i=n(7294);const a={},s=i.createContext(a);function r(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/31caa863.a5c0076e.js b/assets/js/31caa863.98841c3b.js similarity index 99% rename from assets/js/31caa863.a5c0076e.js rename to assets/js/31caa863.98841c3b.js index 4b40e7abd..a08dea924 100644 --- a/assets/js/31caa863.a5c0076e.js +++ b/assets/js/31caa863.98841c3b.js @@ -1 +1 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4063],{2592:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>c,frontMatter:()=>s,metadata:()=>d,toc:()=>r});var i=t(5893),o=t(1151);const s={hide_table_of_contents:!1},a="Deployment",d={id:"develop/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/deployment.md",sourceDirName:"develop",slug:"/develop/deployment",permalink:"/docs/develop/deployment",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Architecture",permalink:"/docs/develop/architecture"},next:{title:"Testing",permalink:"/docs/develop/testing"}},l={},r=[{value:"Documenting deployment versions",id:"documenting-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"0. Prerequisites",id:"0-prerequisites",level:2},{value:"1. Update the ChangeLog",id:"1-update-the-changelog",level:2},{value:"2. Build the deployment files",id:"2-build-the-deployment-files",level:2},{value:"3. Deploy the iOS app",id:"3-deploy-the-ios-app",level:2},{value:"3. Deploy the Android App",id:"3-deploy-the-android-app",level:2},{value:"Adding new beta testers (iOS)",id:"adding-new-beta-testers-ios",level:2},{value:"Adding new beta testers (Android)",id:"adding-new-beta-testers-android",level:2},{value:"Testing on a physical device without deployment",id:"testing-on-a-physical-device-without-deployment",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"deployment",children:"Deployment"})}),"\n",(0,i.jsx)(n.p,{children:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."}),"\n",(0,i.jsx)(n.h2,{id:"documenting-deployment-versions",children:"Documenting deployment versions"}),"\n",(0,i.jsxs)(n.p,{children:["Each new deployment requires a new version number (specified in the pubspec.yml file), and we document what has changed in each new version via ",(0,i.jsx)(n.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md",children:"CHANGELOG.md"}),". To manage version numbers and the changelog file, we use ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/cider",children:"Cider"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. So, when we do a deployment, the ",(0,i.jsx)(n.code,{children:"run_deploy.sh"})," script will copy the top-level CHANGELOG.md file into the assets/ folder so that it gets included in the various apps."]}),"\n",(0,i.jsx)(n.p,{children:"We adhere to two standards:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["For the changelog format, we adhere to ",(0,i.jsx)(n.a,{href:"https://keepachangelog.com/en/1.0.0/",children:"Keep a Changelog"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["For the version number format, we adhere to ",(0,i.jsx)(n.a,{href:"https://semver.org/spec/v2.0.0.html",children:"Semantic Versioning"}),". For Release 1.0 (Technology Evaluation), the ",(0,i.jsx)(n.em,{children:"major"}),' version is "1". The deploy script automatically increments the minor version and increments the build number.']}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"deployment-management",children:"Deployment management"}),"\n",(0,i.jsx)(n.p,{children:'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'}),"\n",(0,i.jsx)(n.h2,{id:"0-prerequisites",children:"0. Prerequisites"}),"\n",(0,i.jsx)(n.p,{children:"Prior to a deployment, it is good practice to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Do a ",(0,i.jsx)(n.a,{href:"/docs/develop/backups",children:"backup"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run the integrity checker and resolve any violations."}),"\n",(0,i.jsx)(n.li,{children:"Run the integration tests and make sure the main branch does not generate any errors."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"1-update-the-changelog",children:"1. Update the ChangeLog"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"cider log added "})," to document new additions (or use ",(0,i.jsx)(n.code,{children:"changed"})," or ",(0,i.jsx)(n.code,{children:"fixed"}),") since the last release. Enclose the message in quotes. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'cider log added "Terms and Conditions"\n'})}),"\n",(0,i.jsx)(n.h2,{id:"2-build-the-deployment-files",children:"2. Build the deployment files"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"./run_deploy.sh"}),". This script does the following:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"cider bump minor"})," and ",(0,i.jsx)(n.code,{children:"cider release"})," so that the unreleased changes are moved to a new release number, and that release number is recorded in the pubspec.yml."]}),"\n",(0,i.jsx)(n.li,{children:"Commits the updated CHANGELOG.md and pubspec.yml files to GitHub."}),"\n",(0,i.jsxs)(n.li,{children:['Creates a "deploy directory" at ',(0,i.jsx)(n.code,{children:"~/Desktop/ggc-deploy-"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Builds the ggc_app.ipa file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Builds the app-release.aab file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Gets the release notes for the current release and copies them to the deploy directory."}),"\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"firebase deploy"})," to build and deploy the web version of the app."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"3-deploy-the-ios-app",children:"3. Deploy the iOS app"}),"\n",(0,i.jsx)(n.p,{children:"First, open the Transporter app and drag the ggc_app.ipa file from the Desktop folder onto the App."}),"\n",(0,i.jsxs)(n.p,{children:["Second, login to ",(0,i.jsx)(n.a,{href:"https://appstoreconnect.apple.com/login",children:"App Store Connect"}),'. Click on "Apps", then "GeoGardenClub", then "TestFlight".']}),"\n",(0,i.jsx)(n.p,{children:"Wait for a few minutes for the uploaded version to become available for distribution via TestFlight."}),"\n",(0,i.jsx)(n.p,{children:'Once available, the "internal" testers will be automatically notified.'}),"\n",(0,i.jsx)(n.p,{children:'Now submit the build for external testing. Click on "External" on the left sidebar, then click the "+" button next to the "Builds" section, and add the most recent build. It will then be submitted for review. This review appears to take 3-7 days to complete. At that point, the public URL can be distributed and anyone who already installed the app via that link should be able to update to the new build.'}),"\n",(0,i.jsx)(n.h2,{id:"3-deploy-the-android-app",children:"3. Deploy the Android App"}),"\n",(0,i.jsxs)(n.p,{children:["Open the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/tracks/internal-testing",children:"Google Play Console Internal Testing Page"}),' and click on "Create new release".']}),"\n",(0,i.jsx)(n.p,{children:"Upload app-release.aab file from the folder containing the newly created release."}),"\n",(0,i.jsx)(n.p,{children:'Once uploaded, click "Next" to go to the Preview and Confirm page. Ensure that everything looks OK, then click "Save and Publish".'}),"\n",(0,i.jsxs)(n.p,{children:["Now go to the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/pre-launch-report/overview",children:"Prelaunch Report Overview"})," page. After about an hour, you will be able to check the results of testing on the new version and see if there are any issues that need to be addressed."]}),"\n",(0,i.jsx)(n.p,{children:'Finally, "promote" this version to the "Closed testing" track. This triggers an internal review by Google that takes a few days, but is useful as it results in additional quality assurance testing by Google.'}),"\n",(0,i.jsx)(n.h2,{id:"adding-new-beta-testers-ios",children:"Adding new beta testers (iOS)"}),"\n",(0,i.jsx)(n.p,{children:"Previously, we needed the email address they use with their Apple ID in order to add them as an internal tester in App Store Connect."}),"\n",(0,i.jsx)(n.p,{children:"We are now trying to use external testing so that we can simply distribute a URL to anyone who wants to test the app."}),"\n",(0,i.jsx)(n.h2,{id:"adding-new-beta-testers-android",children:"Adding new beta testers (Android)"}),"\n",(0,i.jsx)(n.p,{children:"Currently, we need the gmail address that the user has associated with their Android device so that we can add them as an internal tester."}),"\n",(0,i.jsx)(n.h2,{id:"testing-on-a-physical-device-without-deployment",children:"Testing on a physical device without deployment"}),"\n",(0,i.jsx)(n.p,{children:"Sometimes it is useful to try out the app on a physical device without having to go through deployment steps. Currently only Philip can do this due to iOS signing issues."}),"\n",(0,i.jsx)(n.p,{children:"Here is what you need to do:"}),"\n",(0,i.jsx)(n.p,{children:"First, delete the app from your physical device."}),"\n",(0,i.jsx)(n.p,{children:"Second, connect the physical device to a laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Third, run ",(0,i.jsx)(n.code,{children:"flutter devices"})," to verify that the device shows up. You should see output like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:" % flutter devices \nFound 5 connected devices:\n Philip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\n iPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n (simulator)\n macOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\n Mac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\n Chrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n"})}),"\n",(0,i.jsx)(n.p,{children:"Notice that the first device is my physical device connected to my laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Now, to run the code in release mode on this physical device, you invoke ",(0,i.jsx)(n.code,{children:"flutter run --release"})," and select the physical device like so. (Make sure your device is unlocked while running this command.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"% flutter run --release\nConnected devices:\nPhilip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\niPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n(simulator)\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\nMac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n\nChecking for wireless devices...\n\nNo wireless devices were found.\n\n[1]: Philip's iPhone (00008030-000364940A98802E)\n[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)\n[3]: macOS (macos)\n[4]: Mac Designed for iPad (mac-designed-for-ipad)\n[5]: Chrome (chrome)\nPlease choose one (or \"q\" to quit): 1\nLaunching lib/main.dart on Philip's iPhone in release mode...\nAutomatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 8.8s\nXcode build done. 67.9s\nInstalling and launching... 7.1s\n\nFlutter run key commands.\nh List all available interactive commands.\nc Clear the screen\nq Quit (terminate the application on the device).\n\n"})})]})}function c(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>d,a:()=>a});var i=t(7294);const o={},s=i.createContext(o);function a(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4063],{2592:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>c,frontMatter:()=>s,metadata:()=>d,toc:()=>r});var i=t(5893),o=t(1151);const s={hide_table_of_contents:!1},a="Deployment",d={id:"develop/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/deployment.md",sourceDirName:"develop",slug:"/develop/deployment",permalink:"/docs/develop/deployment",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Architecture",permalink:"/docs/develop/architecture"},next:{title:"Backups",permalink:"/docs/develop/backups"}},l={},r=[{value:"Documenting deployment versions",id:"documenting-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"0. Prerequisites",id:"0-prerequisites",level:2},{value:"1. Update the ChangeLog",id:"1-update-the-changelog",level:2},{value:"2. Build the deployment files",id:"2-build-the-deployment-files",level:2},{value:"3. Deploy the iOS app",id:"3-deploy-the-ios-app",level:2},{value:"3. Deploy the Android App",id:"3-deploy-the-android-app",level:2},{value:"Adding new beta testers (iOS)",id:"adding-new-beta-testers-ios",level:2},{value:"Adding new beta testers (Android)",id:"adding-new-beta-testers-android",level:2},{value:"Testing on a physical device without deployment",id:"testing-on-a-physical-device-without-deployment",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"deployment",children:"Deployment"})}),"\n",(0,i.jsx)(n.p,{children:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."}),"\n",(0,i.jsx)(n.h2,{id:"documenting-deployment-versions",children:"Documenting deployment versions"}),"\n",(0,i.jsxs)(n.p,{children:["Each new deployment requires a new version number (specified in the pubspec.yml file), and we document what has changed in each new version via ",(0,i.jsx)(n.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md",children:"CHANGELOG.md"}),". To manage version numbers and the changelog file, we use ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/cider",children:"Cider"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. So, when we do a deployment, the ",(0,i.jsx)(n.code,{children:"run_deploy.sh"})," script will copy the top-level CHANGELOG.md file into the assets/ folder so that it gets included in the various apps."]}),"\n",(0,i.jsx)(n.p,{children:"We adhere to two standards:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["For the changelog format, we adhere to ",(0,i.jsx)(n.a,{href:"https://keepachangelog.com/en/1.0.0/",children:"Keep a Changelog"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["For the version number format, we adhere to ",(0,i.jsx)(n.a,{href:"https://semver.org/spec/v2.0.0.html",children:"Semantic Versioning"}),". For Release 1.0 (Technology Evaluation), the ",(0,i.jsx)(n.em,{children:"major"}),' version is "1". The deploy script automatically increments the minor version and increments the build number.']}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"deployment-management",children:"Deployment management"}),"\n",(0,i.jsx)(n.p,{children:'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'}),"\n",(0,i.jsx)(n.h2,{id:"0-prerequisites",children:"0. Prerequisites"}),"\n",(0,i.jsx)(n.p,{children:"Prior to a deployment, it is good practice to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Do a ",(0,i.jsx)(n.a,{href:"/docs/develop/backups",children:"backup"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run the integrity checker and resolve any violations."}),"\n",(0,i.jsx)(n.li,{children:"Run the integration tests and make sure the main branch does not generate any errors."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"1-update-the-changelog",children:"1. Update the ChangeLog"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"cider log added "})," to document new additions (or use ",(0,i.jsx)(n.code,{children:"changed"})," or ",(0,i.jsx)(n.code,{children:"fixed"}),") since the last release. Enclose the message in quotes. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'cider log added "Terms and Conditions"\n'})}),"\n",(0,i.jsx)(n.h2,{id:"2-build-the-deployment-files",children:"2. Build the deployment files"}),"\n",(0,i.jsxs)(n.p,{children:["Invoke ",(0,i.jsx)(n.code,{children:"./run_deploy.sh"}),". This script does the following:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"cider bump minor"})," and ",(0,i.jsx)(n.code,{children:"cider release"})," so that the unreleased changes are moved to a new release number, and that release number is recorded in the pubspec.yml."]}),"\n",(0,i.jsx)(n.li,{children:"Commits the updated CHANGELOG.md and pubspec.yml files to GitHub."}),"\n",(0,i.jsxs)(n.li,{children:['Creates a "deploy directory" at ',(0,i.jsx)(n.code,{children:"~/Desktop/ggc-deploy-"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Builds the ggc_app.ipa file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Builds the app-release.aab file and copies it to the deploy directory."}),"\n",(0,i.jsx)(n.li,{children:"Gets the release notes for the current release and copies them to the deploy directory."}),"\n",(0,i.jsxs)(n.li,{children:["Invokes ",(0,i.jsx)(n.code,{children:"firebase deploy"})," to build and deploy the web version of the app."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"3-deploy-the-ios-app",children:"3. Deploy the iOS app"}),"\n",(0,i.jsx)(n.p,{children:"First, open the Transporter app and drag the ggc_app.ipa file from the Desktop folder onto the App."}),"\n",(0,i.jsxs)(n.p,{children:["Second, login to ",(0,i.jsx)(n.a,{href:"https://appstoreconnect.apple.com/login",children:"App Store Connect"}),'. Click on "Apps", then "GeoGardenClub", then "TestFlight".']}),"\n",(0,i.jsx)(n.p,{children:"Wait for a few minutes for the uploaded version to become available for distribution via TestFlight."}),"\n",(0,i.jsx)(n.p,{children:'Once available, the "internal" testers will be automatically notified.'}),"\n",(0,i.jsx)(n.p,{children:'Now submit the build for external testing. Click on "External" on the left sidebar, then click the "+" button next to the "Builds" section, and add the most recent build. It will then be submitted for review. This review appears to take 3-7 days to complete. At that point, the public URL can be distributed and anyone who already installed the app via that link should be able to update to the new build.'}),"\n",(0,i.jsx)(n.h2,{id:"3-deploy-the-android-app",children:"3. Deploy the Android App"}),"\n",(0,i.jsxs)(n.p,{children:["Open the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/tracks/internal-testing",children:"Google Play Console Internal Testing Page"}),' and click on "Create new release".']}),"\n",(0,i.jsx)(n.p,{children:"Upload app-release.aab file from the folder containing the newly created release."}),"\n",(0,i.jsx)(n.p,{children:'Once uploaded, click "Next" to go to the Preview and Confirm page. Ensure that everything looks OK, then click "Save and Publish".'}),"\n",(0,i.jsxs)(n.p,{children:["Now go to the ",(0,i.jsx)(n.a,{href:"https://play.google.com/console/u/0/developers/8896023390666377316/app/4974477500315919596/pre-launch-report/overview",children:"Prelaunch Report Overview"})," page. After about an hour, you will be able to check the results of testing on the new version and see if there are any issues that need to be addressed."]}),"\n",(0,i.jsx)(n.p,{children:'Finally, "promote" this version to the "Closed testing" track. This triggers an internal review by Google that takes a few days, but is useful as it results in additional quality assurance testing by Google.'}),"\n",(0,i.jsx)(n.h2,{id:"adding-new-beta-testers-ios",children:"Adding new beta testers (iOS)"}),"\n",(0,i.jsx)(n.p,{children:"Previously, we needed the email address they use with their Apple ID in order to add them as an internal tester in App Store Connect."}),"\n",(0,i.jsx)(n.p,{children:"We are now trying to use external testing so that we can simply distribute a URL to anyone who wants to test the app."}),"\n",(0,i.jsx)(n.h2,{id:"adding-new-beta-testers-android",children:"Adding new beta testers (Android)"}),"\n",(0,i.jsx)(n.p,{children:"Currently, we need the gmail address that the user has associated with their Android device so that we can add them as an internal tester."}),"\n",(0,i.jsx)(n.h2,{id:"testing-on-a-physical-device-without-deployment",children:"Testing on a physical device without deployment"}),"\n",(0,i.jsx)(n.p,{children:"Sometimes it is useful to try out the app on a physical device without having to go through deployment steps. Currently only Philip can do this due to iOS signing issues."}),"\n",(0,i.jsx)(n.p,{children:"Here is what you need to do:"}),"\n",(0,i.jsx)(n.p,{children:"First, delete the app from your physical device."}),"\n",(0,i.jsx)(n.p,{children:"Second, connect the physical device to a laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Third, run ",(0,i.jsx)(n.code,{children:"flutter devices"})," to verify that the device shows up. You should see output like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:" % flutter devices \nFound 5 connected devices:\n Philip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\n iPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n (simulator)\n macOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\n Mac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\n Chrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n"})}),"\n",(0,i.jsx)(n.p,{children:"Notice that the first device is my physical device connected to my laptop."}),"\n",(0,i.jsxs)(n.p,{children:["Now, to run the code in release mode on this physical device, you invoke ",(0,i.jsx)(n.code,{children:"flutter run --release"})," and select the physical device like so. (Make sure your device is unlocked while running this command.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"% flutter run --release\nConnected devices:\nPhilip's iPhone (mobile) \u2022 00008030-000364940A98802E \u2022 ios \u2022 iOS 17.5.1 21F90\niPhone 11 (mobile) \u2022 8E550E86-3173-4342-B197-A557B83E40A2 \u2022 ios \u2022 com.apple.CoreSimulator.SimRuntime.iOS-17-5\n(simulator)\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 14.4.1 23E224 darwin-arm64\nMac Designed for iPad (desktop) \u2022 mac-designed-for-ipad \u2022 darwin \u2022 macOS 14.4.1 23E224 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 125.0.6422.113\n\nChecking for wireless devices...\n\nNo wireless devices were found.\n\n[1]: Philip's iPhone (00008030-000364940A98802E)\n[2]: iPhone 11 (8E550E86-3173-4342-B197-A557B83E40A2)\n[3]: macOS (macos)\n[4]: Mac Designed for iPad (mac-designed-for-ipad)\n[5]: Chrome (chrome)\nPlease choose one (or \"q\" to quit): 1\nLaunching lib/main.dart on Philip's iPhone in release mode...\nAutomatically signing iOS for device deployment using specified development team in Xcode project: 8M69898HLM\nRunning Xcode build... \n \u2514\u2500Compiling, linking and signing... 8.8s\nXcode build done. 67.9s\nInstalling and launching... 7.1s\n\nFlutter run key commands.\nh List all available interactive commands.\nc Clear the screen\nq Quit (terminate the application on the device).\n\n"})})]})}function c(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>d,a:()=>a});var i=t(7294);const o={},s=i.createContext(o);function a(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/39838d4e.295e1e77.js b/assets/js/39838d4e.295e1e77.js new file mode 100644 index 000000000..dc6818275 --- /dev/null +++ b/assets/js/39838d4e.295e1e77.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[234],{8901:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var i=o(5893),t=o(1151);const r={hide_table_of_contents:!1},s="Onboarding",a={id:"develop/onboarding",title:"Onboarding",description:"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.",source:"@site/docs/develop/onboarding.md",sourceDirName:"develop",slug:"/develop/onboarding",permalink:"/docs/develop/onboarding",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Welcome",permalink:"/docs/develop/"},next:{title:"Installation",permalink:"/docs/develop/installation"}},l={},c=[{value:"Site access",id:"site-access",level:2},{value:"Proficiency in Dart and Flutter",id:"proficiency-in-dart-and-flutter",level:2},{value:"Assignment of rights",id:"assignment-of-rights",level:2},{value:"Development process basics",id:"development-process-basics",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"onboarding",children:"Onboarding"})}),"\n",(0,i.jsx)(n.p,{children:"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology."}),"\n",(0,i.jsx)(n.h2,{id:"site-access",children:"Site access"}),"\n",(0,i.jsx)(n.p,{children:"You will need access to the following:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"The GGC Discord server. Request an invite from Philip or Jenna."}),"\n",(0,i.jsx)(n.li,{children:"The GGC GitHub organization. Please send Philip your GitHub username and he will invite you."}),"\n",(0,i.jsx)(n.li,{children:"The ggc_app Firebase project. Please send Philip your gmail account name and he will add you."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"proficiency-in-dart-and-flutter",children:"Proficiency in Dart and Flutter"}),"\n",(0,i.jsxs)(n.p,{children:["We assume that you already have basic proficiency in Dart and Flutter. If you are not sure of your proficiency, then we recommend that you work through the ",(0,i.jsx)(n.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/dartapalooza/",children:"Dartapalooza"})," and ",(0,i.jsx)(n.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/flutterpalooza/",children:"Flutterpalooza"})," modules of Philip's mobile application development course."]}),"\n",(0,i.jsx)(n.h2,{id:"assignment-of-rights",children:"Assignment of rights"}),"\n",(0,i.jsx)(n.p,{children:"Before you can contribute code to this project, you will need to sign a document that assigns the ownership of the code you contribute to Geo Garden Club, LLC. Please contact Philip or Jenna for details on how to do this."}),"\n",(0,i.jsx)(n.h2,{id:"development-process-basics",children:"Development process basics"}),"\n",(0,i.jsx)(n.p,{children:"We use the following process for development:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Tasks are specified in a GitHub project board. The project board for GGC is available at: ",(0,i.jsx)(n.a,{href:"https://github.com/orgs/geogardenclub/projects/1/views/1",children:"https://github.com/orgs/geogardenclub/projects/1/views/1"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Code is developed using a branch-and-merge model. The ",(0,i.jsx)(n.code,{children:"main"}),' branch should always contain a working version of the system. Do your development work in a branch off of the main branch. Please name the branch "issue-XXX", where XXX is the issue number associated with the task on the project board that you are working on. When that task is complete, you merge your branch back into main. (We do not currently use pull requests, although that could change in the future.)']}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"If you are making a trivial fix, you can commit directly to the main branch."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Before merging your code into the main branch:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Be sure that ",(0,i.jsx)(n.code,{children:"dart analyze"})," does not generate any errors (this will be verified via a GitHub action)."]}),"\n",(0,i.jsxs)(n.li,{children:["Be sure that ",(0,i.jsx)(n.code,{children:"./run_tests.sh"})," does not generate any errors (this is not currently verified via a GitHub action)."]}),"\n"]}),"\n"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},1151:(e,n,o)=>{o.d(n,{Z:()=>a,a:()=>s});var i=o(7294);const t={},r=i.createContext(t);function s(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/39838d4e.76946f78.js b/assets/js/39838d4e.76946f78.js deleted file mode 100644 index 9803e2ebb..000000000 --- a/assets/js/39838d4e.76946f78.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[234],{8901:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var i=o(5893),t=o(1151);const r={hide_table_of_contents:!1},s="Onboarding",a={id:"develop/onboarding",title:"Onboarding",description:"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.",source:"@site/docs/develop/onboarding.md",sourceDirName:"develop",slug:"/develop/onboarding",permalink:"/docs/develop/onboarding",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Welcome",permalink:"/docs/develop/"},next:{title:"Installation",permalink:"/docs/develop/installation"}},l={},c=[{value:"Site access",id:"site-access",level:2},{value:"Proficiency in Dart and Flutter",id:"proficiency-in-dart-and-flutter",level:2},{value:"Assignment of rights",id:"assignment-of-rights",level:2},{value:"Developer workflow",id:"developer-workflow",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"onboarding",children:"Onboarding"})}),"\n",(0,i.jsx)(n.p,{children:"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology."}),"\n",(0,i.jsx)(n.h2,{id:"site-access",children:"Site access"}),"\n",(0,i.jsx)(n.p,{children:"You will need access to the following:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"The GGC Discord server. Request an invite from Philip or Jenna."}),"\n",(0,i.jsx)(n.li,{children:"The GGC GitHub organization. Please send Philip your GitHub username and he will invite you."}),"\n",(0,i.jsx)(n.li,{children:"The ggc_app Firebase project. Please send Philip your gmail account name and he will add you."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"proficiency-in-dart-and-flutter",children:"Proficiency in Dart and Flutter"}),"\n",(0,i.jsxs)(n.p,{children:["We assume that you already have basic proficiency in Dart and Flutter. If you are not sure of your proficiency, then we recommend that you work through the ",(0,i.jsx)(n.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/dartapalooza/",children:"Dartapalooza"})," and ",(0,i.jsx)(n.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/flutterpalooza/",children:"Flutterpalooza"})," modules of Philip's mobile application development course."]}),"\n",(0,i.jsx)(n.h2,{id:"assignment-of-rights",children:"Assignment of rights"}),"\n",(0,i.jsx)(n.p,{children:"Before you can contribute code to this project, you will need to sign a document that assigns the ownership of the code you contribute to Geo Garden Club, LLC. Please contact Philip or Jenna for details on how to do this."}),"\n",(0,i.jsx)(n.h2,{id:"developer-workflow",children:"Developer workflow"}),"\n",(0,i.jsx)(n.p,{children:"We use a basic process for development:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Tasks are specified in a GitHub project board. The project board for GGC is available at: ",(0,i.jsx)(n.a,{href:"https://github.com/orgs/geogardenclub/projects/1/views/1",children:"https://github.com/orgs/geogardenclub/projects/1/views/1"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:'Code is developed using a branch-and-merge model. Please name the branch "issue-XXX", where XXX is the issue number associated with the task associated with your coding.'}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"If you are making a trivial fix, feel free to commit directly to the main branch."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["We run a CI task to ensure that all code committed to the main branch passes ",(0,i.jsx)(n.code,{children:"dart analyze"})," without triggering warnings or errors."]}),"\n"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},1151:(e,n,o)=>{o.d(n,{Z:()=>a,a:()=>s});var i=o(7294);const t={},r=i.createContext(t);function s(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/505d7517.829b9dc4.js b/assets/js/505d7517.829b9dc4.js new file mode 100644 index 000000000..8b140c437 --- /dev/null +++ b/assets/js/505d7517.829b9dc4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[196],{4230:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>c,frontMatter:()=>s,metadata:()=>r,toc:()=>h});var i=o(5893),n=o(1151);const s={hide_table_of_contents:!1},a="Technology Goals",r={id:"develop/releases/release-1.0/goals",title:"Technology Goals",description:"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.",source:"@site/docs/develop/releases/release-1.0/goals.md",sourceDirName:"develop/releases/release-1.0",slug:"/develop/releases/release-1.0/goals",permalink:"/docs/develop/releases/release-1.0/goals",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Testing",permalink:"/docs/develop/testing"},next:{title:"Core Value Propositions",permalink:"/docs/develop/releases/release-1.0/cvp"}},l={},h=[{value:"1. Provide a fast, reliable, robust app that satisfies the Core Value Propositions",id:"1-provide-a-fast-reliable-robust-app-that-satisfies-the-core-value-propositions",level:2},{value:"2. Determine what it means to be a champion/chapter chair",id:"2-determine-what-it-means-to-be-a-championchapter-chair",level:2},{value:"3. Determine how to provide high quality documentation",id:"3-determine-how-to-provide-high-quality-documentation",level:2},{value:"4. Gather evidence-based "testimonials"",id:"4-gather-evidence-based-testimonials",level:2},{value:"5. Determine how to usefully integrate AI",id:"5-determine-how-to-usefully-integrate-ai",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",ul:"ul",...(0,n.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"technology-goals",children:"Technology Goals"})}),"\n",(0,i.jsxs)(t.p,{children:["Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by ",(0,i.jsx)(t.a,{href:"https://www.gitpod.io/blog/champion-building",children:"Champion Building: How to successfully adopt a developer tool"}),". Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC."]}),"\n",(0,i.jsx)(t.h2,{id:"1-provide-a-fast-reliable-robust-app-that-satisfies-the-core-value-propositions",children:"1. Provide a fast, reliable, robust app that satisfies the Core Value Propositions"}),"\n",(0,i.jsxs)(t.p,{children:["By the end of the 1.0 release period, we need to have an app that satisfies the ",(0,i.jsx)(t.a,{href:"/docs/develop/releases/release-1.0/cvp",children:"Core Value Propositions"})," while being fast, easy to use, and not prone to crashing."]}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Checklist of CVP functions satisfied."}),"\n",(0,i.jsx)(t.li,{children:"Time to retrieve each page in app using DevTools Performance View"}),"\n",(0,i.jsx)(t.li,{children:"Crashlytics data"}),"\n",(0,i.jsx)(t.li,{children:"Usability Evaluation of 1.0 user base with respect to app usability."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"2-determine-what-it-means-to-be-a-championchapter-chair",children:"2. Determine what it means to be a champion/chapter chair"}),"\n",(0,i.jsx)(t.p,{children:"Successful chapters will need one or more champions. We hope to use the 1.0 release period to develop an understanding of what it means to be champion: what their responsibilities are, how to carry out their responsibilities, and how the GGC organization should support and/or compensate them."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation criteria:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"User-facing documentation for Chapter Chairs."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"3-determine-how-to-provide-high-quality-documentation",children:"3. Determine how to provide high quality documentation"}),"\n",(0,i.jsx)(t.p,{children:"We hope to use the 1.0 release to determine what needs to be documented, and how it should be documented. Some possible documentation sources include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"In-app documentation (the ? icon)"}),"\n",(0,i.jsx)(t.li,{children:"YouTube (shorts and/or regular videos)"}),"\n",(0,i.jsx)(t.li,{children:"User Guide (within geogardenclub.com)"}),"\n",(0,i.jsx)(t.li,{children:'GGC Discord Server: channels visible to those in the "User" role.'}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Evaluation criteria:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Usability evaluation of 1.0 user base with respect to app documentation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"4-gather-evidence-based-testimonials",children:'4. Gather evidence-based "testimonials"'}),"\n",(0,i.jsx)(t.p,{children:'We hope that the 1.0 release period will result in examples of "successful" use of GGC to solve real world problems for gardeners. Example use cases include:'}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"The gardener can plan/manage their garden more easily"}),"\n",(0,i.jsx)(t.li,{children:"The gardener makes better choices for their seeds and/or management of plants"}),"\n",(0,i.jsx)(t.li,{children:"The garden produces more food"}),"\n",(0,i.jsx)(t.li,{children:"The garden/gardener incorporates new best practices due to the app"}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"The more empirical this data, the better. Much of this will require some information about what the garden/gardener was doing before the introduction of GGC."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Analysis of "pre" and "post" questionnaires. The pre questionnaires establish what each gardener was doing prior to GGC: what their technologies were, what their pain points were, etc. The post questionnaires establish their feelings after using GGC: what worked, what didn\'t work, what they wish would be changed about the app.'}),"\n",(0,i.jsx)(t.li,{children:"Documentation of use cases."}),"\n",(0,i.jsx)(t.li,{children:"Evaluation by entreprenurial board members and/or other external stakeholders."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"5-determine-how-to-usefully-integrate-ai",children:"5. Determine how to usefully integrate AI"}),"\n",(0,i.jsx)(t.p,{children:"AI technologies like ChatGPT impact the viability of our business model. We must be able to show users why they should pay for our app instead of using a generic AI. One way to answer this question is to provide access to AI capabilities within the app, and provide concrete use cases of how using AI within the app is better than using AI without the app."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Test results of AI within app vs. AI external to app on basic gardening scenarios."}),"\n"]})]})}function c(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>r,a:()=>a});var i=o(7294);const n={},s=i.createContext(n);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/505d7517.e701aaa5.js b/assets/js/505d7517.e701aaa5.js deleted file mode 100644 index 0c355c67d..000000000 --- a/assets/js/505d7517.e701aaa5.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[196],{4230:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>a,metadata:()=>r,toc:()=>h});var i=o(5893),n=o(1151);const a={hide_table_of_contents:!1},s="Technology Goals",r={id:"develop/releases/release-1.0/goals",title:"Technology Goals",description:"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.",source:"@site/docs/develop/releases/release-1.0/goals.md",sourceDirName:"develop/releases/release-1.0",slug:"/develop/releases/release-1.0/goals",permalink:"/docs/develop/releases/release-1.0/goals",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Data Mutation",permalink:"/docs/develop/design/data-mutation"},next:{title:"Core Value Propositions",permalink:"/docs/develop/releases/release-1.0/cvp"}},l={},h=[{value:"1. Provide a fast, reliable, robust app that satisfies the Core Value Propositions",id:"1-provide-a-fast-reliable-robust-app-that-satisfies-the-core-value-propositions",level:2},{value:"2. Determine what it means to be a champion/chapter chair",id:"2-determine-what-it-means-to-be-a-championchapter-chair",level:2},{value:"3. Determine how to provide high quality documentation",id:"3-determine-how-to-provide-high-quality-documentation",level:2},{value:"4. Gather evidence-based "testimonials"",id:"4-gather-evidence-based-testimonials",level:2},{value:"5. Determine how to usefully integrate AI",id:"5-determine-how-to-usefully-integrate-ai",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",ul:"ul",...(0,n.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"technology-goals",children:"Technology Goals"})}),"\n",(0,i.jsxs)(t.p,{children:["Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by ",(0,i.jsx)(t.a,{href:"https://www.gitpod.io/blog/champion-building",children:"Champion Building: How to successfully adopt a developer tool"}),". Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC."]}),"\n",(0,i.jsx)(t.h2,{id:"1-provide-a-fast-reliable-robust-app-that-satisfies-the-core-value-propositions",children:"1. Provide a fast, reliable, robust app that satisfies the Core Value Propositions"}),"\n",(0,i.jsxs)(t.p,{children:["By the end of the 1.0 release period, we need to have an app that satisfies the ",(0,i.jsx)(t.a,{href:"/docs/develop/releases/release-1.0/cvp",children:"Core Value Propositions"})," while being fast, easy to use, and not prone to crashing."]}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Checklist of CVP functions satisfied."}),"\n",(0,i.jsx)(t.li,{children:"Time to retrieve each page in app using DevTools Performance View"}),"\n",(0,i.jsx)(t.li,{children:"Crashlytics data"}),"\n",(0,i.jsx)(t.li,{children:"Usability Evaluation of 1.0 user base with respect to app usability."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"2-determine-what-it-means-to-be-a-championchapter-chair",children:"2. Determine what it means to be a champion/chapter chair"}),"\n",(0,i.jsx)(t.p,{children:"Successful chapters will need one or more champions. We hope to use the 1.0 release period to develop an understanding of what it means to be champion: what their responsibilities are, how to carry out their responsibilities, and how the GGC organization should support and/or compensate them."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation criteria:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"User-facing documentation for Chapter Chairs."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"3-determine-how-to-provide-high-quality-documentation",children:"3. Determine how to provide high quality documentation"}),"\n",(0,i.jsx)(t.p,{children:"We hope to use the 1.0 release to determine what needs to be documented, and how it should be documented. Some possible documentation sources include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"In-app documentation (the ? icon)"}),"\n",(0,i.jsx)(t.li,{children:"YouTube (shorts and/or regular videos)"}),"\n",(0,i.jsx)(t.li,{children:"User Guide (within geogardenclub.com)"}),"\n",(0,i.jsx)(t.li,{children:'GGC Discord Server: channels visible to those in the "User" role.'}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Evaluation criteria:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Usability evaluation of 1.0 user base with respect to app documentation."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"4-gather-evidence-based-testimonials",children:'4. Gather evidence-based "testimonials"'}),"\n",(0,i.jsx)(t.p,{children:'We hope that the 1.0 release period will result in examples of "successful" use of GGC to solve real world problems for gardeners. Example use cases include:'}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"The gardener can plan/manage their garden more easily"}),"\n",(0,i.jsx)(t.li,{children:"The gardener makes better choices for their seeds and/or management of plants"}),"\n",(0,i.jsx)(t.li,{children:"The garden produces more food"}),"\n",(0,i.jsx)(t.li,{children:"The garden/gardener incorporates new best practices due to the app"}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"The more empirical this data, the better. Much of this will require some information about what the garden/gardener was doing before the introduction of GGC."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:'Analysis of "pre" and "post" questionnaires. The pre questionnaires establish what each gardener was doing prior to GGC: what their technologies were, what their pain points were, etc. The post questionnaires establish their feelings after using GGC: what worked, what didn\'t work, what they wish would be changed about the app.'}),"\n",(0,i.jsx)(t.li,{children:"Documentation of use cases."}),"\n",(0,i.jsx)(t.li,{children:"Evaluation by entreprenurial board members and/or other external stakeholders."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"5-determine-how-to-usefully-integrate-ai",children:"5. Determine how to usefully integrate AI"}),"\n",(0,i.jsx)(t.p,{children:"AI technologies like ChatGPT impact the viability of our business model. We must be able to show users why they should pay for our app instead of using a generic AI. One way to answer this question is to provide access to AI capabilities within the app, and provide concrete use cases of how using AI within the app is better than using AI without the app."}),"\n",(0,i.jsx)(t.p,{children:"Evaluation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Test results of AI within app vs. AI external to app on basic gardening scenarios."}),"\n"]})]})}function c(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>r,a:()=>s});var i=o(7294);const n={},a=i.createContext(n);function s(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:s(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6b1fc3de.240ddb3d.js b/assets/js/6b1fc3de.240ddb3d.js deleted file mode 100644 index b6a94139f..000000000 --- a/assets/js/6b1fc3de.240ddb3d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8754],{8312:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var o=n(5893),i=n(1151);const a={hide_table_of_contents:!1},r="Installation",s={id:"develop/installation",title:"Installation",description:"Flutter",source:"@site/docs/develop/installation.md",sourceDirName:"develop",slug:"/develop/installation",permalink:"/docs/develop/installation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Onboarding",permalink:"/docs/develop/onboarding"},next:{title:"Scripts",permalink:"/docs/develop/scripts"}},l={},c=[{value:"Flutter",id:"flutter",level:2},{value:"Tool versions",id:"tool-versions",level:2},{value:"GGC App",id:"ggc-app",level:2},{value:"Integration tests",id:"integration-tests",level:2},{value:"Editor",id:"editor",level:2},{value:"Monarch",id:"monarch",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.header,{children:(0,o.jsx)(t.h1,{id:"installation",children:"Installation"})}),"\n",(0,o.jsx)(t.h2,{id:"flutter",children:"Flutter"}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://docs.flutter.dev/get-started/install",children:"Flutter Installation"})," instructions."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/flutterpalooza/",children:"Flutterpalooza"})," module has some additional documentation."]}),"\n",(0,o.jsxs)(t.p,{children:["It is important that you are able to run ",(0,o.jsx)(t.code,{children:"flutter doctor"})," without error:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% flutter doctor\nDoctor summary (to see all details, run flutter doctor -v):\n[\u2713] Flutter (Channel stable, 3.10.0, on macOS 13.3.1 22E772610a darwin-arm64, locale en-US)\n[\u2713] Android toolchain - develop for Android devices (Android SDK version 33.0.1)\n[\u2713] Xcode - develop for iOS and macOS (Xcode 14.3)\n[\u2713] Chrome - develop for the web\n[\u2713] Android Studio (version 2021.3)\n[\u2713] IntelliJ IDEA Ultimate Edition (version 2023.1)\n[\u2713] Connected device (3 available)\n[\u2713] Network resources\n\n\u2022 No issues found!\n"})}),"\n",(0,o.jsx)(t.h2,{id:"tool-versions",children:"Tool versions"}),"\n",(0,o.jsx)(t.p,{children:"It turns out that getting Flutter Doctor to report no issues is not enough. There are other tech stack components which must also be at an appropriate version in order for the app to run successfully during development. In some cases, there might be multiple appropriate versions, but every developer must be using the same version of the tools; otherwise the app will run for some developers but not for others."}),"\n",(0,o.jsxs)(t.p,{children:["In order to help developers keep on the same page with respect to tech stack versions, we have implemented a script called ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"}),' that prints versions of the tech stack tools important to getting GGC to run correctly. Our Discord server has a channel called "#tool-versions" where developers post the output from running this script. This helps all of us to stay on the same page, and when one person updates a component of the tech stack, they can post the new output from the script so that everyone else can update their tech stack as well.']}),"\n",(0,o.jsxs)(t.p,{children:["Here is an example of the output from ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% ./run_tool_versions.sh\nComputer Name: PMJ M2 2023\nCocoapods 1.15.2\nDart SDK version: 3.5.0 \nFlutter 3.24.0 \nMacOS 14.5\nMonarch version 2.2.7\nruby 3.2.2 (202\nXcode 15.4\n"})}),"\n",(0,o.jsx)(t.p,{children:"Be sure to run this script and check it against the output from the Discord channel. If there are differences, try to update your tech stack to match the versions in the Discord channel."}),"\n",(0,o.jsx)(t.p,{children:"Note that GGC development is done using MacOS. We do not support Windows or Unix-based development at this time."}),"\n",(0,o.jsx)(t.h2,{id:"ggc-app",children:"GGC App"}),"\n",(0,o.jsxs)(t.p,{children:["To install the app, first clone the sources from ",(0,o.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app",children:"https://github.com/geogardenclub/ggc_app"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["Next, cd into the ggc_app directory and run ",(0,o.jsx)(t.code,{children:"flutter pub get"}),". For example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter pub get\nRunning "flutter pub get" in ggc_app...\nResolving dependencies... (1.4s)\n _fe_analyzer_shared 58.0.0 (59.0.0 available)\n analyzer 5.10.0 (5.11.1 available)\n async 2.10.0 (2.11.0 available)\n build_daemon 3.1.1 (4.0.0 available)\n build_runner 2.3.3 (2.4.1 available)\n characters 1.2.1 (1.3.0 available)\n collection 1.17.0 (1.17.1 available)\n flex_color_scheme 7.0.3 (7.0.4 available)\n flutter_form_builder 7.8.0 (8.0.0 available)\n flutter_riverpod 2.3.5 (2.3.6 available)\n flutter_svg 1.1.6 (2.0.5 available)\n go_router 6.5.7 (6.5.8 available)\n intl 0.17.0 (0.18.1 available)\n js 0.6.5 (0.6.7 available)\n matcher 0.12.13 (0.12.15 available)\n material_color_utilities 0.2.0 (0.3.0 available)\n meta 1.8.0 (1.9.1 available)\n monarch 3.0.1 (3.4.0 available)\n path 1.8.2 (1.8.3 available)\n path_provider_windows 2.1.5 (2.1.6 available)\n petitparser 5.1.0 (5.4.0 available)\n riverpod 2.3.5 (2.3.6 available)\n source_span 1.9.1 (1.10.0 available)\n sqflite 2.2.6 (2.2.7 available)\n sqflite_common 2.4.3 (2.4.4 available)\n synchronized 3.0.1 (3.1.0 available)\n test_api 0.4.16 (0.5.2 available)\n vm_service 11.3.0 (11.4.0 available)\n win32 3.1.4 (4.1.3 available)\n xml 6.2.2 (6.3.0 available)\nGot dependencies!\n'})}),"\n",(0,o.jsxs)(t.p,{children:["Next, to check that the ggc_app actually runs in your environment, the simplest thing to do is to invoke ",(0,o.jsx)(t.code,{children:"flutter run"})," and select Chrome:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter run\nMultiple devices found:\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 13.3.1 22E261 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 112.0.5615.137\n[1]: macOS (macos)\n[2]: Chrome (chrome)\nPlease choose one (To quit, press "q/Q"): 2\nLaunching lib/main.dart on Chrome in debug mode...\nWaiting for connection from debug service on Chrome... 16.5s\nThis app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\nDebug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\n\n\ud83d\udcaa Running with sound null safety \ud83d\udcaa\n\n\ud83d\udd25 To hot restart changes while running, press "r" or "R".\nFor a more detailed help message, press "h". To quit, press "q".\n\nAn Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=\nWARNING: found an existing tag. Flutter Web uses its own viewport configuration for better compatibility with\nFlutter. This tag will be replaced.\nThe Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=\n\n'})}),"\n",(0,o.jsx)(t.p,{children:"If all goes well, you should see a window similar to the following appear:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-ggc-chrome.png"}),"\n",(0,o.jsx)(t.p,{children:"At this point, you can login as one of the existing users to make sure communication with Firebase is working correctly. Contact Philip for credentials."}),"\n",(0,o.jsx)(t.h2,{id:"integration-tests",children:"Integration tests"}),"\n",(0,o.jsxs)(t.p,{children:["The next step is to ensure that you can run the integration tests. Please follow the instructions on the ",(0,o.jsx)(t.a,{href:"/docs/develop/testing",children:"Testing"})," page. If you cannot run the tests without encountering a test failure, please contact a developer for assistance."]}),"\n",(0,o.jsx)(t.h2,{id:"editor",children:"Editor"}),"\n",(0,o.jsx)(t.p,{children:"There are three good choices for your Editor: Visual Studio, Android Studio, or IntelliJ IDEA Ultimate (with the Dart and Flutter plugins, which makes it almost equivalent to Android Studio)."}),"\n",(0,o.jsx)(t.p,{children:"With IntelliJ IDEA Ultimate, after bringing up the project, you should see a run toolbar at the top which gives you (on a Mac) the option of opening the iOS simulator:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-open-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"After opening the simulator, it should appear and you should be able to emulate the system on an iOS device:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"It takes a couple of minutes to do all of the XCode shenanigans the first time you run it, but eventually you should see something like the following:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios-2.png"}),"\n",(0,o.jsx)(t.p,{children:"As before, consult with Philip for login credentials."}),"\n",(0,o.jsx)(t.h2,{id:"monarch",children:"Monarch"}),"\n",(0,o.jsxs)(t.p,{children:["According to their home page, ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/",children:"Monarch"}),' is a "tool for building Flutter widgets in isolation. It makes it easy to build, test and debug complex UIs." Monarch is basically a Flutter port of React Storybook, which is tremendously popular in React UI development.']}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/docs/install",children:"Monarch installation instructions"})," to install the tool."]}),"\n",(0,o.jsxs)(t.p,{children:["Then, invoke ",(0,o.jsx)(t.code,{children:"monarch run --reload hot-restart"})," (or, for less typing, the ",(0,o.jsx)(t.code,{children:"./run_monarch.sh"})," shell script)."]}),"\n",(0,o.jsx)(t.p,{children:"You will see the Monarch UI appear, which enables you to view all of the GGC UI elements individually, and (where useful) in different states:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/monarch.png"}),"\n",(0,o.jsx)(t.p,{children:'Note that you need to manually select our theme (currently, "Green Theme: Light"). Monarch defaults to the Material Light theme when it is first invoked.'}),"\n",(0,o.jsx)(t.p,{children:'For the design and development of basic UI elements, Monarch appears to be faster, easier, and more efficient than running the iOS simulator. Creating Monarch stories also creates an easy to browse "catalog" of UI elements which are far easier to review than paging through the emulated system to get to the correct state.'})]})}function d(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(7294);const i={},a=o.createContext(i);function r(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6b1fc3de.f192a887.js b/assets/js/6b1fc3de.f192a887.js new file mode 100644 index 000000000..48d66d6e9 --- /dev/null +++ b/assets/js/6b1fc3de.f192a887.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8754],{8312:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var o=n(5893),i=n(1151);const a={hide_table_of_contents:!1},r="Installation",s={id:"develop/installation",title:"Installation",description:"Flutter",source:"@site/docs/develop/installation.md",sourceDirName:"develop",slug:"/develop/installation",permalink:"/docs/develop/installation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Onboarding",permalink:"/docs/develop/onboarding"},next:{title:"Scripts",permalink:"/docs/develop/scripts"}},l={},c=[{value:"Flutter",id:"flutter",level:2},{value:"Tool versions",id:"tool-versions",level:2},{value:"GGC App",id:"ggc-app",level:2},{value:"Integration tests",id:"integration-tests",level:2},{value:"Editor",id:"editor",level:2},{value:"Monarch",id:"monarch",level:2}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.header,{children:(0,o.jsx)(t.h1,{id:"installation",children:"Installation"})}),"\n",(0,o.jsx)(t.h2,{id:"flutter",children:"Flutter"}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://docs.flutter.dev/get-started/install",children:"Flutter Installation"})," instructions."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/flutterpalooza/",children:"Flutterpalooza"})," module has some additional documentation."]}),"\n",(0,o.jsxs)(t.p,{children:["It is important that you are able to run ",(0,o.jsx)(t.code,{children:"flutter doctor"})," without error:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% flutter doctor\nDoctor summary (to see all details, run flutter doctor -v):\n[\u2713] Flutter (Channel stable, 3.10.0, on macOS 13.3.1 22E772610a darwin-arm64, locale en-US)\n[\u2713] Android toolchain - develop for Android devices (Android SDK version 33.0.1)\n[\u2713] Xcode - develop for iOS and macOS (Xcode 14.3)\n[\u2713] Chrome - develop for the web\n[\u2713] Android Studio (version 2021.3)\n[\u2713] IntelliJ IDEA Ultimate Edition (version 2023.1)\n[\u2713] Connected device (3 available)\n[\u2713] Network resources\n\n\u2022 No issues found!\n"})}),"\n",(0,o.jsx)(t.h2,{id:"tool-versions",children:"Tool versions"}),"\n",(0,o.jsx)(t.p,{children:"It turns out that getting Flutter Doctor to report no issues is not enough. There are other tech stack components which must also be at an appropriate version in order for the app to run successfully during development. In some cases, there might be multiple possible versions, but every developer must be using the same version of the tools; otherwise the app will run for some developers but not for others."}),"\n",(0,o.jsxs)(t.p,{children:["In order to help developers keep on the same page with respect to tech stack versions, we have implemented a script called ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"})," that prints versions of the tech stack tools important to getting GGC to run correctly. Our Discord server has a channel called ",(0,o.jsx)(t.code,{children:"#tool-versions"})," where developers post the output from running this script. This helps all of us to stay on the same page, and when one person updates a component of the tech stack, they can post the new output from the script so that everyone else can update their tech stack as well."]}),"\n",(0,o.jsxs)(t.p,{children:["Here is an example of the output from ",(0,o.jsx)(t.code,{children:"run_tool_versions.sh"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-shell",children:"% ./run_tool_versions.sh\nComputer Name: PMJ M2 2023\nCocoapods 1.15.2\nDart SDK version: 3.5.4 \nFlutter 3.24.4 \nMacOS 14.6.1\nMonarch version 2.2.7\nruby 3.2.2 (202\nXcode 16.0\n"})}),"\n",(0,o.jsx)(t.p,{children:"Be sure to run this script locally and check it against the output from the Discord channel. If there are differences, try to update your tech stack to match the versions in the Discord channel."}),"\n",(0,o.jsx)(t.p,{children:"Note that all GGC development is done using macOS. We do not support Windows or Unix-based development at this time."}),"\n",(0,o.jsx)(t.h2,{id:"ggc-app",children:"GGC App"}),"\n",(0,o.jsxs)(t.p,{children:["To install the app, first clone the sources from ",(0,o.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app",children:"https://github.com/geogardenclub/ggc_app"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["Next, cd into the ggc_app directory and run ",(0,o.jsx)(t.code,{children:"flutter pub get"}),". For example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter pub get\nRunning "flutter pub get" in ggc_app...\nResolving dependencies... (1.4s)\n _fe_analyzer_shared 58.0.0 (59.0.0 available)\n analyzer 5.10.0 (5.11.1 available)\n async 2.10.0 (2.11.0 available)\n build_daemon 3.1.1 (4.0.0 available)\n build_runner 2.3.3 (2.4.1 available)\n characters 1.2.1 (1.3.0 available)\n collection 1.17.0 (1.17.1 available)\n flex_color_scheme 7.0.3 (7.0.4 available)\n flutter_form_builder 7.8.0 (8.0.0 available)\n flutter_riverpod 2.3.5 (2.3.6 available)\n flutter_svg 1.1.6 (2.0.5 available)\n go_router 6.5.7 (6.5.8 available)\n intl 0.17.0 (0.18.1 available)\n js 0.6.5 (0.6.7 available)\n matcher 0.12.13 (0.12.15 available)\n material_color_utilities 0.2.0 (0.3.0 available)\n meta 1.8.0 (1.9.1 available)\n monarch 3.0.1 (3.4.0 available)\n path 1.8.2 (1.8.3 available)\n path_provider_windows 2.1.5 (2.1.6 available)\n petitparser 5.1.0 (5.4.0 available)\n riverpod 2.3.5 (2.3.6 available)\n source_span 1.9.1 (1.10.0 available)\n sqflite 2.2.6 (2.2.7 available)\n sqflite_common 2.4.3 (2.4.4 available)\n synchronized 3.0.1 (3.1.0 available)\n test_api 0.4.16 (0.5.2 available)\n vm_service 11.3.0 (11.4.0 available)\n win32 3.1.4 (4.1.3 available)\n xml 6.2.2 (6.3.0 available)\nGot dependencies!\n'})}),"\n",(0,o.jsxs)(t.p,{children:["Next, to check that the ggc_app actually runs in your environment, the simplest thing to do is to invoke ",(0,o.jsx)(t.code,{children:"flutter run"})," and select Chrome:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:'% flutter run\nMultiple devices found:\nmacOS (desktop) \u2022 macos \u2022 darwin-arm64 \u2022 macOS 13.3.1 22E261 darwin-arm64\nChrome (web) \u2022 chrome \u2022 web-javascript \u2022 Google Chrome 112.0.5615.137\n[1]: macOS (macos)\n[2]: Chrome (chrome)\nPlease choose one (To quit, press "q/Q"): 2\nLaunching lib/main.dart on Chrome in debug mode...\nWaiting for connection from debug service on Chrome... 16.5s\nThis app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\nDebug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws\n\n\ud83d\udcaa Running with sound null safety \ud83d\udcaa\n\n\ud83d\udd25 To hot restart changes while running, press "r" or "R".\nFor a more detailed help message, press "h". To quit, press "q".\n\nAn Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=\nWARNING: found an existing tag. Flutter Web uses its own viewport configuration for better compatibility with\nFlutter. This tag will be replaced.\nThe Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=\n\n'})}),"\n",(0,o.jsx)(t.p,{children:"If all goes well, you should see a window similar to the following appear:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-ggc-chrome.png"}),"\n",(0,o.jsx)(t.p,{children:"At this point, you can login as one of the existing users to make sure communication with Firebase is working correctly. Contact Philip for credentials."}),"\n",(0,o.jsx)(t.h2,{id:"integration-tests",children:"Integration tests"}),"\n",(0,o.jsxs)(t.p,{children:["The next step is to ensure that you can run the integration tests. Please follow the instructions on the ",(0,o.jsx)(t.a,{href:"/docs/develop/testing",children:"Testing"})," page. If you cannot run the tests without encountering a test failure, please contact a developer for assistance."]}),"\n",(0,o.jsx)(t.h2,{id:"editor",children:"Editor"}),"\n",(0,o.jsx)(t.p,{children:"There are three good choices for your Editor: Visual Studio, Android Studio, or IntelliJ IDEA Ultimate (with the Dart and Flutter plugins, which makes it almost equivalent to Android Studio)."}),"\n",(0,o.jsx)(t.p,{children:"With IntelliJ IDEA Ultimate, after bringing up the project, you should see a run toolbar at the top which gives you (on a Mac) the option of opening the iOS simulator:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-open-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"After opening the simulator, it should appear and you should be able to emulate the system on an iOS device:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios.png"}),"\n",(0,o.jsx)(t.p,{children:"It takes a couple of minutes to do all of the XCode shenanigans the first time you run it, but eventually you should see something like the following:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/installation-run-ios-2.png"}),"\n",(0,o.jsx)(t.p,{children:"As before, consult with Philip for login credentials."}),"\n",(0,o.jsx)(t.h2,{id:"monarch",children:"Monarch"}),"\n",(0,o.jsxs)(t.p,{children:["According to their home page, ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/",children:"Monarch"}),' is a "tool for building Flutter widgets in isolation. It makes it easy to build, test and debug complex UIs." Monarch is basically a Flutter port of React Storybook, which is tremendously popular in React UI development.']}),"\n",(0,o.jsxs)(t.p,{children:["Follow the ",(0,o.jsx)(t.a,{href:"https://monarchapp.io/docs/install",children:"Monarch installation instructions"})," to install the tool."]}),"\n",(0,o.jsxs)(t.p,{children:["Then, invoke ",(0,o.jsx)(t.code,{children:"monarch run --reload hot-restart"})," (or, for less typing, the ",(0,o.jsx)(t.code,{children:"./run_monarch.sh"})," shell script)."]}),"\n",(0,o.jsx)(t.p,{children:"You will see the Monarch UI appear, which enables you to view all of the GGC UI elements individually, and (where useful) in different states:"}),"\n",(0,o.jsx)("img",{src:"/img/develop/getting-started/monarch.png"}),"\n",(0,o.jsx)(t.p,{children:'Note that you need to manually select our theme (currently, "Green Theme: Light"). Monarch defaults to the Material Light theme when it is first invoked.'}),"\n",(0,o.jsx)(t.p,{children:'For the design and development of basic UI elements, Monarch appears to be faster, easier, and more efficient than running the iOS simulator. Creating Monarch stories also creates an easy to browse "catalog" of UI elements which are far easier to review than paging through the emulated system to get to the correct state.'})]})}function d(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(7294);const i={},a=o.createContext(i);function r(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d1225b6.82c0a513.js b/assets/js/7d1225b6.82c0a513.js deleted file mode 100644 index d0fd206de..000000000 --- a/assets/js/7d1225b6.82c0a513.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4076],{7576:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>l,default:()=>p,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var n=o(5893),a=o(1151);const s={hide_table_of_contents:!0},l="Backups",i={id:"develop/backups",title:"Backups",description:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work.",source:"@site/docs/develop/backups.md",sourceDirName:"develop",slug:"/develop/backups",permalink:"/docs/develop/backups",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Testing",permalink:"/docs/develop/testing"},next:{title:"Data Model",permalink:"/docs/develop/design/data-model"}},r={},c=[{value:"1. Download and install Firefoo",id:"1-download-and-install-firefoo",level:2},{value:"2. Export a JSON file containing all collections",id:"2-export-a-json-file-containing-all-collections",level:2},{value:"3. Rename, compress, and upload the backup",id:"3-rename-compress-and-upload-the-backup",level:2},{value:"4. Restoring from backup",id:"4-restoring-from-backup",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"backups",children:"Backups"})}),"\n",(0,n.jsx)(t.p,{children:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work."}),"\n",(0,n.jsx)(t.p,{children:"Here are the steps."}),"\n",(0,n.jsx)(t.h2,{id:"1-download-and-install-firefoo",children:"1. Download and install Firefoo"}),"\n",(0,n.jsxs)(t.p,{children:["Firefoo is located ",(0,n.jsx)(t.a,{href:"https://www.firefoo.app/",children:"here"}),"."]}),"\n",(0,n.jsx)(t.p,{children:"Once installed, you need to load the GGC Firestore database. When successful, the Firefoo screen should look something like this:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-0.png"}),"\n",(0,n.jsx)(t.h2,{id:"2-export-a-json-file-containing-all-collections",children:"2. Export a JSON file containing all collections"}),"\n",(0,n.jsx)(t.p,{children:'Right click on "ggc-app" in the left side-bar, and then select "Export All Collections..."'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-1.png"}),"\n",(0,n.jsx)(t.p,{children:'Select "Newline-delimited JSON" as the format, then click "Export" to export a file containing a separate line for each document in the database.'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-2.png"}),"\n",(0,n.jsx)(t.p,{children:'This will create a file named something like "ggc-app-2de7b-1711139889.jsonl".'}),"\n",(0,n.jsx)(t.h2,{id:"3-rename-compress-and-upload-the-backup",children:"3. Rename, compress, and upload the backup"}),"\n",(0,n.jsx)(t.p,{children:'Next, rename the file with the format "ggc-backup-YYYY-MM-DD.jsonl" so that the file name contains the date that the backup was made.'}),"\n",(0,n.jsx)(t.p,{children:'Next, compress the file. The compressed file will have the name "ggc-backup-YYYY-MM-DD.jsonl.zip" and will be much, much smaller, typically 10% or less. For example, as of May 2024, the uncompressed file was around 1 MB, and the compressed file was 87 KB.'}),"\n",(0,n.jsx)(t.p,{children:'Finally, upload the file to the geogardenclub "backups" repo. When done, the repo will look something like this:'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-3.png"}),"\n",(0,n.jsx)(t.h2,{id:"4-restoring-from-backup",children:"4. Restoring from backup"}),"\n",(0,n.jsx)(t.p,{children:"This should involve the following steps:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:"Download the latest zip file from the backups repository and uncompress."}),"\n",(0,n.jsx)(t.li,{children:'Right-click on the ggc-app and select "Import collections".'}),"\n",(0,n.jsx)(t.li,{children:'Select the backup file and click "Import".'}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"I have never done this, so I cannot verify that this will work."})]})}function p(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>i,a:()=>l});var n=o(7294);const a={},s=n.createContext(a);function l(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:l(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d1225b6.a1eabaf6.js b/assets/js/7d1225b6.a1eabaf6.js new file mode 100644 index 000000000..ce9c209c7 --- /dev/null +++ b/assets/js/7d1225b6.a1eabaf6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4076],{7576:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>s,default:()=>p,frontMatter:()=>l,metadata:()=>i,toc:()=>c});var n=o(5893),a=o(1151);const l={hide_table_of_contents:!0},s="Backups",i={id:"develop/backups",title:"Backups",description:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work.",source:"@site/docs/develop/backups.md",sourceDirName:"develop",slug:"/develop/backups",permalink:"/docs/develop/backups",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Data Model",permalink:"/docs/develop/design/data-model"}},r={},c=[{value:"1. Download and install Firefoo",id:"1-download-and-install-firefoo",level:2},{value:"2. Export a JSON file containing all collections",id:"2-export-a-json-file-containing-all-collections",level:2},{value:"3. Rename, compress, and upload the backup",id:"3-rename-compress-and-upload-the-backup",level:2},{value:"4. Restoring from backup",id:"4-restoring-from-backup",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"backups",children:"Backups"})}),"\n",(0,n.jsx)(t.p,{children:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work."}),"\n",(0,n.jsx)(t.p,{children:"Here are the steps."}),"\n",(0,n.jsx)(t.h2,{id:"1-download-and-install-firefoo",children:"1. Download and install Firefoo"}),"\n",(0,n.jsxs)(t.p,{children:["Firefoo is located ",(0,n.jsx)(t.a,{href:"https://www.firefoo.app/",children:"here"}),"."]}),"\n",(0,n.jsx)(t.p,{children:"Once installed, you need to load the GGC Firestore database. When successful, the Firefoo screen should look something like this:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-0.png"}),"\n",(0,n.jsx)(t.h2,{id:"2-export-a-json-file-containing-all-collections",children:"2. Export a JSON file containing all collections"}),"\n",(0,n.jsx)(t.p,{children:'Right click on "ggc-app" in the left side-bar, and then select "Export All Collections..."'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-1.png"}),"\n",(0,n.jsx)(t.p,{children:'Select "Newline-delimited JSON" as the format, then click "Export" to export a file containing a separate line for each document in the database.'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-2.png"}),"\n",(0,n.jsx)(t.p,{children:'This will create a file named something like "ggc-app-2de7b-1711139889.jsonl".'}),"\n",(0,n.jsx)(t.h2,{id:"3-rename-compress-and-upload-the-backup",children:"3. Rename, compress, and upload the backup"}),"\n",(0,n.jsx)(t.p,{children:'Next, rename the file with the format "ggc-backup-YYYY-MM-DD.jsonl" so that the file name contains the date that the backup was made.'}),"\n",(0,n.jsx)(t.p,{children:'Next, compress the file. The compressed file will have the name "ggc-backup-YYYY-MM-DD.jsonl.zip" and will be much, much smaller, typically 10% or less. For example, as of May 2024, the uncompressed file was around 1 MB, and the compressed file was 87 KB.'}),"\n",(0,n.jsx)(t.p,{children:'Finally, upload the file to the geogardenclub "backups" repo. When done, the repo will look something like this:'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-3.png"}),"\n",(0,n.jsx)(t.h2,{id:"4-restoring-from-backup",children:"4. Restoring from backup"}),"\n",(0,n.jsx)(t.p,{children:"This should involve the following steps:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:"Download the latest zip file from the backups repository and uncompress."}),"\n",(0,n.jsx)(t.li,{children:'Right-click on the ggc-app and select "Import collections".'}),"\n",(0,n.jsx)(t.li,{children:'Select the backup file and click "Import".'}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"I have never done this, so I cannot verify that this will work."})]})}function p(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>i,a:()=>s});var n=o(7294);const a={},l=n.createContext(a);function s(e){const t=n.useContext(l);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),n.createElement(l.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d56ced7.3bfef52c.js b/assets/js/7d56ced7.3bfef52c.js deleted file mode 100644 index aed5fc4d9..000000000 --- a/assets/js/7d56ced7.3bfef52c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[9572],{616:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>a,contentTitle:()=>d,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>l});var r=s(5893),n=s(1151);const i={hide_table_of_contents:!1},d="Scripts",o={id:"develop/scripts",title:"Scripts",description:'GGC development is supported by a number of Unix shell scripts. All scripts are named starting with "run" and use snake case to separate words in the script name.',source:"@site/docs/develop/scripts.md",sourceDirName:"develop",slug:"/develop/scripts",permalink:"/docs/develop/scripts",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Installation",permalink:"/docs/develop/installation"},next:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"}},a={},l=[];function c(e){const t={code:"code",h1:"h1",header:"header",p:"p",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,n.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"scripts",children:"Scripts"})}),"\n",(0,r.jsx)(t.p,{children:'GGC development is supported by a number of Unix shell scripts. All scripts are named starting with "run" and use snake case to separate words in the script name.'}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Name"}),(0,r.jsx)(t.th,{children:"Action"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_build_runner.sh"}),(0,r.jsx)(t.td,{children:"Invokes the builder running code generation facility, needed for entities using Freezed, JSON Serializable, and Riverpod."})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_deploy.sh"}),(0,r.jsx)(t.td,{children:"Bumps version number, updates release notes, builds the .ipa and .apk files, and deploys the web version."})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_flutter_clean.sh"}),(0,r.jsxs)(t.td,{children:["After upgrading Flutter or pub.dev packages, the build might fail either during the ",(0,r.jsx)(t.code,{children:"pod install"})," or ",(0,r.jsx)(t.code,{children:"xcode build"})," steps. When this happens, google searches based on the error message often provide bad advice for Flutter developers. Try this script instead."]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_flutter_downgrade.sh"}),(0,r.jsx)(t.td,{children:"In the event that a new release breaks things, you can use this script to back out to the previous release of flutter."})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_monarch.sh"}),(0,r.jsx)(t.td,{children:"A standard way to invoke the Monarch UI display system."})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_pub_add.sh"}),(0,r.jsx)(t.td,{children:"If you are rebuilding the ggc_app with a fresh install of Flutter, this script installs all of the packages in one command. Before running it, make sure the list of packages is up to date!"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_rebuild_podfile.sh"}),(0,r.jsx)(t.td,{children:"If the build fails due to a podfile problem, this script rebuilds it appropriately."})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:"run_tool_versions.sh"}),(0,r.jsx)(t.td,{children:"Prints out the names and versions of important tech stack components. Useful when trying to diagnose why the system builds and runs for one developer but produces errors for another."})]})]})]})]})}function h(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},1151:(e,t,s)=>{s.d(t,{Z:()=>o,a:()=>d});var r=s(7294);const n={},i=r.createContext(n);function d(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:d(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d56ced7.e01aba21.js b/assets/js/7d56ced7.e01aba21.js new file mode 100644 index 000000000..2231c91a1 --- /dev/null +++ b/assets/js/7d56ced7.e01aba21.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[9572],{616:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>a,contentTitle:()=>d,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>l});var n=s(5893),r=s(1151);const i={hide_table_of_contents:!1},d="Scripts",o={id:"develop/scripts",title:"Scripts",description:'GGC development is supported by a number of Unix shell scripts. All scripts are named starting with "run" and use snake case to separate words in the script name.',source:"@site/docs/develop/scripts.md",sourceDirName:"develop",slug:"/develop/scripts",permalink:"/docs/develop/scripts",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Installation",permalink:"/docs/develop/installation"},next:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"}},a={},l=[];function c(e){const t={code:"code",h1:"h1",header:"header",p:"p",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"scripts",children:"Scripts"})}),"\n",(0,n.jsx)(t.p,{children:'GGC development is supported by a number of Unix shell scripts. All scripts are named starting with "run" and use snake case to separate words in the script name.'}),"\n",(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{children:"Name"}),(0,n.jsx)(t.th,{children:"Action"})]})}),(0,n.jsxs)(t.tbody,{children:[(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_build_runner.sh"}),(0,n.jsx)(t.td,{children:"Invokes the builder running code generation facility, needed for entities using Freezed, JSON Serializable, and Riverpod."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_deploy.sh"}),(0,n.jsx)(t.td,{children:"Bumps version number, updates release notes, builds the .ipa and .apk files, and deploys the web version."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_flutter_clean.sh"}),(0,n.jsxs)(t.td,{children:["After upgrading Flutter or pub.dev packages, the build might fail either during the ",(0,n.jsx)(t.code,{children:"pod install"})," or ",(0,n.jsx)(t.code,{children:"xcode build"})," steps. When this happens, google searches based on the error message often provide bad advice for Flutter developers. Try this script instead."]})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_flutter_downgrade.sh"}),(0,n.jsx)(t.td,{children:"In the event that a new release breaks things, you can use this script to back out to the previous release of flutter."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_lakos.sh"}),(0,n.jsx)(t.td,{children:"Generates a visualization of the dependency graph."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_monarch.sh"}),(0,n.jsx)(t.td,{children:"A standard way to invoke the Monarch UI display system."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_pub_add.sh"}),(0,n.jsx)(t.td,{children:"If you are rebuilding the ggc_app with a fresh install of Flutter, this script installs all of the packages in one command. Before running it, make sure the list of packages is up to date!"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_rebuild_podfile.sh"}),(0,n.jsx)(t.td,{children:"If the build fails due to a podfile problem, this script rebuilds it appropriately."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_tests.sh"}),(0,n.jsx)(t.td,{children:"Runs the entire integration test suite."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_tests_single.sh"}),(0,n.jsx)(t.td,{children:"Runs a single integration test."})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"run_tool_versions.sh"}),(0,n.jsx)(t.td,{children:"Prints out the names and versions of important tech stack components. Useful when trying to diagnose why the system builds and runs for one developer but produces errors for another."})]})]})]})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},1151:(e,t,s)=>{s.d(t,{Z:()=>o,a:()=>d});var n=s(7294);const r={},i=n.createContext(r);function d(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/9140a56a.63431e4b.js b/assets/js/9140a56a.63431e4b.js new file mode 100644 index 000000000..d00bc9200 --- /dev/null +++ b/assets/js/9140a56a.63431e4b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[9095],{8002:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>s,toc:()=>c});var a=t(5893),o=t(1151);const r={hide_table_of_contents:!1},i="Dart analyze",s={id:"develop/dart-analyze",title:"Dart analyze",description:"One form of quality assurance in GGC is the use of Dart Analyze. This is a static analysis tool that looks for common code problems.",source:"@site/docs/develop/dart-analyze.md",sourceDirName:"develop",slug:"/develop/dart-analyze",permalink:"/docs/develop/dart-analyze",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Data Mutation",permalink:"/docs/develop/design/data-mutation"},next:{title:"Testing",permalink:"/docs/develop/testing"}},l={},c=[];function d(e){const n={a:"a",code:"code",h1:"h1",header:"header",p:"p",pre:"pre",...(0,o.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.header,{children:(0,a.jsx)(n.h1,{id:"dart-analyze",children:"Dart analyze"})}),"\n",(0,a.jsxs)(n.p,{children:["One form of quality assurance in GGC is the use of ",(0,a.jsx)(n.a,{href:"https://dart.dev/tools/dart-analyze",children:"Dart Analyze"}),". This is a static analysis tool that looks for common code problems."]}),"\n",(0,a.jsx)(n.p,{children:"In GGC, we want the main branch to always be free of any errors raised by Dart Analyze."}),"\n",(0,a.jsx)(n.p,{children:"From the command line, you can invoke it from the top-level directory like this:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:"~/GitHub/geogardenclub/ggc_app git:[main]\ndart analyze lib\nAnalyzing lib... 1.6s\nNo issues found!\n"})}),"\n",(0,a.jsx)(n.p,{children:"More commonly, you will want to monitor it from within your IDE. In IntelliJ, there is a Dart Analysis window with an icon that changes color when an issue is discovered. For example:"}),"\n",(0,a.jsx)("img",{src:"/img/develop/release-1.0/dart-analyze.png"}),"\n",(0,a.jsx)(n.p,{children:"There is a GitHub Action that runs each time there is a commit to the main branch which invokes Dart Analyze and fails the build if any errors are reported."}),"\n",(0,a.jsx)(n.p,{children:"Therefore, be sure your code does not have any analysis errors prior to merging it to the main branch."})]})}function h(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>i});var a=t(7294);const o={},r=a.createContext(o);function i(e){const n=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),a.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a5b8d3e9.07f3c936.js b/assets/js/a5b8d3e9.07f3c936.js deleted file mode 100644 index dc6a82df2..000000000 --- a/assets/js/a5b8d3e9.07f3c936.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[6957],{6058:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var n=a(5893),r=a(1151);const o={hide_table_of_contents:!1},i="Data Mutation",s={id:"develop/design/data-mutation",title:"Data Mutation",description:"Prelude: AsyncValue",source:"@site/docs/develop/design/data-mutation.md",sourceDirName:"develop/design",slug:"/develop/design/data-mutation",permalink:"/docs/develop/design/data-mutation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:'"With" widgets',permalink:"/docs/develop/design/with-widgets"},next:{title:"Technology Goals",permalink:"/docs/develop/releases/release-1.0/goals"}},d={},c=[{value:"Prelude: AsyncValue",id:"prelude-asyncvalue",level:2},{value:"Data mutation in GGC",id:"data-mutation-in-ggc",level:2},{value:"1. The data mutation widget",id:"1-the-data-mutation-widget",level:3},{value:"2. The onSubmit() method",id:"2-the-onsubmit-method",level:3},{value:"3. Mutate controller create, update, delete methods",id:"3-mutate-controller-create-update-delete-methods",level:3},{value:"4. Database methods",id:"4-database-methods",level:3},{value:"A template for the controller class",id:"a-template-for-the-controller-class",level:3}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"data-mutation",children:"Data Mutation"})}),"\n",(0,n.jsx)(t.h2,{id:"prelude-asyncvalue",children:"Prelude: AsyncValue"}),"\n",(0,n.jsx)(t.p,{children:"When your code interacts with the database (or some other external service), you are generally in one of two situations:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsxs)(t.li,{children:["Reading data: In this case, use a ",(0,n.jsx)(t.code,{children:"with"})," widget to retrieve the appropriate data for display, and separate the asynchronous code (to retrieve data from the database) from the synchronous code (to display it in the UI.)"]}),"\n",(0,n.jsx)(t.li,{children:"Writing data: In this case you must write asynchronous code to update the contents of the database."}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:'The Flutterverse is filled with articles and example code on how to accomplish (2). For GGC, we will use the "Riverpod" design pattern, which involves:'}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsxs)(t.li,{children:["Define a Riverpod provider (using the ",(0,n.jsx)(t.code,{children:"@riverpod"})," annotation) to perform the manipulation."]}),"\n",(0,n.jsx)(t.li,{children:"Handle the resulting AsyncValue's three possible states: loading, error, data."}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Here are some useful readings to get you started:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/morea/state/reading-eli5-riverpod.html",children:"Explain like I'm 5: Riverpod"})}),"\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://codewithandrea.com/articles/flutter-riverpod-generator/",children:"How to autogenerate your providers with Flutter Riverpod Generator"})}),"\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://codewithandrea.com/articles/data-mutations-riverpod/",children:"How to fetch data and perform data mutations with the Riverpod architecture"})}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Now let's look at how we implement data mutation in GGC"}),"\n",(0,n.jsx)(t.h2,{id:"data-mutation-in-ggc",children:"Data mutation in GGC"}),"\n",(0,n.jsx)(t.p,{children:'In GGC, "data mutation" refers to creating, updating, and deleting entities from the database. In some cases, mutating one entity (i.e. deleting a Garden) requires the implicit mutation of many other entities (i.e. deleting the Garden\'s associated Beds, Plantings, Observations, Outcomes, and Tasks).'}),"\n",(0,n.jsxs)(t.p,{children:["Accomplishing a data mutation involves a complex interaction between the front-end user interface and the back-end database. There are many potential ways to accomplish this interaction, but we will follow a design pattern documented by Andrea Bizzotti in his various ",(0,n.jsx)(t.a,{href:"https://codewithandrea.com/",children:"Code With Andrea"})," tutorials, with some additional customizations to suit our own GGC architecture."]}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/garden/presentation/create_garden_screen.dart",children:"CreateGardenScreen"})," and ",(0,n.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/garden/presentation/mutate_garden_controller.dart",children:"MutateGardenController"})," classes illustrate our data mutation design pattern."]}),"\n",(0,n.jsx)(t.p,{children:"Here is a walkthrough of some of the Garden code to illustrate the basic ideas of this design pattern."}),"\n",(0,n.jsx)(t.h3,{id:"1-the-data-mutation-widget",children:"1. The data mutation widget"}),"\n",(0,n.jsx)(t.p,{children:'A "Data mutation widget" (for example, UpdateGardenScreen) presents a user interface for performing a data mutation. The actual UI component displayed at any moment in time by the widget is determined by an associated controller (for example, MutateGardenController). The controller indicates which of four UI components to present: (1) an initial UI component (typically a form), (2) a loading indicator UI component (while waiting for an asynchronous action to complete, (3) a "success" component (displayed if the asynchronous action completes successfully) or (4) an error UI component (displayed if the asynchronous completes with an error).'}),"\n",(0,n.jsx)(t.p,{children:"Here's an excerpt of UpdateGardenScreen illustrating the basic way in which the controller controls the UI state of the screen:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/update_garden_screen.dart"',children:" AsyncValue asyncUpdate = ref.watch(mutateGardenControllerProvider);\n return Scaffold(\n appBar: AppBar(\n title: const Text('Update Garden'),\n actions: [HelpButton(routeName: AppRoute.updateGarden.name)],\n ),\n body: asyncUpdate.when(\n data: (_) => updateGardenForm(),\n loading: () => const GgcLoadingIndicator(),\n error: (e, st) => GgcError(e.toString(), st.toString())));\n}\n"})}),"\n",(0,n.jsx)(t.h3,{id:"2-the-onsubmit-method",children:"2. The onSubmit() method"}),"\n",(0,n.jsx)(t.p,{children:"If the initial UI component is a form, then it should have an async onSubmit() callback method. This method typically involves a sequence of three phases. The first phase checks that the form field values pass any validation criteria. If so, the second phase creates domain model entities as indicated by the form values. The third phase calls the appropriate mutate controller method, passing it the domain entities and an onSuccess() callback, which tells the controller which page to go to if the data mutation is successful."}),"\n",(0,n.jsx)(t.p,{children:"Here's an example:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/edit_garden_screen.dart"',children:" onSubmit() async {\n // 1. Check that form fields are valid.\n bool isValid = _formKey.currentState?.saveAndValidate() ?? false;\n if (!isValid) return;\n // 2. Create domain objects to send to controller.\n String name = FieldKey.gardenTextField.currentState?.value;\n List xFiles =\n FieldKey.singleImagePicker.currentState?.value ?? [];\n String editorsString =\n FieldKey.editorsTextField.currentState?.value ?? '';\n Garden garden = gardens.getGarden(gardenID);\n List updatedEditorUserIDs = users.parseUsernames(editorsString);\n List editorsToAdd = gardens.editors.makeNewEditors(\n gardenID: gardenID,\n chapterID: garden.chapterID,\n gardenerIDs: updatedEditorUserIDs);\n List editorsToDelete = gardens.editors.getEditors(gardenID);\n // Only update Editors collection if the field has changed.\n if (gardens.editors.sameEditorList(editorsToAdd, editorsToDelete)) {\n editorsToAdd = [];\n editorsToDelete = [];\n }\n String profilePictureUrl = (xFiles.isNotEmpty && xFiles[0] is XFile)\n ? await ImageStorage.cropAndUploadImage(\n xFile: xFiles[0], entityID: gardenID, context: context)\n : garden.profilePicture;\n Garden updatedGarden = Garden(\n gardenID: gardenID,\n name: name,\n profilePicture: profilePictureUrl,\n chapterID: chapters.currentChapterID,\n cropIDs: garden.cropIDs,\n sharedSeedIDs: garden.sharedSeedIDs,\n lastUpdate: DateTime.now(),\n ownerID: users.currentUserID,\n pictures: []);\n // 3. Use controller to invoke updates on database.\n ref.read(mutateGardenControllerProvider.notifier).updateGarden(\n garden: updatedGarden,\n editorsToAdd: editorsToAdd,\n editorsToDelete: editorsToDelete,\n onSuccess: () {\n context.pop();\n GlobalSnackBar.show('Garden \"$name\" updated.');\n },\n );\n }\n"})}),"\n",(0,n.jsx)(t.admonition,{title:"Don't pass Collection classes to the controller method",type:"important",children:(0,n.jsxs)(t.p,{children:["To maintain separation of concerns, the values passed to mutate controller methods should be individual domain entities (i.e. ",(0,n.jsx)(t.code,{children:"Garden"}),", ",(0,n.jsx)(t.code,{children:"Editor"}),"), lists of domain entities (i.e. ",(0,n.jsx)(t.code,{children:"List"}),", ",(0,n.jsx)(t.code,{children:"List"}),"), or primitive types (",(0,n.jsx)(t.code,{children:"String"}),", ",(0,n.jsx)(t.code,{children:"int"}),", etc). Don't pass collections (i.e. ",(0,n.jsx)(t.code,{children:"GardenCollection"}),", ",(0,n.jsx)(t.code,{children:"EditorCollection"}),"). Use these collection classes within the onSubmit() method to determine the domain entities to pass."]})}),"\n",(0,n.jsx)(t.h3,{id:"3-mutate-controller-create-update-delete-methods",children:"3. Mutate controller create, update, delete methods"}),"\n",(0,n.jsxs)(t.p,{children:["The Mutate Controller class typically implements create, update, and delete methods to handle the associated mutation. These methods will often need to make multiple asynchronous calls to the backend database. To do this efficiently, and also to provide atomicity, the controller should use the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes",children:"Firestore batched write"})," facility."]}),"\n",(0,n.jsx)(t.p,{children:"Here is an example from MutateGardenController for creating a new Garden. Note that both the Garden and Editor databases are mutated:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/mutate_garden_controller.dart"',children:" Future createGarden({\n required Garden garden,\n required List editors,\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n GardenDatabase gardenDatabase = ref.watch(gardenDatabaseProvider);\n EditorDatabase editorDatabase = ref.watch(editorDatabaseProvider);\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n gardenDatabase.setGardenBatch(batch, garden);\n editorDatabase.addEditorsBatch(batch, editors);\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Following the CodeWithAndrea guidelines, this method first sets the controller state to ",(0,n.jsx)(t.code,{children:"AsyncLoading"}),". Then it gets the databases of interest, creates a ",(0,n.jsx)(t.code,{children:"batch"})," variable, and adds mutations to that batch variable by passing it into the appropriate methods in the variable database classes. Finally, it invokes the ",(0,n.jsx)(t.code,{children:"batch.commit()"})," method to do all of the mutations at once, and either sets the state to ",(0,n.jsx)(t.code,{children:"AsyncData()"})," if everything went well or ",(0,n.jsx)(t.code,{children:"AsyncError()"})," if a problem occurred. A nice feature of batched writes is that they are performed as a transaction---either all of the writes succeed, or none of them do."]}),"\n",(0,n.jsx)(t.h3,{id:"4-database-methods",children:"4. Database methods"}),"\n",(0,n.jsxs)(t.p,{children:["The final part of this coding standard involves the appropriate definition of database methods. As shown above, database methods should be written to accept a ",(0,n.jsx)(t.code,{children:"batch"})," parameter, and result in that parameter being updated with additional operations to perform. Here is an example:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/data/editor_database.dart',children:" void createEditorsBatch(WriteBatch batch, List editors) {\n for (Editor editor in editors) {\n _service.setDataBatch(\n batch: batch,\n path: FirestorePath.editor(editor.editorID),\n data: editor.toJson());\n }\n }\n"})}),"\n",(0,n.jsx)(t.h3,{id:"a-template-for-the-controller-class",children:"A template for the controller class"}),"\n",(0,n.jsx)(t.p,{children:'There is some boilerplate code for controllers. To make it a little easier to create new controllers, here is a template. See the TODO comments for places where code needs to be added, and replace all occurrences of "TEMPLATE" by the entity being controller (i.e. Garden, User, Task, etc).'}),"\n",(0,n.jsx)(t.p,{children:'Note that we\'ll use "create" rather than "add" to conform to the CRUD acronym. This means that the associated screens should be changed from "AddX" to "CreateX".'}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",children:"import 'package:cloud_firestore/cloud_firestore.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\n\npart 'mutate_TEMPLATE_controller.g.dart';\n\n@riverpod\nclass MutateTEMPLATEController extends _$MutateTEMPLATEController {\n bool mounted = true;\n\n @override\n FutureOr build() {\n ref.onDispose(() => mounted = false);\n state = const AsyncData(null);\n }\n\n Future createTEMPLATE({\n /// TODO: Pass in domain object here.\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n // TODO: Watch the appropriate database instances here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n // TODO: Invoke the database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n\n\n Future updateTEMPLATE({\n /// TODO: Pass in domain data here\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n /// TODO: ref.watch the appropriate databases here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n /// TODO: Invoke the appropriate database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n \n Future deleteTEMPLATE({\n /// TODO: Pass in the appropriate domain objects here\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n /// TODO: Watch the appropriate databases here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n /// TODO: Invoke the appropriate database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n}\n"})})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>s,a:()=>i});var n=a(7294);const r={},o=n.createContext(r);function i(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a5b8d3e9.e7cd2989.js b/assets/js/a5b8d3e9.e7cd2989.js new file mode 100644 index 000000000..5e8633b39 --- /dev/null +++ b/assets/js/a5b8d3e9.e7cd2989.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[6957],{6058:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var n=a(5893),r=a(1151);const o={hide_table_of_contents:!1},i="Data Mutation",s={id:"develop/design/data-mutation",title:"Data Mutation",description:"Prelude: AsyncValue",source:"@site/docs/develop/design/data-mutation.md",sourceDirName:"develop/design",slug:"/develop/design/data-mutation",permalink:"/docs/develop/design/data-mutation",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:'"With" widgets',permalink:"/docs/develop/design/with-widgets"},next:{title:"Dart analyze",permalink:"/docs/develop/dart-analyze"}},d={},c=[{value:"Prelude: AsyncValue",id:"prelude-asyncvalue",level:2},{value:"Data mutation in GGC",id:"data-mutation-in-ggc",level:2},{value:"1. The data mutation widget",id:"1-the-data-mutation-widget",level:3},{value:"2. The onSubmit() method",id:"2-the-onsubmit-method",level:3},{value:"3. Mutate controller create, update, delete methods",id:"3-mutate-controller-create-update-delete-methods",level:3},{value:"4. Database methods",id:"4-database-methods",level:3},{value:"A template for the controller class",id:"a-template-for-the-controller-class",level:3}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"data-mutation",children:"Data Mutation"})}),"\n",(0,n.jsx)(t.h2,{id:"prelude-asyncvalue",children:"Prelude: AsyncValue"}),"\n",(0,n.jsx)(t.p,{children:"When your code interacts with the database (or some other external service), you are generally in one of two situations:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsxs)(t.li,{children:["Reading data: In this case, use a ",(0,n.jsx)(t.code,{children:"with"})," widget to retrieve the appropriate data for display, and separate the asynchronous code (to retrieve data from the database) from the synchronous code (to display it in the UI.)"]}),"\n",(0,n.jsx)(t.li,{children:"Writing data: In this case you must write asynchronous code to update the contents of the database."}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:'The Flutterverse is filled with articles and example code on how to accomplish (2). For GGC, we will use the "Riverpod" design pattern, which involves:'}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsxs)(t.li,{children:["Define a Riverpod provider (using the ",(0,n.jsx)(t.code,{children:"@riverpod"})," annotation) to perform the manipulation."]}),"\n",(0,n.jsx)(t.li,{children:"Handle the resulting AsyncValue's three possible states: loading, error, data."}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Here are some useful readings to get you started:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/morea/state/reading-eli5-riverpod.html",children:"Explain like I'm 5: Riverpod"})}),"\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://codewithandrea.com/articles/flutter-riverpod-generator/",children:"How to autogenerate your providers with Flutter Riverpod Generator"})}),"\n",(0,n.jsx)(t.li,{children:(0,n.jsx)(t.a,{href:"https://codewithandrea.com/articles/data-mutations-riverpod/",children:"How to fetch data and perform data mutations with the Riverpod architecture"})}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Now let's look at how we implement data mutation in GGC"}),"\n",(0,n.jsx)(t.h2,{id:"data-mutation-in-ggc",children:"Data mutation in GGC"}),"\n",(0,n.jsx)(t.p,{children:'In GGC, "data mutation" refers to creating, updating, and deleting entities from the database. In some cases, mutating one entity (i.e. deleting a Garden) requires the implicit mutation of many other entities (i.e. deleting the Garden\'s associated Beds, Plantings, Observations, Outcomes, and Tasks).'}),"\n",(0,n.jsxs)(t.p,{children:["Accomplishing a data mutation involves a complex interaction between the front-end user interface and the back-end database. There are many potential ways to accomplish this interaction, but we will follow a design pattern documented by Andrea Bizzotti in his various ",(0,n.jsx)(t.a,{href:"https://codewithandrea.com/",children:"Code With Andrea"})," tutorials, with some additional customizations to suit our own GGC architecture."]}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/garden/presentation/create_garden_screen.dart",children:"CreateGardenScreen"})," and ",(0,n.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/garden/presentation/mutate_garden_controller.dart",children:"MutateGardenController"})," classes illustrate our data mutation design pattern."]}),"\n",(0,n.jsx)(t.p,{children:"Here is a walkthrough of some of the Garden code to illustrate the basic ideas of this design pattern."}),"\n",(0,n.jsx)(t.h3,{id:"1-the-data-mutation-widget",children:"1. The data mutation widget"}),"\n",(0,n.jsx)(t.p,{children:'A "Data mutation widget" (for example, UpdateGardenScreen) presents a user interface for performing a data mutation. The actual UI component displayed at any moment in time by the widget is determined by an associated controller (for example, MutateGardenController). The controller indicates which of four UI components to present: (1) an initial UI component (typically a form), (2) a loading indicator UI component (while waiting for an asynchronous action to complete, (3) a "success" component (displayed if the asynchronous action completes successfully) or (4) an error UI component (displayed if the asynchronous completes with an error).'}),"\n",(0,n.jsx)(t.p,{children:"Here's an excerpt of UpdateGardenScreen illustrating the basic way in which the controller controls the UI state of the screen:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/update_garden_screen.dart"',children:" AsyncValue asyncUpdate = ref.watch(mutateGardenControllerProvider);\n return Scaffold(\n appBar: AppBar(\n title: const Text('Update Garden'),\n actions: [HelpButton(routeName: AppRoute.updateGarden.name)],\n ),\n body: asyncUpdate.when(\n data: (_) => updateGardenForm(),\n loading: () => const GgcLoadingIndicator(),\n error: (e, st) => GgcError(e.toString(), st.toString())));\n}\n"})}),"\n",(0,n.jsx)(t.h3,{id:"2-the-onsubmit-method",children:"2. The onSubmit() method"}),"\n",(0,n.jsx)(t.p,{children:"If the initial UI component is a form, then it should have an async onSubmit() callback method. This method typically involves a sequence of three phases. The first phase checks that the form field values pass any validation criteria. If so, the second phase creates domain model entities as indicated by the form values. The third phase calls the appropriate mutate controller method, passing it the domain entities and an onSuccess() callback, which tells the controller which page to go to if the data mutation is successful."}),"\n",(0,n.jsx)(t.p,{children:"Here's an example:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/edit_garden_screen.dart"',children:" onSubmit() async {\n // 1. Check that form fields are valid.\n bool isValid = _formKey.currentState?.saveAndValidate() ?? false;\n if (!isValid) return;\n // 2. Create domain objects to send to controller.\n String name = FieldKey.gardenTextField.currentState?.value;\n List xFiles =\n FieldKey.singleImagePicker.currentState?.value ?? [];\n String editorsString =\n FieldKey.editorsTextField.currentState?.value ?? '';\n Garden garden = gardens.getGarden(gardenID);\n List updatedEditorUserIDs = users.parseUsernames(editorsString);\n List editorsToAdd = gardens.editors.makeNewEditors(\n gardenID: gardenID,\n chapterID: garden.chapterID,\n gardenerIDs: updatedEditorUserIDs);\n List editorsToDelete = gardens.editors.getEditors(gardenID);\n // Only update Editors collection if the field has changed.\n if (gardens.editors.sameEditorList(editorsToAdd, editorsToDelete)) {\n editorsToAdd = [];\n editorsToDelete = [];\n }\n String profilePictureUrl = (xFiles.isNotEmpty && xFiles[0] is XFile)\n ? await ImageStorage.cropAndUploadImage(\n xFile: xFiles[0], entityID: gardenID, context: context)\n : garden.profilePicture;\n Garden updatedGarden = Garden(\n gardenID: gardenID,\n name: name,\n profilePicture: profilePictureUrl,\n chapterID: chapters.currentChapterID,\n cropIDs: garden.cropIDs,\n sharedSeedIDs: garden.sharedSeedIDs,\n lastUpdate: DateTime.now(),\n ownerID: users.currentUserID,\n pictures: []);\n // 3. Use controller to invoke updates on database.\n ref.read(mutateGardenControllerProvider.notifier).updateGarden(\n garden: updatedGarden,\n editorsToAdd: editorsToAdd,\n editorsToDelete: editorsToDelete,\n onSuccess: () {\n context.pop();\n GlobalSnackBar.show('Garden \"$name\" updated.');\n },\n );\n }\n"})}),"\n",(0,n.jsx)(t.admonition,{title:"Don't pass Collection classes to the controller method",type:"important",children:(0,n.jsxs)(t.p,{children:["To maintain separation of concerns, the values passed to mutate controller methods should be individual domain entities (i.e. ",(0,n.jsx)(t.code,{children:"Garden"}),", ",(0,n.jsx)(t.code,{children:"Editor"}),"), lists of domain entities (i.e. ",(0,n.jsx)(t.code,{children:"List"}),", ",(0,n.jsx)(t.code,{children:"List"}),"), or primitive types (",(0,n.jsx)(t.code,{children:"String"}),", ",(0,n.jsx)(t.code,{children:"int"}),", etc). Don't pass collections (i.e. ",(0,n.jsx)(t.code,{children:"GardenCollection"}),", ",(0,n.jsx)(t.code,{children:"EditorCollection"}),"). Use these collection classes within the onSubmit() method to determine the domain entities to pass."]})}),"\n",(0,n.jsx)(t.h3,{id:"3-mutate-controller-create-update-delete-methods",children:"3. Mutate controller create, update, delete methods"}),"\n",(0,n.jsxs)(t.p,{children:["The Mutate Controller class typically implements create, update, and delete methods to handle the associated mutation. These methods will often need to make multiple asynchronous calls to the backend database. To do this efficiently, and also to provide atomicity, the controller should use the ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes",children:"Firestore batched write"})," facility."]}),"\n",(0,n.jsx)(t.p,{children:"Here is an example from MutateGardenController for creating a new Garden. Note that both the Garden and Editor databases are mutated:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/presentation/mutate_garden_controller.dart"',children:" Future createGarden({\n required Garden garden,\n required List editors,\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n GardenDatabase gardenDatabase = ref.watch(gardenDatabaseProvider);\n EditorDatabase editorDatabase = ref.watch(editorDatabaseProvider);\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n gardenDatabase.setGardenBatch(batch, garden);\n editorDatabase.addEditorsBatch(batch, editors);\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Following the CodeWithAndrea guidelines, this method first sets the controller state to ",(0,n.jsx)(t.code,{children:"AsyncLoading"}),". Then it gets the databases of interest, creates a ",(0,n.jsx)(t.code,{children:"batch"})," variable, and adds mutations to that batch variable by passing it into the appropriate methods in the variable database classes. Finally, it invokes the ",(0,n.jsx)(t.code,{children:"batch.commit()"})," method to do all of the mutations at once, and either sets the state to ",(0,n.jsx)(t.code,{children:"AsyncData()"})," if everything went well or ",(0,n.jsx)(t.code,{children:"AsyncError()"})," if a problem occurred. A nice feature of batched writes is that they are performed as a transaction---either all of the writes succeed, or none of them do."]}),"\n",(0,n.jsx)(t.h3,{id:"4-database-methods",children:"4. Database methods"}),"\n",(0,n.jsxs)(t.p,{children:["The final part of this coding standard involves the appropriate definition of database methods. As shown above, database methods should be written to accept a ",(0,n.jsx)(t.code,{children:"batch"})," parameter, and result in that parameter being updated with additional operations to perform. Here is an example:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",metastring:'title="lib/features/garden/data/editor_database.dart',children:" void createEditorsBatch(WriteBatch batch, List editors) {\n for (Editor editor in editors) {\n _service.setDataBatch(\n batch: batch,\n path: FirestorePath.editor(editor.editorID),\n data: editor.toJson());\n }\n }\n"})}),"\n",(0,n.jsx)(t.h3,{id:"a-template-for-the-controller-class",children:"A template for the controller class"}),"\n",(0,n.jsx)(t.p,{children:'There is some boilerplate code for controllers. To make it a little easier to create new controllers, here is a template. See the TODO comments for places where code needs to be added, and replace all occurrences of "TEMPLATE" by the entity being controller (i.e. Garden, User, Task, etc).'}),"\n",(0,n.jsx)(t.p,{children:'Note that we\'ll use "create" rather than "add" to conform to the CRUD acronym. This means that the associated screens should be changed from "AddX" to "CreateX".'}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",children:"import 'package:cloud_firestore/cloud_firestore.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:riverpod_annotation/riverpod_annotation.dart';\n\npart 'mutate_TEMPLATE_controller.g.dart';\n\n@riverpod\nclass MutateTEMPLATEController extends _$MutateTEMPLATEController {\n bool mounted = true;\n\n @override\n FutureOr build() {\n ref.onDispose(() => mounted = false);\n state = const AsyncData(null);\n }\n\n Future createTEMPLATE({\n /// TODO: Pass in domain object here.\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n // TODO: Watch the appropriate database instances here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n // TODO: Invoke the database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n\n\n Future updateTEMPLATE({\n /// TODO: Pass in domain data here\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n /// TODO: ref.watch the appropriate databases here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n /// TODO: Invoke the appropriate database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n \n Future deleteTEMPLATE({\n /// TODO: Pass in the appropriate domain objects here\n required VoidCallback onSuccess,\n }) async {\n state = const AsyncLoading();\n AsyncValue nextState = const AsyncLoading();\n /// TODO: Watch the appropriate databases here.\n final WriteBatch batch = FirebaseFirestore.instance.batch();\n /// TODO: Invoke the appropriate database batch methods here.\n await batch\n .commit()\n .then((_) => nextState = const AsyncValue.data(null))\n .catchError((e, st) => nextState = AsyncValue.error(e, st));\n if (mounted) {\n state = nextState;\n }\n if (!state.hasError) {\n onSuccess();\n }\n }\n}\n"})})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>s,a:()=>i});var n=a(7294);const r={},o=n.createContext(r);function i(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/c7c467a1.79a7edaa.js b/assets/js/c7c467a1.79a7edaa.js deleted file mode 100644 index 4718c4a63..000000000 --- a/assets/js/c7c467a1.79a7edaa.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[2522],{4811:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var a=n(5893),i=n(1151);const r={hide_table_of_contents:!1},o="Coding Standards",s={id:"develop/coding-standards",title:"Coding Standards",description:'In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid "technical debt".',source:"@site/docs/develop/coding-standards.md",sourceDirName:"develop",slug:"/develop/coding-standards",permalink:"/docs/develop/coding-standards",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Scripts",permalink:"/docs/develop/scripts"},next:{title:"Architecture",permalink:"/docs/develop/architecture"}},d={},l=[{value:"Delete debugging/unused code",id:"delete-debuggingunused-code",level:2},{value:"Don't inline multi-statement callbacks",id:"dont-inline-multi-statement-callbacks",level:2},{value:"Don't inline form field definitions",id:"dont-inline-form-field-definitions",level:2},{value:"Avoid deep indentation",id:"avoid-deep-indentation",level:2},{value:"Don't write media-adaptive code",id:"dont-write-media-adaptive-code",level:2},{value:"Use named routes",id:"use-named-routes",level:2},{value:"Prefer widgets to helper methods",id:"prefer-widgets-to-helper-methods",level:2},{value:"Don't repeat titles",id:"dont-repeat-titles",level:2},{value:"Prefer late to dummy field values",id:"prefer-late-to-dummy-field-values",level:2},{value:"Case Study: Task Card",id:"case-study-task-card",level:2},{value:"The problem",id:"the-problem",level:3},{value:"One solution",id:"one-solution",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"coding-standards",children:"Coding Standards"})}),"\n",(0,a.jsx)(t.p,{children:'In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid "technical debt".'}),"\n",(0,a.jsx)(t.p,{children:"Technical debt refers to implementation practices that result in the need for refactoring of the code base at a future time."}),"\n",(0,a.jsx)(t.admonition,{title:"Coding standards apply to main branch only",type:"tip",children:(0,a.jsx)(t.p,{children:"The following standards apply only to code that you are about to merge into the main branch. You may want to violate these standards temporarily during initial development of a feature in your non-main branch. That's OK."})}),"\n",(0,a.jsx)(t.h2,{id:"delete-debuggingunused-code",children:"Delete debugging/unused code"}),"\n",(0,a.jsx)(t.p,{children:"Often during development, you will insert debugging statements to help diagnose a problem. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="home_screen_observations_view.dart"',children:" Widget build(BuildContext context) {\n // logger.d('HomeScreenObservationsView.build $chapters');\n // logger.d('HomeScreenObservationsView.build ${chapters.observations}');\n // logger.d('HomeScreenObservationsView.build ${chapters.observations.size()}');\n // logger.d('current user: ${users.currentUser}');\n // logger.d('current gardener: ${gardens.gardeners.getGardener(users.currentUserID)}');\n // List chapterNames = chapters.getChapterNames();\n List observations = chapters.observations.getAllObservations();\n"})}),"\n",(0,a.jsx)(t.p,{children:"Or, you might try one way to implement a feature, but eventually decide upon another way. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="home_screen_observations_view.dart"',children:"child: ListView(\n// children: observations\n// .map((observation) => InstagramCard(\n// observation: observation,\n// chapterName: chapterNames[0],\n// observations: chapters.observations,\n// tags: chapters.tags,\n// users: users))\n// .toList()));\n children: observations.map((observation) => ObservationCard(observation: observation, chapters: chapters, gardens: gardens, users: users)).toList()));\n"})}),"\n",(0,a.jsx)(t.p,{children:"Rather than comment out debugging or unused code, please delete it prior to merging into main. Deleting this code improves the signal-to-noise ratio for future readers. In the case of debugging statements, it is easy to re-insert them later if needed (and often, you will want to inspect different values later, so the commented lines aren't helpful)."}),"\n",(0,a.jsxs)(t.p,{children:["If you are concerned about deleting potentially valuable code, then feel free to copy the file into the ",(0,a.jsx)(t.code,{children:"graveyard/"})," directory prior to deleting the commented out code."]}),"\n",(0,a.jsx)(t.p,{children:"I can think of one possible exception to this rule: you are debating between two alternative implementations, and you want others to experiment by commenting out one alternative and then the other. But in all the cases I can think of, we have decided these kinds of issues via screen shots rather than code."}),"\n",(0,a.jsx)(t.h2,{id:"dont-inline-multi-statement-callbacks",children:"Don't inline multi-statement callbacks"}),"\n",(0,a.jsx)(t.p,{children:"We want to keep code modular and avoid deeply indented code. Deeply indented code is more difficult to read, and (because it's deeply indented) more cognitively demanding to understand."}),"\n",(0,a.jsx)(t.p,{children:"One good heuristic to avoid deeply indented code is to not inline multi-line callbacks. For example, consider the following implementation of a PopupMenuButton:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="task_card.dart"',children:" PopupMenuButton(\n initialValue: _selectedMenu,\n onSelected: (SampleItem result) {\n setState(() {\n _selectedMenu = result;\n });\n switch (result) {\n case SampleItem.editTask:\n context.pushNamed(AppRoute.editTask.name,\n pathParameters: {'taskID': widget.task.taskID});\n break;\n case SampleItem.gardenDetails:\n context.goNamed(AppRoute.gardenDetails.name,\n pathParameters: {\n 'gardenID': widget.task.gardenID\n });\n break;\n // case SampleItem.editPlanting:\n // context.pushNamed(AppRoute.planting.name,\n // pathParameters: {\n // 'gardenID': widget.task.gardenID,\n // 'plantingID': widget.task.plantingID\n // });\n // break;\n }\n },\n itemBuilder: (BuildContext context) => popupMenuItems),\n"})}),"\n",(0,a.jsx)(t.p,{children:"In this case, some of the code is indented 34 spaces, using up almost half of the allotted 80 character line width."}),"\n",(0,a.jsxs)(t.p,{children:["To avoid this situation, notice that the ",(0,a.jsx)(t.code,{children:"onSelected:"})," argument is an inline callback, which could be easily rewritten as a local function:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"void _onSelected(SampleItem result) {\n setState(() => _selectedMenu = result);\n switch (result) {\n case SampleItem.editTask:\n context.pushNamed(AppRoute.editTask.name,\n pathParameters: {'taskID': widget.task.taskID});\n break;\n case SampleItem.gardenDetails:\n context.goNamed(AppRoute.gardenDetails.name,\n pathParameters: {'gardenID': widget.task.gardenID});\n break;\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"And then provided as the callback value as follows:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" PopupMenuButton(\n initialValue: _selectedMenu,\n onSelected: _onSelected,\n itemBuilder: (BuildContext context) => popupMenuItems),\n"})}),"\n",(0,a.jsx)(t.p,{children:"This rewrite makes it easier to understand the PopupMenuButton invocation (because it is now only four lines long) as well as the onSelected callback (because it now has access to almost the full 80 character line width)."}),"\n",(0,a.jsxs)(t.p,{children:["This PopupMenuButton code snippet is also useful because it illustrates the situation in which inlining a callback is appropriate! This is when the callback is a one-liner, such as the argument to the ",(0,a.jsx)(t.code,{children:"itemBuilder:"})," parameter."]}),"\n",(0,a.jsx)(t.h2,{id:"dont-inline-form-field-definitions",children:"Don't inline form field definitions"}),"\n",(0,a.jsx)(t.p,{children:"Another situation that often leads to deeply indented code is when form field definitions are inline. For example, consider the first 30 lines of this call to FormBuilder:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="add_outcome_screen.dart"',children:" FormBuilder(\n child: Column(\n children: [\n Text('Outcome for $plantingName.',\n style: Theme.of(context).textTheme.titleLarge),\n const SizedBox(height: 5),\n const Text(\n 'Please rate the following on a scale of 1 to 5, with 5 being the best.'),\n const SizedBox(height: 10),\n FormBuilderSlider(\n key: _germinationFieldKey,\n name: 'Germination',\n min: 1,\n max: 5,\n divisions: 4,\n decoration: ggcInputDecoration(\n label: 'Germination',\n required: true,\n hintText: ''),\n initialValue: _germinationValue,\n valueTransformer: (value) {\n return value!.toInt();\n },\n onChanged: (value) {\n setState(() {\n _germinationValue = value!;\n });\n },\n ),\n"})}),"\n",(0,a.jsx)(t.p,{children:"This is hard to read due to all the indentation, and it also has the potential to create very long form definitions. In this case, the complete call to FormBuilder is 200 lines long!"}),"\n",(0,a.jsxs)(t.p,{children:["To create more readable code, and also to create opportunities for reuse, define form fields as widgets in the ",(0,a.jsx)(t.code,{children:"lib/common/input-fields"})," directory. For example, here is the definition for a text field that allows the user to name (or rename) their garden:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="garden_text_field.dart"',children:"class GardenTextField extends StatelessWidget {\n const GardenTextField(\n {super.key, required this.gardens, this.onTap, this.currName});\n\n final GardenCollection gardens;\n final void Function(String value)? onTap;\n final String? currName;\n\n @override\n Widget build(BuildContext context) {\n String fieldName = 'Garden Name';\n return FieldPadding(\n child: FormBuilderTextField(\n name: fieldName,\n key: FieldKey.gardenTextField,\n decoration: ggcInputDecoration(\n label: fieldName,\n required: true,\n hintText: '4-20 chars, alphanumeric/spaces, unique',\n ),\n initialValue: currName,\n validator: FormBuilderValidators.compose([\n GgcValidators.validName(),\n GgcValidators.uniqueGardenName(gardens, currName)\n ])),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Now you can use it in a call to FormBuilder, where the total number of lines in the definition will typically be only a few more than the total number of fields:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="add_garden_screen.dart"',children:" FormBuilder(\n key: _formKey,\n child: Column(\n children: [\n GardenTextField(gardens: widget.gardens),\n const SingleImagePicker(required: false),\n EditorsTextField(users: widget.users),\n FormButtons(onSubmit: onSubmit, onCancel: onCancel),\n ],\n ),\n );\n"})}),"\n",(0,a.jsx)(t.p,{children:"As a bonus, GardenTextField is used in both the AddGarden form and the EditGarden Form, which avoids duplicate code."}),"\n",(0,a.jsxs)(t.p,{children:["Also note that the ",(0,a.jsx)(t.code,{children:"onSubmit:"})," and ",(0,a.jsx)(t.code,{children:"onCancel:"})," callbacks are not inlined, conforming to the prior coding standard."]}),"\n",(0,a.jsx)(t.h2,{id:"avoid-deep-indentation",children:"Avoid deep indentation"}),"\n",(0,a.jsx)(t.p,{children:"The prior two coding standards should significantly reduce the depth of indentation, but there may be other situations which result in deeply indented code."}),"\n",(0,a.jsx)(t.p,{children:"As a heuristic, if indentation exceeds 5 or 6 levels, think about creating local functions to encapsulate semantically meaningful units of functionality, and then invoking them instead of inlining all of the code."}),"\n",(0,a.jsx)(t.h2,{id:"dont-write-media-adaptive-code",children:"Don't write media-adaptive code"}),"\n",(0,a.jsx)(t.p,{children:"For the 1.0 release, we are not going to optimize layout for different screen sizes. So, please do not (for example) use MediaQuery to adjust values for different screen sizes. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"double width = MediaQuery.of(context).size.width;\nif (!widget.readOnly) {\n // compensate for the checkbox\n width = width - 50;\n}\nif (width > 400) {\n // horizontal mode so remove more.\n width = width - 110;\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The reason for this is to avoid: (a) investing time into writing code that we might abandon later once we decide on a comprehensive approach to screen-dependent layout, and (b) an inconsistent UI that is sometimes adaptive and sometimes not."}),"\n",(0,a.jsx)(t.p,{children:"For more information on this issue, see:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://docs.flutter.dev/ui/layout/responsive/adaptive-responsive",children:"Creating responsive and adaptive apps"})," provides an overview of the issue."]}),"\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://www.youtube.com/watch?v=yytBENOnF0w",children:"Flutter Folio walkthrough"})," illustrates how a single app can provide different behaviors to support the strengths of different platforms."]}),"\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://pub.dev/packages?q=responsive",children:'Search pub.dev for "responsive"'})," to see the many packages available to support responsive design."]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"If you need to adjust the screen size for some other reason, that's OK."}),"\n",(0,a.jsx)(t.h2,{id:"use-named-routes",children:"Use named routes"}),"\n",(0,a.jsx)(t.p,{children:"Use named routing. For example, write this:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"onPressed: () =>\n context.pushNamed(AppRoute.editObservation.name, pathParameters: {'observationID': widget.observation.observationID, 'gardenID': widget.observation.gardenID}),\n"})}),"\n",(0,a.jsx)(t.p,{children:"Not this:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"onPressed: () =>\n context.push('/editObservation/${widget.observation.observationID}/${widget.observation.gardenID}');\n"})}),"\n",(0,a.jsx)(t.p,{children:"The reason is that if you change the path, you will have to change all the links to that path. If you use named routing, you only have to change the path in one place."}),"\n",(0,a.jsx)(t.h2,{id:"prefer-widgets-to-helper-methods",children:"Prefer widgets to helper methods"}),"\n",(0,a.jsx)(t.p,{children:'It is possible to create "helper" functions that return widgets, such as:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="lib/common/functions/make_fab.dart"',children:"FloatingActionButton makeFAB(String route, BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route);\n },\n child: const Icon(Icons.add),\n );\n}\n\nFloatingActionButton makeFABWithParameters(\n String route, Map pathParameters, BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route, pathParameters: pathParameters);\n },\n child: const Icon(Icons.add),\n );\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"There are several reasons why it is better to create widgets than helper methods, as is explained here:"}),"\n",(0,a.jsx)("iframe",{width:"100%",height:"415",src:"https://www.youtube.com/embed/IOyq-eTRhvo?si=arTUGsj7E-6iK0_Q",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowfullscreen:!0}),"\n",(0,a.jsx)(t.p,{children:"In this case, here's what the stateless widget version would look like:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="lib/common/widgets/ggc_fab.dart"',children:"class GgcFAB extends StatelessWidget {\n const GgcFAB(\n {super.key, required this.route, this.pathParameters = const {}});\n\n final String route;\n final Map pathParameters;\n\n @override\n Widget build(BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route, pathParameters: pathParameters);\n },\n child: const Icon(Icons.add),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The following code illustrates the very minimal differences in how they are called:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" Widget? getFloatingActionButton(BuildContext context, int selectedIndex) {\n if (selectedIndex == 0) {\n return GgcFAB(route: AppRoute.createPlanting.name);\n }\n if (selectedIndex == 3) {\n return makeFABWithParameters(AppRoute.createGardenTask.name,\n {'gardenID': widget.gardenID}, context);\n }\n return null;\n }\n"})}),"\n",(0,a.jsx)(t.p,{children:"It gets a little nicer if you convert to the stateless widget approach entirely, since you can tighten up the return type and remove the context argument:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" GgcFAB? getFloatingActionButton(int selectedIndex) {\n if (selectedIndex == 0) {\n return GgcFAB(route: AppRoute.createPlanting.name);\n }\n if (selectedIndex == 3) {\n return GgcFAB(route: AppRoute.createGardenTask.name,\n pathParameters: {'gardenID': widget.gardenID});\n }\n return null;\n }\n"})}),"\n",(0,a.jsx)(t.h2,{id:"dont-repeat-titles",children:"Don't repeat titles"}),"\n",(0,a.jsx)(t.p,{children:"The title should appear in the scaffold. It does not need to be repeated in the body:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/repeated-title.png"}),"\n",(0,a.jsx)(t.h2,{id:"prefer-late-to-dummy-field-values",children:"Prefer late to dummy field values"}),"\n",(0,a.jsx)(t.p,{children:'Sometimes you need to create an entity that has required fields before you know what those fields are. It is tempting to create a "dummy" entity with clearly incorrect values and then overwrite the fields once you know what the correct values are. For example, here\'s some code from TaskCard:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" Planting updatedPlanting = Planting(\n plantingID: 'plantingID',\n chapterID: 'chapterID',\n gardenID: 'gardenID',\n cropID: 'cropID',\n cropName: 'cropName',\n lastUpdate: DateTime.now());\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n"})}),"\n",(0,a.jsxs)(t.p,{children:["You can make the code shorter, and communicate your intent more clearly, by using the ",(0,a.jsx)(t.code,{children:"late"})," keyword:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" late Planting updatedPlanting;\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n"})}),"\n",(0,a.jsxs)(t.p,{children:["A more important reason to use ",(0,a.jsx)(t.code,{children:"late"})," is that if you fail to initialize the entity, you will get a runtime error that clearly indicates the problem, rather than a runtime error that initially seems unrelated (i.e. failure to find a chapterID)."]}),"\n",(0,a.jsx)(t.h2,{id:"case-study-task-card",children:"Case Study: Task Card"}),"\n",(0,a.jsx)(t.p,{children:"I've recently refactored the code for Task Cards and believe a short description of the experience could provide some insight into our current design and coding best practices."}),"\n",(0,a.jsx)(t.p,{children:"The GGC Task Card (at the time of writing) looked like this:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/task-card.png"}),"\n",(0,a.jsx)(t.p,{children:'As you can see, the "description" is a little wordy. My initial goal was to simply change the implementation of this card so that the description would be more tabular in nature, provide the garden and bed names (if available) from the task document, and include the description field only in the case of "custom" tasks.'}),"\n",(0,a.jsx)(t.h3,{id:"the-problem",children:"The problem"}),"\n",(0,a.jsxs)(t.p,{children:["So, I went to ",(0,a.jsx)(t.code,{children:"task_card.dart"}),", and discovered this:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:intl/intl.dart';\nimport 'package:jiffy/jiffy.dart';\n\nimport '../../../repositories/firestore/firestore_path.dart';\nimport '../../../repositories/firestore/firestore_service.dart';\nimport '../../../router.dart';\nimport '../../chapter/domain/chapter_collection.dart';\nimport '../../common/widgets/ggc_card.dart';\nimport '../../common/widgets/ggc_loading_indicator.dart';\nimport '../../garden/domain/garden_collection.dart';\nimport '../../global_snackbar.dart';\nimport '../../planting/domain/planting.dart';\nimport '../../user/domain/user_collection.dart';\nimport '../domain/task.dart';\n\nclass TaskCard extends StatefulWidget {\n final Task task;\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n final bool readOnly;\n\n const TaskCard(\n {super.key,\n required this.task,\n required this.chapters,\n required this.gardens,\n required this.users,\n required this.readOnly});\n\n @override\n State createState() => _TaskCardState();\n}\n\nenum TaskCardAction { updateTask, deleteTask }\n\nclass _TaskCardState extends State {\n final _service = FirestoreService.instance;\n bool _isWorking = false;\n bool isChecked = false;\n TaskCardAction? _selectedAction;\n\n Future getPlanting(String plantingID) {\n return _service.fetchDocument(\n path: FirestorePath.planting(plantingID),\n builder: (data, documentId) => Planting.fromJson(data!));\n }\n\n @override\n Widget build(BuildContext context) {\n DateTime now = DateTime.now();\n bool late = widget.task.dueDate.isBefore(now);\n final difference = widget.task.dueDate.difference(now);\n final days = difference.inDays;\n String dateStr = '';\n if (days > 60) {\n dateStr = DateFormat.yMd().format(widget.task.dueDate);\n } else {\n dateStr = Jiffy.parseFromDateTime(widget.task.dueDate).fromNow();\n }\n TextStyle? textStyle;\n if (late) {\n textStyle = TextStyle(\n color: Theme.of(context).colorScheme.error,\n // fontWeight: FontWeight.bold\n );\n }\n double width = MediaQuery.of(context).size.width;\n if (!widget.readOnly) {\n // compensate for the checkbox\n width = width - 50;\n }\n width = width - 120;\n List> popupMenuItems = [\n const PopupMenuItem(\n value: TaskCardAction.updateTask,\n child: Text('Update Task'),\n ),\n const PopupMenuItem(\n value: TaskCardAction.deleteTask, child: Text('Delete Task'))\n ];\n\n return _isWorking\n ? const GgcLoadingIndicator()\n : GgcCard(\n child: ListTile(\n dense: false,\n contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n horizontalTitleGap: 6,\n //Code runs with this line commented out but theme isn't used.\n // tileColor: tileColor,\n title: Row(\n children: [\n SizedBox(\n width: width,\n child: Text(widget.task.title,\n style: textStyle,\n softWrap: false,\n overflow: TextOverflow.ellipsis)),\n const Spacer(),\n PopupMenuButton(\n initialValue: _selectedAction,\n onSelected: (TaskCardAction result) {\n setState(() {\n _selectedAction = result;\n });\n switch (result) {\n case TaskCardAction.updateTask:\n context.pushNamed(AppRoute.taskUpdate.name,\n pathParameters: {\n 'taskID': widget.task.taskID,\n 'gardenID': widget.task.gardenID\n });\n break;\n case TaskCardAction.deleteTask:\n context.pushNamed(AppRoute.taskDelete.name,\n pathParameters: {\n 'gardenID': widget.task.gardenID,\n 'taskID': widget.task.taskID\n });\n break;\n }\n },\n itemBuilder: (BuildContext context) => popupMenuItems),\n ],\n ),\n subtitle: Text('${widget.task.description} Due $dateStr',\n style: textStyle),\n isThreeLine: true,\n leading: !widget.readOnly\n ? Checkbox(\n checkColor: Theme.of(context).primaryColor,\n fillColor: MaterialStateProperty.resolveWith(\n (Set states) {\n if (states.contains(MaterialState.pressed)) {\n return Theme.of(context)\n .primaryColor; // Color when checkbox is checked\n }\n return Colors\n .transparent; // Transparent fill color when checkbox is not checked\n }),\n value: isChecked,\n onChanged: (bool? value) async {\n if (value == true) {\n DateTime? completedDate = await showDatePicker(\n context: context,\n helpText:\n 'When did you complete ${widget.task.title}?',\n initialDate: widget.task.dueDate,\n firstDate: DateTime(2020),\n lastDate: DateTime((DateTime.now().year + 1)));\n if (completedDate != null) {\n setState(() {\n _isWorking = true;\n });\n updatePlanting(widget.task, completedDate)\n .then((_) => setState(() {\n _isWorking = false;\n }));\n }\n }\n },\n )\n : null,\n ),\n );\n }\n\n Future updatePlanting(Task task, DateTime completedDate) async {\n String plantingID = widget.task.plantingID;\n Planting planting = await getPlanting(plantingID);\n late Planting updatedPlanting;\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n // update the planting if completed\n if (updatedPlanting.plantingID != 'plantingID') {\n _service\n .setData(\n path: FirestorePath.planting(updatedPlanting.plantingID),\n data: updatedPlanting.toJson())\n .then((val) => GlobalSnackBar.show('Planting update succeeded.'))\n .catchError((e) =>\n GlobalSnackBar.show('Planting update failed\\n${e.toString()}.'));\n }\n // remove the task\n deleteTask(task);\n }\n\n Future deleteTask(Task task) async {\n _service\n .deleteData(path: FirestorePath.task(task.taskID))\n .then((val) => GlobalSnackBar.show('Task delete succeeded.'))\n .catchError(\n (e) => GlobalSnackBar.show('Task delete failed\\n${e.toString()}.'));\n }\n}\n"})}),"\n",(0,a.jsxs)(t.p,{children:["Here are a few of the things I noticed about ",(0,a.jsx)(t.code,{children:"task_card.dart"}),":"]}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"It is over 200 LOC. Generally, our top-level Card implementations are around 50 LOC. This is a red flag."}),"\n",(0,a.jsx)(t.li,{children:'The implementation is what I would call "flat", or "inline". In other words, there is no modularization of the TaskCard UI components. You can see this by looking at the import statements: there is not a single import of a widget in the same directory.'}),"\n",(0,a.jsx)(t.li,{children:"The code to implement the popup menu is approximately 35 LOC, but is scattered across 100 LOC."}),"\n",(0,a.jsx)(t.li,{children:'The implementation of a UI component (TaskCard) includes code making asynchronous database calls. Our current best practice calls for the use of "mutator controllers" to bridge between UI components and the backend database.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"These issues make understanding this single file of code difficult. For example:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"How and where should I change to code to conditionally format the description field based on task type?"}),"\n",(0,a.jsxs)(t.li,{children:["What are the functions of the ",(0,a.jsx)(t.code,{children:"_isWorking"}),", ",(0,a.jsx)(t.code,{children:"_isChecked"}),", and ",(0,a.jsx)(t.code,{children:"_selectedAction"})," state variables?"]}),"\n",(0,a.jsx)(t.li,{children:'Changes or enhancements following this "inline" design will make this code even more complicated. At some point, it will become very difficult to understand and maintain.'}),"\n"]}),"\n",(0,a.jsx)(t.h3,{id:"one-solution",children:"One solution"}),"\n",(0,a.jsx)(t.p,{children:"There are two simple design patterns that I used to modularize and simplify the code so that I could implement my table-based description enhancement."}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"I made each visible UI component into its own widget."}),' Looking at the TaskCard, an obvious top-level decomposition is into two Widgets: a "Title" widget and a "Description" widget. The "Title" widget can be further decomposed into three widgets: a "Checkbox", "Title", and "PopUp Menu". The following annotated screenshot of the TaskCard illustrates this breakdown with the top-level decomposition in red and the nested decomposition in green:']}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/task-card-widgets.png"}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"I used the mutator controller design pattern to move the database access code out of the UI component and into the controller."})," Interestingly, this not only made the DB access code more simple, it even made it a bit more efficient because multiple collections needed updates and the mutator controller supports batch updates."]}),"\n",(0,a.jsxs)(t.p,{children:["After implementing these changes, ",(0,a.jsx)(t.code,{children:"task_card.dart"})," now looks like this:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"import 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nimport '../../chapter/domain/chapter_collection.dart';\nimport '../../common/widgets/ggc_card.dart';\nimport '../../common/widgets/ggc_error.dart';\nimport '../../common/widgets/ggc_loading_indicator.dart';\nimport '../../garden/domain/garden_collection.dart';\nimport '../../global_snackbar.dart';\nimport '../../user/domain/user_collection.dart';\nimport '../domain/task.dart';\nimport 'mutate_task_controller.dart';\nimport 'task_card_description.dart';\nimport 'task_card_title_row.dart';\n\ntypedef OnCompletedCallback = void Function(Task task, DateTime completedDate);\n\nclass TaskCard extends ConsumerWidget {\n final Task task;\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n final bool readOnly;\n\n const TaskCard(\n {super.key,\n required this.task,\n required this.chapters,\n required this.gardens,\n required this.users,\n required this.readOnly});\n\n @override\n Widget build(BuildContext context, WidgetRef ref) {\n void onCompleted(Task task, DateTime completedDate) {\n ref.read(mutateTaskControllerProvider.notifier).completeTask(\n task: task,\n completedDate: completedDate,\n onSuccess: () {\n GlobalSnackBar.show('Task completed.');\n });\n }\n\n AsyncValue asyncUpdate = ref.watch(mutateTaskControllerProvider);\n return asyncUpdate.when(\n data: (_) => GgcCard(\n child: Column(children: [\n TaskCardTitleRow(task: task, onCompleted: onCompleted),\n TaskCardDescription(task: task),\n ])),\n loading: () => const GgcLoadingIndicator(),\n error: (e, st) => GgcError(e.toString(), st.toString()));\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Let's see how the problems with the original implementation have been addressed."}),"\n",(0,a.jsxs)(t.p,{children:["First, the size of ",(0,a.jsx)(t.code,{children:"task_card.dart"}),' is now around 50 lines of code, back to a typical size for a GGC "Card" UI component.']}),"\n",(0,a.jsx)(t.p,{children:"Second, the UI code is modularized into five widgets: TaskCard, TaskCardTitleRow, TaskCardDescription, TaskCardCheckbox, and TaskCardPopupMenu."}),"\n",(0,a.jsx)(t.p,{children:"Third, the code to implement the PopupMenu is now encapsulated within a single widget. Interestingly, this refactoring revealed that there is a popup menu in ObservationCard with a very similar structure! It would be straight forward to do an additional refactoring to create a single generic popup menu (for example, GgcPopupMenu) that can be used anywhere we need one."}),"\n",(0,a.jsx)(t.p,{children:"Fourth, as already noted, the asynchronous DB access code is now entirely encapsulated within the completeTask method of the mutator. The completeTask method is 25 LOC, while the original inline approach required approximately 60 LOC. That is a significant simplification."}),"\n",(0,a.jsx)(t.p,{children:"Finally, here's what my new version of TaskCard looks like:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/tasks-revised.png"}),"\n",(0,a.jsx)(t.p,{children:'The top and bottom tasks are "implicit" tasks (based on Planting dates), while the middle task is an "explicit" task (defined by the gardener.)'}),"\n",(0,a.jsx)(t.p,{children:'No code is ever "perfect" or "complete". I am sure that there are more improvements to be made to TaskCard. But I hope this case study helps improve our collective intuition about how to design and implement Flutter code.'})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>o});var a=n(7294);const i={},r=a.createContext(i);function o(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/c7c467a1.f201450f.js b/assets/js/c7c467a1.f201450f.js new file mode 100644 index 000000000..d3623b388 --- /dev/null +++ b/assets/js/c7c467a1.f201450f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[2522],{4811:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var a=n(5893),i=n(1151);const r={hide_table_of_contents:!1},o="Coding Standards",s={id:"develop/coding-standards",title:"Coding Standards",description:'In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid "technical debt".',source:"@site/docs/develop/coding-standards.md",sourceDirName:"develop",slug:"/develop/coding-standards",permalink:"/docs/develop/coding-standards",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Scripts",permalink:"/docs/develop/scripts"},next:{title:"Architecture",permalink:"/docs/develop/architecture"}},d={},l=[{value:"Delete debugging/unused code",id:"delete-debuggingunused-code",level:2},{value:"Don't inline multi-statement callbacks",id:"dont-inline-multi-statement-callbacks",level:2},{value:"Don't inline form field definitions",id:"dont-inline-form-field-definitions",level:2},{value:"Avoid deep indentation",id:"avoid-deep-indentation",level:2},{value:"Don't write media-adaptive code",id:"dont-write-media-adaptive-code",level:2},{value:"Use named routes",id:"use-named-routes",level:2},{value:"Prefer widgets to helper methods",id:"prefer-widgets-to-helper-methods",level:2},{value:"Don't repeat titles",id:"dont-repeat-titles",level:2},{value:"Prefer late to dummy field values",id:"prefer-late-to-dummy-field-values",level:2},{value:"Case Study: Task Card",id:"case-study-task-card",level:2},{value:"The problem",id:"the-problem",level:3},{value:"One solution",id:"one-solution",level:3}];function c(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"coding-standards",children:"Coding Standards"})}),"\n",(0,a.jsx)(t.p,{children:'In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid "technical debt".'}),"\n",(0,a.jsx)(t.p,{children:"Technical debt refers to implementation practices that result in the need for refactoring of the code base at a future time."}),"\n",(0,a.jsx)(t.admonition,{title:"Coding standards apply to main branch only",type:"tip",children:(0,a.jsx)(t.p,{children:"The following standards apply only to code that you are about to merge into the main branch. You may want to violate these standards temporarily during initial development of a feature in your non-main branch. That's OK."})}),"\n",(0,a.jsx)(t.h2,{id:"delete-debuggingunused-code",children:"Delete debugging/unused code"}),"\n",(0,a.jsx)(t.p,{children:"Often during development, you will insert debugging statements to help diagnose a problem. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="home_screen_observations_view.dart"',children:" Widget build(BuildContext context) {\n // logger.d('HomeScreenObservationsView.build $chapters');\n // logger.d('HomeScreenObservationsView.build ${chapters.observations}');\n // logger.d('HomeScreenObservationsView.build ${chapters.observations.size()}');\n // logger.d('current user: ${users.currentUser}');\n // logger.d('current gardener: ${gardens.gardeners.getGardener(users.currentUserID)}');\n // List chapterNames = chapters.getChapterNames();\n List observations = chapters.observations.getAllObservations();\n"})}),"\n",(0,a.jsx)(t.p,{children:"Or, you might try one way to implement a feature, but eventually decide upon another way. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="home_screen_observations_view.dart"',children:"child: ListView(\n// children: observations\n// .map((observation) => InstagramCard(\n// observation: observation,\n// chapterName: chapterNames[0],\n// observations: chapters.observations,\n// tags: chapters.tags,\n// users: users))\n// .toList()));\n children: observations.map((observation) => ObservationCard(observation: observation, chapters: chapters, gardens: gardens, users: users)).toList()));\n"})}),"\n",(0,a.jsx)(t.p,{children:"Rather than comment out debugging or unused code, please delete it prior to merging into main. Deleting this code improves the signal-to-noise ratio for future readers. In the case of debugging statements, it is easy to re-insert them later if needed (and often, you will want to inspect different values later, so the commented lines aren't helpful)."}),"\n",(0,a.jsxs)(t.p,{children:["If you are concerned about deleting potentially valuable code, then feel free to copy the file into the ",(0,a.jsx)(t.code,{children:"graveyard/"})," directory prior to deleting the commented out code."]}),"\n",(0,a.jsx)(t.p,{children:"I can think of one possible exception to this rule: you are debating between two alternative implementations, and you want others to experiment by commenting out one alternative and then the other. But in all the cases I can think of, we have decided these kinds of issues via screen shots rather than code."}),"\n",(0,a.jsx)(t.h2,{id:"dont-inline-multi-statement-callbacks",children:"Don't inline multi-statement callbacks"}),"\n",(0,a.jsx)(t.p,{children:"We want to keep code modular and avoid deeply indented code. Deeply indented code is more difficult to read, and (because it's deeply indented) more cognitively demanding to understand."}),"\n",(0,a.jsx)(t.p,{children:"One good heuristic to avoid deeply indented code is to not inline multi-line callbacks. For example, consider the following implementation of a PopupMenuButton:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="task_card.dart"',children:" PopupMenuButton(\n initialValue: _selectedMenu,\n onSelected: (SampleItem result) {\n setState(() {\n _selectedMenu = result;\n });\n switch (result) {\n case SampleItem.editTask:\n context.pushNamed(AppRoute.editTask.name,\n pathParameters: {'taskID': widget.task.taskID});\n break;\n case SampleItem.gardenDetails:\n context.goNamed(AppRoute.gardenDetails.name,\n pathParameters: {\n 'gardenID': widget.task.gardenID\n });\n break;\n }\n },\n itemBuilder: (BuildContext context) => popupMenuItems),\n"})}),"\n",(0,a.jsx)(t.p,{children:"In this case, some of the code is indented 34 spaces, using up almost half of the allotted 80 character line width."}),"\n",(0,a.jsxs)(t.p,{children:["To avoid this situation, notice that the ",(0,a.jsx)(t.code,{children:"onSelected:"})," argument is an inline callback, which could be easily rewritten as a local function:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"void _onSelected(SampleItem result) {\n setState(() => _selectedMenu = result);\n switch (result) {\n case SampleItem.editTask:\n context.pushNamed(AppRoute.editTask.name,\n pathParameters: {'taskID': widget.task.taskID});\n break;\n case SampleItem.gardenDetails:\n context.goNamed(AppRoute.gardenDetails.name,\n pathParameters: {'gardenID': widget.task.gardenID});\n break;\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"And then provided as the callback value as follows:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" PopupMenuButton(\n initialValue: _selectedMenu,\n onSelected: _onSelected,\n itemBuilder: (BuildContext context) => popupMenuItems),\n"})}),"\n",(0,a.jsx)(t.p,{children:"This rewrite makes it easier to understand the PopupMenuButton invocation (because it is now only four lines long) as well as the onSelected callback (because it now has access to almost the full 80 character line width)."}),"\n",(0,a.jsxs)(t.p,{children:["This PopupMenuButton code snippet is also useful because it illustrates the situation in which inlining a callback is appropriate! This is when the callback is a one-liner, such as the argument to the ",(0,a.jsx)(t.code,{children:"itemBuilder:"})," parameter."]}),"\n",(0,a.jsx)(t.h2,{id:"dont-inline-form-field-definitions",children:"Don't inline form field definitions"}),"\n",(0,a.jsx)(t.p,{children:"Another situation that often leads to deeply indented code is when form field definitions are inline. For example, consider the first 30 lines of this call to FormBuilder:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="add_outcome_screen.dart"',children:" FormBuilder(\n child: Column(\n children: [\n Text('Outcome for $plantingName.',\n style: Theme.of(context).textTheme.titleLarge),\n const SizedBox(height: 5),\n const Text(\n 'Please rate the following on a scale of 1 to 5, with 5 being the best.'),\n const SizedBox(height: 10),\n FormBuilderSlider(\n key: _germinationFieldKey,\n name: 'Germination',\n min: 1,\n max: 5,\n divisions: 4,\n decoration: ggcInputDecoration(\n label: 'Germination',\n required: true,\n hintText: ''),\n initialValue: _germinationValue,\n valueTransformer: (value) {\n return value!.toInt();\n },\n onChanged: (value) {\n setState(() {\n _germinationValue = value!;\n });\n },\n ),\n"})}),"\n",(0,a.jsx)(t.p,{children:"This is hard to read due to all the indentation, and it also has the potential to create very long form definitions. In this case, the complete call to FormBuilder is 200 lines long!"}),"\n",(0,a.jsxs)(t.p,{children:["To create more readable code, and also to create opportunities for reuse, define form fields as widgets in the ",(0,a.jsx)(t.code,{children:"lib/common/input-fields"})," directory. For example, here is the definition for a text field that allows the user to name (or rename) their garden:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="garden_text_field.dart"',children:"class GardenTextField extends StatelessWidget {\n const GardenTextField(\n {super.key, required this.gardens, this.onTap, this.currName});\n\n final GardenCollection gardens;\n final void Function(String value)? onTap;\n final String? currName;\n\n @override\n Widget build(BuildContext context) {\n String fieldName = 'Garden Name';\n return FieldPadding(\n child: FormBuilderTextField(\n name: fieldName,\n key: FieldKey.gardenTextField,\n decoration: ggcInputDecoration(\n label: fieldName,\n required: true,\n hintText: '4-20 chars, alphanumeric/spaces, unique',\n ),\n initialValue: currName,\n validator: FormBuilderValidators.compose([\n GgcValidators.validName(),\n GgcValidators.uniqueGardenName(gardens, currName)\n ])),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Now you can use it in a call to FormBuilder, where the total number of lines in the definition will typically be only a few more than the total number of fields:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="add_garden_screen.dart"',children:" FormBuilder(\n key: _formKey,\n child: Column(\n children: [\n GardenTextField(gardens: widget.gardens),\n const SingleImagePicker(required: false),\n EditorsTextField(users: widget.users),\n FormButtons(onSubmit: onSubmit, onCancel: onCancel),\n ],\n ),\n );\n"})}),"\n",(0,a.jsx)(t.p,{children:"As a bonus, GardenTextField is used in both the AddGarden form and the EditGarden Form, which avoids duplicate code."}),"\n",(0,a.jsxs)(t.p,{children:["Also note that the ",(0,a.jsx)(t.code,{children:"onSubmit:"})," and ",(0,a.jsx)(t.code,{children:"onCancel:"})," callbacks are not inlined, conforming to the prior coding standard."]}),"\n",(0,a.jsx)(t.h2,{id:"avoid-deep-indentation",children:"Avoid deep indentation"}),"\n",(0,a.jsx)(t.p,{children:"The prior two coding standards should significantly reduce the depth of indentation, but there may be other situations which result in deeply indented code."}),"\n",(0,a.jsx)(t.p,{children:"As a heuristic, if indentation exceeds 5 or 6 levels, think about creating local functions to encapsulate semantically meaningful units of functionality, and then invoking them instead of inlining all of the code."}),"\n",(0,a.jsx)(t.h2,{id:"dont-write-media-adaptive-code",children:"Don't write media-adaptive code"}),"\n",(0,a.jsx)(t.p,{children:"For the 1.0 release, we are not going to optimize layout for different screen sizes. So, please do not (for example) use MediaQuery to adjust values for different screen sizes. For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"double width = MediaQuery.of(context).size.width;\nif (!widget.readOnly) {\n // compensate for the checkbox\n width = width - 50;\n}\nif (width > 400) {\n // horizontal mode so remove more.\n width = width - 110;\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The reason for this is to avoid: (a) investing time into writing code that we might abandon later once we decide on a comprehensive approach to screen-dependent layout, and (b) an inconsistent UI that is sometimes adaptive and sometimes not."}),"\n",(0,a.jsx)(t.p,{children:"For more information on this issue, see:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://docs.flutter.dev/ui/layout/responsive/adaptive-responsive",children:"Creating responsive and adaptive apps"})," provides an overview of the issue."]}),"\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://www.youtube.com/watch?v=yytBENOnF0w",children:"Flutter Folio walkthrough"})," illustrates how a single app can provide different behaviors to support the strengths of different platforms."]}),"\n",(0,a.jsxs)(t.li,{children:[(0,a.jsx)(t.a,{href:"https://pub.dev/packages?q=responsive",children:'Search pub.dev for "responsive"'})," to see the many packages available to support responsive design."]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"If you need to adjust the screen size for some other reason, that's OK."}),"\n",(0,a.jsx)(t.h2,{id:"use-named-routes",children:"Use named routes"}),"\n",(0,a.jsx)(t.p,{children:"Use named routing. For example, write this:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"onPressed: () =>\n context.pushNamed(AppRoute.editObservation.name, pathParameters: {'observationID': widget.observation.observationID, 'gardenID': widget.observation.gardenID}),\n"})}),"\n",(0,a.jsx)(t.p,{children:"Not this:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"onPressed: () =>\n context.push('/editObservation/${widget.observation.observationID}/${widget.observation.gardenID}');\n"})}),"\n",(0,a.jsx)(t.p,{children:"The reason is that if you change the path, you will have to change all the links to that path. If you use named routing, you only have to change the path in one place."}),"\n",(0,a.jsx)(t.h2,{id:"prefer-widgets-to-helper-methods",children:"Prefer widgets to helper methods"}),"\n",(0,a.jsx)(t.p,{children:'It is possible to create "helper" functions that return widgets, such as:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="lib/common/functions/make_fab.dart"',children:"FloatingActionButton makeFAB(String route, BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route);\n },\n child: const Icon(Icons.add),\n );\n}\n\nFloatingActionButton makeFABWithParameters(\n String route, Map pathParameters, BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route, pathParameters: pathParameters);\n },\n child: const Icon(Icons.add),\n );\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"There are several reasons why it is better to create widgets than helper methods, as is explained here:"}),"\n",(0,a.jsx)("iframe",{width:"100%",height:"415",src:"https://www.youtube.com/embed/IOyq-eTRhvo?si=arTUGsj7E-6iK0_Q",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowfullscreen:!0}),"\n",(0,a.jsx)(t.p,{children:"In this case, here's what the stateless widget version would look like:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",metastring:'title="lib/common/widgets/ggc_fab.dart"',children:"class GgcFAB extends StatelessWidget {\n const GgcFAB(\n {super.key, required this.route, this.pathParameters = const {}});\n\n final String route;\n final Map pathParameters;\n\n @override\n Widget build(BuildContext context) {\n return FloatingActionButton(\n onPressed: () {\n context.pushNamed(route, pathParameters: pathParameters);\n },\n child: const Icon(Icons.add),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The following code illustrates the very minimal differences in how they are called:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" Widget? getFloatingActionButton(BuildContext context, int selectedIndex) {\n if (selectedIndex == 0) {\n return GgcFAB(route: AppRoute.createPlanting.name);\n }\n if (selectedIndex == 3) {\n return makeFABWithParameters(AppRoute.createGardenTask.name,\n {'gardenID': widget.gardenID}, context);\n }\n return null;\n }\n"})}),"\n",(0,a.jsx)(t.p,{children:"It gets a little nicer if you convert to the stateless widget approach entirely, since you can tighten up the return type and remove the context argument:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" GgcFAB? getFloatingActionButton(int selectedIndex) {\n if (selectedIndex == 0) {\n return GgcFAB(route: AppRoute.createPlanting.name);\n }\n if (selectedIndex == 3) {\n return GgcFAB(route: AppRoute.createGardenTask.name,\n pathParameters: {'gardenID': widget.gardenID});\n }\n return null;\n }\n"})}),"\n",(0,a.jsx)(t.h2,{id:"dont-repeat-titles",children:"Don't repeat titles"}),"\n",(0,a.jsx)(t.p,{children:"The title should appear in the scaffold. It does not need to be repeated in the body:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/repeated-title.png"}),"\n",(0,a.jsx)(t.h2,{id:"prefer-late-to-dummy-field-values",children:"Prefer late to dummy field values"}),"\n",(0,a.jsx)(t.p,{children:'Sometimes you need to create an entity that has required fields before you know what those fields are. It is tempting to create a "dummy" entity with clearly incorrect values and then overwrite the fields once you know what the correct values are. For example, here\'s some code from TaskCard:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" Planting updatedPlanting = Planting(\n plantingID: 'plantingID',\n chapterID: 'chapterID',\n gardenID: 'gardenID',\n cropID: 'cropID',\n cropName: 'cropName',\n lastUpdate: DateTime.now());\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n"})}),"\n",(0,a.jsxs)(t.p,{children:["You can make the code shorter, and communicate your intent more clearly, by using the ",(0,a.jsx)(t.code,{children:"late"})," keyword:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" late Planting updatedPlanting;\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n"})}),"\n",(0,a.jsxs)(t.p,{children:["A more important reason to use ",(0,a.jsx)(t.code,{children:"late"})," is that if you fail to initialize the entity, you will get a runtime error that clearly indicates the problem, rather than a runtime error that initially seems unrelated (i.e. failure to find a chapterID)."]}),"\n",(0,a.jsx)(t.h2,{id:"case-study-task-card",children:"Case Study: Task Card"}),"\n",(0,a.jsx)(t.p,{children:"I've recently refactored the code for Task Cards and believe a short description of the experience could provide some insight into our current design and coding best practices."}),"\n",(0,a.jsx)(t.p,{children:"The GGC Task Card (at the time of writing) looked like this:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/task-card.png"}),"\n",(0,a.jsx)(t.p,{children:'As you can see, the "description" is a little wordy. My initial goal was to simply change the implementation of this card so that the description would be more tabular in nature, provide the garden and bed names (if available) from the task document, and include the description field only in the case of "custom" tasks.'}),"\n",(0,a.jsx)(t.h3,{id:"the-problem",children:"The problem"}),"\n",(0,a.jsxs)(t.p,{children:["So, I went to ",(0,a.jsx)(t.code,{children:"task_card.dart"}),", and discovered this:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:intl/intl.dart';\nimport 'package:jiffy/jiffy.dart';\n\nimport '../../../repositories/firestore/firestore_path.dart';\nimport '../../../repositories/firestore/firestore_service.dart';\nimport '../../../router.dart';\nimport '../../chapter/domain/chapter_collection.dart';\nimport '../../common/widgets/ggc_card.dart';\nimport '../../common/widgets/ggc_loading_indicator.dart';\nimport '../../garden/domain/garden_collection.dart';\nimport '../../global_snackbar.dart';\nimport '../../planting/domain/planting.dart';\nimport '../../user/domain/user_collection.dart';\nimport '../domain/task.dart';\n\nclass TaskCard extends StatefulWidget {\n final Task task;\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n final bool readOnly;\n\n const TaskCard(\n {super.key,\n required this.task,\n required this.chapters,\n required this.gardens,\n required this.users,\n required this.readOnly});\n\n @override\n State createState() => _TaskCardState();\n}\n\nenum TaskCardAction { updateTask, deleteTask }\n\nclass _TaskCardState extends State {\n final _service = FirestoreService.instance;\n bool _isWorking = false;\n bool isChecked = false;\n TaskCardAction? _selectedAction;\n\n Future getPlanting(String plantingID) {\n return _service.fetchDocument(\n path: FirestorePath.planting(plantingID),\n builder: (data, documentId) => Planting.fromJson(data!));\n }\n\n @override\n Widget build(BuildContext context) {\n DateTime now = DateTime.now();\n bool late = widget.task.dueDate.isBefore(now);\n final difference = widget.task.dueDate.difference(now);\n final days = difference.inDays;\n String dateStr = '';\n if (days > 60) {\n dateStr = DateFormat.yMd().format(widget.task.dueDate);\n } else {\n dateStr = Jiffy.parseFromDateTime(widget.task.dueDate).fromNow();\n }\n TextStyle? textStyle;\n if (late) {\n textStyle = TextStyle(\n color: Theme.of(context).colorScheme.error,\n // fontWeight: FontWeight.bold\n );\n }\n double width = MediaQuery.of(context).size.width;\n if (!widget.readOnly) {\n // compensate for the checkbox\n width = width - 50;\n }\n width = width - 120;\n List> popupMenuItems = [\n const PopupMenuItem(\n value: TaskCardAction.updateTask,\n child: Text('Update Task'),\n ),\n const PopupMenuItem(\n value: TaskCardAction.deleteTask, child: Text('Delete Task'))\n ];\n\n return _isWorking\n ? const GgcLoadingIndicator()\n : GgcCard(\n child: ListTile(\n dense: false,\n contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n horizontalTitleGap: 6,\n //Code runs with this line commented out but theme isn't used.\n // tileColor: tileColor,\n title: Row(\n children: [\n SizedBox(\n width: width,\n child: Text(widget.task.title,\n style: textStyle,\n softWrap: false,\n overflow: TextOverflow.ellipsis)),\n const Spacer(),\n PopupMenuButton(\n initialValue: _selectedAction,\n onSelected: (TaskCardAction result) {\n setState(() {\n _selectedAction = result;\n });\n switch (result) {\n case TaskCardAction.updateTask:\n context.pushNamed(AppRoute.taskUpdate.name,\n pathParameters: {\n 'taskID': widget.task.taskID,\n 'gardenID': widget.task.gardenID\n });\n break;\n case TaskCardAction.deleteTask:\n context.pushNamed(AppRoute.taskDelete.name,\n pathParameters: {\n 'gardenID': widget.task.gardenID,\n 'taskID': widget.task.taskID\n });\n break;\n }\n },\n itemBuilder: (BuildContext context) => popupMenuItems),\n ],\n ),\n subtitle: Text('${widget.task.description} Due $dateStr',\n style: textStyle),\n isThreeLine: true,\n leading: !widget.readOnly\n ? Checkbox(\n checkColor: Theme.of(context).primaryColor,\n fillColor: MaterialStateProperty.resolveWith(\n (Set states) {\n if (states.contains(MaterialState.pressed)) {\n return Theme.of(context)\n .primaryColor; // Color when checkbox is checked\n }\n return Colors\n .transparent; // Transparent fill color when checkbox is not checked\n }),\n value: isChecked,\n onChanged: (bool? value) async {\n if (value == true) {\n DateTime? completedDate = await showDatePicker(\n context: context,\n helpText:\n 'When did you complete ${widget.task.title}?',\n initialDate: widget.task.dueDate,\n firstDate: DateTime(2020),\n lastDate: DateTime((DateTime.now().year + 1)));\n if (completedDate != null) {\n setState(() {\n _isWorking = true;\n });\n updatePlanting(widget.task, completedDate)\n .then((_) => setState(() {\n _isWorking = false;\n }));\n }\n }\n },\n )\n : null,\n ),\n );\n }\n\n Future updatePlanting(Task task, DateTime completedDate) async {\n String plantingID = widget.task.plantingID;\n Planting planting = await getPlanting(plantingID);\n late Planting updatedPlanting;\n switch (task.taskType) {\n case 'sow':\n updatedPlanting = planting.copyWith(\n startDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'transplant':\n updatedPlanting = planting.copyWith(\n transplantDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'firstHarvest':\n updatedPlanting = planting.copyWith(\n firstHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'endHarvest':\n updatedPlanting = planting.copyWith(\n endHarvestDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'pull':\n updatedPlanting = planting.copyWith(\n pullDate: completedDate, lastUpdate: DateTime.now());\n break;\n case 'other':\n // TODO: implement other what do we do if they are finishing a non planting task?\n break;\n }\n // update the planting if completed\n if (updatedPlanting.plantingID != 'plantingID') {\n _service\n .setData(\n path: FirestorePath.planting(updatedPlanting.plantingID),\n data: updatedPlanting.toJson())\n .then((val) => GlobalSnackBar.show('Planting update succeeded.'))\n .catchError((e) =>\n GlobalSnackBar.show('Planting update failed\\n${e.toString()}.'));\n }\n // remove the task\n deleteTask(task);\n }\n\n Future deleteTask(Task task) async {\n _service\n .deleteData(path: FirestorePath.task(task.taskID))\n .then((val) => GlobalSnackBar.show('Task delete succeeded.'))\n .catchError(\n (e) => GlobalSnackBar.show('Task delete failed\\n${e.toString()}.'));\n }\n}\n"})}),"\n",(0,a.jsxs)(t.p,{children:["Here are a few of the things I noticed about ",(0,a.jsx)(t.code,{children:"task_card.dart"}),":"]}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"It is over 200 LOC. Generally, our top-level Card implementations are around 50 LOC. This is a red flag."}),"\n",(0,a.jsx)(t.li,{children:'The implementation is what I would call "flat", or "inline". In other words, there is no modularization of the TaskCard UI components. You can see this by looking at the import statements: there is not a single import of a widget in the same directory.'}),"\n",(0,a.jsx)(t.li,{children:"The code to implement the popup menu is approximately 35 LOC, but is scattered across 100 LOC."}),"\n",(0,a.jsx)(t.li,{children:'The implementation of a UI component (TaskCard) includes code making asynchronous database calls. Our current best practice calls for the use of "mutator controllers" to bridge between UI components and the backend database.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"These issues make understanding this single file of code difficult. For example:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"How and where should I change to code to conditionally format the description field based on task type?"}),"\n",(0,a.jsxs)(t.li,{children:["What are the functions of the ",(0,a.jsx)(t.code,{children:"_isWorking"}),", ",(0,a.jsx)(t.code,{children:"_isChecked"}),", and ",(0,a.jsx)(t.code,{children:"_selectedAction"})," state variables?"]}),"\n",(0,a.jsx)(t.li,{children:'Changes or enhancements following this "inline" design will make this code even more complicated. At some point, it will become very difficult to understand and maintain.'}),"\n"]}),"\n",(0,a.jsx)(t.h3,{id:"one-solution",children:"One solution"}),"\n",(0,a.jsx)(t.p,{children:"There are two simple design patterns that I used to modularize and simplify the code so that I could implement my table-based description enhancement."}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"I made each visible UI component into its own widget."}),' Looking at the TaskCard, an obvious top-level decomposition is into two Widgets: a "Title" widget and a "Description" widget. The "Title" widget can be further decomposed into three widgets: a "Checkbox", "Title", and "PopUp Menu". The following annotated screenshot of the TaskCard illustrates this breakdown with the top-level decomposition in red and the nested decomposition in green:']}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/task-card-widgets.png"}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.strong,{children:"I used the mutator controller design pattern to move the database access code out of the UI component and into the controller."})," Interestingly, this not only made the DB access code more simple, it even made it a bit more efficient because multiple collections needed updates and the mutator controller supports batch updates."]}),"\n",(0,a.jsxs)(t.p,{children:["After implementing these changes, ",(0,a.jsx)(t.code,{children:"task_card.dart"})," now looks like this:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"import 'package:flutter/material.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\n\nimport '../../chapter/domain/chapter_collection.dart';\nimport '../../common/widgets/ggc_card.dart';\nimport '../../common/widgets/ggc_error.dart';\nimport '../../common/widgets/ggc_loading_indicator.dart';\nimport '../../garden/domain/garden_collection.dart';\nimport '../../global_snackbar.dart';\nimport '../../user/domain/user_collection.dart';\nimport '../domain/task.dart';\nimport 'mutate_task_controller.dart';\nimport 'task_card_description.dart';\nimport 'task_card_title_row.dart';\n\ntypedef OnCompletedCallback = void Function(Task task, DateTime completedDate);\n\nclass TaskCard extends ConsumerWidget {\n final Task task;\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n final bool readOnly;\n\n const TaskCard(\n {super.key,\n required this.task,\n required this.chapters,\n required this.gardens,\n required this.users,\n required this.readOnly});\n\n @override\n Widget build(BuildContext context, WidgetRef ref) {\n void onCompleted(Task task, DateTime completedDate) {\n ref.read(mutateTaskControllerProvider.notifier).completeTask(\n task: task,\n completedDate: completedDate,\n onSuccess: () {\n GlobalSnackBar.show('Task completed.');\n });\n }\n\n AsyncValue asyncUpdate = ref.watch(mutateTaskControllerProvider);\n return asyncUpdate.when(\n data: (_) => GgcCard(\n child: Column(children: [\n TaskCardTitleRow(task: task, onCompleted: onCompleted),\n TaskCardDescription(task: task),\n ])),\n loading: () => const GgcLoadingIndicator(),\n error: (e, st) => GgcError(e.toString(), st.toString()));\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Let's see how the problems with the original implementation have been addressed."}),"\n",(0,a.jsxs)(t.p,{children:["First, the size of ",(0,a.jsx)(t.code,{children:"task_card.dart"}),' is now around 50 lines of code, back to a typical size for a GGC "Card" UI component.']}),"\n",(0,a.jsx)(t.p,{children:"Second, the UI code is modularized into five widgets: TaskCard, TaskCardTitleRow, TaskCardDescription, TaskCardCheckbox, and TaskCardPopupMenu."}),"\n",(0,a.jsx)(t.p,{children:"Third, the code to implement the PopupMenu is now encapsulated within a single widget. Interestingly, this refactoring revealed that there is a popup menu in ObservationCard with a very similar structure! It would be straight forward to do an additional refactoring to create a single generic popup menu (for example, GgcPopupMenu) that can be used anywhere we need one."}),"\n",(0,a.jsx)(t.p,{children:"Fourth, as already noted, the asynchronous DB access code is now entirely encapsulated within the completeTask method of the mutator. The completeTask method is 25 LOC, while the original inline approach required approximately 60 LOC. That is a significant simplification."}),"\n",(0,a.jsx)(t.p,{children:"Finally, here's what my new version of TaskCard looks like:"}),"\n",(0,a.jsx)("img",{width:"300px",style:{borderStyle:"solid"},src:"/img/develop/release-1.0/coding-standards/tasks-revised.png"}),"\n",(0,a.jsx)(t.p,{children:'The top and bottom tasks are "implicit" tasks (based on Planting dates), while the middle task is an "explicit" task (defined by the gardener.)'}),"\n",(0,a.jsx)(t.p,{children:'No code is ever "perfect" or "complete". I am sure that there are more improvements to be made to TaskCard. But I hope this case study helps improve our collective intuition about how to design and implement Flutter code.'})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>o});var a=n(7294);const i={},r=a.createContext(i);function o(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f3759001.356c3aa2.js b/assets/js/f3759001.356c3aa2.js new file mode 100644 index 000000000..cb2f6ae06 --- /dev/null +++ b/assets/js/f3759001.356c3aa2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[7346],{1714:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>n,metadata:()=>o,toc:()=>l});var r=a(5893),i=a(1151);const n={hide_table_of_contents:!1},s="Architecture",o={id:"develop/architecture",title:"Architecture",description:'The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.',source:"@site/docs/develop/architecture.md",sourceDirName:"develop",slug:"/develop/architecture",permalink:"/docs/develop/architecture",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"},next:{title:"Deployment",permalink:"/docs/develop/deployment"}},c={},l=[{value:"Client-server architecture perspective",id:"client-server-architecture-perspective",level:2},{value:"Layered application architecture perspective",id:"layered-application-architecture-perspective",level:2},{value:"Directory structure perspective",id:"directory-structure-perspective",level:2},{value:"Data flow perspective",id:"data-flow-perspective",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",strong:"strong",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,r.jsxs)(t.p,{children:['The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the ',(0,r.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/architecture/",children:"architecture module"})," in my mobile application development course."]}),"\n",(0,r.jsx)(t.h2,{id:"client-server-architecture-perspective",children:"Client-server architecture perspective"}),"\n",(0,r.jsxs)(t.p,{children:["To begin, GGC can be viewed as a simple client-server application: there is a central back-end server (in our case, ",(0,r.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Firestore"}),") that communicates with front-end clients (in our case, the Flutter ggc_app application):"]}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-architecture.png"}),"\n",(0,r.jsx)(t.h2,{id:"layered-application-architecture-perspective",children:"Layered application architecture perspective"}),"\n",(0,r.jsx)(t.p,{children:"In the above diagram, the ggc_app is represented as four layers. This layering is strict, in that each layer communicates only with the layer above and below it. Let's introduce each layer, moving from bottom to top:"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Repository Layer"}),". This bottom-most layer implements generic code for communication with Firebase: querying collections for documents; adding, deleting, and modifying documents, and so forth. In this layer, data is represented in JSON format (for entities) or binary format (for images)."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Data Layer"}),'. The data layer implements "feature-specific" communication with Firebase. For example, the Data Layer code for the Chapter feature implements classes that queries the Chapter collection in appropriate ways. In this layer, data is still represented as JSON or binary.']}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Domain Layer"}),'. The domain layer implements code to translate between the data representations used at the data layer (i.e. JSON and Binary) and the data representations used at the Presentation Layer (i.e. Dart classes for entities and collections). The domain layer also implements the "business logic" of the application as discussed in the ',(0,r.jsx)(t.a,{href:"/docs/develop/design/data-model#collections-and-business-logic",children:"Collections and business logic"})," section."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Presentation Layer"}),'. The presentation layer implements the Flutter-based user interface. All the classes at the presentation layer are Widgets. GGC implements many kinds of UI objects; for example, "Screens" and "Views". Screens implement a "top-level" page: they return a Scaffold Widget and they can be routed to. Views are "components": they are the building blocks for Screens and can potentially appear in multiple Screens.']}),"\n",(0,r.jsx)(t.h2,{id:"directory-structure-perspective",children:"Directory structure perspective"}),"\n",(0,r.jsx)(t.p,{children:"There is a relatively straightforward correspondence between the above layers and the directory structure in the ggc_app repository. The top-level of the repo is more or less like any Flutter app. Here is a semi-annotated version of some of the top-level files and directories to give you an idea of the organization:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"ggc_app/\n .github/ # GitHub Actions\n android/ # (Managed by Flutter)\n assets/ # Static data resources\n ios/ # (Managed by Flutter)\n integration_test # Test code \n lib/ # Source code \n linux/ # (Managed by Flutter)\n macos/ # (Managed by Flutter)\n stories/ # Monarch code\n web/ # (Managed by Flutter)\n windows/ # (Managed by Flutter)\n analysis_options.yaml\n pubspec.yaml\n run_build_runner.sh # Example of a GGC script \n"})}),"\n",(0,r.jsx)(t.p,{children:"The lib/ directory is where most of the action is. Here's a semi-annotated perspective of some the top-level of the lib/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:'lib/\n features/ # Feature-based organization for Data, Domain, and Presentation layers\n repositories/ # Implements the "Repository" layer\n theme/ # UI Theme (fonts, colors, etc.\n main.dart # Main entry point\n main_test_fixture.dart # An entry point that uses the test fixture data.\n router.dart # Implements routes using go_router \n'})}),"\n",(0,r.jsx)(t.p,{children:"Finally, here's a look inside the features/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"features/\n admin/\n authentication/ # Authentication using firebase_ui_auth.\n /presentation # Implementation only requires UI widgets.\n common/ # Cross-cutting code\n chapter/ # Implementation of Chapter feature\n data/ # Firebase interface\n domain/ # Chapter, ChapterCollection, etc.\n presentation/ # ChapterIndexScreen, ChapterView, etc. \n crop/\n garden/\n gardener/\n home/\n observation/\n :\n :\n"})}),"\n",(0,r.jsx)(t.p,{children:"Each feature can have one or more of the following subdirectories: domain/, data/, and presentation/. The authentication feature only requires a presentation/ subdirectory, while the chapter feature requires all three."}),"\n",(0,r.jsx)(t.h2,{id:"data-flow-perspective",children:"Data flow perspective"}),"\n",(0,r.jsx)(t.p,{children:"A final architectural perspective illustrates the way data flows between external sources (i.e. Firestore) and the app. Here is a diagram that illustrates some of the key components:"}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-dataflow-diagram.png"}),"\n",(0,r.jsx)(t.p,{children:'Note that at the Firestore level, data is stored in a set of "flat" collections corresponding in many cases to "features" (i.e. Beds, Chapters, Crops, etc.). For the most part, the data model is similar to the table structure favored by SQL databases, even though Firestore is a document-oriented (NoSQL) database.'}),"\n",(0,r.jsx)(t.p,{children:'This diagram also illustrates the way GGC addresses the issue of communication with external services, which is intrinsically asynchronous. In asynchronous communications, there will be an unpredictable delay between the "request" (i.e. to retrieve or to store data) and the "response" (i.e. the request succeeded or failed). During this time, the UI should show some sort of "loading" indicator rather than freezing, or potentially just as bad, allowing the user to interact further with the UI. Finally, if the request fails (network or service is down), the UI should indicate the failure.'}),"\n",(0,r.jsx)(t.p,{children:'Managing asynchronous communication in code is complex. In GGC, we have encapsulated the complexities of asynchronous communication into two mechanisms: the "With" widgets (for retrieval of data asynchronously), and the MutateController (for persisting data asynchronously). What this provides is the ability to write app code with clearly defined boundaries between asynchronous and synchronous actions, which simplifies the code and reduces the chances for bugs in data storage and retrieval.'}),"\n",(0,r.jsxs)(t.p,{children:['The "With" widgets (such as WithCoreData, WithObservationData, WithUserGardenData, WithAllData) are responsible for setting up Riverpod providers that watch the various Firestore collections in various ways, and the populating an instance of each of three classes (ChapterCollection, GardenCollection, and UserCollection) with the data retrieved. Each With widget is responsible for displaying a "loading" icon while it waits for the required data from Firestore to arrive (or display an error if one occurs). Once the data arrives, it invokes a callback function supplied to it, passing the populated ChapterCollection, GardenCollection, and UserCollection instances. As a bonus, if any of the Firestore collections are updated by some other client, the use of Riverpod means that the widget will be automatically rebuilt, refreshing the UI with the changed data automatically. Consult the ',(0,r.jsx)(t.a,{href:"/docs/develop/design/with-widgets",children:'"With" Widgets Design Pattern'})," documentation for more details."]}),"\n",(0,r.jsx)(t.p,{children:"While the With widgets address the issue of asynchronous data retrieval, we must also address the issue of asynchronous data persistence. It's the same problem, in reverse: when the app wants to persist data back to Firestore, the storage request will take an unpredictable amount of time, we'll want the UI to display a loading indicator during that time, and if an unforeseen error occurs, we'll want to show that to the user."}),"\n",(0,r.jsxs)(t.p,{children:["GGC addresses the persistence problem with a single class called MutateController. Whenever UI code wants to update the database, it invokes this controller which takes a large number of optional arguments to support any combination of updates to the underlying entities. Consult the ",(0,r.jsx)(t.a,{href:"/docs/develop/design/data-mutation",children:"Data Mutation Design Pattern"})," documentation for more details."]})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>o,a:()=>s});var r=a(7294);const i={},n=r.createContext(i);function s(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f3759001.ecb01d32.js b/assets/js/f3759001.ecb01d32.js deleted file mode 100644 index 38d6174e5..000000000 --- a/assets/js/f3759001.ecb01d32.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[7346],{1714:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var n=r(5893),i=r(1151);const a={hide_table_of_contents:!1},s="Architecture",o={id:"develop/architecture",title:"Architecture",description:'The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.',source:"@site/docs/develop/architecture.md",sourceDirName:"develop",slug:"/develop/architecture",permalink:"/docs/develop/architecture",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"},next:{title:"Deployment",permalink:"/docs/develop/deployment"}},c={},l=[{value:"Client-server architecture perspective",id:"client-server-architecture-perspective",level:2},{value:"Layered application architecture perspective",id:"layered-application-architecture-perspective",level:2},{value:"Directory structure perspective",id:"directory-structure-perspective",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",strong:"strong",...(0,i.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,n.jsxs)(t.p,{children:['The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the ',(0,n.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/architecture/",children:"architecture module"})," in my mobile application development course."]}),"\n",(0,n.jsx)(t.h2,{id:"client-server-architecture-perspective",children:"Client-server architecture perspective"}),"\n",(0,n.jsxs)(t.p,{children:["To begin, GGC can be viewed as a simple client-server application: there is a central back-end server (in our case, ",(0,n.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Firestore"}),") that communicates with front-end clients (in our case, the Flutter ggc_app application):"]}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/ggc-architecture.png"}),"\n",(0,n.jsx)(t.h2,{id:"layered-application-architecture-perspective",children:"Layered application architecture perspective"}),"\n",(0,n.jsx)(t.p,{children:"In the above diagram, the client app is structured as four layers. This layering is strict, in that each layer communicates only with the layer above and below it."}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"Repository Layer"}),". This bottom-most layer implements generic code for communication with Firebase: querying collections for documents; adding, deleting, and modifying documents, and so forth. In this layer, data is represented in JSON format (for entities) or binary format (for images)."]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"Data Layer"}),'. The data layer implements "feature-specific" communication with Firebase. For example, the Data Layer code for the Chapter feature implements classes that queries the Chapter collection in appropriate ways. In this layer, data is still represented as JSON or binary.']}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"Domain Layer"}),'. The domain layer implements code to translate between the data representations used at the data layer (i.e. JSON and Binary) and the data representations used at the Presentation Layer (i.e. Dart classes for entities and collections). The domain layer also implements the "business logic" of the application as discussed in the ',(0,n.jsx)(t.a,{href:"/docs/develop/design/data-model#collections-and-business-logic",children:"Collections and business logic"})," section."]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"Presentation Layer"}),'. The presentation layer implements the Flutter-based user interface. All of the classes at the presentation layer are Widgets. GGC divides UI classes into two types: "Screens" and "Views". Screens implement a "top-level" page: they return a Scaffold Widget and can be routed to. Views are "components": they are the building blocks for Screens and can potentially appear in multiple Screens.']}),"\n",(0,n.jsx)(t.h2,{id:"directory-structure-perspective",children:"Directory structure perspective"}),"\n",(0,n.jsx)(t.p,{children:"There is a relatively straightforward correspondence between the above layers and the directory structure in the ggc_app repository. The top-level of the repo is more or less like any Flutter app. Here is a semi-annotated version of most of the top-level files and directories:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:"ggc_app/\n .github/ # CI GitHub Actions\n android/\n assets/\n ios/\n lib/ # Source code here\n linux/\n macos/\n stories/ # Monarch stories here.\n test/\n web/\n windows/\n analysis_options.yaml\n build_runner.sh # Useful if you change data model.\n lakos.sh # Build a diagram of the architecture\n pubspec.yaml\n run_monarch.sh # Run the Monarch UI Story system\n"})}),"\n",(0,n.jsx)(t.p,{children:"The lib/ directory is where most of the action is. Here's a semi-annotated perspective of some the top-level of the lib/ directory:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:'lib/\n features/ # Feature-based organization for Data, Domain, and Presentation layers\n repositories/ # Implements the "Repository" layer\n main.dart # Main entry point\n router.dart # Implements routes using go_router \n theme_data.dart # Implements a theme using FlexColorScheme\n'})}),"\n",(0,n.jsx)(t.p,{children:"Finally, here's a look inside the features/ directory:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{children:"features/\n authentication/ # Authentication using firebase_ui_auth.\n /presentation # Implementation only requires UI widgets.\n \n common/ # Cross-cutting code\n \n chapter/ # Implementation of Chapter feature\n data/ # Firebase interface\n domain/ # Chapter, ChapterCollection, etc.\n presentation/ # ChapterIndexScreen, ChapterView, etc.\n \n crop/\n garden/\n gardener/\n home/\n observation/\n :\n :\n"})}),"\n",(0,n.jsx)(t.p,{children:"Each feature can have one or more of the following subdirectories: domain/, data/, and presentation/. The authentication feature only requires a presentation/ subdirectory, while the chapter feature requires all three."})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,r)=>{r.d(t,{Z:()=>o,a:()=>s});var n=r(7294);const i={},a=n.createContext(i);function s(e){const t=n.useContext(a);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),n.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/main.3278aba6.js b/assets/js/main.3278aba6.js new file mode 100644 index 000000000..236da44da --- /dev/null +++ b/assets/js/main.3278aba6.js @@ -0,0 +1,2 @@ +/*! For license information please see main.3278aba6.js.LICENSE.txt */ +(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[179],{723:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});n(7294);var r=n(8356),o=n.n(r),a=n(6887);const i={"0058b4c6":[()=>n.e(4088).then(n.t.bind(n,6462,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-175.json",6462],"0bd3a280":[()=>n.e(7222).then(n.bind(n,3581)),"@site/docs/home/food-security.md",3581],"0f1af657":[()=>n.e(7540).then(n.bind(n,9631)),"@site/docs/user-guide/badges.md",9631],"10921c5b":[()=>n.e(1744).then(n.bind(n,4437)),"@site/docs/develop/design/with-widgets.md",4437],"11f6a8a1":[()=>n.e(9866).then(n.bind(n,883)),"@site/blog/2023-02-10-welcome.md?truncated=true",883],"1506d638":[()=>n.e(1549).then(n.bind(n,8420)),"@site/docs/home/sneak-peek.md",8420],17896441:[()=>Promise.all([n.e(532),n.e(2700),n.e(7918)]).then(n.bind(n,9152)),"@theme/DocItem",9152],"17d8eee1":[()=>Promise.all([n.e(5655),n.e(3560)]).then(n.bind(n,3722)),"@site/docs/home/welcome.md",3722],"1863cff0":[()=>n.e(9929).then(n.bind(n,1965)),"@site/docs/user-guide/guided-tour.md",1965],"18fc9463":[()=>n.e(9601).then(n.bind(n,1928)),"@site/docs/user-guide/adding-plantings.md",1928],"1e5c498d":[()=>n.e(1217).then(n.bind(n,798)),"@site/docs/develop/testing.md",798],"1f391b9e":[()=>Promise.all([n.e(532),n.e(2700),n.e(3085)]).then(n.bind(n,4247)),"@theme/MDXPage",4247],"2450005c":[()=>n.e(1585).then(n.bind(n,6651)),"@site/docs/user-guide/overview.md",6651],"2cd8ab24":[()=>n.e(860).then(n.bind(n,7451)),"@site/docs/develop/releases/release-0.0/entrepreneur-feedback.md",7451],"2f9db241":[()=>n.e(6800).then(n.bind(n,8074)),"@site/docs/user-guide/observations.md",8074],"31caa863":[()=>n.e(4063).then(n.bind(n,2592)),"@site/docs/develop/deployment.md",2592],"3463d78f":[()=>n.e(8294).then(n.bind(n,8067)),"@site/docs/user-guide/adding-vendors-crops-varieties.md",8067],"36994c47":[()=>n.e(9208).then(n.t.bind(n,4468,19)),"@generated/docusaurus-plugin-content-blog/default/__plugin.json",4468],"38346c4b":[()=>n.e(5014).then(n.bind(n,1050)),"@site/docs/user-guide/explore-a-garden.md",1050],"393be207":[()=>n.e(7414).then(n.bind(n,9925)),"@site/src/pages/markdown-page.md",9925],"39838d4e":[()=>n.e(234).then(n.bind(n,8901)),"@site/docs/develop/onboarding.md",8901],"3ad77611":[()=>n.e(403).then(n.bind(n,9657)),"@site/docs/develop/design/badges.md",9657],"3b4579e8":[()=>n.e(9586).then(n.bind(n,3012)),"@site/docs/user-guide/scenarios.md",3012],"3bea7cd1":[()=>n.e(9256).then(n.bind(n,9340)),"@site/docs/business/index.md",9340],"3d832522":[()=>n.e(6414).then(n.bind(n,9797)),"@site/docs/business/roadmap.md",9797],"3e240fbf":[()=>n.e(5857).then(n.bind(n,7541)),"@site/docs/develop/releases/release-0.0/customer-feedback.md",7541],"49882d99":[()=>n.e(1937).then(n.bind(n,6949)),"@site/docs/user-guide/chat-rooms.md",6949],"505d7517":[()=>n.e(196).then(n.bind(n,4230)),"@site/docs/develop/releases/release-1.0/goals.md",4230],"59628a4d":[()=>n.e(6427).then(n.bind(n,6394)),"@site/docs/user-guide/seeds.md",6394],"5e95c892":[()=>n.e(9661).then(n.bind(n,1892)),"@theme/DocsRoot",1892],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,6809)),"@generated/docusaurus.config",6809],"6741c1a9":[()=>n.e(1420).then(n.bind(n,374)),"@site/blog/2023-02-10-welcome.md",374],"6b1fc3de":[()=>n.e(8754).then(n.bind(n,8312)),"@site/docs/develop/installation.md",8312],"772c3429":[()=>n.e(3844).then(n.bind(n,5537)),"@site/docs/user-guide/outcomes.md",5537],"7be5f79d":[()=>n.e(825).then(n.bind(n,6544)),"@site/docs/user-guide/registration.md",6544],"7d1225b6":[()=>n.e(4076).then(n.bind(n,7576)),"@site/docs/develop/backups.md",7576],"7d56ced7":[()=>n.e(9572).then(n.bind(n,616)),"@site/docs/develop/scripts.md",616],"814f3328":[()=>n.e(2535).then(n.t.bind(n,5641,19)),"~blog/default/blog-post-list-prop-default.json",5641],"906ac375":[()=>Promise.all([n.e(5655),n.e(6265)]).then(n.bind(n,8586)),"@site/docs/home/related-work.md",8586],"9140a56a":[()=>n.e(9095).then(n.bind(n,8002)),"@site/docs/develop/dart-analyze.md",8002],"95f4d37c":[()=>n.e(814).then(n.bind(n,4653)),"@site/docs/develop/index.md",4653],"96083bb9":[()=>n.e(183).then(n.bind(n,3473)),"@site/docs/business/market-size.md",3473],"968b4846":[()=>n.e(2743).then(n.bind(n,6729)),"@site/docs/develop/releases/release-1.0/onboarding-feedback.md",6729],"9e4087bc":[()=>n.e(3608).then(n.bind(n,3169)),"@theme/BlogArchivePage",3169],"9ebba4ea":[()=>n.e(6906).then(n.bind(n,1393)),"@site/docs/user-guide/terms-and-conditions.md",1393],a1e7621f:[()=>n.e(2889).then(n.bind(n,7920)),"@site/docs/develop/design/data-model.md",7920],a5b8d3e9:[()=>n.e(6957).then(n.bind(n,6058)),"@site/docs/develop/design/data-mutation.md",6058],a6aa9e1f:[()=>Promise.all([n.e(532),n.e(2700),n.e(8041),n.e(3089)]).then(n.bind(n,2727)),"@theme/BlogListPage",2727],a7456010:[()=>n.e(5980).then(n.t.bind(n,9365,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",9365],a7bd4aaa:[()=>n.e(8518).then(n.bind(n,4974)),"@theme/DocVersionRoot",4974],a8521eb9:[()=>n.e(186).then(n.bind(n,2522)),"@site/docs/develop/releases/release-1.0/end-of-season-feedback.md",2522],a94703ab:[()=>Promise.all([n.e(532),n.e(4368)]).then(n.bind(n,4547)),"@theme/DocRoot",4547],aba21aa0:[()=>n.e(3629).then(n.t.bind(n,1765,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",1765],acecf23e:[()=>n.e(7393).then(n.t.bind(n,1838,19)),"~blog/default/blogMetadata-default.json",1838],af21c641:[()=>n.e(6974).then(n.bind(n,4998)),"@site/docs/home/innovations.md",4998],afc29949:[()=>Promise.all([n.e(532),n.e(4e3)]).then(n.bind(n,4149)),"@site/docs/user-guide/downloading.md",4149],ba771284:[()=>n.e(9268).then(n.bind(n,7735)),"@site/docs/user-guide/tasks.md",7735],bc03b1b7:[()=>n.e(4713).then(n.bind(n,586)),"@site/docs/user-guide/geobot.md",586],bc1f8660:[()=>n.e(2446).then(n.bind(n,9504)),"@site/docs/develop/design/data-model-old.md",9504],bebdd554:[()=>n.e(6142).then(n.bind(n,1615)),"@site/docs/user-guide/privacy.md",1615],c03baef0:[()=>n.e(4057).then(n.bind(n,3794)),"@site/docs/home/team.md",3794],c15d9823:[()=>n.e(6642).then(n.t.bind(n,2506,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-bd9.json",2506],c401bc0d:[()=>n.e(1666).then(n.bind(n,6988)),"@site/docs/user-guide/explore-a-chapter.md",6988],c48bbb24:[()=>n.e(7937).then(n.bind(n,7683)),"@site/docs/user-guide/define-a-garden.md",7683],c4f5d8e4:[()=>Promise.all([n.e(532),n.e(5655),n.e(4195)]).then(n.bind(n,5629)),"@site/src/pages/index.js",5629],c7c467a1:[()=>n.e(2522).then(n.bind(n,4811)),"@site/docs/develop/coding-standards.md",4811],ccc49370:[()=>Promise.all([n.e(532),n.e(2700),n.e(8041),n.e(6103)]).then(n.bind(n,9209)),"@theme/BlogPostPage",9209],e2299c6d:[()=>n.e(1340).then(n.bind(n,3831)),"@site/docs/develop/releases/release-1.0/cvp.md",3831],e27695c2:[()=>n.e(4524).then(n.bind(n,6538)),"@site/docs/home/serious-gardeners.md",6538],e4eb6786:[()=>n.e(2967).then(n.bind(n,1630)),"@site/docs/develop/releases/release-0.0/design.md",1630],ec0f34d7:[()=>n.e(8653).then(n.bind(n,34)),"@site/docs/business/milestones.md",34],ed0568ab:[()=>n.e(8392).then(n.bind(n,9987)),"@site/docs/develop/design/input-fields.md",9987],f3759001:[()=>n.e(7346).then(n.bind(n,1714)),"@site/docs/develop/architecture.md",1714],f81c1134:[()=>n.e(4031).then(n.t.bind(n,4108,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-archive-f05.json",4108],ff0b8175:[()=>n.e(1).then(n.bind(n,1350)),"@site/docs/develop/releases/release-0.0/chatgpt-feedback.md",1350]};var s=n(5893);function l(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,s.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,s.jsx)("p",{children:String(t)}),(0,s.jsx)("div",{children:(0,s.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,s.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,s.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,s.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,s.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(9670),u=n(226);function d(e,t){if("*"===e)return o()({loading:l,loader:()=>n.e(1772).then(n.bind(n,1772)),modules:["@theme/NotFound"],webpack:()=>[1772],render(e,t){const n=e.default;return(0,s.jsx)(u.z,{value:{plugin:{name:"native",id:"default"}},children:(0,s.jsx)(n,{...t})})}});const r=a[`${e}-${t}`],d={},p=[],f=[],g=(0,c.Z)(r);return Object.entries(g).forEach((e=>{let[t,n]=e;const r=i[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),o().Map({loading:l,loader:d,modules:p,webpack:()=>f,render(t,n){const o=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let i=o;const s=n.split(".");s.slice(0,-1).forEach((e=>{i=i[e]})),i[s[s.length-1]]=a}));const a=o.__comp;delete o.__comp;const i=o.__context;delete o.__context;const l=o.__props;return delete o.__props,(0,s.jsx)(u.z,{value:i,children:(0,s.jsx)(a,{...o,...l,...n})})}})}const p=[{path:"/blog",component:d("/blog","d00"),exact:!0},{path:"/blog/2023/02/10/welcome",component:d("/blog/2023/02/10/welcome","229"),exact:!0},{path:"/blog/archive",component:d("/blog/archive","182"),exact:!0},{path:"/markdown-page",component:d("/markdown-page","3d7"),exact:!0},{path:"/docs",component:d("/docs","fc9"),routes:[{path:"/docs",component:d("/docs","2f2"),routes:[{path:"/docs",component:d("/docs","f38"),routes:[{path:"/docs/business",component:d("/docs/business","896"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/market-size",component:d("/docs/business/market-size","3a5"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/milestones",component:d("/docs/business/milestones","6b2"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/roadmap",component:d("/docs/business/roadmap","2c2"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/develop",component:d("/docs/develop","48a"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/architecture",component:d("/docs/develop/architecture","0cc"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/backups",component:d("/docs/develop/backups","e5f"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/coding-standards",component:d("/docs/develop/coding-standards","001"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/dart-analyze",component:d("/docs/develop/dart-analyze","4cc"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/deployment",component:d("/docs/develop/deployment","87a"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/badges",component:d("/docs/develop/design/badges","fc0"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/data-model",component:d("/docs/develop/design/data-model","c3c"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/data-model-old",component:d("/docs/develop/design/data-model-old","1c8"),exact:!0},{path:"/docs/develop/design/data-mutation",component:d("/docs/develop/design/data-mutation","7ce"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/input-fields",component:d("/docs/develop/design/input-fields","bde"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/with-widgets",component:d("/docs/develop/design/with-widgets","9f7"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/installation",component:d("/docs/develop/installation","7f5"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/onboarding",component:d("/docs/develop/onboarding","d33"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/chatgpt-feedback",component:d("/docs/develop/releases/release-0.0/chatgpt-feedback","596"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/customer-feedback",component:d("/docs/develop/releases/release-0.0/customer-feedback","fb7"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/design",component:d("/docs/develop/releases/release-0.0/design","35c"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/entrepreneur-feedback",component:d("/docs/develop/releases/release-0.0/entrepreneur-feedback","c03"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/cvp",component:d("/docs/develop/releases/release-1.0/cvp","391"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/end-of-season-feedback",component:d("/docs/develop/releases/release-1.0/end-of-season-feedback","a2f"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/goals",component:d("/docs/develop/releases/release-1.0/goals","088"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/onboarding-feedback",component:d("/docs/develop/releases/release-1.0/onboarding-feedback","e62"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/scripts",component:d("/docs/develop/scripts","c76"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/testing",component:d("/docs/develop/testing","ed4"),exact:!0,sidebar:"developSidebar"},{path:"/docs/home/food-security",component:d("/docs/home/food-security","05f"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/innovations",component:d("/docs/home/innovations","4fa"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/related-work",component:d("/docs/home/related-work","3cc"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/serious-gardeners",component:d("/docs/home/serious-gardeners","e3e"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/sneak-peek",component:d("/docs/home/sneak-peek","e8b"),exact:!0},{path:"/docs/home/team",component:d("/docs/home/team","322"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/welcome",component:d("/docs/home/welcome","e24"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/adding-plantings",component:d("/docs/user-guide/adding-plantings","36e"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/adding-vendors-crops-varieties",component:d("/docs/user-guide/adding-vendors-crops-varieties","949"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/badges",component:d("/docs/user-guide/badges","e10"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/chat-rooms",component:d("/docs/user-guide/chat-rooms","655"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/define-a-garden",component:d("/docs/user-guide/define-a-garden","e94"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/downloading",component:d("/docs/user-guide/downloading","791"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/explore-a-chapter",component:d("/docs/user-guide/explore-a-chapter","271"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/explore-a-garden",component:d("/docs/user-guide/explore-a-garden","ed5"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/geobot",component:d("/docs/user-guide/geobot","9e0"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/guided-tour",component:d("/docs/user-guide/guided-tour","cc9"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/observations",component:d("/docs/user-guide/observations","dfc"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/outcomes",component:d("/docs/user-guide/outcomes","d0c"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/overview",component:d("/docs/user-guide/overview","b62"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/privacy",component:d("/docs/user-guide/privacy","f6a"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/registration",component:d("/docs/user-guide/registration","0ae"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/scenarios",component:d("/docs/user-guide/scenarios","5f9"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/seeds",component:d("/docs/user-guide/seeds","5ba"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/tasks",component:d("/docs/user-guide/tasks","7c3"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/terms-and-conditions",component:d("/docs/user-guide/terms-and-conditions","bed"),exact:!0,sidebar:"homeSidebar"}]}]}]},{path:"/",component:d("/","2e1"),exact:!0},{path:"*",component:d("*")}]},8934:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,t:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{i(!0)}),[]),(0,o.jsx)(a.Provider,{value:n,children:t})}},2849:(e,t,n)=>{"use strict";var r=n(7294),o=n(745),a=n(405),i=n(3727),s=n(6809),l=n(412);const c=[n(2497),n(3310),n(8320),n(2295)];var u=n(723),d=n(6550),p=n(8790),f=n(5893);function g(e){let{children:t}=e;return(0,f.jsx)(f.Fragment,{children:t})}var h=n(5742),m=n(2263),b=n(4996),y=n(6668),v=n(8264),w=n(4711),k=n(9727);const x="default";var S=n(8780),_=n(197);function E(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,m.Z)(),r=(0,w.l)(),o=n[e].htmlLang,a=e=>e.replace("-","_");return(0,f.jsxs)(h.Z,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,f.jsx)("meta",{property:"og:locale",content:a(o)}),Object.values(n).filter((e=>o!==e.htmlLang)).map((e=>(0,f.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function C(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,m.Z)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,m.Z)(),{pathname:r}=(0,d.TH)();return e+(0,S.Do)((0,b.ZP)(r),{trailingSlash:n,baseUrl:t})}(),o=t?`${n}${t}`:r;return(0,f.jsxs)(h.Z,{children:[(0,f.jsx)("meta",{property:"og:url",content:o}),(0,f.jsx)("link",{rel:"canonical",href:o})]})}function T(){const{i18n:{currentLocale:e}}=(0,m.Z)(),{metadata:t,image:n}=(0,y.L)();return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(h.Z,{children:[(0,f.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,f.jsx)("body",{className:k.h})]}),n&&(0,f.jsx)(v.d,{image:n}),(0,f.jsx)(C,{}),(0,f.jsx)(E,{}),(0,f.jsx)(_.Z,{tag:x,locale:e}),(0,f.jsx)(h.Z,{children:t.map(((e,t)=>(0,f.jsx)("meta",{...e},t)))})]})}const j=new Map;var N=n(8934),P=n(8940),A=n(469);function L(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>o.forEach((e=>e?.()))}const O=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,A.Z)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,o=t.hash===n.hash,a=t.search===n.search;if(r&&o&&!a)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),L("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function R(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,p.f)(u.Z,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class I extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=l.Z.canUseDOM?L("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=L("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),R(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,f.jsx)(O,{previousLocation:this.previousLocation,location:t,children:(0,f.jsx)(d.AW,{location:t,render:()=>e})})}}const F=I,D="__docusaurus-base-url-issue-banner-container",M="__docusaurus-base-url-issue-banner",z="__docusaurus-base-url-issue-banner-suggestion-container";function B(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${D}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{let{route:t}=e;return!0===t.exact})))return j.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return j.set(e.pathname,t),{...e,pathname:t}}((0,d.TH)());return(0,f.jsx)(F,{location:e,children:G})}function Y(){return(0,f.jsx)(H.Z,{children:(0,f.jsx)(P.M,{children:(0,f.jsxs)(N.t,{children:[(0,f.jsxs)(g,{children:[(0,f.jsx)(Z,{}),(0,f.jsx)(T,{}),(0,f.jsx)(U,{}),(0,f.jsx)(q,{})]}),(0,f.jsx)(W,{})]})})})}var Q=n(6887);const K=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;o?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var X=n(9670);const J=new Set,ee=new Set,te=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ne={prefetch:e=>{if(!(e=>!te()&&!ee.has(e)&&!J.has(e))(e))return!1;J.add(e);const t=(0,p.f)(u.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(Q).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,X.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?K(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!te()&&!ee.has(e))(e)&&(ee.add(e),R(e))},re=Object.freeze(ne);function oe(e){let{children:t}=e;return"hash"===s.default.future.experimental_router?(0,f.jsx)(i.UT,{children:t}):(0,f.jsx)(i.VK,{children:t})}const ae=Boolean(!0);if(l.Z.canUseDOM){window.docusaurus=re;const e=document.getElementById("__docusaurus"),t=(0,f.jsx)(a.B6,{children:(0,f.jsx)(oe,{children:(0,f.jsx)(Y,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},i=()=>{if(window.docusaurusRoot)window.docusaurusRoot.render(t);else if(ae)window.docusaurusRoot=o.hydrateRoot(e,t,{onRecoverableError:n});else{const r=o.createRoot(e,{onRecoverableError:n});r.render(t),window.docusaurusRoot=r}};R(window.location.pathname).then((()=>{(0,r.startTransition)(i)}))}},8940:(e,t,n)=>{"use strict";n.d(t,{_:()=>d,M:()=>p});var r=n(7294),o=n(6809);const a=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"Next","isLast":true,"path":"/docs","mainDocId":"home/welcome","docs":[{"id":"business/index","path":"/docs/business/","sidebar":"businessSidebar"},{"id":"business/market-size","path":"/docs/business/market-size","sidebar":"businessSidebar"},{"id":"business/milestones","path":"/docs/business/milestones","sidebar":"businessSidebar"},{"id":"business/roadmap","path":"/docs/business/roadmap","sidebar":"businessSidebar"},{"id":"develop/architecture","path":"/docs/develop/architecture","sidebar":"developSidebar"},{"id":"develop/backups","path":"/docs/develop/backups","sidebar":"developSidebar"},{"id":"develop/coding-standards","path":"/docs/develop/coding-standards","sidebar":"developSidebar"},{"id":"develop/dart-analyze","path":"/docs/develop/dart-analyze","sidebar":"developSidebar"},{"id":"develop/deployment","path":"/docs/develop/deployment","sidebar":"developSidebar"},{"id":"develop/design/badges","path":"/docs/develop/design/badges","sidebar":"developSidebar"},{"id":"develop/design/data-model","path":"/docs/develop/design/data-model","sidebar":"developSidebar"},{"id":"develop/design/data-model-old","path":"/docs/develop/design/data-model-old"},{"id":"develop/design/data-mutation","path":"/docs/develop/design/data-mutation","sidebar":"developSidebar"},{"id":"develop/design/input-fields","path":"/docs/develop/design/input-fields","sidebar":"developSidebar"},{"id":"develop/design/with-widgets","path":"/docs/develop/design/with-widgets","sidebar":"developSidebar"},{"id":"develop/index","path":"/docs/develop/","sidebar":"developSidebar"},{"id":"develop/installation","path":"/docs/develop/installation","sidebar":"developSidebar"},{"id":"develop/onboarding","path":"/docs/develop/onboarding","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/chatgpt-feedback","path":"/docs/develop/releases/release-0.0/chatgpt-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/customer-feedback","path":"/docs/develop/releases/release-0.0/customer-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/design","path":"/docs/develop/releases/release-0.0/design","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/entrepreneur-feedback","path":"/docs/develop/releases/release-0.0/entrepreneur-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/cvp","path":"/docs/develop/releases/release-1.0/cvp","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/end-of-season-feedback","path":"/docs/develop/releases/release-1.0/end-of-season-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/goals","path":"/docs/develop/releases/release-1.0/goals","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/onboarding-feedback","path":"/docs/develop/releases/release-1.0/onboarding-feedback","sidebar":"developSidebar"},{"id":"develop/scripts","path":"/docs/develop/scripts","sidebar":"developSidebar"},{"id":"develop/testing","path":"/docs/develop/testing","sidebar":"developSidebar"},{"id":"home/food-security","path":"/docs/home/food-security","sidebar":"homeSidebar"},{"id":"home/innovations","path":"/docs/home/innovations","sidebar":"homeSidebar"},{"id":"home/related-work","path":"/docs/home/related-work","sidebar":"homeSidebar"},{"id":"home/serious-gardeners","path":"/docs/home/serious-gardeners","sidebar":"homeSidebar"},{"id":"home/sneak-peek","path":"/docs/home/sneak-peek"},{"id":"home/team","path":"/docs/home/team","sidebar":"homeSidebar"},{"id":"home/welcome","path":"/docs/home/welcome","sidebar":"homeSidebar"},{"id":"user-guide/adding-plantings","path":"/docs/user-guide/adding-plantings","sidebar":"homeSidebar"},{"id":"user-guide/adding-vendors-crops-varieties","path":"/docs/user-guide/adding-vendors-crops-varieties","sidebar":"homeSidebar"},{"id":"user-guide/badges","path":"/docs/user-guide/badges","sidebar":"homeSidebar"},{"id":"user-guide/chat-rooms","path":"/docs/user-guide/chat-rooms","sidebar":"homeSidebar"},{"id":"user-guide/define-a-garden","path":"/docs/user-guide/define-a-garden","sidebar":"homeSidebar"},{"id":"user-guide/downloading","path":"/docs/user-guide/downloading","sidebar":"homeSidebar"},{"id":"user-guide/explore-a-chapter","path":"/docs/user-guide/explore-a-chapter","sidebar":"homeSidebar"},{"id":"user-guide/explore-a-garden","path":"/docs/user-guide/explore-a-garden","sidebar":"homeSidebar"},{"id":"user-guide/geobot","path":"/docs/user-guide/geobot","sidebar":"homeSidebar"},{"id":"user-guide/guided-tour","path":"/docs/user-guide/guided-tour","sidebar":"homeSidebar"},{"id":"user-guide/observations","path":"/docs/user-guide/observations","sidebar":"homeSidebar"},{"id":"user-guide/outcomes","path":"/docs/user-guide/outcomes","sidebar":"homeSidebar"},{"id":"user-guide/overview","path":"/docs/user-guide/overview","sidebar":"homeSidebar"},{"id":"user-guide/privacy","path":"/docs/user-guide/privacy","sidebar":"homeSidebar"},{"id":"user-guide/registration","path":"/docs/user-guide/registration","sidebar":"homeSidebar"},{"id":"user-guide/scenarios","path":"/docs/user-guide/scenarios","sidebar":"homeSidebar"},{"id":"user-guide/seeds","path":"/docs/user-guide/seeds","sidebar":"homeSidebar"},{"id":"user-guide/tasks","path":"/docs/user-guide/tasks","sidebar":"homeSidebar"},{"id":"user-guide/terms-and-conditions","path":"/docs/user-guide/terms-and-conditions","sidebar":"homeSidebar"}],"draftIds":[],"sidebars":{"homeSidebar":{"link":{"path":"/docs/home/welcome","label":"home/welcome"}},"businessSidebar":{"link":{"path":"/docs/business/","label":"business/index"}},"developSidebar":{"link":{"path":"/docs/develop/","label":"develop/index"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var s=n(7529);const l=JSON.parse('{"docusaurusVersion":"3.5.2","siteVersion":"1.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.5.2"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.5.2"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.5.2"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.5.2"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.5.2"}}}');var c=n(5893);const u={siteConfig:o.default,siteMetadata:l,globalData:a,i18n:i,codeTranslations:s},d=r.createContext(u);function p(e){let{children:t}=e;return(0,c.jsx)(d.Provider,{value:u,children:t})}},4763:(e,t,n)=>{"use strict";n.d(t,{Z:()=>h});var r=n(7294),o=n(412),a=n(5742),i=n(8780),s=n(6040),l=n(226),c=n(5893);function u(e){let{error:t,tryAgain:n}=e;return(0,c.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,c.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,c.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,c.jsx)(d,{error:t})]})}function d(e){let{error:t}=e;const n=(0,i.BN)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,c.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function p(e){let{children:t}=e;return(0,c.jsx)(l.z,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:t})}function f(e){let{error:t,tryAgain:n}=e;return(0,c.jsx)(p,{children:(0,c.jsxs)(h,{fallback:()=>(0,c.jsx)(u,{error:t,tryAgain:n}),children:[(0,c.jsx)(a.Z,{children:(0,c.jsx)("title",{children:"Page Error"})}),(0,c.jsx)(s.Z,{children:(0,c.jsx)(u,{error:t,tryAgain:n})})]})})}const g=e=>(0,c.jsx)(f,{...e});class h extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){o.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??g)(e)}return e??null}}},412:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,o={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5742:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(405),o=n(5893);function a(e){return(0,o.jsx)(r.ql,{...e})}},3692:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(7294),o=n(3727),a=n(8780),i=n(2263),s=n(3919),l=n(412),c=n(8138),u=n(4996),d=n(5893);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:g,isActive:h,"data-noBrokenLinkCheck":m,autoAddBaseUrl:b=!0,...y}=e;const{siteConfig:v}=(0,i.Z)(),{trailingSlash:w,baseUrl:k}=v,x=v.future.experimental_router,{withBaseUrl:S}=(0,u.Cg)(),_=(0,c.Z)(),E=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>E.current));const C=p||f;const T=(0,s.Z)(C),j=C?.replace("pathname://","");let N=void 0!==j?(P=j,b&&(e=>e.startsWith("/"))(P)?S(P):P):void 0;var P;"hash"===x&&N?.startsWith("./")&&(N=N?.slice(1)),N&&T&&(N=(0,a.Do)(N,{trailingSlash:w,baseUrl:k}));const A=(0,r.useRef)(!1),L=n?o.OL:o.rU,O=l.Z.canUseIntersectionObserver,R=(0,r.useRef)(),I=()=>{A.current||null==N||(window.docusaurus.preload(N),A.current=!0)};(0,r.useEffect)((()=>(!O&&T&&l.Z.canUseDOM&&null!=N&&window.docusaurus.prefetch(N),()=>{O&&R.current&&R.current.disconnect()})),[R,N,O,T]);const F=N?.startsWith("#")??!1,D=!y.target||"_self"===y.target,M=!N||!T||!D||F&&"hash"!==x;m||!F&&M||_.collectLink(N),y.id&&_.collectAnchor(y.id);const z={};return M?(0,d.jsx)("a",{ref:E,href:N,...C&&!T&&{target:"_blank",rel:"noopener noreferrer"},...y,...z}):(0,d.jsx)(L,{...y,onMouseEnter:I,onTouchStart:I,innerRef:e=>{E.current=e,O&&e&&T&&(R.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(R.current.unobserve(e),R.current.disconnect(),null!=N&&window.docusaurus.prefetch(N))}))})),R.current.observe(e))},to:N,...n&&{isActive:h,activeClassName:g},...z})}const f=r.forwardRef(p)},1875:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const r=()=>null},5999:(e,t,n)=>{"use strict";n.d(t,{Z:()=>c,I:()=>l});var r=n(7294),o=n(5893);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(7529);function s(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function l(e,t){let{message:n,id:r}=e;return a(s({message:n,id:r}),t)}function c(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=s({message:t,id:n});return(0,o.jsx)(o.Fragment,{children:a(i,r)})}},9935:(e,t,n)=>{"use strict";n.d(t,{m:()=>r});const r="default"},3919:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function o(e){return void 0!==e&&!r(e)}n.d(t,{Z:()=>o,b:()=>r})},4996:(e,t,n)=>{"use strict";n.d(t,{Cg:()=>i,ZP:()=>s});var r=n(7294),o=n(2263),a=n(3919);function i(){const{siteConfig:e}=(0,o.Z)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router,s=(0,r.useCallback)(((e,r)=>function(e){let{siteUrl:t,baseUrl:n,url:r,options:{forcePrependBaseUrl:o=!1,absolute:i=!1}={},router:s}=e;if(!r||r.startsWith("#")||(0,a.b)(r))return r;if("hash"===s)return r.startsWith("/")?`.${r}`:`./${r}`;if(o)return n+r.replace(/^\//,"");if(r===n.replace(/\/$/,""))return n;const l=r.startsWith(n)?r:n+r.replace(/^\//,"");return i?t+l:l}({siteUrl:n,baseUrl:t,url:e,options:r,router:i})),[n,t,i]);return{withBaseUrl:s}}function s(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},8138:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var r=n(7294);n(5893);const o=r.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,r.useContext)(o);function i(){return a()}},2263:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8940);function a(){return(0,r.useContext)(o._)}},2389:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8934);function a(){return(0,r.useContext)(o._)}},469:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294);const o=n(412).Z.canUseDOM?r.useLayoutEffect:r.useEffect},5102:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(226);function a(){const e=r.useContext(o._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}},9670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function o(e){const t={};return function e(n,o){Object.entries(n).forEach((n=>{let[a,i]=n;const s=o?`${o}.${a}`:a;r(i)?e(i,s):t[s]=i}))}(e),t}},226:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,z:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(null);function i(e){let{children:t,value:n}=e;const i=r.useContext(a),s=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:i,value:n})),[i,n]);return(0,o.jsx)(a.Provider,{value:s,children:t})}},298:(e,t,n)=>{"use strict";n.d(t,{J:()=>y,L5:()=>m});var r=n(7294),o=n(143),a=n(9935),i=n(6668),s=n(812),l=n(902),c=n(5893);const u=e=>`docs-preferred-version-${e}`,d={save:(e,t,n)=>{(0,s.WA)(u(e),{persistence:t}).set(n)},read:(e,t)=>(0,s.WA)(u(e),{persistence:t}).get(),clear:(e,t)=>{(0,s.WA)(u(e),{persistence:t}).del()}},p=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const f=r.createContext(null);function g(){const e=(0,o._r)(),t=(0,i.L)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[a,s]=(0,r.useState)((()=>p(n)));(0,r.useEffect)((()=>{s(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function o(e){const t=d.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(d.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,o(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){d.save(e,t,n),s((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function h(e){let{children:t}=e;const n=g();return(0,c.jsx)(f.Provider,{value:n,children:t})}function m(e){let{children:t}=e;return(0,c.jsx)(h,{children:t})}function b(){const e=(0,r.useContext)(f);if(!e)throw new l.i6("DocsPreferredVersionContextProvider");return e}function y(e){void 0===e&&(e=a.m);const t=(0,o.zh)(e),[n,i]=b(),{preferredVersionName:s}=n[e];return{preferredVersion:t.versions.find((e=>e.name===s))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}},4731:(e,t,n)=>{"use strict";n.d(t,{V:()=>c,b:()=>l});var r=n(7294),o=n(902),a=n(5893);const i=Symbol("EmptyContext"),s=r.createContext(i);function l(e){let{children:t,name:n,items:o}=e;const i=(0,r.useMemo)((()=>n&&o?{name:n,items:o}:null),[n,o]);return(0,a.jsx)(s.Provider,{value:i,children:t})}function c(){const e=(0,r.useContext)(s);if(e===i)throw new o.i6("DocsSidebarProvider");return e}},9690:(e,t,n)=>{"use strict";n.d(t,{LM:()=>p,SN:()=>S,_F:()=>h,f:()=>b,lO:()=>w,oz:()=>k,s1:()=>v,vY:()=>x});var r=n(7294),o=n(6550),a=n(8790),i=n(143),s=n(8596),l=n(7392),c=n(298),u=n(3797),d=n(4731);function p(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=p(t);if(e)return e}}(e):void 0:e.href}const f=(e,t)=>void 0!==e&&(0,s.Mg)(e,t),g=(e,t)=>e.some((e=>h(e,t)));function h(e,t){return"link"===e.type?f(e.href,t):"category"===e.type&&(f(e.href,t)||g(e.items,t))}function m(e,t){switch(e.type){case"category":return h(e,t)||e.items.some((e=>m(e,t)));case"link":return!e.unlisted||h(e,t);default:return!0}}function b(e,t){return(0,r.useMemo)((()=>e.filter((e=>m(e,t)))),[e,t])}function y(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const o=[];return function e(t){for(const a of t)if("category"===a.type&&((0,s.Mg)(a.href,n)||e(a.items))||"link"===a.type&&(0,s.Mg)(a.href,n)){return r&&"category"!==a.type||o.unshift(a),!0}return!1}(t),o}function v(){const e=(0,d.V)(),{pathname:t}=(0,o.TH)(),n=(0,i.gA)()?.pluginData.breadcrumbs;return!1!==n&&e?y({sidebarItems:e.items,pathname:t}):null}function w(e){const{activeVersion:t}=(0,i.Iw)(e),{preferredVersion:n}=(0,c.J)(e),o=(0,i.yW)(e);return(0,r.useMemo)((()=>(0,l.jj)([t,n,o].filter(Boolean))),[t,n,o])}function k(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function x(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,l.jj)(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function S(e){let{route:t}=e;const n=(0,o.TH)(),r=(0,u.E)(),i=t.routes,s=i.find((e=>(0,o.LX)(n.pathname,e)));if(!s)return null;const l=s.sidebar,c=l?r.docsSidebars[l]:void 0;return{docElement:(0,a.H)(i),sidebarName:l,sidebarItems:c}}},3797:(e,t,n)=>{"use strict";n.d(t,{E:()=>l,q:()=>s});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function s(e){let{children:t,version:n}=e;return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,r.useContext)(i);if(null===e)throw new o.i6("DocsVersionProvider");return e}},143:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>h,gA:()=>p,_r:()=>u,Jo:()=>m,zh:()=>d,yW:()=>g,gB:()=>f});var r=n(6550),o=n(2263),a=n(9935);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,o.Z)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const s=e=>e.versions.find((e=>e.isLast));function l(e,t){const n=function(e,t){return[...e.versions].sort(((e,t)=>e.path===t.path?0:e.path.includes(t.path)?-1:t.path.includes(e.path)?1:0)).find((e=>!!(0,r.LX)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),o=n?.docs.find((e=>!!(0,r.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:o,alternateDocVersions:o?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(o.id):{}}}const c={},u=()=>i("docusaurus-plugin-content-docs")??c,d=e=>{try{return function(e,t,n){void 0===t&&(t=a.m),void 0===n&&(n={});const r=i(e),o=r?.[t];if(!o&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return o}("docusaurus-plugin-content-docs",e,{failfast:!0})}catch(t){throw new Error("You are using a feature of the Docusaurus docs plugin, but this plugin does not seem to be enabled"+("Default"===e?"":` (pluginId=${e}`),{cause:t})}};function p(e){void 0===e&&(e={});const t=u(),{pathname:n}=(0,r.TH)();return function(e,t,n){void 0===n&&(n={});const o=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.LX)(t,{path:n.path,exact:!1,strict:!1})})),a=o?{pluginId:o[0],pluginData:o[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function f(e){return d(e).versions}function g(e){const t=d(e);return s(t)}function h(e){const t=d(e),{pathname:n}=(0,r.TH)();return l(t,n)}function m(e){const t=d(e),{pathname:n}=(0,r.TH)();return function(e,t){const n=s(e);return{latestDocSuggestion:l(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},8320:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(4865),o=n.n(r);o().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{o().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){o().done()}}},3310:(e,t,n)=>{"use strict";n.r(t);var r=n(4798),o=n(6809);!function(e){const{themeConfig:{prism:t}}=o.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(6854),n(6046)(`./prism-${e}`)})),delete globalThis.Prism}(r.p1)},2503:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(6905),o=n(5999),a=n(6668),i=n(3692),s=n(8138);const l={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var c=n(5893);function u(e){let{as:t,id:n,...u}=e;const d=(0,s.Z)(),{navbar:{hideOnScroll:p}}=(0,a.L)();if("h1"===t||!n)return(0,c.jsx)(t,{...u,id:void 0});d.collectAnchor(n);const f=(0,o.I)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof u.children?u.children:n});return(0,c.jsxs)(t,{...u,className:(0,r.Z)("anchor",p?l.anchorWithHideOnScrollNavbar:l.anchorWithStickyNavbar,u.className),id:n,children:[u.children,(0,c.jsx)(i.Z,{className:"hash-link",to:`#${n}`,"aria-label":f,title:f,children:"\u200b"})]})}},9471:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);const r={iconExternalLink:"iconExternalLink_nPIU"};var o=n(5893);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,o.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,o.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},6040:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ft});var r=n(7294),o=n(6905),a=n(4763),i=n(8264),s=n(6550),l=n(5999),c=n(5936),u=n(5893);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,s.k6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,c.S)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const g=(0,l.I)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??g,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":g,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var m=n(5281),b=n(9727);const y={skipToContent:"skipToContent_fXgn"};function v(){return(0,u.jsx)(h,{className:y.skipToContent})}var w=n(6668),k=n(9689);function x(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:a,...i}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,u.jsx)("g",{stroke:r,strokeWidth:o,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const S={closeButton:"closeButton_CVFx"};function _(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,l.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,o.Z)("clean-btn close",S.closeButton,e.className),children:(0,u.jsx)(x,{width:14,height:14,strokeWidth:3.1})})}const E={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,w.L)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,o.Z)(E.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function j(){const{announcementBar:e}=(0,w.L)(),{isActive:t,close:n}=(0,k.n)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:a}=e;return(0,u.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:r,color:o},role:"banner",children:[a&&(0,u.jsx)("div",{className:T.announcementBarPlaceholder}),(0,u.jsx)(C,{className:T.announcementBarContent}),a&&(0,u.jsx)(_,{onClick:n,className:T.announcementBarClose})]})}var N=n(3163),P=n(2466);var A=n(902),L=n(3102);const O=r.createContext(null);function R(e){let{children:t}=e;const n=function(){const e=(0,N.e)(),t=(0,L.HY)(),[n,o]=(0,r.useState)(!1),a=null!==t.component,i=(0,A.D9)(a);return(0,r.useEffect)((()=>{a&&!i&&o(!0)}),[a,i]),(0,r.useEffect)((()=>{a?e.shown||o(!0):o(!1)}),[e.shown,a]),(0,r.useMemo)((()=>[n,o]),[n])}();return(0,u.jsx)(O.Provider,{value:n,children:t})}function I(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function F(){const e=(0,r.useContext)(O);if(!e)throw new A.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,o=(0,r.useCallback)((()=>n(!1)),[n]),a=(0,L.HY)();return(0,r.useMemo)((()=>({shown:t,hide:o,content:I(a)})),[o,a,t])}function D(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:a}=F();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,o.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var M=n(2949),z=n(2389);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function Z(e){let{className:t,buttonClassName:n,value:r,onChange:a}=e;const i=(0,z.Z)(),s=(0,l.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,l.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,l.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,o.Z)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,o.Z)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===r?"light":"dark"),disabled:!i,title:s,"aria-label":s,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,o.Z)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,o.Z)(U.toggleIcon,U.darkToggleIcon)})]})})}const H=r.memo(Z),V={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function W(e){let{className:t}=e;const n=(0,w.L)().navbar.style,r=(0,w.L)().colorMode.disableSwitch,{colorMode:o,setColorMode:a}=(0,M.I)();return r?null:(0,u.jsx)(H,{className:t,buttonClassName:"dark"===n?V.darkNavbarColorModeToggle:void 0,value:o,onChange:a})}var G=n(1327);function q(){return(0,u.jsx)(G.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Y(){const e=(0,N.e)();return(0,u.jsx)("button",{type:"button","aria-label":(0,l.I)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(x,{color:"var(--ifm-color-emphasis-600)"})})}function Q(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(q,{}),(0,u.jsx)(W,{className:"margin-right--md"}),(0,u.jsx)(Y,{})]})}var K=n(3692),X=n(4996),J=n(3919);function ee(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var te=n(9471);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:a,html:i,isDropdownLink:s,prependBaseUrlToHref:l,...c}=e;const d=(0,X.ZP)(r),p=(0,X.ZP)(t),f=(0,X.ZP)(o,{forcePrependBaseUrl:!0}),g=a&&o&&!(0,J.Z)(o),h=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,g&&(0,u.jsx)(te.Z,{...s&&{width:12,height:12}})]})};return o?(0,u.jsx)(K.Z,{href:l?f:o,...c,...h}):(0,u.jsx)(K.Z,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?ee(n,t.pathname):t.pathname.startsWith(p)},...c,...h})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const a=(0,u.jsx)(ne,{className:(0,o.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:a}):a}function oe(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,o.Z)("menu__link",t),...r})})}function ae(e){let{mobile:t=!1,position:n,...r}=e;const o=t?oe:re;return(0,u.jsx)(o,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(6043),se=n(8596),le=n(2263);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,se.Mg)(e.to,t)||!!ee(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:a,onClick:i,...s}=e;const l=(0,r.useRef)(null),[c,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{l.current&&!l.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[l]),(0,u.jsxs)("div",{ref:l,className:(0,o.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:s.to?void 0:"#",className:(0,o.Z)("navbar__link",a),...s,onClick:s.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!c))},children:s.children??s.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(Ee,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:i,...l}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,le.Z)(),{pathname:t}=(0,s.TH)();return t.replace(e,"/")}(),d=ue(t,c),{collapsed:p,toggleCollapsed:f,setCollapsed:g}=(0,ie.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&g(!d)}),[c,d,g]),(0,u.jsxs)("li",{className:(0,o.Z)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,o.Z)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...l,onClick:e=>{e.preventDefault(),f()},children:l.children??l.label}),(0,u.jsx)(ie.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(Ee,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var ge=n(4711);function he(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const me="iconLanguage_nlXk";var be=n(1875);const ye={navbarSearchContainer:"navbarSearchContainer_Bca1"};function ve(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,o.Z)(n,ye.navbarSearchContainer),children:t})}var we=n(143),ke=n(9690);var xe=n(298);function Se(e,t){return t.alternateDocVersions[e.name]??function(e){return e.docs.find((t=>t.id===e.mainDocId))}(e)}const _e={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:o="",...a}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:d}}=(0,le.Z)(),p=(0,ge.l)(),{search:f,hash:g}=(0,s.TH)(),h=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${g}${o}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...r],m=t?(0,l.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[i].label;return(0,u.jsx)(fe,{...a,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(he,{className:me}),m]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(ve,{className:n,children:(0,u.jsx)(be.Z,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:a=!1}=e;const i=a?"li":"div";return(0,u.jsx)(i,{className:(0,o.Z)({navbar__item:!r&&!a,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.vY)(t,r),s=a?.path===i?.path;return null===i||i.unlisted&&!s?null:(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>s||!!a?.sidebar&&a.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.oz)(t,r).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>a?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const a=(0,ke.lO)(r)[0],i=t??a.label,s=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,u.jsx)(ae,{...o,label:i,to:s})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:a,...i}=e;const{search:c,hash:d}=(0,s.TH)(),p=(0,we.Iw)(n),f=(0,we.gB)(n),{savePreferredVersionName:g}=(0,xe.J)(n),h=[...o,...f.map((function(e){const t=Se(e,p);return{label:e.label,to:`${t.path}${c}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>g(e.name)}})),...a],m=(0,ke.lO)(n)[0],b=t&&h.length>1?(0,l.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):m.label,y=t&&h.length>1?void 0:Se(m,p).path;return h.length<=1?(0,u.jsx)(ae,{...i,mobile:t,label:b,to:y,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...i,mobile:t,label:b,to:y,items:h,isActive:r?()=>!1:void 0})}};function Ee(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=_e[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(o,{...n})}function Ce(){const e=(0,N.e)(),t=(0,w.L)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(Ee,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function Te(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(l.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function je(){const e=0===(0,w.L)().navbar.items.length,t=F();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(Te,{onClick:()=>t.hide()}),t.content]})}function Ne(){const e=(0,N.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(D,{header:(0,u.jsx)(Q,{}),primaryMenu:(0,u.jsx)(Ce,{}),secondaryMenu:(0,u.jsx)(je,{})}):null}const Pe={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ae(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,o.Z)("navbar-sidebar__backdrop",e.className)})}function Le(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,w.L)(),i=(0,N.e)(),{navbarRef:s,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),o=(0,r.useRef)(!1),a=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,P.RF)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i=s?n(!1):i+c{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return o.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:s,"aria-label":(0,l.I)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,o.Z)("navbar","navbar--fixed-top",n&&[Pe.navbarHideable,!d&&Pe.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":i.shown}),children:[t,(0,u.jsx)(Ae,{onClick:i.toggle}),(0,u.jsx)(Ne,{})]})}var Oe=n(8780);const Re={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function Ie(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(l.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function Fe(e){let{error:t}=e;const n=(0,Oe.BN)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Re.errorBoundaryError,children:n})}class De extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const Me="right";function ze(e){let{width:t=30,height:n=30,className:r,...o}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...o,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function Be(){const{toggle:e,shown:t}=(0,N.e)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,l.I)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(ze,{})})}const $e={colorModeToggle:"colorModeToggle_DEke"};function Ue(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)(De,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(Ee,{...e})},t)))})}function Ze(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function He(){const e=(0,N.e)(),t=(0,w.L)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??Me)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return(0,u.jsx)(Ze,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(Be,{}),(0,u.jsx)(q,{}),(0,u.jsx)(Ue,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(Ue,{items:r}),(0,u.jsx)(W,{className:$e.colorModeToggle}),!o&&(0,u.jsx)(ve,{children:(0,u.jsx)(be.Z,{})})]})})}function Ve(){return(0,u.jsx)(Le,{children:(0,u.jsx)(He,{})})}function We(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:a,...i}=t,s=(0,X.ZP)(n),l=(0,X.ZP)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(K.Z,{className:"footer__link-item",...r?{href:a?l:r}:{to:s},...i,children:[o,r&&!(0,J.Z)(r)&&(0,u.jsx)(te.Z,{})]})}function Ge(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(We,{item:t})},t.href??t.to)}function qe(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(Ge,{item:e},t)))})]})}function Ye(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(qe,{column:e},t)))})}function Qe(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function Ke(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(We,{item:t})}function Xe(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(Ke,{item:e}),t.length!==n+1&&(0,u.jsx)(Qe,{})]},n)))})})}function Je(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(Ye,{columns:t}):(0,u.jsx)(Xe,{links:t})}var et=n(9965);const tt={footerLogoLink:"footerLogoLink_BH7S"};function nt(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.Cg)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(et.Z,{className:(0,o.Z)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function rt(e){let{logo:t}=e;return t.href?(0,u.jsx)(K.Z,{href:t.href,className:tt.footerLogoLink,target:t.target,children:(0,u.jsx)(nt,{logo:t})}):(0,u.jsx)(nt,{logo:t})}function ot(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function at(e){let{style:t,links:n,logo:r,copyright:a}=e;return(0,u.jsx)("footer",{className:(0,o.Z)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||a)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),a]})]})})}function it(){const{footer:e}=(0,w.L)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return(0,u.jsx)(at,{style:o,links:n&&n.length>0&&(0,u.jsx)(Je,{links:n}),logo:r&&(0,u.jsx)(rt,{logo:r}),copyright:t&&(0,u.jsx)(ot,{copyright:t})})}const st=r.memo(it),lt=(0,A.Qc)([M.S,k.p,P.OC,xe.L5,i.VC,function(e){let{children:t}=e;return(0,u.jsx)(L.n2,{children:(0,u.jsx)(N.M,{children:(0,u.jsx)(R,{children:t})})})}]);function ct(e){let{children:t}=e;return(0,u.jsx)(lt,{children:t})}var ut=n(2503);function dt(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(ut.Z,{as:"h1",className:"hero__title",children:(0,u.jsx)(l.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(Ie,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(Fe,{error:t})})]})})})}const pt={mainWrapper:"mainWrapper_z2l0"};function ft(e){const{children:t,noFooter:n,wrapperClassName:r,title:s,description:l}=e;return(0,b.t)(),(0,u.jsxs)(ct,{children:[(0,u.jsx)(i.d,{title:s,description:l}),(0,u.jsx)(v,{}),(0,u.jsx)(j,{}),(0,u.jsx)(Ve,{}),(0,u.jsx)("div",{id:d,className:(0,o.Z)(m.k.wrapper.main,pt.mainWrapper,r),children:(0,u.jsx)(a.Z,{fallback:e=>(0,u.jsx)(dt,{...e}),children:t})}),!n&&(0,u.jsx)(st,{})]})}},1327:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(3692),o=n(4996),a=n(2263),i=n(6668),s=n(9965),l=n(5893);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const a={light:(0,o.ZP)(t.src),dark:(0,o.ZP)(t.srcDark||t.src)},i=(0,l.jsx)(s.Z,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,l.jsx)("div",{className:r,children:i}):i}function u(e){const{siteConfig:{title:t}}=(0,a.Z)(),{navbar:{title:n,logo:s}}=(0,i.L)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,o.ZP)(s?.href||"/"),g=n?"":t,h=s?.alt??g;return(0,l.jsxs)(r.Z,{to:f,...p,...s?.target&&{target:s.target},children:[s&&(0,l.jsx)(c,{logo:s,alt:h,imageClassName:u}),null!=n&&(0,l.jsx)("b",{className:d,children:n})]})}},197:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(5742),o=n(5893);function a(e){let{locale:t,version:n,tag:a}=e;const i=t;return(0,o.jsxs)(r.Z,{children:[t&&(0,o.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,o.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,o.jsx)("meta",{name:"docusaurus_tag",content:a}),i&&(0,o.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,o.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,o.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},9965:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(7294),o=n(788),a=n(2389),i=n(2949);const s={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var l=n(5893);function c(e){let{className:t,children:n}=e;const c=(0,a.Z)(),{colorMode:u}=(0,i.I)();return(0,l.jsx)(l.Fragment,{children:(c?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,o.Z)(t,s.themedComponent,s[`themedComponent--${e}`])});return(0,l.jsx)(r.Fragment,{children:a},e)}))})}function u(e){const{sources:t,className:n,alt:r,...o}=e;return(0,l.jsx)(c,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,l.jsx)("img",{src:t[n],alt:r,className:a,...o})}})}},6043:(e,t,n)=>{"use strict";n.d(t,{u:()=>c,z:()=>b});var r=n(7294),o=n(412),a=n(469),i=n(1442),s=n(5893);const l="ease-in-out";function c(e){let{initialState:t}=e;const[n,o]=(0,r.useState)(t??!1),a=(0,r.useCallback)((()=>{o((e=>!e))}),[]);return{collapsed:n,setCollapsed:o,toggleCollapsed:a}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:o}=e;const a=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=o?.duration??function(e){if((0,i.n)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${o?.easing??l}`,height:`${t}px`}}function s(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(s(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{s()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,o])}function g(e){if(!o.Z.canUseDOM)return e?u:d}function h(e){let{as:t="div",collapsed:n,children:o,animation:a,onCollapseTransitionEnd:i,className:l,disableSSRStyle:c}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:a}),(0,s.jsx)(t,{ref:u,style:c?void 0:g(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),i?.(n))},className:l,children:o})}function m(e){let{collapsed:t,...n}=e;const[o,i]=(0,r.useState)(!t),[l,c]=(0,r.useState)(t);return(0,a.Z)((()=>{t||i(!0)}),[t]),(0,a.Z)((()=>{o&&c(t)}),[o,t]),o?(0,s.jsx)(h,{...n,collapsed:l}):null}function b(e){let{lazy:t,...n}=e;const r=t?m:h;return(0,s.jsx)(r,{...n})}},9689:(e,t,n)=>{"use strict";n.d(t,{n:()=>h,p:()=>g});var r=n(7294),o=n(2389),a=n(812),i=n(902),s=n(6668),l=n(5893);const c=(0,a.WA)("docusaurus.announcement.dismiss"),u=(0,a.WA)("docusaurus.announcement.id"),d=()=>"true"===c.get(),p=e=>c.set(String(e)),f=r.createContext(null);function g(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,s.L)(),t=(0,o.Z)(),[n,a]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{a(d())}),[]);const i=(0,r.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||a(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,l.jsx)(f.Provider,{value:n,children:t})}function h(){const e=(0,r.useContext)(f);if(!e)throw new i.i6("AnnouncementBarProvider");return e}},2949:(e,t,n)=>{"use strict";n.d(t,{I:()=>b,S:()=>m});var r=n(7294),o=n(412),a=n(902),i=n(812),s=n(6668),l=n(5893);const c=r.createContext(void 0),u="theme",d=(0,i.WA)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,g=e=>o.Z.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),h=e=>{d.set(f(e))};function m(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,s.L)(),[o,a]=(0,r.useState)(g(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const i=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:o=!0}=r;t?(a(t),o&&h(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&i(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const l=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||l.current?l.current=window.matchMedia("print").matches:i(null)};return e.addListener(r),()=>e.removeListener(r)}),[i,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:i,get isDarkTheme(){return o===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[o,i])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(c);if(null==e)throw new a.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},3163:(e,t,n)=>{"use strict";n.d(t,{M:()=>p,e:()=>f});var r=n(7294),o=n(3102),a=n(7524),i=n(1980),s=n(6668),l=n(902),c=n(5893);const u=r.createContext(void 0);function d(){const e=function(){const e=(0,o.HY)(),{items:t}=(0,s.L)().navbar;return 0===t.length&&!e.component}(),t=(0,a.i)(),n=!e&&"mobile"===t,[l,c]=(0,r.useState)(!1);(0,i.Rb)((()=>{if(l)return c(!1),!1}));const u=(0,r.useCallback)((()=>{c((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&c(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:l})),[e,n,u,l])}function p(e){let{children:t}=e;const n=d();return(0,c.jsx)(u.Provider,{value:n,children:t})}function f(){const e=r.useContext(u);if(void 0===e)throw new l.i6("NavbarMobileSidebarProvider");return e}},3102:(e,t,n)=>{"use strict";n.d(t,{HY:()=>l,Zo:()=>c,n2:()=>s});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function s(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,r.useContext)(i);if(!e)throw new o.i6("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const a=(0,r.useContext)(i);if(!a)throw new o.i6("NavbarSecondaryMenuContentProvider");const[,s]=a,l=(0,o.Ql)(n);return(0,r.useEffect)((()=>{s({component:t,props:l})}),[s,t,l]),(0,r.useEffect)((()=>()=>s({component:null,props:null})),[s]),null}},9727:(e,t,n)=>{"use strict";n.d(t,{h:()=>o,t:()=>a});var r=n(7294);const o="navigation-with-keyboard";function a(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(o),"mousedown"===e.type&&document.body.classList.remove(o)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(o),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},7524:(e,t,n)=>{"use strict";n.d(t,{i:()=>s});var r=n(7294),o=n(412);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function s(e){let{desktopBreakpoint:t=i}=void 0===e?{}:e;const[n,s]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){s(function(e){if(!o.Z.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},5281:(e,t,n)=>{"use strict";n.d(t,{k:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",blogAuthorsListPage:"blog-authors-list-page",blogAuthorsPostsPage:"blog-authors-posts-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",draftBanner:"theme-draft-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{blogFooterTagsRow:"theme-blog-footer-tags-row",blogFooterEditMetaRow:"theme-blog-footer-edit-meta-row"},pages:{pageFooterEditMetaRow:"theme-pages-footer-edit-meta-row"}}},1442:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{n:()=>r})},1980:(e,t,n)=>{"use strict";n.d(t,{Rb:()=>i,_X:()=>l});var r=n(7294),o=n(6550),a=n(902);function i(e){!function(e){const t=(0,o.k6)(),n=(0,a.zX)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}function s(e){const t=(0,o.k6)();return(0,r.useSyncExternalStore)(t.listen,(()=>e(t)),(()=>e(t)))}function l(e){return s((t=>null===e?null:new URLSearchParams(t.location.search).get(e)))}},7392:(e,t,n)=>{"use strict";function r(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,r)=>e.findIndex((e=>t(e,n)))!==r))}function o(e){return Array.from(new Set(e))}function a(e,t){const n={};let r=0;for(const o of e){const e=t(o,r);n[e]??=[],n[e].push(o),r+=1}return n}n.d(t,{jj:()=>o,lx:()=>r,vM:()=>a})},8264:(e,t,n)=>{"use strict";n.d(t,{FG:()=>p,d:()=>u,VC:()=>f});var r=n(7294),o=n(788),a=n(5742),i=n(5102),s=n(4996),l=n(2263);var c=n(5893);function u(e){let{title:t,description:n,keywords:r,image:o,children:i}=e;const u=function(e){const{siteConfig:t}=(0,l.Z)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}(t),{withBaseUrl:d}=(0,s.Cg)(),p=o?d(o,{absolute:!0}):void 0;return(0,c.jsxs)(a.Z,{children:[t&&(0,c.jsx)("title",{children:u}),t&&(0,c.jsx)("meta",{property:"og:title",content:u}),n&&(0,c.jsx)("meta",{name:"description",content:n}),n&&(0,c.jsx)("meta",{property:"og:description",content:n}),r&&(0,c.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,c.jsx)("meta",{property:"og:image",content:p}),p&&(0,c.jsx)("meta",{name:"twitter:image",content:p}),i]})}const d=r.createContext(void 0);function p(e){let{className:t,children:n}=e;const i=r.useContext(d),s=(0,o.Z)(i,t);return(0,c.jsxs)(d.Provider,{value:s,children:[(0,c.jsx)(a.Z,{children:(0,c.jsx)("html",{className:s})}),n]})}function f(e){let{children:t}=e;const n=(0,i.Z)(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,c.jsx)(p,{className:(0,o.Z)(r,a),children:t})}},902:(e,t,n)=>{"use strict";n.d(t,{D9:()=>s,Qc:()=>u,Ql:()=>c,i6:()=>l,zX:()=>i});var r=n(7294),o=n(469),a=n(5893);function i(e){const t=(0,r.useRef)(e);return(0,o.Z)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function s(e){const t=(0,r.useRef)();return(0,o.Z)((()=>{t.current=e})),t.current}class l extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},8596:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>i,Ns:()=>s});var r=n(7294),o=n(723),a=n(2263);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function s(){const{baseUrl:e}=(0,a.Z)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function o(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(o).flatMap((e=>e.routes??[])))}(n)}({routes:o.Z,baseUrl:e})),[e])}},2466:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>h,OC:()=>u,RF:()=>f,o5:()=>g});var r=n(7294),o=n(412),a=n(2389),i=n(469),s=n(902),l=n(5893);const c=r.createContext(void 0);function u(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function d(){const e=(0,r.useContext)(c);if(null==e)throw new s.i6("ScrollControllerProvider");return e}const p=()=>o.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function f(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=d(),o=(0,r.useRef)(p()),a=(0,s.zX)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=p();a(e,o.current),o.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function g(){const e=d(),t=function(){const e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,r.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}}),[]);return(0,r.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,r.useRef)(void 0),o=(0,r.useCallback)((r=>{t.save(r),e.disableScrollEvents(),n.current=()=>{const{restored:r}=t.restore();if(n.current=void 0,r){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,i.Z)((()=>{queueMicrotask((()=>n.current?.()))})),{blockElementScrollPositionUntilNextRender:o}}function h(){const e=(0,r.useRef)(null),t=(0,a.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const o=document.documentElement.scrollTop;(n&&o>e||!n&&ot&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},812:(e,t,n)=>{"use strict";n.d(t,{WA:()=>u,Nk:()=>d});var r=n(7294);const o=JSON.parse('{"d":"localStorage","u":""}'),a=o.d;function i(e){let{key:t,oldValue:n,newValue:r,storage:o}=e;if(n===r)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,o),window.dispatchEvent(a)}function s(e){if(void 0===e&&(e=a),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,l||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),l=!0),null}var t}let l=!1;const c={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function u(e,t){const n=`${e}${o.u}`;if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(n);const r=s(t?.persistence);return null===r?c:{get:()=>{try{return r.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{const t=r.getItem(n);r.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:r})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{const e=r.getItem(n);r.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:r})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{const t=t=>{t.storageArea===r&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(t){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,t),()=>{}}}}}function d(e,t){const n=(0,r.useRef)((()=>null===e?c:u(e,t))).current(),o=(0,r.useCallback)((e=>"undefined"==typeof window?()=>{}:n.listen(e)),[n]);return[(0,r.useSyncExternalStore)(o,(()=>"undefined"==typeof window?null:n.get()),(()=>null)),n]}},4711:(e,t,n)=>{"use strict";n.d(t,{l:()=>i});var r=n(2263),o=n(6550),a=n(8780);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:s}}=(0,r.Z)(),{pathname:l}=(0,o.TH)(),c=(0,a.Do)(l,{trailingSlash:n,baseUrl:e}),u=s===i?e:e.replace(`/${s}/`,"/"),d=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(n)}${d}`}}}},5936:(e,t,n)=>{"use strict";n.d(t,{S:()=>i});var r=n(7294),o=n(6550),a=n(902);function i(e){const t=(0,o.TH)(),n=(0,a.D9)(t),i=(0,a.zX)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6668:(e,t,n)=>{"use strict";n.d(t,{L:()=>o});var r=n(2263);function o(){return(0,r.Z)().siteConfig.themeConfig}},8802:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addTrailingSlash=o,t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[i]=e.split(/[#?]/),s="/"===i||i===r?i:(l=i,c=n,c?o(l):a(l));var l,c;return e.replace(i,s)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=a;const r=n(5913);function o(e){return e.endsWith("/")?e:`${e}/`}function a(e){return(0,r.removeSuffix)(e,"/")}},4143:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=function e(t){if(t.cause)return[t,...e(t.cause)];return[t]}},8780:(e,t,n)=>{"use strict";t.BN=t.Do=t.uR=void 0;const r=n(7582);t.uR="__blog-post-container";var o=n(8802);Object.defineProperty(t,"Do",{enumerable:!0,get:function(){return r.__importDefault(o).default}});var a=n(5913);var i=n(4143);Object.defineProperty(t,"BN",{enumerable:!0,get:function(){return i.getErrorCausalChain}})},5913:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){if(""===t)return e;return e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},9318:(e,t,n)=>{"use strict";n.d(t,{lX:()=>w,q_:()=>C,ob:()=>f,PP:()=>j,Ep:()=>p});var r=n(7462);function o(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,r=n+1,o=e.length;r=0;p--){var f=i[p];"."===f?a(i,p):".."===f?(a(i,p),d++):d&&(a(i,p),d--)}if(!c)for(;d--;d)i.unshift("..");!c||""===i[0]||i[0]&&o(i[0])||i.unshift("");var g=i.join("/");return n&&"/"!==g.substr(-1)&&(g+="/"),g};var s=n(8776);function l(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),a.state=t):(void 0===(a=(0,r.Z)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(s){throw s instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):s}return n&&(a.key=n),o?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=i(a.pathname,o.pathname)):a.pathname=o.pathname:a.pathname||(a.pathname="/"),a}function g(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof r?r(a,o):o(!0):o(!1!==a)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,o):n.push(o),d({action:r,location:o,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",o=f(e,t,h(),w.location);u.confirmTransitionTo(o,r,n,(function(e){e&&(w.entries[w.index]=o,d({action:r,location:o}))}))},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=w.index+e;return t>=0&&t{"use strict";var r=n(9864),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function l(e){return r.isMemo(e)?i:s[e.$$typeof]||o}s[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,g=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(g){var o=f(n);o&&o!==g&&e(t,o,r)}var i=u(n);d&&(i=i.concat(d(n)));for(var s=l(t),h=l(n),m=0;m{"use strict";e.exports=function(e,t,n,r,o,a,i,s){if(!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,s],u=0;(l=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw l.framesToPop=1,l}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},2497:(e,t,n)=>{"use strict";n.r(t)},2295:(e,t,n)=>{"use strict";n.r(t)},4865:function(e,t,n){var r,o;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function o(e,t,n){return en?n:e}function a(e){return 100*(-1+e)}function i(e,t,n){var o;return(o="translate3d"===r.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,o}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=o(e,r.minimum,1),n.status=1===e?null:e;var a=n.render(!t),c=a.querySelector(r.barSelector),u=r.speed,d=r.easing;return a.offsetWidth,s((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),l(c,i(e,u,d)),1===e?(l(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){l(a,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var o,i=t.querySelector(r.barSelector),s=e?"-100":a(n.status||0),c=document.querySelector(r.parent);return l(i,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),r.showSpinner||(o=t.querySelector(r.spinnerSelector))&&f(o),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var s=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),l=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,o=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);o--;)if((r=e[o]+a)in n)return r;return t}function o(e){return e=n(e),t[e]||(t[e]=r(e))}function a(e,t,n){t=o(t),e.style[t]=n}return function(e,t){var n,r,o=arguments;if(2==o.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&a(e,n,r);else a(e,o[1],o[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;c(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);c(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},4779:(e,t,n)=>{var r=n(5826);e.exports=f,e.exports.parse=a,e.exports.compile=function(e,t){return s(a(e,t),t)},e.exports.tokensToFunction=s,e.exports.tokensToRegExp=p;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,r=[],a=0,i=0,s="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],p=n[1],f=n.index;if(s+=e.slice(i,f),i=f+d.length,p)s+=p[1];else{var g=e[i],h=n[2],m=n[3],b=n[4],y=n[5],v=n[6],w=n[7];s&&(r.push(s),s="");var k=null!=h&&null!=g&&g!==h,x="+"===v||"*"===v,S="?"===v||"*"===v,_=n[2]||u,E=b||y;r.push({name:m||a++,prefix:h||"",delimiter:_,optional:S,repeat:x,partial:k,asterisk:!!w,pattern:E?c(E):w?".*":"[^"+l(_)+"]+?"})}}return i{!function(e){var t=[/\b(?:async|sync|yield)\*/,/\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/],n=/(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}}}};e.languages.dart=e.languages.extend("clike",{"class-name":[r,{pattern:RegExp(n+/[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source),lookbehind:!0,inside:r.inside}],keyword:t,operator:/\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*\/%&^|=!<>]=?|\?/}),e.languages.insertBefore("dart","string",{"string-literal":{pattern:/r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,lookbehind:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:e.languages.dart}}},string:/[\s\S]+/}},string:void 0}),e.languages.insertBefore("dart","class-name",{metadata:{pattern:/@\w+/,alias:"function"}}),e.languages.insertBefore("dart","class-name",{generics:{pattern:/<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism)},6854:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,o,a){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(o,(function(e){if("function"==typeof a&&!a(e))return e;for(var o,s=i.length;-1!==n.code.indexOf(o=t(r,s));)++s;return i[s]=e,o})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var o=0,a=Object.keys(n.tokenStack);!function i(s){for(var l=0;l=a.length);l++){var c=s[l];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=a[o],d=n.tokenStack[u],p="string"==typeof c?c:c.content,f=t(r,u),g=p.indexOf(f);if(g>-1){++o;var h=p.substring(0,g),m=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(g+f.length),y=[];h&&y.push.apply(y,i([h])),y.push(m),b&&y.push.apply(y,i([b])),"string"==typeof c?s.splice.apply(s,[l,1].concat(y)):c.content=y}}else c.content&&i(c.content)}return s}(n.tokens)}}}})}(Prism)},6046:(e,t,n)=>{var r={"./prism-dart":7065};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=6046},2703:(e,t,n)=>{"use strict";var r=n(414);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,i){if(i!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},5697:(e,t,n)=>{e.exports=n(2703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:(e,t,n)=>{"use strict";var r=n(7294),o=n(3840);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n