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
jobs:
# Run the stable test script on the beta channel. Since this branch will soon
# be merged into main as our stable-targeting code, this is the key thing we
# need to test.
stable-tests-on-beta:
# Test all samples on the beta channel. Since the beta channel will soon be
# promoted to stable, this branch is only concerned with the beta.
Beta-CI:
name: Test flutter beta channel
runs-on: ${{ matrix.os }}
if: github.repository == 'flutter/samples'
strategy:
@@ -27,43 +27,43 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
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
# channel.
# android-build:
# runs-on: ubuntu-latest
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: 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'
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
# - 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'
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
# - 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:
fail-fast: false
matrix:
flutter_version: [stable, beta, master]
os: [ubuntu-latest, macos-latest, windows-latest]
flutter_version: [stable, beta]
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046
with:
channel: ${{ matrix.flutter_version }}
- run: ./tool/flutter_ci_script_${{ matrix.flutter_version }}.sh
# 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
- run: flutter pub get && dart tool/ci_script.dart

4
.gitignore vendored
View File

@@ -27,6 +27,7 @@
.pub-cache/
.pub/
/build/
**/build/
# Android related
**/gradle-wrapper.jar
@@ -81,3 +82,6 @@ yarn.lock
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/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
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.
@@ -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.
> 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?
See the [contributor's guide]!

View File

@@ -23,7 +23,12 @@ class Cell extends StatefulWidget {
class _CellState extends State<Cell> with WidgetsBindingObserver {
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;
Random? _random;
@@ -82,7 +87,10 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
builder: (context) {
return Card(
// Mimic the platform Material look.
margin: const EdgeInsets.symmetric(horizontal: 36, vertical: 24),
margin: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 24,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
@@ -112,22 +120,22 @@ class _CellState extends State<Cell> with WidgetsBindingObserver {
child: StreamBuilder<AccelerometerEvent>(
// Don't continuously rebuild for nothing when the
// cell isn't visible.
stream:
appLifecycleState == AppLifecycleState.resumed
? accelerometerEventStream()
: Stream.value(defaultPosition),
stream: appLifecycleState == AppLifecycleState.resumed
? accelerometerEventStream()
: Stream.value(defaultPosition),
initialData: defaultPosition,
builder: (context, snapshot) {
return Transform(
// Figure out the phone's orientation relative
// to gravity's direction. Ignore the z vector.
transform: Matrix4.rotationX(
snapshot.data!.y / gravity * pi / 2,
)..multiply(
Matrix4.rotationY(
snapshot.data!.x / gravity * pi / 2,
),
),
transform:
Matrix4.rotationX(
snapshot.data!.y / gravity * pi / 2,
)..multiply(
Matrix4.rotationY(
snapshot.data!.x / gravity * pi / 2,
),
),
alignment: Alignment.center,
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.
version: 1.0.0+1
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.8.1-0
dependencies:
flutter:
sdk: flutter
provider: ^6.0.2
url_launcher: ^6.0.20
sensors_plus: ^5.0.1
provider: ^6.1.5
url_launcher: ^6.3.2
sensors_plus: ^6.1.1
dev_dependencies:
analysis_defaults:

View File

@@ -6,7 +6,7 @@
// tree, read text, and verify that the values of widget properties are correct.
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:provider/provider.dart';

View File

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

View File

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

View File

@@ -20,11 +20,15 @@ void main() {
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();
await tester.pumpWidget(
MaterialApp(home: BookDetail(book: Book(), hostApi: mockHostApi)),
MaterialApp(
home: BookDetail(book: Book(), hostApi: mockHostApi),
),
);
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.
version: 1.0.0+1
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
flutter:

View File

@@ -3,7 +3,7 @@
// found in the LICENSE file.
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:provider/provider.dart';

View File

@@ -3,7 +3,7 @@
// found in the LICENSE file.
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
// https://pub.dev/packages/espresso for details.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,8 @@ const double windowWidth = 480;
const double windowHeight = 854;
void setupWindow() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized();
setWindowTitle('Animation Samples');
setWindowMinSize(const Size(windowWidth, windowHeight));
@@ -43,7 +44,11 @@ class Demo {
final String route;
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 = [

View File

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

View File

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

View File

@@ -74,7 +74,10 @@ class _TweenSequenceDemoState extends State<TweenSequenceDemo>
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(
appBar: AppBar(
title: const Text('AnimatedList'),
actions: [IconButton(icon: const Icon(Icons.add), onPressed: addUser)],
actions: [
IconButton(icon: const Icon(Icons.add), onPressed: addUser),
],
),
body: SafeArea(
child: AnimatedList(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,10 +29,12 @@ class Grid extends StatelessWidget {
),
itemBuilder: (context, index) {
return (index >= 20)
? const SmallCard(imageAssetName: 'assets/eat_cape_town_sm.jpg')
? const SmallCard(
imageAssetName: 'assets/eat_cape_town_sm.jpg',
)
: 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);
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(
child: Hero(
tag: 'hero-page-child',
child: _createHeroContainer(size: 50.0, color: Colors.grey.shade300),
child: _createHeroContainer(
size: 50.0,
color: Colors.grey.shade300,
),
),
onTap:
() => Navigator.of(context).push<void>(
onTap: () =>
Navigator.of(
context,
).push<void>(
MaterialPageRoute(builder: (context) => const HeroPage()),
),
),
@@ -52,7 +57,9 @@ StatelessWidget _createHeroContainer({
height: size,
width: size,
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),
child: const FlutterLogo(),
);

View File

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

View File

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

View File

@@ -2,15 +2,16 @@ name: animations
description: A new Flutter project.
version: 1.0.0+1
publish_to: none
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
flutter:
sdk: flutter
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
git:
url: https://github.com/google/flutter-desktop-embedding.git
@@ -22,7 +23,6 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:

View File

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

View File

@@ -24,7 +24,10 @@ void main() {
await tester.pumpAndSettle();
// 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 {
@@ -36,7 +39,10 @@ void main() {
// Swipe out all cards.
for (var i = 0; i < totalCards; i++) {
// 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();
}

View File

@@ -18,7 +18,11 @@ void main() {
expect(imageList.length, 2);
// 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();
// 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 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 {

View File

@@ -33,7 +33,9 @@ void main() {
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());
// 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.
expect(
(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());
// Get the initial Screen.
final initialScreen = tester.firstWidget(find.byType(HeroAnimationDemo));
final initialScreen = tester.firstWidget(
find.byType(HeroAnimationDemo),
);
// Tap on the 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.
// `--input` is the original asset file that this program should transform.
// `--output` is where flutter expects the transformation output to be written to.
final parser =
ArgParser()
..addOption(inputOptionName, mandatory: true, abbr: 'i')
..addOption(outputOptionName, mandatory: true, abbr: 'o');
final parser = ArgParser()
..addOption(inputOptionName, mandatory: true, abbr: 'i')
..addOption(outputOptionName, mandatory: true, abbr: 'o');
ArgResults argResults = parser.parse(arguments);
final String inputFilePath = argResults[inputOptionName];

View File

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

View File

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

View File

@@ -69,8 +69,8 @@ class _MyHomePageState extends State<MyHomePage> {
.then(
(sharedPreferences) => sharedPreferences.setBool('isDebug', true),
);
final Future<Directory> tempDirFuture =
path_provider.getTemporaryDirectory();
final Future<Directory> tempDirFuture = path_provider
.getTemporaryDirectory();
// Wait until the [SharedPreferences] value is set and the temporary
// directory is received before opening the database. If
@@ -130,8 +130,9 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SearchBar(
hintText: 'Search',
onChanged:
_database == null ? null : (query) => _refresh(query: query),
onChanged: _database == null
? null
: (query) => _refresh(query: query),
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
// the background isolate.
// ----------------------------------------------------------------------
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
BackgroundIsolateBinaryMessenger.ensureInitialized(
rootIsolateToken,
);
_sendPort.send(const _Command(_Codes.ack, arg0: null));
case _Codes.add:
_doAddEntry(command.arg0 as String);

View File

@@ -1,12 +1,13 @@
name: background_isolate_channels
description: A new Flutter project.
resolution: workspace
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
cupertino_icons: ^1.0.2
@@ -23,6 +24,5 @@ dev_dependencies:
path: ../analysis_defaults
flutter_test:
sdk: flutter
flutter:
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.
publish_to: "none"
version: 1.0.0+1
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
cupertino_icons: ^1.0.2
@@ -15,7 +16,9 @@ dependencies:
path: ../shared
dev_dependencies:
flutter_lints: ^5.0.0
analysis_defaults:
path: ../../analysis_defaults
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter

View File

@@ -9,10 +9,9 @@ import 'package:shelf_router/shelf_router.dart';
int count = 0;
// Configure routes.
final _router =
Router()
..post('/', _incrementHandler)
..get('/', _getValueHandler);
final _router = Router()
..post('/', _incrementHandler)
..get('/', _getValueHandler);
Future<Response> _incrementHandler(Request request) async {
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.
version: 1.0.0
publish_to: "none"
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
args: ^2.0.0
@@ -14,6 +15,8 @@ dependencies:
path: ../shared
dev_dependencies:
analysis_defaults:
path: ../../analysis_defaults
http: ^1.0.0
lints: ^6.0.0
test: ^1.15.0

View File

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

View File

@@ -1,15 +1,18 @@
name: shared
description: Common data models required by our client and server
version: 1.0.0
resolution: workspace
environment:
sdk: ^3.7.0-0
sdk: ^3.9.0-0
dependencies:
freezed_annotation: ">=2.1.0 <4.0.0"
json_annotation: ^4.7.0
dev_dependencies:
analysis_defaults:
path: ../../analysis_defaults
build_runner: ^2.2.1
freezed: ">=2.1.1 <4.0.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 = [
Provider(
lazy: true,
create:
(context) => BookingCreateUseCase(
destinationRepository: context.read(),
activityRepository: context.read(),
bookingRepository: context.read(),
),
create: (context) => BookingCreateUseCase(
destinationRepository: context.read(),
activityRepository: context.read(),
bookingRepository: context.read(),
),
),
Provider(
lazy: true,
@@ -57,46 +56,40 @@ List<SingleChildWidget> get providersRemote {
Provider(create: (context) => ApiClient()),
Provider(create: (context) => SharedPreferencesService()),
ChangeNotifierProvider(
create:
(context) =>
AuthRepositoryRemote(
authApiClient: context.read(),
apiClient: context.read(),
sharedPreferencesService: context.read(),
)
as AuthRepository,
create: (context) =>
AuthRepositoryRemote(
authApiClient: context.read(),
apiClient: context.read(),
sharedPreferencesService: context.read(),
)
as AuthRepository,
),
Provider(
create:
(context) =>
DestinationRepositoryRemote(apiClient: context.read())
as DestinationRepository,
create: (context) =>
DestinationRepositoryRemote(apiClient: context.read())
as DestinationRepository,
),
Provider(
create:
(context) =>
ContinentRepositoryRemote(apiClient: context.read())
as ContinentRepository,
create: (context) =>
ContinentRepositoryRemote(apiClient: context.read())
as ContinentRepository,
),
Provider(
create:
(context) =>
ActivityRepositoryRemote(apiClient: context.read())
as ActivityRepository,
create: (context) =>
ActivityRepositoryRemote(apiClient: context.read())
as ActivityRepository,
),
Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
),
Provider(
create:
(context) =>
BookingRepositoryRemote(apiClient: context.read())
as BookingRepository,
create: (context) =>
BookingRepositoryRemote(apiClient: context.read())
as BookingRepository,
),
Provider(
create:
(context) =>
UserRepositoryRemote(apiClient: context.read()) as UserRepository,
create: (context) =>
UserRepositoryRemote(apiClient: context.read()) as UserRepository,
),
..._sharedProviders,
];
@@ -110,37 +103,32 @@ List<SingleChildWidget> get providersLocal {
ChangeNotifierProvider.value(value: AuthRepositoryDev() as AuthRepository),
Provider.value(value: LocalDataService()),
Provider(
create:
(context) =>
DestinationRepositoryLocal(localDataService: context.read())
as DestinationRepository,
create: (context) =>
DestinationRepositoryLocal(localDataService: context.read())
as DestinationRepository,
),
Provider(
create:
(context) =>
ContinentRepositoryLocal(localDataService: context.read())
as ContinentRepository,
create: (context) =>
ContinentRepositoryLocal(localDataService: context.read())
as ContinentRepository,
),
Provider(
create:
(context) =>
ActivityRepositoryLocal(localDataService: context.read())
as ActivityRepository,
create: (context) =>
ActivityRepositoryLocal(localDataService: context.read())
as ActivityRepository,
),
Provider(
create:
(context) =>
BookingRepositoryLocal(localDataService: context.read())
as BookingRepository,
create: (context) =>
BookingRepositoryLocal(localDataService: context.read())
as BookingRepository,
),
Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
),
Provider(
create:
(context) =>
UserRepositoryLocal(localDataService: context.read())
as UserRepository,
create: (context) =>
UserRepositoryLocal(localDataService: context.read())
as UserRepository,
),
..._sharedProviders,
];

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,36 +91,30 @@ class _$BookingApiModelCopyWithImpl<$Res, $Val extends BookingApiModel>
}) {
return _then(
_value.copyWith(
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef:
null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
activitiesRef:
null == activitiesRef
? _value.activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
activitiesRef: null == activitiesRef
? _value.activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
)
as $Val,
);
@@ -169,36 +163,30 @@ class __$$BookingApiModelImplCopyWithImpl<$Res>
}) {
return _then(
_$BookingApiModelImpl(
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef:
null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
activitiesRef:
null == activitiesRef
? _value._activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
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),
name: json['name'] as String,
destinationRef: json['destinationRef'] as String,
activitiesRef:
(json['activitiesRef'] as List<dynamic>).map((e) => e as String).toList(),
activitiesRef: (json['activitiesRef'] as List<dynamic>)
.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$$BookingApiModelImplToJson(

View File

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

View File

@@ -75,26 +75,22 @@ class _$UserApiModelCopyWithImpl<$Res, $Val extends UserApiModel>
}) {
return _then(
_value.copyWith(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email:
null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture:
null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
@@ -134,26 +130,22 @@ class __$$UserApiModelImplCopyWithImpl<$Res>
}) {
return _then(
_$UserApiModelImpl(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email:
null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture:
null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: 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(
_value.copyWith(
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description:
null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName:
null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration:
null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay:
null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly:
null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price:
null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef:
null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
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,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName: null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay: null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly: null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price: null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
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,
);
@@ -215,56 +205,46 @@ class __$$ActivityImplCopyWithImpl<$Res>
}) {
return _then(
_$ActivityImpl(
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description:
null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName:
null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration:
null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay:
null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly:
null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price:
null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef:
null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
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,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName: null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay: null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly: null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price: null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
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(
_value.copyWith(
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
destination:
null == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as Destination,
activity:
null == activity
? _value.activity
: activity // ignore: cast_nullable_to_non_nullable
as List<Activity>,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
destination: null == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as Destination,
activity: null == activity
? _value.activity
: activity // ignore: cast_nullable_to_non_nullable
as List<Activity>,
)
as $Val,
);
@@ -168,31 +163,26 @@ class __$$BookingImplCopyWithImpl<$Res>
}) {
return _then(
_$BookingImpl(
id:
freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
destination:
null == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as Destination,
activity:
null == activity
? _value._activity
: activity // ignore: cast_nullable_to_non_nullable
as List<Activity>,
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
destination: null == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as Destination,
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(
json['destination'] as Map<String, dynamic>,
),
activity:
(json['activity'] as List<dynamic>)
.map((e) => Activity.fromJson(e as Map<String, dynamic>))
.toList(),
activity: (json['activity'] as List<dynamic>)
.map((e) => Activity.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) =>

View File

@@ -75,26 +75,22 @@ class _$BookingSummaryCopyWithImpl<$Res, $Val extends BookingSummary>
}) {
return _then(
_value.copyWith(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
@@ -134,26 +130,22 @@ class __$$BookingSummaryImplCopyWithImpl<$Res>
}) {
return _then(
_$BookingSummaryImpl(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
startDate:
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate:
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: 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}) {
return _then(
_value.copyWith(
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
@@ -106,16 +104,14 @@ class __$$ContinentImplCopyWithImpl<$Res>
$Res call({Object? name = null, Object? imageUrl = null}) {
return _then(
_$ContinentImpl(
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
),
);
}

View File

@@ -95,41 +95,34 @@ class _$DestinationCopyWithImpl<$Res, $Val extends Destination>
}) {
return _then(
_value.copyWith(
ref:
null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country:
null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent:
null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor:
null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags:
null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country: null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent: null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor: null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags: null == tags
? _value.tags
: 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,
);
@@ -180,41 +173,34 @@ class __$$DestinationImplCopyWithImpl<$Res>
}) {
return _then(
_$DestinationImpl(
ref:
null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name:
null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country:
null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent:
null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor:
null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags:
null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl:
null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country: null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent: null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor: null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags: null == tags
? _value._tags
: 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(
_value.copyWith(
continent:
freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate:
freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate:
freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests:
freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination:
freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as String?,
activities:
null == activities
? _value.activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
continent: freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate: freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate: freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests: freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination: freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as String?,
activities: null == activities
? _value.activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
)
as $Val,
);
@@ -168,36 +162,30 @@ class __$$ItineraryConfigImplCopyWithImpl<$Res>
}) {
return _then(
_$ItineraryConfigImpl(
continent:
freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate:
freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate:
freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests:
freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination:
freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as String?,
activities:
null == activities
? _value._activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
continent: freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate: freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate: freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests: freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination: freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
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,
) => _$ItineraryConfigImpl(
continent: json['continent'] as String?,
startDate:
json['startDate'] == null
? null
: DateTime.parse(json['startDate'] as String),
endDate:
json['endDate'] == null
? null
: DateTime.parse(json['endDate'] as String),
startDate: json['startDate'] == null
? null
: DateTime.parse(json['startDate'] as String),
endDate: json['endDate'] == null
? null
: DateTime.parse(json['endDate'] as String),
guests: (json['guests'] as num?)?.toInt(),
destination: json['destination'] as String?,
activities:

View File

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

View File

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

View File

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

View File

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

View File

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

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