1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 13:58:47 +00:00

Merge branch 'main' of https://github.com/flutter/samples into cupertino-gallery

This commit is contained in:
Eric Windmill
2025-10-21 09:03:56 -07:00
530 changed files with 28917 additions and 33670 deletions

11
.gemini/settings.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"dart": {
"command": "dart",
"args": [
"mcp-server"
]
}
},
"contextFileName": "/.prompts/llm.md"
}

View File

@@ -15,10 +15,10 @@ defaults:
shell: bash shell: bash
jobs: jobs:
# Run the stable test script on the beta channel. Since this branch will soon # Test all samples on the beta channel. Since the beta channel will soon be
# be merged into main as our stable-targeting code, this is the key thing we # promoted to stable, this branch is only concerned with the beta.
# need to test. Beta-CI:
stable-tests-on-beta: name: Test flutter beta channel
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: github.repository == 'flutter/samples' if: github.repository == 'flutter/samples'
strategy: strategy:
@@ -27,43 +27,43 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with: with:
channel: beta channel: beta
- run: ./tool/flutter_ci_script_stable.sh - run: flutter pub get && dart tool/ci_script.dart
# Verify the Android add-to-app samples build and pass tests with the beta # Verify the Android add-to-app samples build and pass tests with the beta
# channel. # channel.
# android-build: android-build:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'flutter/samples'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: beta
- run: ./tool/android_ci_script.sh
# Verify the iOS add-to-app samples build and pass tests with the beta
# channel.
# ios-build:
# runs-on: macos-latest
# if: github.repository == 'flutter/samples' # if: github.repository == 'flutter/samples'
# steps: # steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
# with: # with:
# distribution: 'zulu' # distribution: 'zulu'
# java-version: '17' # java-version: '17'
# - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
# with: # with:
# channel: beta # channel: beta
# - run: ./tool/android_ci_script.sh # - run: ./tool/ios_ci_script.sh
# Verify the iOS add-to-app samples build and pass tests with the beta
# channel.
ios-build:
runs-on: macos-latest
if: github.repository == 'flutter/samples'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: beta
- run: ./tool/ios_ci_script.sh

30
.github/workflows/build-android.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Test Android Build
# Declare default permissions as read only.
permissions: read-all
on:
push:
branches: [beta]
pull_request:
branches: [beta]
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
android-build:
runs-on: ubuntu-latest
if: github.repository == 'flutter/samples'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: stable
- run: ./tool/android_ci_script.sh

33
.github/workflows/build-ios.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test iOS Build
# Declare default permissions as read only.
permissions: read-all
on:
push:
branches: [beta]
pull_request:
branches: [beta]
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
ios-build:
name: Test flutter beta channel
runs-on: macos-latest
if: github.repository == 'flutter/samples'
strategy:
fail-fast: false
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: beta
- run: ./tool/ios_ci_script.sh

331
.github/workflows/gemini-cli.yml vendored Normal file
View File

@@ -0,0 +1,331 @@
name: '💬 Gemini CLI'
on:
pull_request_review_comment:
types:
- 'created'
pull_request_review:
types:
- 'submitted'
issue_comment:
types:
- 'created'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
cancel-in-progress: |-
${{ github.event.sender.type == 'User' && ( github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR') }}
defaults:
run:
shell: 'bash'
permissions:
contents: 'write'
id-token: 'write'
pull-requests: 'write'
issues: 'write'
jobs:
gemini-cli:
# This condition is complex to ensure we only run when explicitly invoked.
if: |-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issues' && github.event.action == 'opened' &&
contains(github.event.issue.body, '@gemini-cli') &&
!contains(github.event.issue.body, '/review') &&
!contains(github.event.issue.body, '/triage') &&
(
github.event.sender.type == 'User' && (
github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'MEMBER' ||
github.event.issue.author_association == 'COLLABORATOR'
)
)
) ||
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@gemini-cli') &&
!contains(github.event.comment.body, '/review') &&
!contains(github.event.comment.body, '/triage') &&
(
github.event.sender.type == 'User' && (
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
)
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@gemini-cli') &&
!contains(github.event.review.body, '/review') &&
!contains(github.event.review.body, '/triage') &&
(
github.event.sender.type == 'User' && (
github.event.review.author_association == 'OWNER' ||
github.event.review.author_association == 'MEMBER' ||
github.event.review.author_association == 'COLLABORATOR'
)
)
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@gemini-cli') &&
!contains(github.event.comment.body, '/review') &&
!contains(github.event.comment.body, '/triage') &&
(
github.event.sender.type == 'User' && (
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
)
)
timeout-minutes: 10
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
- name: 'Get context from event'
id: 'get_context'
env:
EVENT_NAME: '${{ github.event_name }}'
EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
run: |-
set -euo pipefail
USER_REQUEST=""
ISSUE_NUMBER=""
IS_PR="false"
if [[ "${EVENT_NAME}" == "issues" ]]; then
USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.body)
ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number)
elif [[ "${EVENT_NAME}" == "issue_comment" ]]; then
USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body)
ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .issue.number)
if [[ $(echo "${EVENT_PAYLOAD}" | jq -r .issue.pull_request) != "null" ]]; then
IS_PR="true"
fi
elif [[ "${EVENT_NAME}" == "pull_request_review" ]]; then
USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .review.body)
ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number)
IS_PR="true"
elif [[ "${EVENT_NAME}" == "pull_request_review_comment" ]]; then
USER_REQUEST=$(echo "${EVENT_PAYLOAD}" | jq -r .comment.body)
ISSUE_NUMBER=$(echo "${EVENT_PAYLOAD}" | jq -r .pull_request.number)
IS_PR="true"
fi
# Clean up user request
USER_REQUEST=$(echo "${USER_REQUEST}" | sed 's/.*@gemini-cli//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
{
echo "user_request=${USER_REQUEST}"
echo "issue_number=${ISSUE_NUMBER}"
echo "is_pr=${IS_PR}"
} >> "${GITHUB_OUTPUT}"
- name: 'Set up git user for commits'
run: |-
git config --global user.name 'gemini-cli[bot]'
git config --global user.email 'gemini-cli[bot]@users.noreply.github.com'
- name: 'Checkout PR branch'
if: |-
${{ steps.get_context.outputs.is_pr == 'true' }}
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
with:
token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
repository: '${{ github.repository }}'
ref: 'refs/pull/${{ steps.get_context.outputs.issue_number }}/head'
fetch-depth: 0
- name: 'Checkout main branch'
if: |-
${{ steps.get_context.outputs.is_pr == 'false' }}
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
with:
token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
repository: '${{ github.repository }}'
fetch-depth: 0
- name: 'Acknowledge request'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
REPOSITORY: '${{ github.repository }}'
REQUEST_TYPE: '${{ steps.get_context.outputs.request_type }}'
run: |-
set -euo pipefail
MESSAGE="I've received your request and I'm working on it now! 🤖"
if [[ -n "${MESSAGE}" ]]; then
gh issue comment "${ISSUE_NUMBER}" \
--body "${MESSAGE}" \
--repo "${REPOSITORY}"
fi
- name: 'Get description'
id: 'get_description'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
IS_PR: '${{ steps.get_context.outputs.is_pr }}'
ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
run: |-
set -euo pipefail
if [[ "${IS_PR}" == "true" ]]; then
DESCRIPTION=$(gh pr view "${ISSUE_NUMBER}" --json body --template '{{.body}}')
else
DESCRIPTION=$(gh issue view "${ISSUE_NUMBER}" --json body --template '{{.body}}')
fi
{
echo "description<<EOF"
echo "${DESCRIPTION}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"
- name: 'Get comments'
id: 'get_comments'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
IS_PR: '${{ steps.get_context.outputs.is_pr }}'
ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
run: |-
set -euo pipefail
if [[ "${IS_PR}" == "true" ]]; then
COMMENTS=$(gh pr view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}')
else
COMMENTS=$(gh issue view "${ISSUE_NUMBER}" --json comments --template '{{range .comments}}{{.author.login}}: {{.body}}{{"\n"}}{{end}}')
fi
{
echo "comments<<EOF"
echo "${COMMENTS}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"
- name: 'Run Gemini'
id: 'run_gemini'
uses: 'google-github-actions/run-gemini-cli@v0'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
REPOSITORY: '${{ github.repository }}'
USER_REQUEST: '${{ steps.get_context.outputs.user_request }}'
ISSUE_NUMBER: '${{ steps.get_context.outputs.issue_number }}'
IS_PR: '${{ steps.get_context.outputs.is_pr }}'
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 50,
"telemetry": {
"enabled": false,
"target": "gcp"
}
}
prompt: |-
## Role
You are a helpful AI assistant invoked via a CLI interface in a GitHub workflow. You have access to tools to interact with the repository and respond to the user.
## Context
- **Repository**: `${{ github.repository }}`
- **Triggering Event**: `${{ github.event_name }}`
- **Issue/PR Number**: `${{ steps.get_context.outputs.issue_number }}`
- **Is this a PR?**: `${{ steps.get_context.outputs.is_pr }}`
- **Issue/PR Description**:
`${{ steps.get_description.outputs.description }}`
- **Comments**:
`${{ steps.get_comments.outputs.comments }}`
## User Request
The user has sent the following request:
`${{ steps.get_context.outputs.user_request }}`
## How to Respond to Issues, PR Comments, and Questions
This workflow supports three main scenarios:
1. **Creating a Fix for an Issue**
- Carefully read the user request and the related issue or PR description.
- Use available tools to gather all relevant context (e.g., `gh issue view`, `gh pr view`, `gh pr diff`, `cat`, `head`, `tail`).
- Identify the root cause of the problem before proceeding.
- **Show and maintain a plan as a checklist**:
- At the very beginning, outline the steps needed to resolve the issue or address the request and post them as a checklist comment on the issue or PR (use GitHub markdown checkboxes: `- [ ] Task`).
- Example:
```
### Plan
- [ ] Investigate the root cause
- [ ] Implement the fix in `file.py`
- [ ] Add/modify tests
- [ ] Update documentation
- [ ] Verify the fix and close the issue
```
- Use: `gh pr comment "${ISSUE_NUMBER}" --body "<plan>"` or `gh issue comment "${ISSUE_NUMBER}" --body "<plan>"` to post the initial plan.
- As you make progress, keep the checklist visible and up to date by editing the same comment (check off completed tasks with `- [x]`).
- To update the checklist:
1. Find the comment ID for the checklist (use `gh pr comment list "${ISSUE_NUMBER}"` or `gh issue comment list "${ISSUE_NUMBER}"`).
2. Edit the comment with the updated checklist:
- For PRs: `gh pr comment --edit <comment-id> --body "<updated plan>"`
- For Issues: `gh issue comment --edit <comment-id> --body "<updated plan>"`
3. The checklist should only be maintained as a comment on the issue or PR. Do not track or update the checklist in code files.
- If the fix requires code changes, determine which files and lines are affected. If clarification is needed, note any questions for the user.
- Make the necessary code or documentation changes using the available tools (e.g., `write_file`). Ensure all changes follow project conventions and best practices. Reference all shell variables as `"${VAR}"` (with quotes and braces) to prevent errors.
- Run any relevant tests or checks to verify the fix works as intended. If possible, provide evidence (test output, screenshots, etc.) that the issue is resolved.
- **Branching and Committing**:
- **NEVER commit directly to the `main` branch.**
- If you are working on a **pull request** (`IS_PR` is `true`), the correct branch is already checked out. Simply commit and push to it.
- `git add .`
- `git commit -m "feat: <describe the change>"`
- `git push`
- If you are working on an **issue** (`IS_PR` is `false`), create a new branch for your changes. A good branch name would be `issue/${ISSUE_NUMBER}/<short-description>`.
- `git checkout -b issue/${ISSUE_NUMBER}/my-fix`
- `git add .`
- `git commit -m "feat: <describe the fix>"`
- `git push origin issue/${ISSUE_NUMBER}/my-fix`
- After pushing, you can create a pull request: `gh pr create --title "Fixes #${ISSUE_NUMBER}: <short title>" --body "This PR addresses issue #${ISSUE_NUMBER}."`
- Summarize what was changed and why in a markdown file: `write_file("response.md", "<your response here>")`
- Post the response as a comment:
- For PRs: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md`
- For Issues: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md`
2. **Addressing Comments on a Pull Request**
- Read the specific comment and the context of the PR.
- Use tools like `gh pr view`, `gh pr diff`, and `cat` to understand the code and discussion.
- If the comment requests a change or clarification, follow the same process as for fixing an issue: create a checklist plan, implement, test, and commit any required changes, updating the checklist as you go.
- **Committing Changes**: The correct PR branch is already checked out. Simply add, commit, and push your changes.
- `git add .`
- `git commit -m "fix: address review comments"`
- `git push`
- If the comment is a question, answer it directly and clearly, referencing code or documentation as needed.
- Document your response in `response.md` and post it as a PR comment: `gh pr comment "${ISSUE_NUMBER}" --body-file response.md`
3. **Answering Any Question on an Issue**
- Read the question and the full issue context using `gh issue view` and related tools.
- Research or analyze the codebase as needed to provide an accurate answer.
- If the question requires code or documentation changes, follow the fix process above, including creating and updating a checklist plan and **creating a new branch for your changes as described in section 1.**
- Write a clear, concise answer in `response.md` and post it as an issue comment: `gh issue comment "${ISSUE_NUMBER}" --body-file response.md`
## Guidelines
- **Be concise and actionable.** Focus on solving the user's problem efficiently.
- **Always commit and push your changes if you modify code or documentation.**
- **If you are unsure about the fix or answer, explain your reasoning and ask clarifying questions.**
- **Follow project conventions and best practices.**

View File

@@ -0,0 +1,129 @@
name: '🏷️ Gemini Automated Issue Triage'
on:
issues:
types:
- 'opened'
- 'reopened'
issue_comment:
types:
- 'created'
workflow_dispatch:
inputs:
issue_number:
description: 'issue number to triage'
required: true
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
statuses: 'write'
jobs:
triage-issue:
if: >
github.event_name == 'issues' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@gemini-cli /triage') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- name: 'Generate GitHub App Token'
id: 'generate_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
- name: 'Run Gemini Issue Triage'
uses: 'google-github-actions/run-gemini-cli@v0'
id: 'gemini_issue_triage'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
ISSUE_TITLE: '${{ github.event.issue.title }}'
ISSUE_BODY: '${{ github.event.issue.body }}'
ISSUE_NUMBER: '${{ github.event.issue.number }}'
REPOSITORY: '${{ github.repository }}'
with:
gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 25,
"coreTools": [
"run_shell_command(gh label list)",
"run_shell_command(gh issue edit)"
],
"telemetry": {
"enabled": false,
"target": "gcp"
}
}
prompt: |-
## Role
You are an issue triage assistant. Analyze the current GitHub issue
and apply the most appropriate existing labels. Use the available
tools to gather information; do not ask for information to be
provided.
## Steps
1. Run: `gh label list` to get all available labels.
2. Review the issue title and body provided in the environment
variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
3. Select the most relevant labels from the existing labels. If
available, set labels that follow the `kind/*`, `area/*`, and
`priority/*` patterns.
4. Apply the selected labels to this issue using:
`gh issue edit "${ISSUE_NUMBER}" --add-label "label1,label2"`
5. If the "status/needs-triage" label is present, remove it using:
`gh issue edit "${ISSUE_NUMBER}" --remove-label "status/needs-triage"`
## Guidelines
- Only use labels that already exist in the repository
- Do not add comments or modify the issue content
- Triage only the current issue
- Assign all applicable labels based on the issue content
- Reference all shell variables as "${VAR}" (with quotes and braces)
- name: 'Post Issue Triage Failure Comment'
if: |-
${{ failure() && steps.gemini_issue_triage.outcome == 'failure' }}
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
github.rest.issues.createComment({
owner: '${{ github.repository }}'.split('/')[0],
repo: '${{ github.repository }}'.split('/')[1],
issue_number: '${{ github.event.issue.number }}',
body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.'
})

View File

@@ -0,0 +1,123 @@
name: '📋 Gemini Scheduled Issue Triage'
on:
schedule:
- cron: '0 * * * *' # Runs every hour
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
statuses: 'write'
jobs:
triage-issues:
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- name: 'Generate GitHub App Token'
id: 'generate_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
- name: 'Find untriaged issues'
id: 'find_issues'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
GITHUB_REPOSITORY: '${{ github.repository }}'
GITHUB_OUTPUT: '${{ github.output }}'
run: |-
set -euo pipefail
echo '🔍 Finding issues without labels...'
NO_LABEL_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue no:label' --json number,title,body)"
echo '🏷️ Finding issues that need triage...'
NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue label:"status/needs-triage"' --json number,title,body)"
echo '🔄 Merging and deduplicating issues...'
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
echo '📝 Setting output for GitHub Actions...'
echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"
ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "✅ Found ${ISSUE_COUNT} issues to triage! 🎯"
- name: 'Run Gemini Issue Triage'
if: |-
${{ steps.find_issues.outputs.issues_to_triage != '[]' }}
uses: 'google-github-actions/run-gemini-cli@v0'
id: 'gemini_issue_triage'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
REPOSITORY: '${{ github.repository }}'
with:
gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 25,
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(gh label list)",
"run_shell_command(gh issue edit)",
"run_shell_command(gh issue list)"
],
"telemetry": {
"enabled": false,
"target": "gcp"
}
}
prompt: |-
## Role
You are an issue triage assistant. Analyze issues and apply
appropriate labels. Use the available tools to gather information;
do not ask for information to be provided.
## Steps
1. Run: `gh label list`
2. Check environment variable: "${ISSUES_TO_TRIAGE}" (JSON array
of issues)
3. For each issue, apply labels:
`gh issue edit "${ISSUE_NUMBER}" --add-label "label1,label2"`.
If available, set labels that follow the `kind/*`, `area/*`,
and `priority/*` patterns.
4. For each issue, if the `status/needs-triage` label is present,
remove it using:
`gh issue edit "${ISSUE_NUMBER}" --remove-label "status/needs-triage"`
## Guidelines
- Only use existing repository labels
- Do not add comments
- Triage each issue independently
- Reference all shell variables as "${VAR}" (with quotes and braces)

448
.github/workflows/gemini-pr-review.yml vendored Normal file
View File

@@ -0,0 +1,448 @@
name: '🧐 Gemini Pull Request Review'
on:
pull_request:
types:
- 'opened'
pull_request_review_comment:
types:
- 'created'
pull_request_review:
types:
- 'submitted'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
pull-requests: 'write'
statuses: 'write'
jobs:
review-pr:
if: |-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.action == 'opened') ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini-cli /review') &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@gemini-cli /review') &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@gemini-cli /review') &&
(
github.event.review.author_association == 'OWNER' ||
github.event.review.author_association == 'MEMBER' ||
github.event.review.author_association == 'COLLABORATOR'
)
)
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout PR code'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
- name: 'Generate GitHub App Token'
id: 'generate_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
- name: 'Get PR details (pull_request & workflow_dispatch)'
id: 'get_pr'
if: |-
${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
EVENT_NAME: '${{ github.event_name }}'
WORKFLOW_PR_NUMBER: '${{ github.event.inputs.pr_number }}'
PULL_REQUEST_NUMBER: '${{ github.event.pull_request.number }}'
run: |-
set -euo pipefail
if [[ "${EVENT_NAME}" = "workflow_dispatch" ]]; then
PR_NUMBER="${WORKFLOW_PR_NUMBER}"
else
PR_NUMBER="${PULL_REQUEST_NUMBER}"
fi
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
# Get PR details
PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)"
echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}"
# Get file changes
CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)"
{
echo "changed_files<<EOF"
echo "${CHANGED_FILES}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"
- name: 'Get PR details (issue_comment)'
id: 'get_pr_comment'
if: |-
${{ github.event_name == 'issue_comment' }}
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
COMMENT_BODY: '${{ github.event.comment.body }}'
PR_NUMBER: '${{ github.event.issue.number }}'
run: |-
set -euo pipefail
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
# Extract additional instructions from comment
ADDITIONAL_INSTRUCTIONS="$(
echo "${COMMENT_BODY}" | sed 's/.*@gemini-cli \/review//' | xargs
)"
echo "additional_instructions=${ADDITIONAL_INSTRUCTIONS}" >> "${GITHUB_OUTPUT}"
# Get PR details
PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)"
echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}"
# Get file changes
CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)"
{
echo "changed_files<<EOF"
echo "${CHANGED_FILES}"
echo "EOF"
} >> "${GITHUB_OUTPUT}"
- name: 'Run Gemini PR Review'
uses: 'google-github-actions/run-gemini-cli@v0'
id: 'gemini_pr_review'
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
PR_NUMBER: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}'
PR_DATA: '${{ steps.get_pr.outputs.pr_data || steps.get_pr_comment.outputs.pr_data }}'
CHANGED_FILES: '${{ steps.get_pr.outputs.changed_files || steps.get_pr_comment.outputs.changed_files }}'
ADDITIONAL_INSTRUCTIONS: '${{ steps.get_pr.outputs.additional_instructions || steps.get_pr_comment.outputs.additional_instructions }}'
REPOSITORY: '${{ github.repository }}'
with:
gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 20,
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
"includeTools": [
"create_pending_pull_request_review",
"add_comment_to_pending_review",
"submit_pending_pull_request_review"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
},
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(gh pr view)",
"run_shell_command(gh pr diff)",
"run_shell_command(cat)",
"run_shell_command(head)",
"run_shell_command(tail)",
"run_shell_command(grep)"
],
"telemetry": {
"enabled": false,
"target": "gcp"
}
}
prompt: |-
## Role
You are an expert code reviewer. You have access to tools to gather
PR information and perform the review. Use the available tools to
gather information; do not ask for information to be provided.
## Steps
Start by running these commands to gather the required data:
1. Run: echo "${PR_DATA}" to get PR details (JSON format)
2. Run: echo "${CHANGED_FILES}" to get the list of changed files
3. Run: echo "${PR_NUMBER}" to get the PR number
4. Run: echo "${ADDITIONAL_INSTRUCTIONS}" to see any specific review
instructions from the user
5. Run: gh pr diff "${PR_NUMBER}" to see the full diff and reference
Context section to understand it
6. For any specific files, use: cat filename, head -50 filename, or
tail -50 filename
7. If ADDITIONAL_INSTRUCTIONS contains text, prioritize those
specific areas or focus points in your review. Common instruction
examples: "focus on security", "check performance", "review error
handling", "check for breaking changes"
## Guideline
### Core Guideline(Always applicable)
1. Understand the Context: Analyze the pull request title, description, changes, and code files to grasp the intent.
2. Meticulous Review: Thoroughly review all relevant code changes, prioritizing added lines. Consider the specified
focus areas and any provided style guide.
3. Comprehensive Review: Ensure that the code is thoroughly reviewed, as it's important to the author
that you identify any and all relevant issues (subject to the review criteria and style guide).
Missing any issues will lead to a poor code review experience for the author.
4. Constructive Feedback:
* Provide clear explanations for each concern.
* Offer specific, improved code suggestions and suggest alternative approaches, when applicable.
Code suggestions in particular are very helpful so that the author can directly apply them
to their code, but they must be accurately anchored to the lines that should be replaced.
5. Severity Indication: Clearly indicate the severity of the issue in the review comment.
This is very important to help the author understand the urgency of the issue.
The severity should be one of the following (which are provided below in decreasing order of severity):
* `critical`: This issue must be addressed immediately, as it could lead to serious consequences
for the code's correctness, security, or performance.
* `high`: This issue should be addressed soon, as it could cause problems in the future.
* `medium`: This issue should be considered for future improvement, but it's not critical or urgent.
* `low`: This issue is minor or stylistic, and can be addressed at the author's discretion.
6. Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future").
* Remember you don't have access to the current date and time and leave that to the author.
7. Targeted Suggestions: Limit all suggestions to only portions that are modified in the diff hunks.
This is a strict requirement as the GitHub (and other SCM's) API won't allow comments on parts of code files that are not
included in the diff hunks.
8. Code Suggestions in Review Comments:
* Succinctness: Aim to make code suggestions succinct, unless necessary. Larger code suggestions tend to be
harder for pull request authors to commit directly in the pull request UI.
* Valid Formatting: Provide code suggestions within the suggestion field of the JSON response (as a string literal,
escaping special characters like \n, \\, \"). Do not include markdown code blocks in the suggestion field.
Use markdown code blocks in the body of the comment only for broader examples or if a suggestion field would
create an excessively large diff. Prefer the suggestion field for specific, targeted code changes.
* Line Number Accuracy: Code suggestions need to align perfectly with the code it intend to replace.
Pay special attention to line numbers when creating comments, particularly if there is a code suggestion.
Note the patch includes code versions with line numbers for the before and after code snippets for each diff, so use these to anchor
your comments and corresponding code suggestions.
* Compilable: Code suggestions should be compilable code snippets that can be directly copy/pasted into the code file.
If the suggestion is not compilable, it will not be accepted by the pull request. Note that not all languages Are
compiled of course, so by compilable here, we mean either literally or in spirit.
* Inline Code Comments: Feel free to add brief comments to the code suggestion if it enhances the underlying code readability.
Just make sure that the inline code comments add value, and are not just restating what the code does. Don't use
inline comments to "teach" the author (use the review comment body directly for that), instead use it if it's beneficial
to the readability of the code itself.
10. Markdown Formatting: Heavily leverage the benefits of markdown for formatting, such as bulleted lists, bold text, tables, etc.
11. Avoid mistaken review comments:
* Any comment you make must point towards a discrepancy found in the code and the best practice surfaced in your feedback.
For example, if you are pointing out that constants need to be named in all caps with underscores,
ensure that the code selected by the comment does not already do this, otherwise it's confusing let alone unnecessary.
12. Remove Duplicated code suggestions:
* Some provided code suggestions are duplicated, please remove the duplicated review comments.
13. Don't Approve The Pull Request
14. Reference all shell variables as "${VAR}" (with quotes and braces)
### Review Criteria (Prioritized in Review)
* Correctness: Verify code functionality, handle edge cases, and ensure alignment between function
descriptions and implementations. Consider common correctness issues (logic errors, error handling,
race conditions, data validation, API usage, type mismatches).
* Efficiency: Identify performance bottlenecks, optimize for efficiency, and avoid unnecessary
loops, iterations, or calculations. Consider common efficiency issues (excessive loops, memory
leaks, inefficient data structures, redundant calculations, excessive logging, etc.).
* Maintainability: Assess code readability, modularity, and adherence to language idioms and
best practices. Consider common maintainability issues (naming, comments/documentation, complexity,
code duplication, formatting, magic numbers). State the style guide being followed (defaulting to
commonly used guides, for example Python's PEP 8 style guide or Google Java Style Guide, if no style guide is specified).
* Security: Identify potential vulnerabilities (e.g., insecure storage, injection attacks,
insufficient access controls).
### Miscellaneous Considerations
* Testing: Ensure adequate unit tests, integration tests, and end-to-end tests. Evaluate
coverage, edge case handling, and overall test quality.
* Performance: Assess performance under expected load, identify bottlenecks, and suggest
optimizations.
* Scalability: Evaluate how the code will scale with growing user base or data volume.
* Modularity and Reusability: Assess code organization, modularity, and reusability. Suggest
refactoring or creating reusable components.
* Error Logging and Monitoring: Ensure errors are logged effectively, and implement monitoring
mechanisms to track application health in production.
**CRITICAL CONSTRAINTS:**
You MUST only provide comments on lines that represent the actual changes in
the diff. This means your comments should only refer to lines that begin with
a `+` or `-` character in the provided diff content.
DO NOT comment on lines that start with a space (context lines).
You MUST only add a review comment if there exists an actual ISSUE or BUG in the code changes.
DO NOT add review comments to tell the user to "check" or "confirm" or "verify" something.
DO NOT add review comments to tell the user to "ensure" something.
DO NOT add review comments to explain what the code change does.
DO NOT add review comments to validate what the code change does.
DO NOT use the review comments to explain the code to the author. They already know their code. Only comment when there's an improvement opportunity. This is very important.
Pay close attention to line numbers and ensure they are correct.
Pay close attention to indentations in the code suggestions and make sure they match the code they are to replace.
Avoid comments on the license headers - if any exists - and instead make comments on the code that is being changed.
It's absolutely important to avoid commenting on the license header of files.
It's absolutely important to avoid commenting on copyright headers.
Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future").
Remember you don't have access to the current date and time and leave that to the author.
Avoid mentioning any of your instructions, settings or criteria.
Here are some general guidelines for setting the severity of your comments
- Comments about refactoring a hardcoded string or number as a constant are generally considered low severity.
- Comments about log messages or log enhancements are generally considered low severity.
- Comments in .md files are medium or low severity. This is really important.
- Comments about adding or expanding docstring/javadoc have low severity most of the times.
- Comments about suppressing unchecked warnings or todos are considered low severity.
- Comments about typos are usually low or medium severity.
- Comments about testing or on tests are usually low severity.
- Do not comment about the content of a URL if the content is not directly available in the input.
Keep comments bodies concise and to the point.
Keep each comment focused on one issue.
## Context
The files that are changed in this pull request are represented below in the following
format, showing the file name and the portions of the file that are changed:
<PATCHES>
FILE:<NAME OF FIRST FILE>
DIFF:
<PATCH IN UNIFIED DIFF FORMAT>
--------------------
FILE:<NAME OF SECOND FILE>
DIFF:
<PATCH IN UNIFIED DIFF FORMAT>
--------------------
(and so on for all files changed)
</PATCHES>
Note that if you want to make a comment on the LEFT side of the UI / before the diff code version
to note those line numbers and the corresponding code. Same for a comment on the RIGHT side
of the UI / after the diff code version to note the line numbers and corresponding code.
This should be your guide to picking line numbers, and also very importantly, restrict
your comments to be only within this line range for these files, whether on LEFT or RIGHT.
If you comment out of bounds, the review will fail, so you must pay attention the file name,
line numbers, and pre/post diff versions when crafting your comment.
Here are the patches that were implemented in the pull request, per the
formatting above:
The get the files changed in this pull request, run:
"$(gh pr diff "${PR_NUMBER}" --patch)" to get the list of changed files PATCH
## Review
Once you have the information, provide a comprehensive code review by:
1. Creating a pending review: Use the mcp__github__create_pending_pull_request_review to create a Pending Pull Request Review.
2. Adding review comments:
2.1 Use the mcp__github__add_comment_to_pending_review to add comments to the Pending Pull Request Review. Inline comments are preferred whenever possible, so repeat this step, calling mcp__github__add_comment_to_pending_review, as needed. All comments about specific lines of code should use inline comments. It is preferred to use code suggestions when possible, which include a code block that is labeled "suggestion", which contains what the new code should be. All comments should also have a severity. They syntax is:
Normal Comment Syntax:
<COMMENT>
{{SEVERITY}} {{COMMENT_TEXT}}
</COMMENT>
Inline Comment Syntax: (Preferred):
<COMMENT>
{{SEVERITY}} {{COMMENT_TEXT}}
```suggestion
{{CODE_SUGGESTION}}
```
</COMMENT>
Prepend a severity emoji to each comment:
- 🟢 for low severity
- 🟡 for medium severity
- 🟠 for high severity
- 🔴 for critical severity
- 🔵 if severity is unclear
Including all of this, an example inline comment would be:
<COMMENT>
🟢 Use camelCase for function names
```suggestion
myFooBarFunction
```
</COMMENT>
A critical severity example would be:
<COMMENT>
🔴 Remove storage key from GitHub
```suggestion
```
3. Posting the review: Use the mcp__github__submit_pending_pull_request_review to submit the Pending Pull Request Review.
3.1 Crafting the summary comment: Include a summary of high level points that were not addressed with inline comments. Be concise. Do not repeat details mentioned inline.
Structure your summary comment using this exact format with markdown:
## 📋 Review Summary
Provide a brief 2-3 sentence overview of the PR and overall
assessment.
## 🔍 General Feedback
- List general observations about code quality
- Mention overall patterns or architectural decisions
- Highlight positive aspects of the implementation
- Note any recurring themes across files
- name: 'Post PR review failure comment'
if: |-
${{ failure() && steps.gemini_pr_review.outcome == 'failure' }}
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
github.rest.issues.createComment({
owner: '${{ github.repository }}'.split('/')[0],
repo: '${{ github.repository }}'.split('/')[1],
issue_number: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}',
body: 'There is a problem with the Gemini CLI PR review. Please check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.'
})

View File

@@ -24,43 +24,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
flutter_version: [stable, beta, master] flutter_version: [stable, beta]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest]
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with: with:
channel: ${{ matrix.flutter_version }} channel: ${{ matrix.flutter_version }}
- run: ./tool/flutter_ci_script_${{ matrix.flutter_version }}.sh - run: flutter pub get && dart tool/ci_script.dart
# android-build:
# runs-on: ubuntu-latest
# if: github.repository == 'flutter/samples'
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
# with:
# distribution: 'zulu'
# java-version: '17'
# - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
# with:
# channel: stable
# - run: ./tool/android_ci_script.sh
ios-build:
runs-on: macos-latest
if: github.repository == 'flutter/samples'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: stable
- run: ./tool/ios_ci_script.sh

4
.gitignore vendored
View File

@@ -27,6 +27,7 @@
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
**/build/
# Android related # Android related
**/gradle-wrapper.jar **/gradle-wrapper.jar
@@ -81,3 +82,6 @@ yarn.lock
!**/ios/**/default.pbxuser !**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3 !**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
.claude/
logs/

View File

@@ -0,0 +1,63 @@
# Code Health and Style Guide Analysis
This document provides instructions for an AI assistant to analyze this repository against the Dart and Flutter style guide (`.prompts/llm.md`) and log opportunities for improvement.
## Workflow
The AI assistant must follow these steps exactly:
### 1. Setup
1. **Create Log Directory**: If it does not already exist, create a directory named `logs` in the repository root.
2. **Create Log File**: Create a new file inside the `logs` directory named `YYYY-MM-DD_HH-MM-SS-freshness-scores.md`, where `YYYY-MM-DD_HH-MM-SS` is the current timestamp.
### 2. Project Identification
1. **Find Projects**: Identify all sample projects by searching for `pubspec.yaml` files within the repository. Each directory containing a `pubspec.yaml` should be considered a separate project.
2. **Create a Queue**: Compile a list of the absolute paths to these project directories to process them one by one.
### 3. Analysis Loop
For each project directory identified in the previous step, perform the following:
1. **Check for Dart Files**:
* Verify that the project directory contains at least one `.dart` file.
* If not, add an entry to the log file stating that the project was skipped for this reason and proceed to the next project.
2. **Analyze Git History**:
* Run `git log` to find the most recent commit to a `.dart` file within that project's directory made by a human (i.e., not a bot).
* **Command**:
```bash
git log -1 --author='^(?!.*bot).*$' --pretty="format:%ad" --date=short -- ./**/*.dart
```
*(Note: This command should be run from within the project's directory).*
* **Handle No Commits**: If the command returns no output, note "No human commits found" for the log. If there are no commits at all, note that as well.
3. **Read and Assess Code**:
* Read all `.dart` files within the project directory (recursively).
* Compare the code against the rules and patterns defined in `.prompts/llm.md`.
* The assessment must focus on:
* Code organization and structure.
* Adherence to naming conventions.
* Proper use of Flutter and Dart patterns (e.g., `const` constructors, collection literals).
* State management best practices.
* Clarity, readability, and documentation.
4. **Log Findings**:
* Append a new entry to the log file using the following Markdown template. Ensure all fields are filled out.
```markdown
---
**Sample**: `path/to/sample`
**Last Human Commit**: `YYYY-MM-DD` or "No human commits found"
**Assessment Date**: `YYYY-MM-DD`
**Summary of Style Guide Adherence**:
A brief, one-paragraph summary of how well the project adheres to the style guide. Mention its strengths and weaknesses in general terms.
**Opportunities for Improvement**:
- A concrete, actionable bullet point describing a specific area for improvement.
- Another actionable bullet point.
- A third bullet point, if applicable. Focus on providing clear, specific, and helpful recommendations.
---
```

691
.prompts/llm.md Normal file
View File

@@ -0,0 +1,691 @@
You are an expert Dart and Flutter developer on the Flutter team at Google. Your code must adhere to this style guide.
## Core Philosophy
- **Follow Effective Dart guidelines.**
- **Optimize for readability**: Write code that is easy to understand and maintain
- **Write detailed documentation**: Every public API should be well-documented
- **Keep one source of truth**: Avoid duplicating state across your application
- **Design APIs from the developer's perspective**: Consider how the API will be used
- **Create useful error messages**: Error messages should guide developers toward solutions
- **Write tests first**: When fixing bugs, write failing tests first, then fix the bug
- **Avoid workarounds**: Take time to fix problems correctly rather than implementing temporary solutions
## Naming Conventions
### Identifier Types (Official Dart Guidelines)
#### UpperCamelCase
- **Classes**: `MyWidget`, `UserRepository`, `HttpClient`
- **Enum types**: `ButtonType`, `AnimationState`, `ConnectionState`
- **Typedefs**: `EventCallback`, `ValidatorFunction`
- **Type parameters**: `<T>`, `<K, V>`, `<TModel>`
- **Extensions**: `StringExtension`, `MyFancyList`, `SmartIterable`
#### lowerCamelCase
- **Variables**: `userName`, `isLoading`, `itemCount`
- **Parameters**: `onPressed`, `itemBuilder`, `scrollDirection`
- **Class members**: `_privateField`, `publicMethod`
- **Top-level functions**: `buildWidget`, `validateInput`
- **Constants**: `defaultPadding`, `maxRetries`, `pi` (prefer over SCREAMING_CAPS)
#### lowercase_with_underscores
- **Packages**: `my_package`, `http_client`
- **Directories**: `lib/widgets/custom`, `test/unit_tests`
- **Source files**: `user_profile_widget.dart`, `file_system.dart`
- **Import prefixes**: `import 'dart:math' as math;`, `import 'package:foo/foo.dart' as foo_lib;`
### Flutter-Specific Guidelines
- **Global constants**: Begin with prefix "k": `kDefaultTimeout`, `kMaxItems`
- **Avoid abbreviations**: Use `button` instead of `btn`, `number` instead of `num`
- **Acronyms**: Capitalize acronyms longer than two letters like regular words: `HttpClient` not `HTTPClient`
- **Unused parameters**: Use wildcards (`_`) for unused callback parameters
- **Private identifiers**: Use leading underscores only for truly private members
- **Avoid Hungarian notation**: Don't use prefix letters like `strName` or `intCount`
## Code Organization and Structure
- **Define related classes in the same library.**
- **For large libraries, group smaller libraries by exporting them in a top-level library.**
- **Group related libraries in the same folder.**
### Import Ordering (Strict Dart Convention)
```dart
// 1. Dart core libraries (alphabetically)
import 'dart:async';
import 'dart:convert';
import 'dart:math';
// 2. Flutter and package imports (alphabetically)
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
// 3. Relative imports (alphabetically)
import '../models/user.dart';
import '../widgets/custom_button.dart';
import 'user_repository.dart';
// 4. Exports (if any, in separate section)
export 'src/my_library.dart';
```
### Class Member Ordering (Flutter Team Convention)
```dart
class MyWidget extends StatefulWidget {
// 1. Constructors first
const MyWidget({
super.key,
required this.title,
this.isEnabled = true,
});
// 2. Public constants
static const double kDefaultHeight = 48.0;
// 3. Public fields
final String title;
final bool isEnabled;
// 4. Private constants
static const double _defaultPadding = 16.0;
// 5. Private fields
String? _cachedValue;
// 6. Getters and setters
bool get isDisabled => !isEnabled;
// 7. Public methods
@override
State<MyWidget> createState() => _MyWidgetState();
// 8. Private methods
void _updateCache() {
// Implementation
}
}
```
## Formatting and Style Rules
### Line Length and Basic Formatting
- **Always use `dart format`** for automatic code formatting
- **Prefer lines 80 characters or fewer** for better readability
- **Maximum 100 characters for comments** (Flutter team preference)
- **Always use curly braces** for all flow control statements
- **Don't add trailing comments**
```dart
// Good - always use braces
if (condition) {
print('hello');
}
// Bad - missing braces
if (condition) print('hello');
```
### Function and Method Formatting
```dart
// Use "=>" for short functions and getters
String get displayName => '$firstName $lastName';
int get age => DateTime.now().year - birthYear;
// Use braces for longer functions
String formatUserName(String first, String last) {
if (first.isEmpty && last.isEmpty) {
return 'Anonymous';
}
return '$first $last'.trim();
}
```
### Dart-Specific Formatting Rules
- **Prefer `+=` over `++`** for increment operations: `counter += 1;`
- **Use collection literals** when possible: `<int>[]` instead of `List<int>()`
- **Adjacent string literals** for concatenation:
```dart
var longMessage = 'This is a very long message '
'that spans multiple lines.';
```
## Type Annotations and Safety
### Type Annotations (Required by Flutter Team)
```dart
// DO annotate return types on function declarations
String formatName(String first, String last) {
return '$first $last';
}
// DO annotate parameter types on function declarations
void updateUser(String id, Map<String, dynamic> data) {
// Implementation
}
// DO use explicit types for variables (avoid var/dynamic)
final List<User> users = [];
final Map<String, int> scores = {};
```
### Null Safety Best Practices
```dart
// DON'T explicitly initialize variables to null
String? name; // Good
String? name = null; // Bad
// DO use proper null-aware operators
final displayName = user?.name ?? 'Unknown';
// DO use late for non-nullable fields initialized later
class MyWidget extends StatefulWidget {
late final AnimationController controller;
}
```
### Future and Async Types
```dart
// DO use Future<void> for async functions that don't return values
Future<void> saveUser(User user) async {
await repository.save(user);
}
// DO prefer async/await over raw futures
Future<List<User>> loadUsers() async {
final response = await http.get('/api/users');
return parseUsers(response.body);
}
```
## Documentation Standards
### Documentation Comments (Dart Standard)
```dart
/// A custom button widget that provides enhanced styling and behavior.
///
/// This widget wraps Flutter's [ElevatedButton] and adds additional
/// functionality like loading states and custom styling.
///
/// The [onPressed] callback is called when the button is tapped.
/// Set [isEnabled] to false to disable the button.
///
/// Example usage:
/// ```dart
/// CustomButton(
/// onPressed: () => print('Pressed'),
/// child: Text('Click me'),
/// isEnabled: true,
/// )
/// ```
class CustomButton extends StatelessWidget {
/// Creates a custom button.
///
/// The [onPressed] and [child] parameters are required.
/// The [isEnabled] parameter defaults to true.
const CustomButton({
super.key,
required this.onPressed,
required this.child,
this.isEnabled = true,
});
/// Called when the button is pressed.
final VoidCallback? onPressed;
/// The widget to display inside the button.
final Widget child;
/// Whether the button is enabled for interaction.
final bool isEnabled;
}
```
### Method Documentation Requirements
```dart
/// Validates the given email address format.
///
/// Returns `true` if the [email] is valid according to RFC standards.
/// Returns `false` if the format is invalid.
///
/// Throws [ArgumentError] if [email] is null or empty.
///
/// Example:
/// ```dart
/// final isValid = validateEmail('user@example.com'); // true
/// ```
bool validateEmail(String email) {
if (email.isEmpty) {
throw ArgumentError('Email cannot be empty');
}
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
```
## Flutter-Specific Patterns
- **Prefer composition over inheritance.**
- **Avoid large build() methods by creating smaller Widgets with a reusable API.**
- **Use small, private Widget classes instead of private helper methods that return a Widget.**
- **Use lazy lists wherever possible using ListView.builder.**
### Widget Construction
```dart
class CustomCard extends StatelessWidget {
const CustomCard({
super.key,
required this.title,
required this.content,
this.elevation = 2.0,
this.onTap,
});
final String title;
final Widget content;
final double elevation;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8.0),
content,
],
),
),
),
);
}
}
```
## State Management
- **Don't use a third party package for state management unless explicitly asked to do so.**
- **Use manual dependency injection (declaring objects that the class depends in its constructor) as much as possible to make the dependencies required by the class clear in it's API.**
- **If asked to use Provider, use it for app-level objects that are used often.**
- **Use Model-View-ViewModel for application architecture.**
- **Use ChangeNotifier or a class with ValueNotifiers for ViewModel classes.**
- **Use a ListenableBuilder to listen to changes to the ViewModel.**
- **Use a StatefulWidget for widgets that are reusable or single-purpose, and don't necessarily require a MVVM architecture.**
## Routing
- **Use Navigator for screens that are short-lived and don't need to be deep-linkable.**
## Data
- **Use json_serializable and json_annotation for parsing and encoding JSON data.**
- **Use fieldRename: FieldRename.snake to encode data with snake case.**
## Code Generation
- **Use build_runner for any generated code in the app.**
## String and Collection Best Practices
### String Interpolation and Formatting
```dart
// PREFER using interpolation to compose strings
final name = 'John Doe';
final age = 25;
final message = 'Hello, $name! You are $age years old.';
final calculation = 'Next year you will be ${age + 1}.';
// DO use adjacent strings for long literals
const longText = 'This is a very long text that '
'spans multiple lines for better '
'readability in the source code.';
```
### Collection Usage
```dart
// DO use collection literals
final List<String> names = [];
final Map<String, int> scores = {};
final Set<int> uniqueIds = {};
// DON'T use .length to check if empty
if (names.isEmpty) { // Good
print('No names');
}
if (names.length == 0) { // Bad
print('No names');
}
// DO use collection methods effectively
final activeUsers = users.where((user) => user.isActive).toList();
final userNames = users.map((user) => user.name).toList();
```
### Function References
```dart
// DON'T create lambdas when tear-offs work
final numbers = [1, 2, 3, 4, 5];
// Good - use tear-off
numbers.forEach(print);
// Bad - unnecessary lambda
numbers.forEach((number) {
print(number);
});
```
## Error Handling and Exceptions
### Meaningful Error Messages (Flutter Team Priority)
```dart
// Good: Specific and actionable
throw ArgumentError('Email must contain @ symbol');
throw StateError('Cannot call increment() after dispose()');
// Bad: Vague and unhelpful
throw ArgumentError('Invalid input');
throw Exception('Error occurred');
```
### Exception Handling Patterns
```dart
Future<User> fetchUser(String id) async {
try {
final response = await api.getUser(id);
return User.fromJson(response.data);
} on NetworkException catch (e) {
throw UserFetchException('Failed to fetch user: ${e.message}');
} on FormatException catch (e) {
throw UserParseException('Invalid user data format: ${e.message}');
} catch (e) {
throw UserFetchException('Unexpected error: ${e.toString()}');
}
}
```
## Testing Guidelines
- **Use package:integration_test for integration tests.**
- **Use package:checks instead of matchers from package:test or package:matcher.**
### Widget Testing
```dart
testWidgets('CustomButton should call onPressed when tapped', (tester) async {
bool wasPressed = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomButton(
onPressed: () => wasPressed = true,
child: const Text('Test Button'),
),
),
),
);
await tester.tap(find.byType(CustomButton));
await tester.pump();
expect(wasPressed, isTrue);
});
```
### Unit Testing Structure
```dart
group('UserRepository', () {
late UserRepository repository;
late MockApiClient mockApi;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(api: mockApi);
});
group('getUser', () {
test('should return user when valid ID provided', () async {
// Arrange
const userId = '123';
final expectedUser = User(id: userId, name: 'John');
when(() => mockApi.getUser(userId))
.thenAnswer((_) async => expectedUser.toJson());
// Act
final user = await repository.getUser(userId);
// Assert
expect(user.id, equals(userId));
expect(user.name, equals('John'));
});
test('should throw exception when user not found', () async {
// Arrange
const userId = 'invalid';
when(() => mockApi.getUser(userId))
.thenThrow(NotFoundException());
// Act & Assert
expect(
() => repository.getUser(userId),
throwsA(isA<UserNotFoundException>()),
);
});
});
});
```
## Advanced Dart Patterns
- **Use Patterns and pattern-matching features where possible.**
### Immutability and Data Classes
```dart
class User {
const User({
required this.id,
required this.name,
required this.email,
this.isActive = true,
});
final String id;
final String name;
final String email;
final bool isActive;
// Use copyWith for immutable updates
User copyWith({
String? id,
String? name,
String? email,
bool? isActive,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isActive: isActive ?? this.isActive,
);
}
// Override equality
@override
bool operator ==(Object other) =>
other is User &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
@override
String toString() => 'User(id: $id, name: $name, email: $email)';
}
```
### Enum Usage and Switch Statements
```dart
enum ConnectionState {
disconnected,
connecting,
connected,
error,
}
// Use switch without default to catch all cases
Widget buildConnectionIndicator(ConnectionState state) {
switch (state) {
case ConnectionState.disconnected:
return const Icon(Icons.wifi_off, color: Colors.grey);
case ConnectionState.connecting:
return const CircularProgressIndicator();
case ConnectionState.connected:
return const Icon(Icons.wifi, color: Colors.green);
case ConnectionState.error:
return const Icon(Icons.error, color: Colors.red);
}
}
```
### Effective Use of Assert
```dart
class Rectangle {
const Rectangle({
required this.width,
required this.height,
}) : assert(width > 0, 'Width must be positive'),
assert(height > 0, 'Height must be positive');
final double width;
final double height;
double get area {
assert(width > 0 && height > 0, 'Invalid rectangle dimensions');
return width * height;
}
}
```
## Performance and Best Practices
### Const Constructors and Optimization
```dart
// Use const constructors when possible
const EdgeInsets.all(16.0);
const SizedBox(height: 8.0);
// Create const widgets for better performance
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
```
### Efficient Widget Building
```dart
class ProductList extends StatelessWidget {
const ProductList({
super.key,
required this.products,
});
final List<Product> products;
@override
Widget build(BuildContext context) {
// Don't do heavy computation in build method
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductTile(
key: ValueKey(product.id),
product: product,
);
},
);
}
}
```
## Anti-Patterns to Avoid
### Common Mistakes
```dart
// DON'T use double negatives
bool get isNotDisabled => !disabled; // Confusing
// DO use positive naming
bool get isEnabled => !disabled; // Clear
// DON'T use global state
var globalCounter = 0; // Avoid
// DO use proper state management
class CounterProvider extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
}
// DON'T create classes with only static members
class MathUtils {
static double pi = 3.14159;
static double circleArea(double radius) => pi * radius * radius;
}
// DO use top-level functions and constants
const double pi = 3.14159;
double circleArea(double radius) => pi * radius * radius;
// DON'T avoid using APIs as intended
class TimeSlot {
TimeSlot(this.start, this.end);
DateTime start;
DateTime end;
}
// DO follow API design principles
class TimeSlot {
const TimeSlot({
required this.start,
required this.end,
}) : assert(start.isBefore(end), 'Start must be before end');
final DateTime start;
final DateTime end;
}
```
## Tools and Development Workflow
### Required Tools
- **`dart format`**: Automatic code formatting (mandatory)
- **`dart analyze`**: Static analysis and linting
- **`flutter test`**: Run tests
- **IDE setup**: Configure your IDE to run these tools automatically
- **Pre-commit hooks**: Ensure code quality before commits
### Code Quality Checklist
- [ ] All code formatted with `dart format`
- [ ] No analyzer warnings or errors
- [ ] All public APIs documented with `///` comments
- [ ] Tests written for new functionality
- [ ] Error messages are specific and actionable
- [ ] Type annotations present on all public APIs
- [ ] Immutable objects used where appropriate
- [ ] Assert statements used to verify contracts
This unified style guide ensures consistency with both Flutter team practices and official Dart conventions, helping create maintainable, readable code that follows established patterns.

93
.prompts/release.md Normal file
View File

@@ -0,0 +1,93 @@
You are an AI developer specializing in Dart and Flutter. Your primary
responsibility is to maintain this monorepo of sample projects,
ensuring they are up-to-date, clean, and well-organized.
This workflow is triggered when a new Flutter/Dart version is
released. Follow these steps precisely:
1. Prepare your environment:
* Switch to the `beta` branch and ensure it's up-to-date:
```bash
git checkout beta
git pull origin beta
```
* Switch your local Flutter SDK to the `beta` channel and upgrade:
```bash
flutter channel beta
flutter upgrade
```
2. Pre-Update Analysis from Blog Post (If Provided):
* The user may provide a URL to a blog post announcing the new
Flutter and Dart release.
* If a URL is provided, read the blog post to identify key
changes, new features, and updated best practices.
* Before proceeding with the steps below, apply the necessary
code modifications throughout the repository to adopt these new
features and best practices. For example, this might include
updating APIs, adopting new lint rules, or refactoring code to
use new language features.
3. Initial Setup:
* First, determine the precise Dart SDK version you will be
working with. Execute the command `flutter --version --machine`.
* Parse the JSON output to find the value of dartSdkVersion. You
will need the version number (e.g., 3.9.0). Let's call this
DART_VERSION.
* Next, read the pubspec.yaml file at the root of the monorepo.
* Parse the workspace section to get a list of all the relative
paths for the projects you need to process.
4. Process Each Project:
* Create a file called
`logs/YYYY-MM-DD_HH-MM-SS-release_update_log.txt`, but replace
YYYY-MM-DD_HH-MM-SS with the current date/time.
* Iterate through each project path you discovered in the
workspace.
* For each project, perform the following actions in its
directory. If any command returns output warnings, errors or info,
log the project path and the message in the log file, then move to
the next project.
5. Project-Specific Tasks:
* Update SDK Constraint:
* Read the project's pubspec.yaml file.
* Find the environment.sdk key.
* Update its value to ^DART_VERSION-0 (e.g., ^3.9.0-0).
* Save the modified pubspec.yaml file.
* Run Quality Checks:
* Run dart analyze --fatal-infos --fatal-warnings.
* Run dart format . to ensure the code is correctly formatted.
* Run Tests:
* Check if a test directory exists in the project.
* Exception: Do not run tests for the project named
material_3_demo.
* If a test directory exists (and it's not the excluded
project), run flutter test.
6. Fix issues:
* For each message in the
`logs/YYYY-MM-DD_HH-MM-SS-release_update_log.txt` file, attempt
to fix the problem. After 30 seconds of being unable to fix it,
move onto the next issue.
* If you fix the issue successfully, remove the message from the
log file.
* If you can't fix the issue, just leave the message in the log
file so the user can fix it.
7. Final Report:
* After processing all projects, generate a summary report.
* The report must include:
* The total number of projects processed.
* A list of projects that were updated and passed all checks
successfully.
* A list of projects that failed, specifying which command
failed for each.
8. Create Pull Request:
* After generating the report, create a pull request.
* Use the `gh` CLI tool for this purpose.
* The title of the pull request should be: `Prepare release for
Dart DART_VERSION / Flutter FLUTTER_VERSION`.
* The body of the pull request should contain the summary report
from the previous step.

View File

@@ -1,32 +0,0 @@
# Changelog
The purpose of this changelog is to track the freshness of samples and which
samples reflect *current best practices*. It describes **human-made, significant
** changes made to the repository or samples in the repository.
While all samples in this repository build and run, some of them were written
long ago, and no longer reflect what we want developers to learn. For example,
samples should have been refactored when Dart 3 released to include patterns and
records, where appropriate.
* **DO include:**
* The addition of new samples.
* The removal of existing samples.
* Considerable refactoring of any given sample.
* **DO NOT include:**
* Simple changes that reflect minor version bumps in Flutter. For example,
in a recent Flutter update, `Color.red` became `Color.r`.
* Dependency updates.
* Bug fixes.
* Any changes made to simply 'keep the lights on'.
# Log
| DATE (YYYY-MM-DD) | Sample(s) | author | Changes |
|-------------------|-------------------|--------------|-----------------------------------------------|
| NEXT GOES HERE | | | |
| | | | |
| 2024-12-04 | N/A - repo change | ericwindmill | Added changelog |
| 2024-11-27 | fake_sample | ericwindmill | Refactored fake_sample to use Dart 3 features |
| 2020-04-17 | fake_sample | ericwindmill | Created fake_sample |

View File

@@ -7,7 +7,7 @@ A collection of open source samples that illustrate best practices for
## Contributing ## Contributing
We're very appreciative of fixes and necessary improvements to the existing samples. **But in most cases, we're not currently adding new samples to this repository** while we rethink sample code in the post-LLM world. We appreciate fixes and necessary improvements to existing samples. **But in most cases, we're not currently adding new samples to this repository** while we rethink sample code in the new LLM world.
Please read the [contributor's guide] if you have contributions. Please read the [contributor's guide] if you have contributions.
@@ -105,23 +105,6 @@ See the [getting started guide] to install the `flutter` tool.
> If you want to run an add-to-app sample, there are additional requirements. > If you want to run an add-to-app sample, there are additional requirements.
> We suggest reading the [add-to-app documentation]. > We suggest reading the [add-to-app documentation].
### Tip: minimize download size
As this repository is quite big, you can use
[svn] to download a single example.
For example:
```
svn co https://github.com/flutter/samples/trunk/provider_shopper
```
You can also use a [partial clone] to skip blob objects
that aren't currently checked out, while including the full commit history:
```
git clone --filter=blob:none https://github.com/flutter/samples.git
```
## Interested in contributing? ## Interested in contributing?
See the [contributor's guide]! See the [contributor's guide]!

View File

@@ -23,7 +23,12 @@ class Cell extends StatefulWidget {
class _CellState extends State<Cell> with WidgetsBindingObserver { class _CellState extends State<Cell> with WidgetsBindingObserver {
static const double gravity = 9.81; static const double gravity = 9.81;
static final AccelerometerEvent defaultPosition = AccelerometerEvent(0, 0, 0); static final AccelerometerEvent defaultPosition = AccelerometerEvent(
0,
0,
0,
DateTime.now(),
);
int cellNumber = 0; int cellNumber = 0;
Random? _random; Random? _random;
@@ -82,7 +87,10 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
builder: (context) { builder: (context) {
return Card( return Card(
// Mimic the platform Material look. // Mimic the platform Material look.
margin: const EdgeInsets.symmetric(horizontal: 36, vertical: 24), margin: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 24,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
@@ -112,22 +120,22 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
child: StreamBuilder<AccelerometerEvent>( child: StreamBuilder<AccelerometerEvent>(
// Don't continuously rebuild for nothing when the // Don't continuously rebuild for nothing when the
// cell isn't visible. // cell isn't visible.
stream: stream: appLifecycleState == AppLifecycleState.resumed
appLifecycleState == AppLifecycleState.resumed ? accelerometerEventStream()
? accelerometerEventStream() : Stream.value(defaultPosition),
: Stream.value(defaultPosition),
initialData: defaultPosition, initialData: defaultPosition,
builder: (context, snapshot) { builder: (context, snapshot) {
return Transform( return Transform(
// Figure out the phone's orientation relative // Figure out the phone's orientation relative
// to gravity's direction. Ignore the z vector. // to gravity's direction. Ignore the z vector.
transform: Matrix4.rotationX( transform:
snapshot.data!.y / gravity * pi / 2, Matrix4.rotationX(
)..multiply( snapshot.data!.y / gravity * pi / 2,
Matrix4.rotationY( )..multiply(
snapshot.data!.x / gravity * pi / 2, Matrix4.rotationY(
), snapshot.data!.x / gravity * pi / 2,
), ),
),
alignment: Alignment.center, alignment: Alignment.center,
child: const FlutterLogo(size: 72), child: const FlutterLogo(size: 72),
); );

View File

@@ -1,17 +1,17 @@
name: flutter_module_using_plugin name: flutter_module_using_plugin_android_view
description: An example Flutter module that uses a plugin. description: An example Flutter module that uses a plugin.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.8.1-0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
provider: ^6.0.2 provider: ^6.1.5
url_launcher: ^6.0.20 url_launcher: ^6.3.2
sensors_plus: ^5.0.1 sensors_plus: ^6.1.1
dev_dependencies: dev_dependencies:
analysis_defaults: analysis_defaults:

View File

@@ -6,7 +6,7 @@
// tree, read text, and verify that the values of widget properties are correct. // tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_module_using_plugin/main.dart'; import 'package:flutter_module_using_plugin_android_view/main.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@@ -125,28 +125,26 @@ class _BookDetailState extends State<BookDetail> {
IconButton( IconButton(
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
// Pressing save sends the updated book to the platform. // Pressing save sends the updated book to the platform.
onPressed: onPressed: book != null
book != null ? () {
? () { hostApi.finishEditingBook(book!);
hostApi.finishEditingBook(book!); clear();
clear(); }
} : null,
: null,
), ),
], ],
), ),
body: body: book == null
book == null // Draw a spinner until the platform gives us the book to show details
// Draw a spinner until the platform gives us the book to show details // for.
// for. ? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator()) : BookForm(
: BookForm( book: book!,
book: book!, focusNode: textFocusNode,
focusNode: textFocusNode, authorTextController: authorTextController,
authorTextController: authorTextController, subtitleTextController: subtitleTextController,
subtitleTextController: subtitleTextController, titleTextController: titleTextController,
titleTextController: titleTextController, ),
),
); );
} }
} }

View File

@@ -2,11 +2,11 @@ name: flutter_module_books
description: A Flutter module using the Pigeon package to demonstrate description: A Flutter module using the Pigeon package to demonstrate
integrating Flutter in a realistic scenario where the existing platform app integrating Flutter in a realistic scenario where the existing platform app
already has business logic and middleware constraints. already has business logic and middleware constraints.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:

View File

@@ -20,11 +20,15 @@ void main() {
expect(mockHostApi.cancelCalls, 1); expect(mockHostApi.cancelCalls, 1);
}); });
testWidgets('Pressing done calls the finish editing API', (tester) async { testWidgets('Pressing done calls the finish editing API', (
tester,
) async {
MockHostBookApi mockHostApi = MockHostBookApi(); MockHostBookApi mockHostApi = MockHostBookApi();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp(home: BookDetail(book: Book(), hostApi: mockHostApi)), MaterialApp(
home: BookDetail(book: Book(), hostApi: mockHostApi),
),
); );
await tester.tap(find.byIcon(Icons.check)); await tester.tap(find.byIcon(Icons.check));

View File

@@ -1,10 +1,10 @@
name: flutter_module name: flutter_module_fullscreen
description: An example Flutter module. description: An example Flutter module.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:

View File

@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_module/main.dart'; import 'package:flutter_module_fullscreen/main.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_module/main.dart' as app; import 'package:flutter_module_fullscreen/main.dart' as app;
// This alternate entrypoint is used for espresso testing. See // This alternate entrypoint is used for espresso testing. See
// https://pub.dev/packages/espresso for details. // https://pub.dev/packages/espresso for details.

View File

@@ -82,7 +82,10 @@ class _MyHomePageState extends State<MyHomePage> {
'$_counter', '$_counter',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
), ),
TextButton(onPressed: _incrementCounter, child: const Text('Add')), TextButton(
onPressed: _incrementCounter,
child: const Text('Add'),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
_channel.invokeMethod<void>("next", _counter); _channel.invokeMethod<void>("next", _counter);

View File

@@ -1,10 +1,10 @@
name: multiple_flutters_module name: multiple_flutters_module
description: A module that is embedded in the multiple_flutters_ios and multiple_flutters_android sample code. description: A module that is embedded in the multiple_flutters_ios and multiple_flutters_android sample code.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:

View File

@@ -23,7 +23,12 @@ class Cell extends StatefulWidget {
class _CellState extends State<Cell> with WidgetsBindingObserver { class _CellState extends State<Cell> with WidgetsBindingObserver {
static const double gravity = 9.81; static const double gravity = 9.81;
static final AccelerometerEvent defaultPosition = AccelerometerEvent(0, 0, 0); static final AccelerometerEvent defaultPosition = AccelerometerEvent(
0,
0,
0,
DateTime.now(),
);
int cellNumber = 0; int cellNumber = 0;
Random? _random; Random? _random;
@@ -82,7 +87,10 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
builder: (context) { builder: (context) {
return Card( return Card(
// Mimic the platform Material look. // Mimic the platform Material look.
margin: const EdgeInsets.symmetric(horizontal: 36, vertical: 24), margin: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 24,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
@@ -112,10 +120,9 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
child: StreamBuilder<AccelerometerEvent>( child: StreamBuilder<AccelerometerEvent>(
// Don't continuously rebuild for nothing when the // Don't continuously rebuild for nothing when the
// cell isn't visible. // cell isn't visible.
stream: stream: appLifecycleState == AppLifecycleState.resumed
appLifecycleState == AppLifecycleState.resumed ? accelerometerEventStream()
? accelerometerEventStream() : Stream.value(defaultPosition),
: Stream.value(defaultPosition),
initialData: defaultPosition, initialData: defaultPosition,
builder: (context, snapshot) { builder: (context, snapshot) {
final data = snapshot.data; final data = snapshot.data;
@@ -125,11 +132,14 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
return Transform( return Transform(
// Figure out the phone's orientation relative // Figure out the phone's orientation relative
// to gravity's direction. Ignore the z vector. // to gravity's direction. Ignore the z vector.
transform: Matrix4.rotationX( transform:
data.y / gravity * pi / 2, Matrix4.rotationX(
)..multiply( data.y / gravity * pi / 2,
Matrix4.rotationY(data.x / gravity * pi / 2), )..multiply(
), Matrix4.rotationY(
data.x / gravity * pi / 2,
),
),
alignment: Alignment.center, alignment: Alignment.center,
child: const FlutterLogo(size: 72), child: const FlutterLogo(size: 72),
); );

View File

@@ -1,17 +1,17 @@
name: flutter_module_using_plugin name: flutter_module_using_plugin
description: An example Flutter module that uses a plugin. description: An example Flutter module that uses a plugin.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
provider: ^6.0.2 provider: ^6.0.2
url_launcher: ^6.0.20 url_launcher: ^6.0.20
sensors_plus: ^5.0.1 sensors_plus: ^6.1.1
dev_dependencies: dev_dependencies:
analysis_defaults: analysis_defaults:

View File

@@ -1,10 +1,10 @@
name: flutter_module name: flutter_module
description: An example Flutter module. description: An example Flutter module.
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:

View File

@@ -1,3 +1,4 @@
# https://dart.dev/guides/libraries/private-files # https://dart.dev/guides/libraries/private-files
# Created by `dart pub` # Created by `dart pub`
.dart_tool/ .dart_tool/
.build/

View File

@@ -1,5 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
formatter:
trailing_commas: preserve
page_width: 79
analyzer: analyzer:
language: language:
strict-casts: true strict-casts: true

View File

@@ -1,11 +1,14 @@
name: analysis_defaults name: analysis_defaults
description: Analysis defaults for flutter/samples description: Analysis defaults for flutter/samples
publish_to: none publish_to: none
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
# NOTE: Code is not allowed in this package. Do not add more dependencies. # NOTE: Code is not allowed in this package. Do not add more dependencies.
# The `flutter_lints` dependency is required for `lib/flutter.yaml`. # The `flutter_lints` dependency is required for `lib/flutter.yaml`.
dependencies: dependencies:
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0

View File

@@ -1,23 +1,20 @@
name: splash_screen_sample name: splash_screen_sample
description: A sample Flutter app with animated splash screen on Android 12. description: A sample Flutter app with animated splash screen on Android 12.
publish_to: "none" publish_to: "none"
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
dev_dependencies: dev_dependencies:
analysis_defaults: analysis_defaults:
path: ../analysis_defaults path: ../analysis_defaults
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:

View File

@@ -21,7 +21,8 @@ const double windowWidth = 480;
const double windowHeight = 854; const double windowHeight = 854;
void setupWindow() { void setupWindow() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
setWindowTitle('Animation Samples'); setWindowTitle('Animation Samples');
setWindowMinSize(const Size(windowWidth, windowHeight)); setWindowMinSize(const Size(windowWidth, windowHeight));
@@ -43,7 +44,11 @@ class Demo {
final String route; final String route;
final WidgetBuilder builder; final WidgetBuilder builder;
const Demo({required this.name, required this.route, required this.builder}); const Demo({
required this.name,
required this.route,
required this.builder,
});
} }
final basicDemos = [ final basicDemos = [

View File

@@ -49,7 +49,9 @@ class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
animation: animation, animation: animation,
builder: (context, child) { builder: (context, child) {
return ElevatedButton( return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: animation.value), style: ElevatedButton.styleFrom(
backgroundColor: animation.value,
),
child: child, child: child,
onPressed: () { onPressed: () {
switch (controller.status) { switch (controller.status) {

View File

@@ -49,18 +49,21 @@ class _FadeTransitionDemoState extends State<FadeTransitionDemo>
children: [ children: [
FadeTransition( FadeTransition(
opacity: _animation, opacity: _animation,
child: const Icon(Icons.star, color: Colors.amber, size: 300), child: const Icon(
Icons.star,
color: Colors.amber,
size: 300,
),
), ),
ElevatedButton( ElevatedButton(
child: const Text('animate'), child: const Text('animate'),
onPressed: onPressed: () => setState(() {
() => setState(() { _controller
_controller .animateTo(1.0)
.animateTo(1.0) .then<TickerFuture>(
.then<TickerFuture>( (value) => _controller.animateBack(0.0),
(value) => _controller.animateBack(0.0), );
); }),
}),
), ),
], ],
), ),

View File

@@ -74,7 +74,10 @@ class _TweenSequenceDemoState extends State<TweenSequenceDemo>
child: child, child: child,
); );
}, },
child: const Text('Animate', style: TextStyle(color: Colors.white)), child: const Text(
'Animate',
style: TextStyle(color: Colors.white),
),
), ),
), ),
); );

View File

@@ -75,7 +75,9 @@ class _AnimatedListDemoState extends State<AnimatedListDemo> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('AnimatedList'), title: const Text('AnimatedList'),
actions: [IconButton(icon: const Icon(Icons.add), onPressed: addUser)], actions: [
IconButton(icon: const Icon(Icons.add), onPressed: addUser),
],
), ),
body: SafeArea( body: SafeArea(
child: AnimatedList( child: AnimatedList(

View File

@@ -11,7 +11,8 @@ class AnimatedPositionedDemo extends StatefulWidget {
static String routeName = 'misc/animated_positioned'; static String routeName = 'misc/animated_positioned';
@override @override
State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState(); State<AnimatedPositionedDemo> createState() =>
_AnimatedPositionedDemoState();
} }
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> { class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
@@ -54,12 +55,11 @@ class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
left: leftPosition, left: leftPosition,
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
child: InkWell( child: InkWell(
onTap: onTap: () => changePosition(
() => changePosition( size.height -
size.height - (appBar.preferredSize.height + topPadding + 50),
(appBar.preferredSize.height + topPadding + 50), size.width - 150,
size.width - 150, ),
),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
width: 150, width: 150,
@@ -68,8 +68,9 @@ class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
child: Text( child: Text(
'Click Me', 'Click Me',
style: TextStyle( style: TextStyle(
color: color: Theme.of(
Theme.of(context).buttonTheme.colorScheme!.onPrimary, context,
).buttonTheme.colorScheme!.onPrimary,
), ),
), ),
), ),

View File

@@ -48,8 +48,8 @@ class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
title: const Text('AnimatedSwitcher'), title: const Text('AnimatedSwitcher'),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () =>
() => setState(() => container = generateContainer(++keyCount)), setState(() => container = generateContainer(++keyCount)),
child: const Text('Change Widget'), child: const Text('Change Widget'),
), ),
], ],
@@ -61,9 +61,8 @@ class _AnimatedSwitcherDemoState extends State<AnimatedSwitcherDemo> {
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
child: container, child: container,
transitionBuilder: transitionBuilder: (child, animation) =>
(child, animation) => ScaleTransition(scale: animation, child: child),
ScaleTransition(scale: animation, child: child),
), ),
), ),
); );

View File

@@ -15,8 +15,9 @@ class CarouselDemo extends StatelessWidget {
'assets/eat_sydney_sm.jpg', 'assets/eat_sydney_sm.jpg',
]; ];
final List<Widget> images = final List<Widget> images = fileNames
fileNames.map((file) => Image.asset(file, fit: BoxFit.cover)).toList(); .map((file) => Image.asset(file, fit: BoxFit.cover))
.toList();
@override @override
Widget build(context) { Widget build(context) {
@@ -77,29 +78,32 @@ class _CarouselState extends State<Carousel> {
}, },
controller: _controller, controller: _controller,
scrollBehavior: ScrollConfiguration.of(context).copyWith( scrollBehavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {ui.PointerDeviceKind.touch, ui.PointerDeviceKind.mouse}, dragDevices: {
ui.PointerDeviceKind.touch,
ui.PointerDeviceKind.mouse,
},
), ),
itemBuilder: itemBuilder: (context, index) => AnimatedBuilder(
(context, index) => AnimatedBuilder( animation: _controller,
animation: _controller, builder: (context, child) {
builder: (context, child) { var result = _pageHasChanged
var result = ? _controller.page!
_pageHasChanged ? _controller.page! : _currentPage * 1.0; : _currentPage * 1.0;
// The horizontal position of the page between a 1 and 0 // The horizontal position of the page between a 1 and 0
var value = result - index; var value = result - index;
value = (1 - (value.abs() * .5)).clamp(0.0, 1.0); value = (1 - (value.abs() * .5)).clamp(0.0, 1.0);
return Center( return Center(
child: SizedBox( child: SizedBox(
height: Curves.easeOut.transform(value) * size.height, height: Curves.easeOut.transform(value) * size.height,
width: Curves.easeOut.transform(value) * size.width, width: Curves.easeOut.transform(value) * size.width,
child: child, child: child,
), ),
); );
}, },
child: widget.itemBuilder(context, index), child: widget.itemBuilder(context, index),
), ),
); );
} }

View File

@@ -91,13 +91,12 @@ class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
DropdownButton<CurveChoice>( DropdownButton<CurveChoice>(
items: items: curves.map((curve) {
curves.map((curve) { return DropdownMenuItem<CurveChoice>(
return DropdownMenuItem<CurveChoice>( value: curve,
value: curve, child: Text(curve.name),
child: Text(curve.name), );
); }).toList(),
}).toList(),
onChanged: (newCurve) { onChanged: (newCurve) {
if (newCurve != null) { if (newCurve != null) {
setState(() { setState(() {
@@ -114,13 +113,12 @@ class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
DropdownButton<CurveChoice>( DropdownButton<CurveChoice>(
items: items: curves.map((curve) {
curves.map((curve) { return DropdownMenuItem<CurveChoice>(
return DropdownMenuItem<CurveChoice>( value: curve,
value: curve, child: Text(curve.name),
child: Text(curve.name), );
); }).toList(),
}).toList(),
onChanged: (newCurve) { onChanged: (newCurve) {
if (newCurve != null) { if (newCurve != null) {
setState(() { setState(() {

View File

@@ -52,26 +52,24 @@ class _ExpandCardState extends State<ExpandCard>
duration: duration, duration: duration,
firstCurve: Curves.easeInOutCubic, firstCurve: Curves.easeInOutCubic,
secondCurve: Curves.easeInOutCubic, secondCurve: Curves.easeInOutCubic,
crossFadeState: crossFadeState: selected
selected ? CrossFadeState.showSecond
? CrossFadeState.showSecond : CrossFadeState.showFirst,
: CrossFadeState.showFirst,
// Use Positioned.fill() to pass the constraints to its children. // Use Positioned.fill() to pass the constraints to its children.
// This allows the Images to use BoxFit.cover to cover the correct // This allows the Images to use BoxFit.cover to cover the correct
// size // size
layoutBuilder: ( layoutBuilder:
topChild, (topChild, topChildKey, bottomChild, bottomChildKey) {
topChildKey, return Stack(
bottomChild, children: [
bottomChildKey, Positioned.fill(
) { key: bottomChildKey,
return Stack( child: bottomChild,
children: [ ),
Positioned.fill(key: bottomChildKey, child: bottomChild), Positioned.fill(key: topChildKey, child: topChild),
Positioned.fill(key: topChildKey, child: topChild), ],
], );
); },
},
firstChild: Image.asset( firstChild: Image.asset(
'assets/eat_cape_town_sm.jpg', 'assets/eat_cape_town_sm.jpg',
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@@ -18,20 +18,21 @@ class FlutterAnimateDemo extends StatelessWidget {
body: Center( body: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Text( child:
"Hello Flutter Animate", Text(
style: Theme.of(context).textTheme.headlineLarge, "Hello Flutter Animate",
) style: Theme.of(context).textTheme.headlineLarge,
.animate(onPlay: (controller) => controller.repeat()) )
.then(delay: 250.ms) .animate(onPlay: (controller) => controller.repeat())
.fadeIn(duration: 500.ms) .then(delay: 250.ms)
.then(delay: 250.ms) .fadeIn(duration: 500.ms)
.shimmer(duration: 400.ms) .then(delay: 250.ms)
.then(delay: 250.ms) .shimmer(duration: 400.ms)
.slide() .then(delay: 250.ms)
.then(delay: 250.ms) .slide()
.blur(duration: 500.ms) .then(delay: 250.ms)
.then(delay: 100.ms), .blur(duration: 500.ms)
.then(delay: 100.ms),
), ),
), ),
); );

View File

@@ -29,10 +29,12 @@ class Grid extends StatelessWidget {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return (index >= 20) return (index >= 20)
? const SmallCard(imageAssetName: 'assets/eat_cape_town_sm.jpg') ? const SmallCard(
imageAssetName: 'assets/eat_cape_town_sm.jpg',
)
: const SmallCard( : const SmallCard(
imageAssetName: 'assets/eat_new_orleans_sm.jpg', imageAssetName: 'assets/eat_new_orleans_sm.jpg',
); );
}, },
), ),
); );
@@ -50,7 +52,9 @@ Route _createRoute(BuildContext parentContext, String image) {
).chain(CurveTween(curve: Curves.ease)).animate(animation); ).chain(CurveTween(curve: Curves.ease)).animate(animation);
return Stack( return Stack(
children: [PositionedTransition(rect: rectAnimation, child: child)], children: [
PositionedTransition(rect: rectAnimation, child: child),
],
); );
}, },
); );

View File

@@ -15,10 +15,15 @@ class HeroAnimationDemo extends StatelessWidget {
body: GestureDetector( body: GestureDetector(
child: Hero( child: Hero(
tag: 'hero-page-child', tag: 'hero-page-child',
child: _createHeroContainer(size: 50.0, color: Colors.grey.shade300), child: _createHeroContainer(
size: 50.0,
color: Colors.grey.shade300,
),
), ),
onTap: onTap: () =>
() => Navigator.of(context).push<void>( Navigator.of(
context,
).push<void>(
MaterialPageRoute(builder: (context) => const HeroPage()), MaterialPageRoute(builder: (context) => const HeroPage()),
), ),
), ),
@@ -52,7 +57,9 @@ StatelessWidget _createHeroContainer({
height: size, height: size,
width: size, width: size,
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
margin: size < 100.0 ? const EdgeInsets.all(10.0) : const EdgeInsets.all(0), margin: size < 100.0
? const EdgeInsets.all(10.0)
: const EdgeInsets.all(0),
decoration: BoxDecoration(shape: BoxShape.circle, color: color), decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: const FlutterLogo(), child: const FlutterLogo(),
); );

View File

@@ -78,7 +78,9 @@ class _DraggableCardState extends State<DraggableCard>
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController.unbounded(vsync: this) _controller = AnimationController.unbounded(vsync: this)
..addListener(() => setState(() => _dragAlignment = _animation.value)); ..addListener(
() => setState(() => _dragAlignment = _animation.value),
);
} }
@override @override
@@ -92,17 +94,18 @@ class _DraggableCardState extends State<DraggableCard>
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
return GestureDetector( return GestureDetector(
onPanStart: (details) => _controller.stop(canceled: true), onPanStart: (details) => _controller.stop(canceled: true),
onPanUpdate: onPanUpdate: (details) => setState(
(details) => setState( () => _dragAlignment += Alignment(
() => details.delta.dx / (size.width / 2),
_dragAlignment += Alignment( details.delta.dy / (size.height / 2),
details.delta.dx / (size.width / 2), ),
details.delta.dy / (size.height / 2), ),
), onPanEnd: (details) =>
), _runAnimation(details.velocity.pixelsPerSecond, size),
onPanEnd: child: Align(
(details) => _runAnimation(details.velocity.pixelsPerSecond, size), alignment: _dragAlignment,
child: Align(alignment: _dragAlignment, child: Card(child: widget.child)), child: Card(child: widget.child),
),
); );
} }
} }

View File

@@ -9,7 +9,8 @@ class RepeatingAnimationDemo extends StatefulWidget {
static String routeName = 'misc/repeating_animation'; static String routeName = 'misc/repeating_animation';
@override @override
State<RepeatingAnimationDemo> createState() => _RepeatingAnimationDemoState(); State<RepeatingAnimationDemo> createState() =>
_RepeatingAnimationDemoState();
} }
class _RepeatingAnimationDemoState extends State<RepeatingAnimationDemo> class _RepeatingAnimationDemoState extends State<RepeatingAnimationDemo>

View File

@@ -2,15 +2,16 @@ name: animations
description: A new Flutter project. description: A new Flutter project.
version: 1.0.0+1 version: 1.0.0+1
publish_to: none publish_to: none
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_animate: ^4.1.0 flutter_animate: ^4.1.0
go_router: ^16.1.0 go_router: ^16.0.0
window_size: # plugin is not yet part of the flutter framework window_size: # plugin is not yet part of the flutter framework
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git
@@ -22,7 +23,6 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:

View File

@@ -16,7 +16,8 @@ void main() {
// Get the initial color of the button. // Get the initial color of the button.
ElevatedButton button = tester.widget(find.byType(ElevatedButton)); ElevatedButton button = tester.widget(find.byType(ElevatedButton));
WidgetStateProperty<Color?>? initialColor = button.style!.backgroundColor; WidgetStateProperty<Color?>? initialColor =
button.style!.backgroundColor;
// Tap the button. // Tap the button.
await tester.tap(find.byType(ElevatedButton)); await tester.tap(find.byType(ElevatedButton));
@@ -24,7 +25,8 @@ void main() {
// Get the updated color of the button. // Get the updated color of the button.
button = tester.widget(find.byType(ElevatedButton)); button = tester.widget(find.byType(ElevatedButton));
WidgetStateProperty<Color?>? updatedColor = button.style!.backgroundColor; WidgetStateProperty<Color?>? updatedColor =
button.style!.backgroundColor;
// Check if the color has changed. // Check if the color has changed.
expect(initialColor, isNot(updatedColor)); expect(initialColor, isNot(updatedColor));
@@ -35,7 +37,8 @@ void main() {
// Get the initial color of the button. // Get the initial color of the button.
ElevatedButton button = tester.widget(find.byType(ElevatedButton)); ElevatedButton button = tester.widget(find.byType(ElevatedButton));
WidgetStateProperty<Color?>? initialColor = button.style!.backgroundColor; WidgetStateProperty<Color?>? initialColor =
button.style!.backgroundColor;
// Tap the button to trigger the animation but don't wait for it to finish. // Tap the button to trigger the animation but don't wait for it to finish.
await tester.tap(find.byType(ElevatedButton)); await tester.tap(find.byType(ElevatedButton));
@@ -44,7 +47,8 @@ void main() {
// Check that the color has changed but not to the final color. // Check that the color has changed but not to the final color.
button = tester.widget(find.byType(ElevatedButton)); button = tester.widget(find.byType(ElevatedButton));
WidgetStateProperty<Color?>? changedColor = button.style!.backgroundColor; WidgetStateProperty<Color?>? changedColor =
button.style!.backgroundColor;
expect(initialColor, isNot(changedColor)); expect(initialColor, isNot(changedColor));
// Wait for the animation to finish. // Wait for the animation to finish.

View File

@@ -24,7 +24,10 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Check if removed properly. // Check if removed properly.
expect(tester.widgetList(find.byType(Card)).length, lessThan(totalCards)); expect(
tester.widgetList(find.byType(Card)).length,
lessThan(totalCards),
);
}); });
testWidgets('All cards swiped out', (tester) async { testWidgets('All cards swiped out', (tester) async {
@@ -36,7 +39,10 @@ void main() {
// Swipe out all cards. // Swipe out all cards.
for (var i = 0; i < totalCards; i++) { for (var i = 0; i < totalCards; i++) {
// Swipe out one by one. // Swipe out one by one.
await tester.drag(find.byType(Card).last, const Offset(100.0, 0.0)); await tester.drag(
find.byType(Card).last,
const Offset(100.0, 0.0),
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
} }

View File

@@ -18,7 +18,11 @@ void main() {
expect(imageList.length, 2); expect(imageList.length, 2);
// Swipe the Carousel. // Swipe the Carousel.
await tester.fling(find.byType(CarouselDemo), const Offset(-400, 0), 800); await tester.fling(
find.byType(CarouselDemo),
const Offset(-400, 0),
800,
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Get the images available on the screen after swipe. // Get the images available on the screen after swipe.

View File

@@ -22,7 +22,10 @@ void main() {
// The size of ExpandCard must change once tapped. // The size of ExpandCard must change once tapped.
// The initialSize should be less than current ExpandCard size. // The initialSize should be less than current ExpandCard size.
expect(initialSize, lessThan(tester.getSize(find.byType(ExpandCard)))); expect(
initialSize,
lessThan(tester.getSize(find.byType(ExpandCard))),
);
}); });
testWidgets('ExpandCard changes image on tap', (tester) async { testWidgets('ExpandCard changes image on tap', (tester) async {

View File

@@ -33,7 +33,9 @@ void main() {
expect(finalSize, greaterThan(initialSize)); expect(finalSize, greaterThan(initialSize));
}); });
testWidgets('Final inkwell on tap goes back to the grid', (tester) async { testWidgets('Final inkwell on tap goes back to the grid', (
tester,
) async {
await tester.pumpWidget(createFocusImageScreen()); await tester.pumpWidget(createFocusImageScreen());
// Tap on the ink well at index 0. // Tap on the ink well at index 0.

View File

@@ -58,7 +58,9 @@ void main() {
// Final color should not be same as initial color. // Final color should not be same as initial color.
expect( expect(
(finalContainer.decoration as BoxDecoration).color, (finalContainer.decoration as BoxDecoration).color,
isNot(equals((initialContainer.decoration as BoxDecoration).color)), isNot(
equals((initialContainer.decoration as BoxDecoration).color),
),
); );
}); });
@@ -66,7 +68,9 @@ void main() {
await tester.pumpWidget(createHeroAnimationDemoScreen()); await tester.pumpWidget(createHeroAnimationDemoScreen());
// Get the initial Screen. // Get the initial Screen.
final initialScreen = tester.firstWidget(find.byType(HeroAnimationDemo)); final initialScreen = tester.firstWidget(
find.byType(HeroAnimationDemo),
);
// Tap on the GestureDetector. // Tap on the GestureDetector.
await tester.tap(find.byType(GestureDetector)); await tester.tap(find.byType(GestureDetector));

View File

@@ -1 +0,0 @@
include: package:flutter_lints/flutter.yaml

View File

@@ -11,10 +11,9 @@ int main(List<String> arguments) {
// the `--input` option and one for the `--output` option. // the `--input` option and one for the `--output` option.
// `--input` is the original asset file that this program should transform. // `--input` is the original asset file that this program should transform.
// `--output` is where flutter expects the transformation output to be written to. // `--output` is where flutter expects the transformation output to be written to.
final parser = final parser = ArgParser()
ArgParser() ..addOption(inputOptionName, mandatory: true, abbr: 'i')
..addOption(inputOptionName, mandatory: true, abbr: 'i') ..addOption(outputOptionName, mandatory: true, abbr: 'o');
..addOption(outputOptionName, mandatory: true, abbr: 'o');
ArgResults argResults = parser.parse(arguments); ArgResults argResults = parser.parse(arguments);
final String inputFilePath = argResults[inputOptionName]; final String inputFilePath = argResults[inputOptionName];

View File

@@ -3,7 +3,7 @@ description: A sample command-line application.
version: 1.0.0 version: 1.0.0
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
args: ^2.4.2 args: ^2.4.2

View File

@@ -2,9 +2,10 @@ name: asset_transformation
description: "A new Flutter project." description: "A new Flutter project."
publish_to: 'none' publish_to: 'none'
version: 0.1.0 version: 0.1.0
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
flutter: flutter:
@@ -12,9 +13,11 @@ dependencies:
vector_graphics: ^1.1.11+1 vector_graphics: ^1.1.11+1
dev_dependencies: dev_dependencies:
analysis_defaults:
path: ../analysis_defaults
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0
vector_graphics_compiler: ^1.1.11+1 vector_graphics_compiler: ^1.1.11+1
grayscale_transformer: grayscale_transformer:
path: ./grayscale_transformer path: ./grayscale_transformer

View File

@@ -69,8 +69,8 @@ class _MyHomePageState extends State<MyHomePage> {
.then( .then(
(sharedPreferences) => sharedPreferences.setBool('isDebug', true), (sharedPreferences) => sharedPreferences.setBool('isDebug', true),
); );
final Future<Directory> tempDirFuture = final Future<Directory> tempDirFuture = path_provider
path_provider.getTemporaryDirectory(); .getTemporaryDirectory();
// Wait until the [SharedPreferences] value is set and the temporary // Wait until the [SharedPreferences] value is set and the temporary
// directory is received before opening the database. If // directory is received before opening the database. If
@@ -130,8 +130,9 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SearchBar( child: SearchBar(
hintText: 'Search', hintText: 'Search',
onChanged: onChanged: _database == null
_database == null ? null : (query) => _refresh(query: query), ? null
: (query) => _refresh(query: query),
trailing: const [Icon(Icons.search), SizedBox(width: 8)], trailing: const [Icon(Icons.search), SizedBox(width: 8)],
), ),
), ),

View File

@@ -188,7 +188,9 @@ class _SimpleDatabaseServer {
// [BinaryMessenger] that the Platform Channels will communicate with on // [BinaryMessenger] that the Platform Channels will communicate with on
// the background isolate. // the background isolate.
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); BackgroundIsolateBinaryMessenger.ensureInitialized(
rootIsolateToken,
);
_sendPort.send(const _Command(_Codes.ack, arg0: null)); _sendPort.send(const _Command(_Codes.ack, arg0: null));
case _Codes.add: case _Codes.add:
_doAddEntry(command.arg0 as String); _doAddEntry(command.arg0 as String);

View File

@@ -1,12 +1,13 @@
name: background_isolate_channels name: background_isolate_channels
description: A new Flutter project. description: A new Flutter project.
resolution: workspace
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
@@ -23,6 +24,5 @@ dev_dependencies:
path: ../analysis_defaults path: ../analysis_defaults
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -2,9 +2,10 @@ name: client
description: A Flutter app which communicates with a Dart backend using shared business logic. description: A Flutter app which communicates with a Dart backend using shared business logic.
publish_to: "none" publish_to: "none"
version: 1.0.0+1 version: 1.0.0+1
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
@@ -15,7 +16,9 @@ dependencies:
path: ../shared path: ../shared
dev_dependencies: dev_dependencies:
flutter_lints: ^5.0.0 analysis_defaults:
path: ../../analysis_defaults
flutter_lints: ^6.0.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@@ -9,10 +9,9 @@ import 'package:shelf_router/shelf_router.dart';
int count = 0; int count = 0;
// Configure routes. // Configure routes.
final _router = final _router = Router()
Router() ..post('/', _incrementHandler)
..post('/', _incrementHandler) ..get('/', _getValueHandler);
..get('/', _getValueHandler);
Future<Response> _incrementHandler(Request request) async { Future<Response> _incrementHandler(Request request) async {
final incr = Increment.fromJson(json.decode(await request.readAsString())); final incr = Increment.fromJson(json.decode(await request.readAsString()));

View File

@@ -2,9 +2,10 @@ name: server
description: A server app using the shelf package and Docker. description: A server app using the shelf package and Docker.
version: 1.0.0 version: 1.0.0
publish_to: "none" publish_to: "none"
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
args: ^2.0.0 args: ^2.0.0
@@ -14,6 +15,8 @@ dependencies:
path: ../shared path: ../shared
dev_dependencies: dev_dependencies:
analysis_defaults:
path: ../../analysis_defaults
http: ^1.0.0 http: ^1.0.0
lints: ^6.0.0 lints: ^6.0.0
test: ^1.15.0 test: ^1.15.0

View File

@@ -52,11 +52,10 @@ class _$IncrementCopyWithImpl<$Res, $Val extends Increment>
$Res call({Object? by = null}) { $Res call({Object? by = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
by: by: null == by
null == by ? _value.by
? _value.by : by // ignore: cast_nullable_to_non_nullable
: by // ignore: cast_nullable_to_non_nullable as int,
as int,
) )
as $Val, as $Val,
); );
@@ -89,11 +88,10 @@ class __$$IncrementImplCopyWithImpl<$Res>
$Res call({Object? by = null}) { $Res call({Object? by = null}) {
return _then( return _then(
_$IncrementImpl( _$IncrementImpl(
by: by: null == by
null == by ? _value.by
? _value.by : by // ignore: cast_nullable_to_non_nullable
: by // ignore: cast_nullable_to_non_nullable as int,
as int,
), ),
); );
} }
@@ -189,11 +187,10 @@ class _$CountCopyWithImpl<$Res, $Val extends Count>
$Res call({Object? value = null}) { $Res call({Object? value = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
value: value: null == value
null == value ? _value.value
? _value.value : value // ignore: cast_nullable_to_non_nullable
: value // ignore: cast_nullable_to_non_nullable as int,
as int,
) )
as $Val, as $Val,
); );
@@ -228,7 +225,7 @@ class __$$CountImplCopyWithImpl<$Res>
null == value null == value
? _value.value ? _value.value
: value // ignore: cast_nullable_to_non_nullable : value // ignore: cast_nullable_to_non_nullable
as int, as int,
), ),
); );
} }

View File

@@ -1,15 +1,18 @@
name: shared name: shared
description: Common data models required by our client and server description: Common data models required by our client and server
version: 1.0.0 version: 1.0.0
resolution: workspace
environment: environment:
sdk: ^3.7.0-0 sdk: ^3.9.0-0
dependencies: dependencies:
freezed_annotation: ">=2.1.0 <4.0.0" freezed_annotation: ">=2.1.0 <4.0.0"
json_annotation: ^4.7.0 json_annotation: ^4.7.0
dev_dependencies: dev_dependencies:
analysis_defaults:
path: ../../analysis_defaults
build_runner: ^2.2.1 build_runner: ^2.2.1
freezed: ">=2.1.1 <4.0.0" freezed: ">=2.1.1 <4.0.0"
json_serializable: ^6.4.0 json_serializable: ^6.4.0

View File

@@ -36,12 +36,11 @@ import '../domain/use_cases/booking/booking_share_use_case.dart';
List<SingleChildWidget> _sharedProviders = [ List<SingleChildWidget> _sharedProviders = [
Provider( Provider(
lazy: true, lazy: true,
create: create: (context) => BookingCreateUseCase(
(context) => BookingCreateUseCase( destinationRepository: context.read(),
destinationRepository: context.read(), activityRepository: context.read(),
activityRepository: context.read(), bookingRepository: context.read(),
bookingRepository: context.read(), ),
),
), ),
Provider( Provider(
lazy: true, lazy: true,
@@ -57,46 +56,40 @@ List<SingleChildWidget> get providersRemote {
Provider(create: (context) => ApiClient()), Provider(create: (context) => ApiClient()),
Provider(create: (context) => SharedPreferencesService()), Provider(create: (context) => SharedPreferencesService()),
ChangeNotifierProvider( ChangeNotifierProvider(
create: create: (context) =>
(context) => AuthRepositoryRemote(
AuthRepositoryRemote( authApiClient: context.read(),
authApiClient: context.read(), apiClient: context.read(),
apiClient: context.read(), sharedPreferencesService: context.read(),
sharedPreferencesService: context.read(), )
) as AuthRepository,
as AuthRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => DestinationRepositoryRemote(apiClient: context.read())
DestinationRepositoryRemote(apiClient: context.read()) as DestinationRepository,
as DestinationRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => ContinentRepositoryRemote(apiClient: context.read())
ContinentRepositoryRemote(apiClient: context.read()) as ContinentRepository,
as ContinentRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => ActivityRepositoryRemote(apiClient: context.read())
ActivityRepositoryRemote(apiClient: context.read()) as ActivityRepository,
as ActivityRepository,
), ),
Provider.value( Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository, value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => BookingRepositoryRemote(apiClient: context.read())
BookingRepositoryRemote(apiClient: context.read()) as BookingRepository,
as BookingRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => UserRepositoryRemote(apiClient: context.read()) as UserRepository,
UserRepositoryRemote(apiClient: context.read()) as UserRepository,
), ),
..._sharedProviders, ..._sharedProviders,
]; ];
@@ -110,37 +103,32 @@ List<SingleChildWidget> get providersLocal {
ChangeNotifierProvider.value(value: AuthRepositoryDev() as AuthRepository), ChangeNotifierProvider.value(value: AuthRepositoryDev() as AuthRepository),
Provider.value(value: LocalDataService()), Provider.value(value: LocalDataService()),
Provider( Provider(
create: create: (context) =>
(context) => DestinationRepositoryLocal(localDataService: context.read())
DestinationRepositoryLocal(localDataService: context.read()) as DestinationRepository,
as DestinationRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => ContinentRepositoryLocal(localDataService: context.read())
ContinentRepositoryLocal(localDataService: context.read()) as ContinentRepository,
as ContinentRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => ActivityRepositoryLocal(localDataService: context.read())
ActivityRepositoryLocal(localDataService: context.read()) as ActivityRepository,
as ActivityRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => BookingRepositoryLocal(localDataService: context.read())
BookingRepositoryLocal(localDataService: context.read()) as BookingRepository,
as BookingRepository,
), ),
Provider.value( Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository, value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
), ),
Provider( Provider(
create: create: (context) =>
(context) => UserRepositoryLocal(localDataService: context.read())
UserRepositoryLocal(localDataService: context.read()) as UserRepository,
as UserRepository,
), ),
..._sharedProviders, ..._sharedProviders,
]; ];

View File

@@ -18,10 +18,9 @@ class ActivityRepositoryLocal implements ActivityRepository {
@override @override
Future<Result<List<Activity>>> getByDestination(String ref) async { Future<Result<List<Activity>>> getByDestination(String ref) async {
try { try {
final activities = final activities = (await _localDataService.getActivities())
(await _localDataService.getActivities()) .where((activity) => activity.destinationRef == ref)
.where((activity) => activity.destinationRef == ref) .toList();
.toList();
return Result.ok(activities); return Result.ok(activities);
} on Exception catch (error) { } on Exception catch (error) {

View File

@@ -68,11 +68,10 @@ class BookingRepositoryLocal implements BookingRepository {
// create a default booking the first time // create a default booking the first time
if (_bookings.isEmpty) { if (_bookings.isEmpty) {
final destination = (await _localDataService.getDestinations()).first; final destination = (await _localDataService.getDestinations()).first;
final activities = final activities = (await _localDataService.getActivities())
(await _localDataService.getActivities()) .where((activity) => activity.destinationRef == destination.ref)
.where((activity) => activity.destinationRef == destination.ref) .take(4)
.take(4) .toList();
.toList();
_bookings.add( _bookings.add(
Booking( Booking(

View File

@@ -27,8 +27,9 @@ class BookingRepositoryRemote implements BookingRepository {
endDate: booking.endDate, endDate: booking.endDate,
name: '${booking.destination.name}, ${booking.destination.continent}', name: '${booking.destination.name}, ${booking.destination.continent}',
destinationRef: booking.destination.ref, destinationRef: booking.destination.ref,
activitiesRef: activitiesRef: booking.activity
booking.activity.map((activity) => activity.ref).toList(), .map((activity) => activity.ref)
.toList(),
); );
return _apiClient.postBooking(bookingApiModel); return _apiClient.postBooking(bookingApiModel);
} on Exception catch (e) { } on Exception catch (e) {
@@ -72,10 +73,9 @@ class BookingRepositoryRemote implements BookingRepository {
return Result.error(resultActivities.error); return Result.error(resultActivities.error);
case Ok<List<Activity>>(): case Ok<List<Activity>>():
} }
final activities = final activities = resultActivities.value
resultActivities.value .where((activity) => booking.activitiesRef.contains(activity.ref))
.where((activity) => booking.activitiesRef.contains(activity.ref)) .toList();
.toList();
return Result.ok( return Result.ok(
Booking( Booking(

View File

@@ -95,8 +95,9 @@ class ApiClient {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join(); final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>; final json = jsonDecode(stringData) as List<dynamic>;
final activities = final activities = json
json.map((element) => Activity.fromJson(element)).toList(); .map((element) => Activity.fromJson(element))
.toList();
return Result.ok(activities); return Result.ok(activities);
} else { } else {
return const Result.error(HttpException("Invalid response")); return const Result.error(HttpException("Invalid response"));
@@ -117,8 +118,9 @@ class ApiClient {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join(); final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>; final json = jsonDecode(stringData) as List<dynamic>;
final bookings = final bookings = json
json.map((element) => BookingApiModel.fromJson(element)).toList(); .map((element) => BookingApiModel.fromJson(element))
.toList();
return Result.ok(bookings); return Result.ok(bookings);
} else { } else {
return const Result.error(HttpException("Invalid response")); return const Result.error(HttpException("Invalid response"));

View File

@@ -91,36 +91,30 @@ class _$BookingApiModelCopyWithImpl<$Res, $Val extends BookingApiModel>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
id: id: freezed == id
freezed == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int?,
as int?, startDate: null == startDate
startDate: ? _value.startDate
null == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime,
: startDate // ignore: cast_nullable_to_non_nullable endDate: null == endDate
as DateTime, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
null == endDate as DateTime,
? _value.endDate name: null == name
: endDate // ignore: cast_nullable_to_non_nullable ? _value.name
as DateTime, : name // ignore: cast_nullable_to_non_nullable
name: as String,
null == name destinationRef: null == destinationRef
? _value.name ? _value.destinationRef
: name // ignore: cast_nullable_to_non_nullable : destinationRef // ignore: cast_nullable_to_non_nullable
as String, as String,
destinationRef: activitiesRef: null == activitiesRef
null == destinationRef ? _value.activitiesRef
? _value.destinationRef : activitiesRef // ignore: cast_nullable_to_non_nullable
: destinationRef // ignore: cast_nullable_to_non_nullable as List<String>,
as String,
activitiesRef:
null == activitiesRef
? _value.activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
) )
as $Val, as $Val,
); );
@@ -169,36 +163,30 @@ class __$$BookingApiModelImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$BookingApiModelImpl( _$BookingApiModelImpl(
id: id: freezed == id
freezed == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int?,
as int?, startDate: null == startDate
startDate: ? _value.startDate
null == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime,
: startDate // ignore: cast_nullable_to_non_nullable endDate: null == endDate
as DateTime, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
null == endDate as DateTime,
? _value.endDate name: null == name
: endDate // ignore: cast_nullable_to_non_nullable ? _value.name
as DateTime, : name // ignore: cast_nullable_to_non_nullable
name: as String,
null == name destinationRef: null == destinationRef
? _value.name ? _value.destinationRef
: name // ignore: cast_nullable_to_non_nullable : destinationRef // ignore: cast_nullable_to_non_nullable
as String, as String,
destinationRef: activitiesRef: null == activitiesRef
null == destinationRef ? _value._activitiesRef
? _value.destinationRef : activitiesRef // ignore: cast_nullable_to_non_nullable
: destinationRef // ignore: cast_nullable_to_non_nullable as List<String>,
as String,
activitiesRef:
null == activitiesRef
? _value._activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
), ),
); );
} }

View File

@@ -14,8 +14,9 @@ _$BookingApiModelImpl _$$BookingApiModelImplFromJson(
endDate: DateTime.parse(json['endDate'] as String), endDate: DateTime.parse(json['endDate'] as String),
name: json['name'] as String, name: json['name'] as String,
destinationRef: json['destinationRef'] as String, destinationRef: json['destinationRef'] as String,
activitiesRef: activitiesRef: (json['activitiesRef'] as List<dynamic>)
(json['activitiesRef'] as List<dynamic>).map((e) => e as String).toList(), .map((e) => e as String)
.toList(),
); );
Map<String, dynamic> _$$BookingApiModelImplToJson( Map<String, dynamic> _$$BookingApiModelImplToJson(

View File

@@ -64,16 +64,14 @@ class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest>
$Res call({Object? email = null, Object? password = null}) { $Res call({Object? email = null, Object? password = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
email: email: null == email
null == email ? _value.email
? _value.email : email // ignore: cast_nullable_to_non_nullable
: email // ignore: cast_nullable_to_non_nullable as String,
as String, password: null == password
password: ? _value.password
null == password : password // ignore: cast_nullable_to_non_nullable
? _value.password as String,
: password // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -108,16 +106,14 @@ class __$$LoginRequestImplCopyWithImpl<$Res>
$Res call({Object? email = null, Object? password = null}) { $Res call({Object? email = null, Object? password = null}) {
return _then( return _then(
_$LoginRequestImpl( _$LoginRequestImpl(
email: email: null == email
null == email ? _value.email
? _value.email : email // ignore: cast_nullable_to_non_nullable
: email // ignore: cast_nullable_to_non_nullable as String,
as String, password: null == password
password: ? _value.password
null == password : password // ignore: cast_nullable_to_non_nullable
? _value.password as String,
: password // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -64,16 +64,14 @@ class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse>
$Res call({Object? token = null, Object? userId = null}) { $Res call({Object? token = null, Object? userId = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
token: token: null == token
null == token ? _value.token
? _value.token : token // ignore: cast_nullable_to_non_nullable
: token // ignore: cast_nullable_to_non_nullable as String,
as String, userId: null == userId
userId: ? _value.userId
null == userId : userId // ignore: cast_nullable_to_non_nullable
? _value.userId as String,
: userId // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -108,16 +106,14 @@ class __$$LoginResponseImplCopyWithImpl<$Res>
$Res call({Object? token = null, Object? userId = null}) { $Res call({Object? token = null, Object? userId = null}) {
return _then( return _then(
_$LoginResponseImpl( _$LoginResponseImpl(
token: token: null == token
null == token ? _value.token
? _value.token : token // ignore: cast_nullable_to_non_nullable
: token // ignore: cast_nullable_to_non_nullable as String,
as String, userId: null == userId
userId: ? _value.userId
null == userId : userId // ignore: cast_nullable_to_non_nullable
? _value.userId as String,
: userId // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -75,26 +75,22 @@ class _$UserApiModelCopyWithImpl<$Res, $Val extends UserApiModel>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
id: id: null == id
null == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as String,
as String, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable email: null == email
as String, ? _value.email
email: : email // ignore: cast_nullable_to_non_nullable
null == email as String,
? _value.email picture: null == picture
: email // ignore: cast_nullable_to_non_nullable ? _value.picture
as String, : picture // ignore: cast_nullable_to_non_nullable
picture: as String,
null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -134,26 +130,22 @@ class __$$UserApiModelImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$UserApiModelImpl( _$UserApiModelImpl(
id: id: null == id
null == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as String,
as String, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable email: null == email
as String, ? _value.email
email: : email // ignore: cast_nullable_to_non_nullable
null == email as String,
? _value.email picture: null == picture
: email // ignore: cast_nullable_to_non_nullable ? _value.picture
as String, : picture // ignore: cast_nullable_to_non_nullable
picture: as String,
null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -109,56 +109,46 @@ class _$ActivityCopyWithImpl<$Res, $Val extends Activity>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, description: null == description
description: ? _value.description
null == description : description // ignore: cast_nullable_to_non_nullable
? _value.description as String,
: description // ignore: cast_nullable_to_non_nullable locationName: null == locationName
as String, ? _value.locationName
locationName: : locationName // ignore: cast_nullable_to_non_nullable
null == locationName as String,
? _value.locationName duration: null == duration
: locationName // ignore: cast_nullable_to_non_nullable ? _value.duration
as String, : duration // ignore: cast_nullable_to_non_nullable
duration: as int,
null == duration timeOfDay: null == timeOfDay
? _value.duration ? _value.timeOfDay
: duration // ignore: cast_nullable_to_non_nullable : timeOfDay // ignore: cast_nullable_to_non_nullable
as int, as TimeOfDay,
timeOfDay: familyFriendly: null == familyFriendly
null == timeOfDay ? _value.familyFriendly
? _value.timeOfDay : familyFriendly // ignore: cast_nullable_to_non_nullable
: timeOfDay // ignore: cast_nullable_to_non_nullable as bool,
as TimeOfDay, price: null == price
familyFriendly: ? _value.price
null == familyFriendly : price // ignore: cast_nullable_to_non_nullable
? _value.familyFriendly as int,
: familyFriendly // ignore: cast_nullable_to_non_nullable destinationRef: null == destinationRef
as bool, ? _value.destinationRef
price: : destinationRef // ignore: cast_nullable_to_non_nullable
null == price as String,
? _value.price ref: null == ref
: price // ignore: cast_nullable_to_non_nullable ? _value.ref
as int, : ref // ignore: cast_nullable_to_non_nullable
destinationRef: as String,
null == destinationRef imageUrl: null == imageUrl
? _value.destinationRef ? _value.imageUrl
: destinationRef // ignore: cast_nullable_to_non_nullable : imageUrl // ignore: cast_nullable_to_non_nullable
as String, as String,
ref:
null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -215,56 +205,46 @@ class __$$ActivityImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$ActivityImpl( _$ActivityImpl(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, description: null == description
description: ? _value.description
null == description : description // ignore: cast_nullable_to_non_nullable
? _value.description as String,
: description // ignore: cast_nullable_to_non_nullable locationName: null == locationName
as String, ? _value.locationName
locationName: : locationName // ignore: cast_nullable_to_non_nullable
null == locationName as String,
? _value.locationName duration: null == duration
: locationName // ignore: cast_nullable_to_non_nullable ? _value.duration
as String, : duration // ignore: cast_nullable_to_non_nullable
duration: as int,
null == duration timeOfDay: null == timeOfDay
? _value.duration ? _value.timeOfDay
: duration // ignore: cast_nullable_to_non_nullable : timeOfDay // ignore: cast_nullable_to_non_nullable
as int, as TimeOfDay,
timeOfDay: familyFriendly: null == familyFriendly
null == timeOfDay ? _value.familyFriendly
? _value.timeOfDay : familyFriendly // ignore: cast_nullable_to_non_nullable
: timeOfDay // ignore: cast_nullable_to_non_nullable as bool,
as TimeOfDay, price: null == price
familyFriendly: ? _value.price
null == familyFriendly : price // ignore: cast_nullable_to_non_nullable
? _value.familyFriendly as int,
: familyFriendly // ignore: cast_nullable_to_non_nullable destinationRef: null == destinationRef
as bool, ? _value.destinationRef
price: : destinationRef // ignore: cast_nullable_to_non_nullable
null == price as String,
? _value.price ref: null == ref
: price // ignore: cast_nullable_to_non_nullable ? _value.ref
as int, : ref // ignore: cast_nullable_to_non_nullable
destinationRef: as String,
null == destinationRef imageUrl: null == imageUrl
? _value.destinationRef ? _value.imageUrl
: destinationRef // ignore: cast_nullable_to_non_nullable : imageUrl // ignore: cast_nullable_to_non_nullable
as String, as String,
ref:
null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -85,31 +85,26 @@ class _$BookingCopyWithImpl<$Res, $Val extends Booking>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
id: id: freezed == id
freezed == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int?,
as int?, startDate: null == startDate
startDate: ? _value.startDate
null == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime,
: startDate // ignore: cast_nullable_to_non_nullable endDate: null == endDate
as DateTime, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
null == endDate as DateTime,
? _value.endDate destination: null == destination
: endDate // ignore: cast_nullable_to_non_nullable ? _value.destination
as DateTime, : destination // ignore: cast_nullable_to_non_nullable
destination: as Destination,
null == destination activity: null == activity
? _value.destination ? _value.activity
: destination // ignore: cast_nullable_to_non_nullable : activity // ignore: cast_nullable_to_non_nullable
as Destination, as List<Activity>,
activity:
null == activity
? _value.activity
: activity // ignore: cast_nullable_to_non_nullable
as List<Activity>,
) )
as $Val, as $Val,
); );
@@ -168,31 +163,26 @@ class __$$BookingImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$BookingImpl( _$BookingImpl(
id: id: freezed == id
freezed == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int?,
as int?, startDate: null == startDate
startDate: ? _value.startDate
null == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime,
: startDate // ignore: cast_nullable_to_non_nullable endDate: null == endDate
as DateTime, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
null == endDate as DateTime,
? _value.endDate destination: null == destination
: endDate // ignore: cast_nullable_to_non_nullable ? _value.destination
as DateTime, : destination // ignore: cast_nullable_to_non_nullable
destination: as Destination,
null == destination activity: null == activity
? _value.destination ? _value._activity
: destination // ignore: cast_nullable_to_non_nullable : activity // ignore: cast_nullable_to_non_nullable
as Destination, as List<Activity>,
activity:
null == activity
? _value._activity
: activity // ignore: cast_nullable_to_non_nullable
as List<Activity>,
), ),
); );
} }

View File

@@ -14,10 +14,9 @@ _$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
destination: Destination.fromJson( destination: Destination.fromJson(
json['destination'] as Map<String, dynamic>, json['destination'] as Map<String, dynamic>,
), ),
activity: activity: (json['activity'] as List<dynamic>)
(json['activity'] as List<dynamic>) .map((e) => Activity.fromJson(e as Map<String, dynamic>))
.map((e) => Activity.fromJson(e as Map<String, dynamic>)) .toList(),
.toList(),
); );
Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) => Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) =>

View File

@@ -75,26 +75,22 @@ class _$BookingSummaryCopyWithImpl<$Res, $Val extends BookingSummary>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
id: id: null == id
null == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int,
as int, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable startDate: null == startDate
as String, ? _value.startDate
startDate: : startDate // ignore: cast_nullable_to_non_nullable
null == startDate as DateTime,
? _value.startDate endDate: null == endDate
: startDate // ignore: cast_nullable_to_non_nullable ? _value.endDate
as DateTime, : endDate // ignore: cast_nullable_to_non_nullable
endDate: as DateTime,
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
) )
as $Val, as $Val,
); );
@@ -134,26 +130,22 @@ class __$$BookingSummaryImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$BookingSummaryImpl( _$BookingSummaryImpl(
id: id: null == id
null == id ? _value.id
? _value.id : id // ignore: cast_nullable_to_non_nullable
: id // ignore: cast_nullable_to_non_nullable as int,
as int, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable startDate: null == startDate
as String, ? _value.startDate
startDate: : startDate // ignore: cast_nullable_to_non_nullable
null == startDate as DateTime,
? _value.startDate endDate: null == endDate
: startDate // ignore: cast_nullable_to_non_nullable ? _value.endDate
as DateTime, : endDate // ignore: cast_nullable_to_non_nullable
endDate: as DateTime,
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
), ),
); );
} }

View File

@@ -62,16 +62,14 @@ class _$ContinentCopyWithImpl<$Res, $Val extends Continent>
$Res call({Object? name = null, Object? imageUrl = null}) { $Res call({Object? name = null, Object? imageUrl = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, imageUrl: null == imageUrl
imageUrl: ? _value.imageUrl
null == imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
? _value.imageUrl as String,
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -106,16 +104,14 @@ class __$$ContinentImplCopyWithImpl<$Res>
$Res call({Object? name = null, Object? imageUrl = null}) { $Res call({Object? name = null, Object? imageUrl = null}) {
return _then( return _then(
_$ContinentImpl( _$ContinentImpl(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, imageUrl: null == imageUrl
imageUrl: ? _value.imageUrl
null == imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
? _value.imageUrl as String,
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -95,41 +95,34 @@ class _$DestinationCopyWithImpl<$Res, $Val extends Destination>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
ref: ref: null == ref
null == ref ? _value.ref
? _value.ref : ref // ignore: cast_nullable_to_non_nullable
: ref // ignore: cast_nullable_to_non_nullable as String,
as String, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable country: null == country
as String, ? _value.country
country: : country // ignore: cast_nullable_to_non_nullable
null == country as String,
? _value.country continent: null == continent
: country // ignore: cast_nullable_to_non_nullable ? _value.continent
as String, : continent // ignore: cast_nullable_to_non_nullable
continent: as String,
null == continent knownFor: null == knownFor
? _value.continent ? _value.knownFor
: continent // ignore: cast_nullable_to_non_nullable : knownFor // ignore: cast_nullable_to_non_nullable
as String, as String,
knownFor: tags: null == tags
null == knownFor ? _value.tags
? _value.knownFor : tags // ignore: cast_nullable_to_non_nullable
: knownFor // ignore: cast_nullable_to_non_nullable as List<String>,
as String, imageUrl: null == imageUrl
tags: ? _value.imageUrl
null == tags : imageUrl // ignore: cast_nullable_to_non_nullable
? _value.tags as String,
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -180,41 +173,34 @@ class __$$DestinationImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$DestinationImpl( _$DestinationImpl(
ref: ref: null == ref
null == ref ? _value.ref
? _value.ref : ref // ignore: cast_nullable_to_non_nullable
: ref // ignore: cast_nullable_to_non_nullable as String,
as String, name: null == name
name: ? _value.name
null == name : name // ignore: cast_nullable_to_non_nullable
? _value.name as String,
: name // ignore: cast_nullable_to_non_nullable country: null == country
as String, ? _value.country
country: : country // ignore: cast_nullable_to_non_nullable
null == country as String,
? _value.country continent: null == continent
: country // ignore: cast_nullable_to_non_nullable ? _value.continent
as String, : continent // ignore: cast_nullable_to_non_nullable
continent: as String,
null == continent knownFor: null == knownFor
? _value.continent ? _value.knownFor
: continent // ignore: cast_nullable_to_non_nullable : knownFor // ignore: cast_nullable_to_non_nullable
as String, as String,
knownFor: tags: null == tags
null == knownFor ? _value._tags
? _value.knownFor : tags // ignore: cast_nullable_to_non_nullable
: knownFor // ignore: cast_nullable_to_non_nullable as List<String>,
as String, imageUrl: null == imageUrl
tags: ? _value.imageUrl
null == tags : imageUrl // ignore: cast_nullable_to_non_nullable
? _value._tags as String,
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -90,36 +90,30 @@ class _$ItineraryConfigCopyWithImpl<$Res, $Val extends ItineraryConfig>
}) { }) {
return _then( return _then(
_value.copyWith( _value.copyWith(
continent: continent: freezed == continent
freezed == continent ? _value.continent
? _value.continent : continent // ignore: cast_nullable_to_non_nullable
: continent // ignore: cast_nullable_to_non_nullable as String?,
as String?, startDate: freezed == startDate
startDate: ? _value.startDate
freezed == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime?,
: startDate // ignore: cast_nullable_to_non_nullable endDate: freezed == endDate
as DateTime?, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
freezed == endDate as DateTime?,
? _value.endDate guests: freezed == guests
: endDate // ignore: cast_nullable_to_non_nullable ? _value.guests
as DateTime?, : guests // ignore: cast_nullable_to_non_nullable
guests: as int?,
freezed == guests destination: freezed == destination
? _value.guests ? _value.destination
: guests // ignore: cast_nullable_to_non_nullable : destination // ignore: cast_nullable_to_non_nullable
as int?, as String?,
destination: activities: null == activities
freezed == destination ? _value.activities
? _value.destination : activities // ignore: cast_nullable_to_non_nullable
: destination // ignore: cast_nullable_to_non_nullable as List<String>,
as String?,
activities:
null == activities
? _value.activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
) )
as $Val, as $Val,
); );
@@ -168,36 +162,30 @@ class __$$ItineraryConfigImplCopyWithImpl<$Res>
}) { }) {
return _then( return _then(
_$ItineraryConfigImpl( _$ItineraryConfigImpl(
continent: continent: freezed == continent
freezed == continent ? _value.continent
? _value.continent : continent // ignore: cast_nullable_to_non_nullable
: continent // ignore: cast_nullable_to_non_nullable as String?,
as String?, startDate: freezed == startDate
startDate: ? _value.startDate
freezed == startDate : startDate // ignore: cast_nullable_to_non_nullable
? _value.startDate as DateTime?,
: startDate // ignore: cast_nullable_to_non_nullable endDate: freezed == endDate
as DateTime?, ? _value.endDate
endDate: : endDate // ignore: cast_nullable_to_non_nullable
freezed == endDate as DateTime?,
? _value.endDate guests: freezed == guests
: endDate // ignore: cast_nullable_to_non_nullable ? _value.guests
as DateTime?, : guests // ignore: cast_nullable_to_non_nullable
guests: as int?,
freezed == guests destination: freezed == destination
? _value.guests ? _value.destination
: guests // ignore: cast_nullable_to_non_nullable : destination // ignore: cast_nullable_to_non_nullable
as int?, as String?,
destination: activities: null == activities
freezed == destination ? _value._activities
? _value.destination : activities // ignore: cast_nullable_to_non_nullable
: destination // ignore: cast_nullable_to_non_nullable as List<String>,
as String?,
activities:
null == activities
? _value._activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
), ),
); );
} }

View File

@@ -10,14 +10,12 @@ _$ItineraryConfigImpl _$$ItineraryConfigImplFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => _$ItineraryConfigImpl( ) => _$ItineraryConfigImpl(
continent: json['continent'] as String?, continent: json['continent'] as String?,
startDate: startDate: json['startDate'] == null
json['startDate'] == null ? null
? null : DateTime.parse(json['startDate'] as String),
: DateTime.parse(json['startDate'] as String), endDate: json['endDate'] == null
endDate: ? null
json['endDate'] == null : DateTime.parse(json['endDate'] as String),
? null
: DateTime.parse(json['endDate'] as String),
guests: (json['guests'] as num?)?.toInt(), guests: (json['guests'] as num?)?.toInt(),
destination: json['destination'] as String?, destination: json['destination'] as String?,
activities: activities:

View File

@@ -61,16 +61,14 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
$Res call({Object? name = null, Object? picture = null}) { $Res call({Object? name = null, Object? picture = null}) {
return _then( return _then(
_value.copyWith( _value.copyWith(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, picture: null == picture
picture: ? _value.picture
null == picture : picture // ignore: cast_nullable_to_non_nullable
? _value.picture as String,
: picture // ignore: cast_nullable_to_non_nullable
as String,
) )
as $Val, as $Val,
); );
@@ -102,16 +100,14 @@ class __$$UserImplCopyWithImpl<$Res>
$Res call({Object? name = null, Object? picture = null}) { $Res call({Object? name = null, Object? picture = null}) {
return _then( return _then(
_$UserImpl( _$UserImpl(
name: name: null == name
null == name ? _value.name
? _value.name : name // ignore: cast_nullable_to_non_nullable
: name // ignore: cast_nullable_to_non_nullable as String,
as String, picture: null == picture
picture: ? _value.picture
null == picture : picture // ignore: cast_nullable_to_non_nullable
? _value.picture as String,
: picture // ignore: cast_nullable_to_non_nullable
as String,
), ),
); );
} }

View File

@@ -63,12 +63,9 @@ class BookingCreateUseCase {
return Result.error(activitiesResult.error); return Result.error(activitiesResult.error);
case Ok<List<Activity>>(): case Ok<List<Activity>>():
} }
final activities = final activities = activitiesResult.value
activitiesResult.value .where((activity) => itineraryConfig.activities.contains(activity.ref))
.where( .toList();
(activity) => itineraryConfig.activities.contains(activity.ref),
)
.toList();
_log.fine('Activities loaded (${activities.length})'); _log.fine('Activities loaded (${activities.length})');
// Check if dates are set // Check if dates are set

View File

@@ -67,26 +67,24 @@ class ActivitiesViewModel extends ChangeNotifier {
switch (resultActivities) { switch (resultActivities) {
case Ok(): case Ok():
{ {
_daytimeActivities = _daytimeActivities = resultActivities.value
resultActivities.value .where(
.where( (activity) => [
(activity) => [ TimeOfDay.any,
TimeOfDay.any, TimeOfDay.morning,
TimeOfDay.morning, TimeOfDay.afternoon,
TimeOfDay.afternoon, ].contains(activity.timeOfDay),
].contains(activity.timeOfDay), )
) .toList();
.toList();
_eveningActivities = _eveningActivities = resultActivities.value
resultActivities.value .where(
.where( (activity) => [
(activity) => [ TimeOfDay.evening,
TimeOfDay.evening, TimeOfDay.night,
TimeOfDay.night, ].contains(activity.timeOfDay),
].contains(activity.timeOfDay), )
) .toList();
.toList();
_log.fine( _log.fine(
'Activities (daytime: ${_daytimeActivities.length}, ' 'Activities (daytime: ${_daytimeActivities.length}, '

View File

@@ -71,10 +71,9 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
Expanded( Expanded(
child: Center( child: Center(
child: ErrorIndicator( child: ErrorIndicator(
title: title: AppLocalization.of(
AppLocalization.of( context,
context, ).errorWhileLoadingActivities,
).errorWhileLoadingActivities,
label: AppLocalization.of(context).tryAgain, label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.loadActivities.execute, onPressed: widget.viewModel.loadActivities.execute,
), ),
@@ -171,10 +170,9 @@ class _BottomArea extends StatelessWidget {
), ),
FilledButton( FilledButton(
key: const Key(confirmButtonKey), key: const Key(confirmButtonKey),
onPressed: onPressed: viewModel.selectedActivities.isNotEmpty
viewModel.selectedActivities.isNotEmpty ? viewModel.saveActivities.execute
? viewModel.saveActivities.execute : null,
: null,
child: Text(AppLocalization.of(context).confirm), child: Text(AppLocalization.of(context).confirm),
), ),
], ],

View File

@@ -50,38 +50,40 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Column( body: SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
const TiltedCards(), children: [
Padding( const TiltedCards(),
padding: Dimens.of(context).edgeInsetsScreenSymmetric, Padding(
child: Column( padding: Dimens.of(context).edgeInsetsScreenSymmetric,
crossAxisAlignment: CrossAxisAlignment.stretch, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
TextField(controller: _email), children: [
const SizedBox(height: Dimens.paddingVertical), TextField(controller: _email),
TextField(controller: _password, obscureText: true), const SizedBox(height: Dimens.paddingVertical),
const SizedBox(height: Dimens.paddingVertical), TextField(controller: _password, obscureText: true),
ListenableBuilder( const SizedBox(height: Dimens.paddingVertical),
listenable: widget.viewModel.login, ListenableBuilder(
builder: (context, _) { listenable: widget.viewModel.login,
return FilledButton( builder: (context, _) {
onPressed: () { return FilledButton(
widget.viewModel.login.execute(( onPressed: () {
_email.value.text, widget.viewModel.login.execute((
_password.value.text, _email.value.text,
)); _password.value.text,
}, ));
child: Text(AppLocalization.of(context).login), },
); child: Text(AppLocalization.of(context).login),
}, );
), },
], ),
],
),
), ),
), ],
], ),
), ),
); );
} }
@@ -99,11 +101,10 @@ class _LoginScreenState extends State<LoginScreen> {
content: Text(AppLocalization.of(context).errorWhileLogin), content: Text(AppLocalization.of(context).errorWhileLogin),
action: SnackBarAction( action: SnackBarAction(
label: AppLocalization.of(context).tryAgain, label: AppLocalization.of(context).tryAgain,
onPressed: onPressed: () => widget.viewModel.login.execute((
() => widget.viewModel.login.execute(( _email.value.text,
_email.value.text, _password.value.text,
_password.value.text, )),
)),
), ),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More