From 95f406a2bbd8700cefd7412b55d438c933a52890 Mon Sep 17 00:00:00 2001 From: XinLei Date: Sun, 8 Mar 2026 11:29:43 -0700 Subject: [PATCH 1/9] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20sr?= =?UTF-8?q?c/content/learn/pathway/tutorial/=20=E7=B3=BB=E5=88=97=20(16?= =?UTF-8?q?=E7=AF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 翻译 Flutter 入门教程完整系列,包括: - 教程首页、创建应用、Widget 基础、布局 - 有状态 widget、状态项目设置、用户输入、导航 - HTTP 请求、隐式动画、Slivers、ChangeNotifier - ListenableBuilder、DevTools、高级 UI、自适应布局 Co-Authored-By: Claude Opus 4.6 --- .../learn/pathway/tutorial/adaptive-layout.md | 79 +++++++- .../learn/pathway/tutorial/advanced-ui.md | 123 ++++++++++++- .../learn/pathway/tutorial/change-notifier.md | 73 +++++++- .../learn/pathway/tutorial/create-an-app.md | 78 +++++++- .../learn/pathway/tutorial/devtools.md | 51 +++++- .../learn/pathway/tutorial/http-requests.md | 71 +++++++- .../pathway/tutorial/implicit-animations.md | 66 ++++++- src/content/learn/pathway/tutorial/index.md | 28 ++- src/content/learn/pathway/tutorial/layout.md | 102 ++++++++++- .../pathway/tutorial/listenable-builder.md | 129 ++++++++++++- .../learn/pathway/tutorial/navigation.md | 91 +++++++++- .../pathway/tutorial/set-up-state-project.md | 48 ++++- src/content/learn/pathway/tutorial/slivers.md | 131 +++++++++++++- .../learn/pathway/tutorial/stateful-widget.md | 84 ++++++++- .../learn/pathway/tutorial/user-input.md | 171 +++++++++++++++++- .../pathway/tutorial/widget-fundamentals.md | 113 +++++++++++- 16 files changed, 1401 insertions(+), 37 deletions(-) diff --git a/src/content/learn/pathway/tutorial/adaptive-layout.md b/src/content/learn/pathway/tutorial/adaptive-layout.md index fbddc96b88..03c45c512a 100644 --- a/src/content/learn/pathway/tutorial/adaptive-layout.md +++ b/src/content/learn/pathway/tutorial/adaptive-layout.md @@ -1,11 +1,15 @@ --- -title: LayoutBuilder and adaptive layouts -description: Learn how to use the LayoutBuilder widget. +#title: LayoutBuilder and adaptive layouts +title: LayoutBuilder 与自适应布局 +#description: Learn how to use the LayoutBuilder widget. +description: 学习如何使用 LayoutBuilder widget。 layout: tutorial --- Learn how to create layouts that adapt to different screen widths. +学习如何创建能够适配不同屏幕宽度的布局。 + title: What you'll accomplish items: @@ -21,6 +25,8 @@ items: ### Introduction +### 简介 + Modern apps need to work well on screens of all sizes. On this page, you'll learn how to create layouts that adapt to different screen widths. @@ -28,18 +34,28 @@ This app shows a sidebar on large screens and a navigation-based UI on small screens. Specifically, this app handles two screen sizes: +现代应用需要在各种尺寸的屏幕上都能良好运行。在本页中,你将学习如何创建能够适配不同屏幕宽度的布局。这个应用会在大屏幕上显示侧边栏,在小屏幕上使用基于导航的界面。具体来说,这个应用处理两种屏幕尺寸: + - **Large screens (tablets, desktop)**: Shows contact groups and contact details side-by-side. + **大屏幕(平板、桌面端)**: + 并排显示联系人分组和联系人详情。 - **Small screens (phones)**: Uses navigation to move between contact groups and details. + **小屏幕(手机)**: + 使用导航在联系人分组和详情之间切换。 ### Create the contact groups page +### 创建联系人分组页面 + First, create the basic structure of the `ContactGroupsPage` widget for your contact groups screen. Create `lib/screens/contact_groups.dart` and add the following basic structure: +首先,为联系人分组页面创建 `ContactGroupsPage` widget 的基本结构。创建 `lib/screens/contact_groups.dart` 并添加以下基本结构: + ```dart import 'package:flutter/cupertino.dart'; @@ -60,9 +76,13 @@ class ContactGroupsPage extends StatelessWidget { ### Create the contacts page +### 创建联系人页面 + Similarly, create `lib/screens/contacts.dart` to eventually display individual contacts: +类似地,创建 `lib/screens/contacts.dart` 来最终展示单个联系人: + ```dart import 'package:flutter/cupertino.dart'; @@ -87,11 +107,17 @@ The `ContaactsListPage` widget and `ContactGroupsPage` widget are placeholder pages that are needed to implement the adaptive layout widget, which you'll do next. +`ContaactsListPage` widget 和 `ContactGroupsPage` widget 是占位页面,用于实现自适应布局 widget,你将在下一步完成这项工作。 + ### Build the adaptive layout foundation +### 构建自适应布局基础 + Create `lib/screens/adaptive_layout.dart` and start with the following basic structure: +创建 `lib/screens/adaptive_layout.dart` 并从以下基本结构开始: + ```dart import 'package:flutter/cupertino.dart'; @@ -116,8 +142,12 @@ class _AdaptiveLayoutState extends State { This is a `StatefulWidget` because the adaptive layout eventually manages which contact group is currently selected. +这里使用 `StatefulWidget`,因为自适应布局最终需要管理当前选中的联系人分组。 + Next, add the screen size detection logic: +接下来,添加屏幕尺寸检测逻辑: + ```dart import 'package:flutter/cupertino.dart'; import 'contact_groups.dart'; @@ -156,16 +186,24 @@ the parent's size constraints. In the `builder` callback, you receive a`BoxConstraints` object that tells you the maximum available width and height. +`LayoutBuilder` widget 提供了关于父级尺寸约束的信息。在 `builder` 回调中,你会收到一个 `BoxConstraints` 对象,它会告诉你可用的最大宽度和高度。 + By checking if `constraints.maxWidth > largeScreenMinWidth`, you can decide which layout to show. The 600-pixel threshold is a common breakpoint that separates phone-sized screens from tablet-sized screens. +通过检查 `constraints.maxWidth > largeScreenMinWidth`,你可以决定显示哪种布局。600 像素的阈值是一个常用的断点,用于区分手机尺寸和平板尺寸的屏幕。 + ### Update the main app +### 更新主应用 + Update `main.dart` to use the adaptive layout, so you can see your changes: +更新 `main.dart` 以使用自适应布局,这样你就可以看到变更效果: + ```dart import 'package:flutter/cupertino.dart'; import 'package:rolodex/data/contact_group.dart'; @@ -199,11 +237,17 @@ class RolodexApp extends StatelessWidget { If you're running in Chrome, you can resize the browser window to see layout changes. +如果你正在 Chrome 中运行,可以调整浏览器窗口大小来查看布局变化。 + ### Add list selection functionality +### 添加列表选择功能 + The large screen layout needs to track which contact group is selected. Update the state object with the following code: +大屏幕布局需要追踪当前选中的联系人分组。使用以下代码更新 state 对象: + ```dart import 'package:flutter/cupertino.dart'; @@ -249,12 +293,18 @@ class _AdaptiveLayoutState extends State { The `selectedListId` variable tracks the currently selected contact group, and `_onContactListSelected` updates this value when the user makes a choice. +`selectedListId` 变量追踪当前选中的联系人分组,`_onContactListSelected` 在用户做出选择时更新该值。 + ### Build the large screen layout +### 构建大屏幕布局 + Now, implement the side-by-side layout for large screens. First, replace the temporary text with a widget that contains the proper layout. +现在,实现大屏幕的并排布局。首先,将临时文本替换为包含正确布局的 widget。 + ```dart import 'package:flutter/cupertino.dart'; @@ -318,8 +368,12 @@ place the sidebar and details side-by-side. `SafeArea` ensures that the content doesn't overlap with system UI elements like the status bar. +大屏幕布局使用 `Row` 将侧边栏和详情面板并排放置。`SafeArea` 确保内容不会与状态栏等系统界面元素重叠。 + Now, set the sizes of the two panels and add a visual divider: +现在,设置两个面板的尺寸并添加一个视觉分隔线: + ```dart Widget _buildLargeScreenLayout() { return CupertinoPageScaffold( @@ -350,28 +404,47 @@ Widget _buildLargeScreenLayout() { This layout creates the following: +这个布局创建了以下内容: + - A fixed-width sidebar (320 pixels) for contact groups. + 一个固定宽度的侧边栏(320 像素),用于显示联系人分组。 - A 1-pixel divider between the panels. + 面板之间的 1 像素分隔线。 - A details panel that uses an `Expanded` widget to take the remaining space. + 一个使用 `Expanded` widget 占据剩余空间的详情面板。 ### Test the adaptive layout +### 测试自适应布局 + Hot reload your app and test the responsive behavior. If you're running in Chrome, you can resize the browser window to see the layout change: +热重载你的应用并测试响应式行为。如果你正在 Chrome 中运行,可以调整浏览器窗口大小来查看布局变化: + - **Wide window (> 600px)**: Shows placeholder text for the sidebar and details side-by-side. + **宽窗口(> 600px)**: + 并排显示侧边栏和详情的占位文本。 - **Narrow window (< 600px)**: Shows only the contact groups page. + **窄窗口(< 600px)**: + 仅显示联系人分组页面。 Both the sidebar and main content area show placeholder text for now. +目前侧边栏和主内容区域都显示占位文本。 + In the next lesson, you'll implement slivers to fill in the contact list content. +在下一课中,你将实现 sliver 来填充联系人列表内容。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -400,6 +473,8 @@ items: ### Test yourself +### 自测 + - question: What information does LayoutBuilder provide to its builder callback? options: diff --git a/src/content/learn/pathway/tutorial/advanced-ui.md b/src/content/learn/pathway/tutorial/advanced-ui.md index 989e3b3164..e8683ccea5 100644 --- a/src/content/learn/pathway/tutorial/advanced-ui.md +++ b/src/content/learn/pathway/tutorial/advanced-ui.md @@ -1,13 +1,19 @@ --- -title: Advanced UI features +# title: Advanced UI features +title: 高级 UI 特性 +# description: >- +# A gentle introduction into advanced UI features: +# adaptive layouts, slivers, scrolling, navigation. description: >- - A gentle introduction into advanced UI features: - adaptive layouts, slivers, scrolling, navigation. + 高级 UI 特性的入门介绍: + 自适应布局、sliver、滚动和导航。 layout: tutorial --- Preview the Rolodex app you'll build and set up a Cupertino-based project with data models. +预览你将构建的 Rolodex 应用,并使用数据模型搭建一个基于 Cupertino 的项目。 + title: What you'll accomplish items: @@ -23,10 +29,16 @@ items: ### Introduction +### 简介 + In this third installment of the Flutter tutorial series, you'll use Flutter's Cupertino library to build a partial clone of the iOS Contacts app. +在 Flutter 教程系列的第三部分中, +你将使用 Flutter 的 Cupertino 库来构建一个 +iOS 通讯录应用的部分克隆版本。 + A screenshot of the completed Rolodex contact
 management app showing a list of contacts organized alphabetically. @@ -35,31 +47,57 @@ By the end of this tutorial, you'll have learned how to create adaptive layouts, implement comprehensive theming, build navigation patterns, and use advanced scrolling techniques. +在本教程结束时,你将学会如何创建 +自适应布局、实现全面的主题定制、构建导航 +模式,以及使用高级滚动技术。 + #### What you'll learn +#### 你将学到什么 + This tutorial explores the following topics: +本教程将探讨以下主题: + * Building responsive layouts with `LayoutBuilder`. + 使用 `LayoutBuilder` 构建响应式布局。 * Using advanced scrolling with slivers and search. + 使用 sliver 和搜索实现高级滚动。 * Implementing stack-based navigation patterns. + 实现基于栈的导航模式。 * Creating comprehensive themes with `CupertinoThemeData`. + 使用 `CupertinoThemeData` 创建全面的主题。 * Supporting both light and dark themes. + 同时支持浅色和深色主题。 * Creating an iOS-style UI using Cupertino widgets. + 使用 Cupertino widget 创建 iOS 风格的 UI。 This tutorial assumes that you've completed the previous Flutter tutorials and are comfortable with basic widget composition, state management, and the Flutter project structure. +本教程假设你已经完成了之前的 Flutter 教程, +并且熟悉基本的 widget 组合、状态管理 +以及 Flutter 项目结构。 + ### Create a new Flutter project +### 创建一个新的 Flutter 项目 + To build a Flutter app, you first need a Flutter project. You can create a new app with the [Flutter CLI tool][], which is installed as part of the Flutter SDK. +要构建一个 Flutter 应用,你首先需要一个 Flutter 项目。 +你可以使用 [Flutter CLI 工具][Flutter CLI tool] 来创建新应用, +它作为 Flutter SDK 的一部分被安装。 + Open your preferred terminal and run the following command to create a new Flutter project: +打开你常用的终端,运行以下命令来创建一个新的 Flutter 项目: + ```console $ flutter create rolodex --empty ``` @@ -67,23 +105,36 @@ $ flutter create rolodex --empty This command creates a new Flutter project that uses the minimal "empty" template. +此命令会使用精简的 "empty" 模板创建一个新的 Flutter 项目。 + [Flutter CLI tool]: /reference/flutter-cli ### Add the Cupertino Icons dependency +### 添加 Cupertino Icons 依赖 + This project uses the [`cupertino_icons` package][], an official Flutter package. Add it as a dependency by running the following command: +本项目使用 [`cupertino_icons` package][], +这是一个官方的 Flutter package。 +运行以下命令将其添加为依赖: + ```console $ flutter pub add cupertino_icons ``` ### Set up the project structure +### 搭建项目结构 + First, create the basic directory structure for your app. In your project's `lib` directory, create the following folders: +首先,为你的应用创建基本的目录结构。 +在项目的 `lib` 目录中,创建以下文件夹: + ```console $ cd rolodex $ mkdir lib/data lib/screens lib/theme @@ -92,11 +143,19 @@ $ mkdir lib/data lib/screens lib/theme This command creates folders to organize your code into logical sections: data models, screen widgets, and theme configuration. +此命令会创建文件夹,将你的代码组织为逻辑分区: +数据模型、屏幕 widget 和主题配置。 + ### Replace the starter code +### 替换初始代码 + In your IDE, open the `lib/main.dart` file, and replace its entire contents with the following starter code: +在你的 IDE 中,打开 `lib/main.dart` 文件, +将其全部内容替换为以下初始代码: + ```dart title="lib/main.dart" import 'package:flutter/cupertino.dart'; @@ -132,10 +191,19 @@ this app uses `CupertinoApp` instead of `MaterialApp`. The Cupertino design system provides iOS-style widgets and styling, which is perfect for building apps that feel native on Apple devices. +与前两个教程不同, +此应用使用 `CupertinoApp` 而非 `MaterialApp`。 +Cupertino 设计系统提供了 iOS 风格的 widget 和样式, +非常适合构建在 Apple 设备上具有原生体验的应用。 + ### Run your app +### 运行你的应用 + In your terminal at the root of your Flutter app, run the following command: +在 Flutter 应用的根目录下,在终端中运行以下命令: + ```console $ flutter run -d chrome ``` @@ -143,16 +211,29 @@ $ flutter run -d chrome The app builds and launches in a new instance of Chrome. It displays "Hello Rolodex!" in the center of the screen. +应用会构建并在新的 Chrome 实例中启动。 +屏幕中央会显示 "Hello Rolodex!"。 + ### Create the data models +### 创建数据模型 + Before building the UI, create the data structures and sample data that the app will use. This section is lightly explained because it's not the focus of this tutorial. +在构建 UI 之前, +先创建应用将使用的数据结构和示例数据。 +本节只做简要说明,因为这不是本教程的重点。 + #### `Contact` data +#### `Contact` 数据 + Create a new file, `lib/data/contact.dart`, and add the basic `Contact` class: +创建一个新文件 `lib/data/contact.dart`,并添加基本的 `Contact` 类: + ```dart foldable title="lib/data/contact.dart" class Contact { Contact({ @@ -170,7 +251,7 @@ class Contact { final String? suffix; } -[* - +[* - final johnAppleseed = Contact(id: 0, firstName: 'John', lastName: 'Appleseed'); final kateBell = Contact(id: 1, firstName: 'Kate', lastName: 'Bell'); final annaHaro = Contact(id: 2, firstName: 'Anna', lastName: 'Haro'); @@ -348,12 +429,21 @@ final Set allContacts = { This sample data includes contacts with and without middle names and suffixes. This gives you a variety of data to work with as you build the UI. +这些示例数据包含有中间名和后缀的联系人,也有没有的。 +这为你在构建 UI 时提供了多样化的数据。 + #### `ContactGroup` data +#### `ContactGroup` 数据 + Now, create the contact groups that organize your contacts into lists. Create a new `lib/data/contact_group.dart` file and add the `ContactGroup` class: +现在,创建联系人分组来将你的联系人组织为列表。 +创建一个新的 `lib/data/contact_group.dart` 文件, +并添加 `ContactGroup` 类: + ```dart title="lib/data/contact_group.dart" import 'dart:collection'; import 'package:flutter/cupertino.dart'; @@ -413,8 +503,13 @@ class ContactGroup { A `ContactGroup` represents a collection of contacts, such as "All Contacts" or "Favorites". +`ContactGroup` 代表一组联系人的集合, +例如"所有联系人"或"收藏夹"。 + Add the following helper code and sample data to the same file: +将以下辅助代码和示例数据添加到同一文件中: + ```dart title="lib/data/contact_group.dart" // ... ContactGroup class from above @@ -470,8 +565,13 @@ List generateSeedData() { This code creates three sample groups and a function to generate the initial data for the app. +这段代码创建了三个示例分组和一个用于 +生成应用初始数据的函数。 + Finally, add a class that manages state changes: +最后,添加一个管理状态变更的类: + ```dart title="lib/data/contact_group.dart" // ... @@ -498,13 +598,21 @@ If you aren't familiar with `ValueNotifier`, you should complete the [previous tutorial covering state][] before continuing, which covers state management. +如果你不熟悉 `ValueNotifier`, +你应该先完成[前一个关于状态管理的教程][previous tutorial covering state], +该教程涵盖了状态管理的内容。 + [previous tutorial covering state]: /learn/pathway/tutorial/set-up-state-project ### Connect the data to your app +### 将数据连接到你的应用 + Update your `main.dart` to include the global state and import the new data file: +更新你的 `main.dart` 以包含全局状态并导入新的数据文件: + ```dart title="lib/main.dart" import 'package:flutter/cupertino.dart'; import 'package:rolodex/data/contact_group.dart'; @@ -537,10 +645,15 @@ class RolodexApp extends StatelessWidget { With all the extraneous code out of the way, in the next lesson, you'll start building the app in earnest. +所有准备工作完成后,在下一课中, +你将正式开始构建这个应用。 + [`cupertino_icons` package]: {{site.pub-pkg}}/cupertino_icons ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -569,6 +682,8 @@ items: ### Test yourself +### 测试一下 + - question: What is the main difference between CupertinoApp and MaterialApp? options: diff --git a/src/content/learn/pathway/tutorial/change-notifier.md b/src/content/learn/pathway/tutorial/change-notifier.md index 824dc464a6..1254ab2542 100644 --- a/src/content/learn/pathway/tutorial/change-notifier.md +++ b/src/content/learn/pathway/tutorial/change-notifier.md @@ -1,11 +1,15 @@ --- -title: State management in Flutter -description: Instructions on how to manage state with ChangeNotifiers. +# title: State management in Flutter +title: Flutter 中的状态管理 +# description: Instructions on how to manage state with ChangeNotifiers. +description: 如何使用 ChangeNotifier 管理状态。 layout: tutorial --- Learn to create a ViewModel with ChangeNotifier and manage loading, success, and error states. +学习如何使用 ChangeNotifier 创建 ViewModel,并管理加载、成功和错误状态。 + title: What you'll accomplish items: @@ -21,27 +25,47 @@ items: ### Introduction +### 简介 + When developers talk about state-management in Flutter, they're essentially referring to the pattern by which your app updates the data it needs to render correctly and then tells Flutter to re-render the UI with that new data. +当开发者谈论 Flutter 中的状态管理时, +本质上是指应用更新渲染所需数据, +然后通知 Flutter 使用新数据重新渲染 UI 的模式。 + In MVVM, this responsibility falls to the ViewModel layer, which sits between and connects your UI to your Model layer. In Flutter, ViewModels use Flutter's `ChangeNotifier` class to notify the UI when data changes. +在 MVVM 架构中,这一职责由 ViewModel 层承担, +它位于 UI 和 Model 层之间,负责连接两者。 +在 Flutter 中,ViewModel 使用 Flutter 的 `ChangeNotifier` 类 +在数据变化时通知 UI。 + To use [`ChangeNotifier`][], extend it in your state management class to gain access to the `notifyListeners()` method, which triggers UI rebuilds when called. +要使用 [`ChangeNotifier`][],请在你的状态管理类中继承它, +以获得 `notifyListeners()` 方法的访问权限, +调用该方法即可触发 UI 重建。 + [`ChangeNotifier`]: {{site.api}}/flutter/foundation/ChangeNotifier-class.html ### Create the basic view model structure +### 创建基本的 ViewModel 结构 + Create the `ArticleViewModel` class with its basic structure and state properties: +创建 `ArticleViewModel` 类, +包含其基本结构和状态属性: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; @@ -55,15 +79,24 @@ class ArticleViewModel extends ChangeNotifier { The `ArticleViewModel` holds three pieces of state: +`ArticleViewModel` 持有三个状态: + - `summary`: The current Wikipedia article data. + `summary`:当前的 Wikipedia 文章数据。 - `errorMessage`: Any error that occurred during data fetching. + `errorMessage`:数据获取过程中发生的错误信息。 - `loading`: A flag to show progress indicators. + `loading`:用于显示加载指示器的标志位。 ### Add constructor initialization +### 添加构造函数初始化 + Update the constructor to automatically fetch content when the `ArticleViewModel` is created: +更新构造函数,使 `ArticleViewModel` 创建时自动获取内容: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; @@ -84,10 +117,18 @@ a `ArticleViewModel` object is created. Because constructors can't be asynchronous, it delegates initial content fetching to a separate method. +这种构造函数初始化方式可以在 `ArticleViewModel` 对象创建时立即获取内容。 +由于构造函数不能是异步的, +因此将初始内容获取委托给一个单独的方法。 + ### Set up the `getRandomArticleSummary` method +### 设置 `getRandomArticleSummary` 方法 + Add the `getRandomArticleSummary` that fetches data and manages state updates: +添加用于获取数据和管理状态更新的 `getRandomArticleSummary` 方法: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; @@ -117,14 +158,26 @@ When the operation completes, it toggles the property back. When you build the UI, you'll use this `loading` property to show a loading indicator while fetching a new article. +ViewModel 更新 `loading` 属性并调用 `notifyListeners()` 通知 UI 更新。 +当操作完成时,它会将该属性切换回原值。 +当你构建 UI 时,可以使用 `loading` 属性在获取新文章时显示加载指示器。 + ### Retrieve an article from the `ArticleModel` +### 从 `ArticleModel` 获取文章 + Complete the `getRandomArticleSummary` method to fetch an article summary. Use a [try-catch block][] to gracefully handle network errors and store error messages that the UI can display to users. The method clears previous errors on success and clears the previous article summary on error to maintain a consistent state. +完成 `getRandomArticleSummary` 方法以获取文章摘要。 +使用 [try-catch block][] 优雅地处理网络错误, +并存储 UI 可以展示给用户的错误信息。 +该方法在成功时清除之前的错误, +在出错时清除之前的文章摘要,以维持一致的状态。 + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; @@ -156,11 +209,16 @@ class ArticleViewModel extends ChangeNotifier { ### Test the ViewModel +### 测试 ViewModel + Before building the full UI, test that your HTTP requests work by printing results to the console. First, update the `getRandomArticleSummary` method to print the results: +在构建完整 UI 之前,先通过将结果打印到控制台来测试 HTTP 请求是否正常工作。 +首先,更新 `getRandomArticleSummary` 方法以打印结果: + ```dart Future getRandomArticleSummary() async { loading = true; @@ -182,6 +240,9 @@ Future getRandomArticleSummary() async { Then, update the `MainApp` widget to create the `ArticleViewModel`, which calls the `getRandomArticleSummary` method on creation: +然后,更新 `MainApp` widget 以创建 `ArticleViewModel`, +它会在创建时调用 `getRandomArticleSummary` 方法: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -209,8 +270,14 @@ Hot reload your app and check your console output. You should see either an article title or an error message, which confirms that your Model and ViewModel are wired up correctly. +热重载你的应用并检查控制台输出。 +你应该能看到文章标题或错误信息, +这说明你的 Model 和 ViewModel 已正确连接。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -241,6 +308,8 @@ items: ### Test yourself +### 自测 + - question: What is a ChangeNotifier? options: diff --git a/src/content/learn/pathway/tutorial/create-an-app.md b/src/content/learn/pathway/tutorial/create-an-app.md index 3076dd6fb9..8269791677 100644 --- a/src/content/learn/pathway/tutorial/create-an-app.md +++ b/src/content/learn/pathway/tutorial/create-an-app.md @@ -1,11 +1,15 @@ --- -title: Create an app -description: Instructions on how to create a new Flutter app. +# title: Create an app +title: 创建一个应用 +# description: Instructions on how to create a new Flutter app. +description: 关于如何创建一个新的 Flutter 应用的说明。 layout: tutorial --- Learn the first steps to building a Flutter app, from creating a project to understanding widgets and hot reload. +学习构建 Flutter 应用的第一步,从创建项目到理解 widget 和热重载。 + @@ -23,41 +27,65 @@ items: ### What you'll build +### 你将构建什么 + In this first section of the Flutter tutorial, you'll build the core UI of an app called 'Birdle', a game similar to [Wordle, the popular New York Times game][]. +在 Flutter 教程的第一部分中, +你将构建一个名为 "Birdle" 的应用的核心 UI, +这是一个类似于 [Wordle(纽约时报的热门游戏)][Wordle, the popular New York Times game] 的游戏。 + A screenshot that resembles the popular game Wordle. By the end of this tutorial, you'll have learned the fundamentals of building Flutter UIs, and your app will look like the following screenshot (and it'll even mostly work 😀). +在本教程结束时,你将学会构建 Flutter UI 的基础知识, +你的应用将如下面的截图所示(而且它基本上可以正常运行 😀)。 + [Wordle, the popular New York Times game]: https://www.nytimes.com/games/wordle/index.html ### Create a new Flutter project +### 创建一个新的 Flutter 项目 + The first step to building Flutter apps is to create a new project. You create new apps with the [Flutter CLI tool][], installed as part of the Flutter SDK. +构建 Flutter 应用的第一步是创建一个新项目。 +你可以使用 [Flutter CLI 工具][Flutter CLI tool]来创建新应用, +它作为 Flutter SDK 的一部分被安装。 + Open your terminal or command prompt and run the following command to create a new Flutter project: +打开你的终端或命令提示符,运行以下命令来创建一个新的 Flutter 项目: + ```console $ flutter create birdle --empty ``` This creates a new Flutter project using the minimal "empty" template. +这会使用精简的 "empty" 模板创建一个新的 Flutter 项目。 + [Flutter CLI tool]: /reference/flutter-cli ### Examine the code +### 查看代码 + In your IDE, open the file at `lib/main.dart`. Starting from the top, you'll see this code. +在你的 IDE 中,打开 `lib/main.dart` 文件。 +从文件顶部开始,你会看到以下代码。 + ```dart title"lib/main.dart" import 'package:flutter/material.dart'; // Imports Flutter. @@ -73,8 +101,16 @@ The `runApp` method is part of the Flutter SDK, and it takes a **widget** as an argument. In this case, an instance of the `MainApp` widget is being passed in. +`main` 函数是所有 Dart 程序的入口, +而 Flutter 应用本质上就是一个 **Dart** 程序。 +`runApp` 方法是 Flutter SDK 的一部分, +它接收一个 **widget** 作为参数。 +在这里,传入的是 `MainApp` widget 的一个实例。 + Just below the `main` function, you'll find the `MainApp` class declaration. +在 `main` 函数下方,你会看到 `MainApp` 类的声明。 + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -101,41 +137,70 @@ Essentially, this is what a Flutter app is: a composition of widgets that make up a tree structure called the **widget tree.** +`MainApp` 是**根 widget**, +因为它是传递给 `runApp` 的那个 widget。 +在这个 widget 内部,有一个 `build` 方法, +它返回另一个名为 `MaterialApp` 的 widget。 +本质上,Flutter 应用就是这样的: +通过组合 widget 构成一个树形结构, +称为 **widget 树**。 + Your job as a Flutter developer is to compose widgets from the SDK into larger, custom widgets that display a UI. +作为 Flutter 开发者,你的工作就是 +将 SDK 中的 widget 组合成更大的、自定义的 widget 来展示 UI。 + At the moment, the widget tree is quite simple: +目前,这个 widget 树非常简单: + A screenshot that resembles the popular game Wordle. ### Run your app +### 运行你的应用 + 1. In your terminal, navigate to the root directory of your created Flutter app: + 在终端中,导航到你创建的 Flutter 应用的根目录: + ```console $ cd birdle ``` 1. Run the app using the Flutter CLI tool. + 使用 Flutter CLI 工具运行应用。 + ```console $ flutter run -d chrome ``` The app will build and launch in a new instance of Chrome. + 应用将会被构建并在新的 Chrome 实例中启动。 + A screenshot that resembles the popular game Wordle. ### Use hot reload +### 使用热重载 + **Stateful hot reload**, if you haven't heard of it, allows a running Flutter app to re-render updated business logic or UI code in less than a second – all without losing your place in the app. +**有状态热重载**,如果你还没听说过的话, +它允许正在运行的 Flutter 应用在不到一秒的时间内重新渲染更新后的业务逻辑或 UI 代码—— +而且不会丢失你在应用中的当前状态。 + In your IDE, open the `main.dart` file and navigate to line ~15 and find this code: +在 IDE 中打开 `main.dart` 文件,导航到大约第 15 行,找到以下代码: + ```dart child: Text('Hello World!'), ``` @@ -145,8 +210,14 @@ Then, hot-reload your app by pressing `r` in the terminal where the app is running. The running app should instantly show your updated text. +将字符串中的文本修改为你想要的任何内容。 +然后,在运行应用的终端中按 `r` 键来热重载你的应用。 +运行中的应用应该会立即显示你更新后的文本。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -178,6 +249,8 @@ items: ### Test yourself +### 自我测试 + - question: "What is the purpose of the `runApp` function in a Flutter application?" options: @@ -208,4 +281,3 @@ items: correct: false explanation: "By default, you need to press `r` to trigger hot reload in the terminal." - diff --git a/src/content/learn/pathway/tutorial/devtools.md b/src/content/learn/pathway/tutorial/devtools.md index b7b4bc06b2..efb5604225 100644 --- a/src/content/learn/pathway/tutorial/devtools.md +++ b/src/content/learn/pathway/tutorial/devtools.md @@ -1,11 +1,14 @@ --- title: DevTools -description: Learn to use the Dart DevTools when developing Flutter apps. +# description: Learn to use the Dart DevTools when developing Flutter apps. +description: 学习在开发 Flutter 应用时使用 Dart DevTools。 layout: tutorial --- Learn to use the widget inspector and property editor to debug layout issues and experiment with properties in real-time. +学习使用 widget 检查器和属性编辑器来调试布局问题,并实时体验属性的修改效果。 + @@ -23,15 +26,21 @@ items: ### Introduction +### 简介 + As your Flutter app grows in complexity, it becomes more important to understand how each of the widget properties affects the UI. The [Dart and Flutter DevTools][] provide you with two particularly useful features: the **widget inspector** and the **property editor**. +随着 Flutter 应用复杂度的增加,理解每个 widget 属性如何影响 UI 变得越来越重要。[Dart and Flutter DevTools][] 为你提供了两个特别实用的功能:**widget 检查器**和**属性编辑器**。 + First, launch DevTools by running the following commands while your app is running in debug mode: +首先,在应用以调试模式运行时,执行以下命令启动 DevTools: + ```console $ dart devtools ``` @@ -44,6 +53,8 @@ Code OSS-based editors such as [VS Code][] as well as [IntelliJ and Android Studio][]. The screenshots in this lesson are from VS Code. +如果你已安装了相应的 Flutter 插件,也可以直接在基于 Code OSS 的编辑器(如 [VS Code][])以及 [IntelliJ and Android Studio][] 中运行 DevTools。本课程中的截图来自 VS Code。 + ::: [Dart and Flutter DevTools]: /tools/devtools @@ -52,15 +63,21 @@ The screenshots in this lesson are from VS Code. ### The widget inspector +### Widget 检查器 + The widget inspector allows you to visualize and explore your widget tree. It helps you understand the layout of your UI and identifies which widgets are responsible for different parts of the screen. Running against the app you've built so far, the inspector looks like this: +Widget 检查器允许你可视化地浏览 widget 树。它帮助你了解 UI 的布局结构,并确定哪些 widget 负责屏幕的不同部分。对你目前构建的应用运行检查器,界面如下所示: + A screenshot of the Flutter widget inspector tool. Consider the `GamePage` widget you created in this section: +来看看你在本节中创建的 `GamePage` widget: + ```dart class GamePage extends StatelessWidget { GamePage({super.key}); @@ -90,6 +107,8 @@ class GamePage extends StatelessWidget { And how it's used in `MainApp`: +以及它在 `MainApp` 中的使用方式: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -113,10 +132,16 @@ the `Row` widgets with `Tile` children. You can select any widget in the tree to see its properties and even jump to its source code in your IDE. +在 widget 检查器中,你应该能看到与代码中完全一致的 widget 树:`MaterialApp` 作为根节点,`Scaffold` 作为其 `home`,`AppBar` 作为其 `appBar`,依此类推,一直到包含 `Tile` 子节点的 `Row` widget。你可以选择树中的任意 widget 来查看其属性,甚至可以直接跳转到 IDE 中的源代码。 + ### Debugging layout issues +### 调试布局问题 + The widget inspector is perhaps most useful for debugging layout issues. +Widget 检查器在调试布局问题时可能最为实用。 + In certain situations, a widget's [constraints][] are unbounded, or infinite. This means that either @@ -124,11 +149,15 @@ the maximum width or the maximum height is set to [`double.infinity`][]. A widget that tries to be as big as possible won't function usefully when given an unbounded constraint and, in debug mode, throws an exception. +在某些情况下,widget 的[约束][constraints]是无界的,即无限大。这意味着最大宽度或最大高度被设置为 [`double.infinity`][]。当一个试图尽可能大的 widget 遇到无界约束时,它将无法正常工作,并在调试模式下抛出异常。 + The most common case where a render box ends up with an unbounded constraint is within a flex box widget ([`Row`][] or [`Column`][]), and within a scrollable region, such as a [`ListView`][] or [`ScrollView`][] subclasses. +渲染盒子遇到无界约束最常见的情况是在弹性盒子 widget([`Row`][] 或 [`Column`][])内部,以及在可滚动区域内部,例如 [`ListView`][] 或 [`ScrollView`][] 的子类。 + `ListView`, for example, tries to expand to fit the space available in its cross-direction. Such as if it's a vertically scrolling block that tries to be as wide as its parent. @@ -137,13 +166,19 @@ a horizontally scrolling `ListView`, the inner list tries to be as wide as possible, which is infinitely wide, since the outer one is scrollable in that direction. +例如,`ListView` 会尝试在其交叉方向上扩展以填满可用空间。比如一个垂直滚动的列表会尝试和父级一样宽。如果你将一个垂直滚动的 `ListView` 嵌套在一个水平滚动的 `ListView` 中,内部列表会尝试尽可能宽,而这个宽度是无限大的,因为外部列表在该方向上是可滚动的。 + Perhaps the most common error you'll run into while building a Flutter application is due to incorrectly using layout widgets. This error is referred to as the "unbounded constraints" error. +在构建 Flutter 应用时,你最常遇到的错误可能就是由于布局 widget 使用不当导致的。这个错误被称为"无界约束"错误。 + Watch the following video to get an understanding of how to spot and resolve this issue. +观看以下视频,了解如何发现和解决此问题。 + [constraints]: /ui/layout/constraints @@ -155,15 +190,21 @@ spot and resolve this issue. ### The property editor +### 属性编辑器 + When you select a widget in the widget inspector, the property editor displays all the properties of that selected widget. This is a powerful tool for understanding why a widget looks the way it does and for experimenting with property value changes in real-time. +当你在 widget 检查器中选中一个 widget 时,属性编辑器会显示该 widget 的所有属性。这是一个强大的工具,可以帮助你理解 widget 为何呈现当前的外观,并实时体验属性值的修改效果。 + A screenshot of the Flutter property editor tool. Look at the `Tile` widget's `build` method from earlier: +来看看之前 `Tile` widget 的 `build` 方法: + ```dart class Tile extends StatelessWidget { const Tile(required this.letter, required hitType, {super.key}); @@ -196,6 +237,8 @@ the property editor would show you its You could then expand the `BoxDecoration` to see the `border` and `color` properties. +如果你在 widget 检查器中选中一个 `Tile` widget,属性编辑器会显示其 `width`(60)、`height`(60)以及 `decoration` 属性。你可以展开 `BoxDecoration` 来查看 `border` 和 `color` 属性。 + For many properties, you can even modify their values directly within the property editor. For example, to quickly test how a different `width` or `height` would look @@ -205,8 +248,12 @@ Then instantly see the update on your running app without needing to recompile or even hot reload. This allows for rapid iteration on UI design. +对于许多属性,你甚至可以直接在属性编辑器中修改它们的值。例如,要快速测试 `Tile` widget 中 `Container` 使用不同 `width` 或 `height` 时的效果,只需在属性编辑器中更改数值,就能立即在运行中的应用上看到更新,无需重新编译甚至热重载。这使得 UI 设计的迭代速度大大提升。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -245,6 +292,8 @@ items: ### Test yourself +### 自测 + - question: What is a common cause of "unbounded constraints" errors in Flutter? options: diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index 31bc4c8594..fe59f840f9 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -1,11 +1,15 @@ --- -title: Fetch data from the internet -description: Instructions on how to make HTTP requests and parse responses. +# title: Fetch data from the internet +title: 从网络获取数据 +# description: Instructions on how to make HTTP requests and parse responses. +description: 关于如何发起 HTTP 请求并解析响应的说明。 layout: tutorial --- Learn the MVVM architecture pattern and how to build HTTP requests with async/await. +学习 MVVM 架构模式以及如何使用 async/await 构建 HTTP 请求。 + title: What you'll accomplish items: @@ -21,34 +25,61 @@ items: ### Introduction +### 简介 + The overarching pattern that this tutorial implements is called _Model-View-ViewModel_ or _MVVM_. MVVM is an [architectural pattern][] used in client apps that separates your app into three layers: +本教程所实现的总体模式被称为 +_Model-View-ViewModel_,即 _MVVM_。 +MVVM 是一种用于客户端应用的[架构模式][architectural pattern], +它将你的应用分为三层: + - **Model**: Handles data operations. + **Model**:处理数据操作。 - **View**: Displays the UI. + **View**:展示用户界面。 - **ViewModel**: Manages state and connects the two. + **ViewModel**:管理状态并连接上述两层。 The core tenet of MVVM (and many other patterns) is *separation of concerns*. Managing state in separate classes (outside your UI widgets) makes your code more testable, reusable, and easier to maintain. +MVVM(以及许多其他模式)的核心原则是*关注点分离*。 +在独立的类中管理状态(在 UI widget 之外)可以让你的代码 +更易于测试、复用和维护。 + A single feature in your app contains each one of the MVVM components. In this tutorial, in addition to Flutter widgets, you'll create `ArticleModel`, `ArticleViewModel`, and `ArticleView`. +你的应用中的每个功能都包含 MVVM 的各个组成部分。 +在本教程中,除了 Flutter widget 之外, +你还将创建 `ArticleModel`、`ArticleViewModel` 和 `ArticleView`。 + [architectural pattern]: /app-architecture/guide ### Define the Model +### 定义 Model + The Model is the source-of-truth for your app's data and is responsible for low-level tasks such as making HTTP requests, caching data, or managing system resources such as used by a Flutter plugin. A model doesn't usually need to import Flutter libraries. +Model 是应用数据的唯一可信来源,负责 +底层任务,例如发起 HTTP 请求、缓存数据, +或管理 Flutter 插件所使用的系统资源等。 +通常,Model 不需要导入 Flutter 库。 + Create an empty `ArticleModel` class in your `main.dart` file: +在 `main.dart` 文件中创建一个空的 `ArticleModel` 类: + ```dart title="lib/main.dart" class ArticleModel { // Properties and methods will be added here. @@ -57,15 +88,22 @@ class ArticleModel { ### Build the HTTP request +### 构建 HTTP 请求 + Wikipedia provides a REST API that returns JSON data about articles. For this app, you'll use the endpoint that returns a random article summary. +Wikipedia 提供了一个 REST API,返回有关文章的 JSON 数据。 +在本应用中,你将使用返回随机文章摘要的端点。 + ```text https://en.wikipedia.org/api/rest_v1/page/random/summary ``` Add a method to fetch a random Wikipedia article summary: +添加一个方法来获取随机的 Wikipedia 文章摘要: + ```dart class ArticleModel { Future getRandomArticleSummary() async { @@ -84,21 +122,36 @@ Use the [`async` and `await`][] keywords to handle asynchronous operations. The `async` keyword marks a method as asynchronous, and `await` waits for expressions that return a [`Future`][]. +使用 [`async` 和 `await`][`async` and `await`] 关键字来处理异步操作。 +`async` 关键字将方法标记为异步方法,而 +`await` 则等待返回 [`Future`][] 的表达式完成。 + The `Uri.https` constructor safely builds URLs by handling encoding and formatting. This approach is more reliable than string concatenation, especially when dealing with special characters or query parameters. +`Uri.https` 构造函数通过处理编码和格式化来安全地构建 URL。 +与字符串拼接相比,这种方式更可靠, +尤其是在处理特殊字符或查询参数时。 + [`async` and `await`]: {{site.dart-site}}/language/async [`Future`]: {{site.api}}/flutter/dart-async/Future-class.html ### Handle network errors +### 处理网络错误 + Always handle errors when making HTTP requests. A status code of **200** indicates success, while other codes indicate errors. If the status code isn't **200**, the model throws an error for the UI to display to users. +发起 HTTP 请求时务必处理错误。 +状态码 **200** 表示成功,其他状态码则表示出错。 +如果状态码不是 **200**,Model 会抛出一个错误, +以便 UI 向用户展示。 + ```dart class ArticleModel { Future getRandomArticleSummary() async { @@ -119,10 +172,16 @@ class ArticleModel { ### Parse JSON from Wikipedia +### 解析来自 Wikipedia 的 JSON + The [Wikipedia API][] returns [JSON][] data that you decode into a `Summary` class Complete the `getRandomArticleSummary` method: +[Wikipedia API][] 返回 [JSON][] 数据, +你需要将其解码为一个 `Summary` 类。 +完成 `getRandomArticleSummary` 方法: + ```dart class ArticleModel { Future getRandomArticleSummary() async { @@ -145,12 +204,18 @@ The `Summary` class is defined in `summary.dart`. If you're unfamiliar with JSON parsing, check out the [Getting started with Dart][] tutorial. +`Summary` 类定义在 `summary.dart` 中。 +如果你对 JSON 解析不太熟悉, +请参阅 [Dart 入门][Getting started with Dart] 教程。 + [Wikipedia API]: https://en.wikipedia.org/api/rest_v1/ [JSON]: {{site.dart-site}}/tutorial/json [Getting started with Dart]: {{site.dart-site}}/tutorial ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -182,6 +247,8 @@ items: ### Test yourself +### 自测 + - question: "What do the `async` and `await` keywords do in Dart?" options: diff --git a/src/content/learn/pathway/tutorial/implicit-animations.md b/src/content/learn/pathway/tutorial/implicit-animations.md index 0c75d72a2a..498fdcc4dd 100644 --- a/src/content/learn/pathway/tutorial/implicit-animations.md +++ b/src/content/learn/pathway/tutorial/implicit-animations.md @@ -1,6 +1,8 @@ --- -title: Simple animations -description: Learn the simplest way to implement animations in Flutter. +# title: Simple animations +title: 简单动画 +# description: Learn the simplest way to implement animations in Flutter. +description: 学习在 Flutter 中实现动画的最简单方式。 layout: tutorial --- @@ -10,6 +12,10 @@ start using them is with **implicit animations**. automatically animate changes to their properties without you needing to manage any intermediate behavior. +Flutter 提供了丰富的动画 API,而使用它们最简单的方式就是**隐式动画**。 +"隐式动画"是指一组能够自动对属性变化进行动画处理的 widget, +你无需手动管理任何中间状态行为。 + title: What you'll accomplish items: @@ -26,19 +32,31 @@ versatile implicit animation widgets: [`AnimatedContainer`][]. With just two additional lines of code, the background color of each `Tile` animates to a new color in about half a second. +在本课中,你将学习最常用且最灵活的隐式动画 widget 之一:[`AnimatedContainer`][]。 +只需额外添加两行代码,每个 `Tile` 的背景颜色就能在大约半秒内平滑地过渡到新颜色。 + [`AnimatedContainer`]: {{site.api}}/flutter/widgets/AnimatedContainer-class.html --- ### Convert `Container` to `AnimatedContainer` +### 将 `Container` 转换为 `AnimatedContainer` + Currently, the `Tile.build` method returns a `Container` to display a letter. When the `hitType` changes, like from `HitType.none` to `HitType.hit`, the background color of the tile changes instantly. For example, from white to green in the case of `HitType.none` to `HitType.hit`. +目前,`Tile.build` 方法返回一个 `Container` 来显示字母。 +当 `hitType` 发生变化时,例如从 `HitType.none` 变为 `HitType.hit`, +方块的背景颜色会瞬间改变。 +比如从 `HitType.none` 到 `HitType.hit` 时,颜色会从白色直接变为绿色。 + For reference, here's the current implementation of the `Tile` widget: +以下是 `Tile` widget 的当前实现,供参考: + ```dart class Tile extends StatelessWidget { const Tile(required this.letter, required hitType, {super.key}); @@ -74,6 +92,9 @@ class Tile extends StatelessWidget { To make the color change animate smoothly, replace the `Container` widget with an `AnimatedContainer`. +要让颜色变化平滑过渡, +请将 `Container` widget 替换为 `AnimatedContainer`。 + An `AnimatedContainer` is like a `Container`, but it automatically animates changes to its properties over a specified `duration`. When properties such as @@ -81,8 +102,15 @@ When properties such as `AnimatedContainer` interpolates between the old and new values, creating a smooth transition. +`AnimatedContainer` 类似于 `Container`,但它会在指定的 `duration` 内 +自动对属性变化进行动画处理。 +当 `color`、`height`、`width`、`decoration` 或 `alignment` 等属性发生变化时, +`AnimatedContainer` 会在新旧值之间进行插值,从而实现平滑过渡。 + Modify your `Tile` widget as follows: +按如下方式修改你的 `Tile` widget: + ```dart class Tile extends StatelessWidget { const Tile(required this.letter, required hitType, {super.key}); @@ -122,25 +150,43 @@ In this example, passing `Duration(milliseconds: 500)` means the color transition will take half a second. You can also specify seconds, minutes, and many other units of time. +**`duration`** 是一个必需属性,用于指定动画的持续时间。 +在本例中,传入 `Duration(milliseconds: 500)` 表示颜色过渡将持续半秒。 +你还可以使用秒、分钟等其他时间单位。 + Now, when the `hitType` changes and the `Tile` widget rebuilds (because `setState` was called in `GamePage`), the color of the tile smoothly animates from its old color to the new one over the specified duration. +现在,当 `hitType` 发生变化并且 `Tile` widget 重建时 +(因为在 `GamePage` 中调用了 setState), +方块的颜色会在指定的持续时间内从旧颜色平滑过渡到新颜色。 + ### Adjust the animation curve +### 调整动画曲线 + To add a bit of customization to an implicit animation, you can pass it a different [`Curve`][]. Different curves change the speed of the animation at different points throughout the animation. +要为隐式动画添加一些自定义效果, +你可以传入不同的 [`Curve`][](曲线)。 +不同的曲线会改变动画在各个阶段的速度变化。 + For example, the default curve for Flutter animations is `Curves.linear`. This gif shows how the animation curve behaves: +例如,Flutter 动画的默认曲线是 `Curves.linear`。下面的 gif 展示了该动画曲线的效果: + A gif that shows a linear curve. Compare that to `Curve.bounceIn`, another common curve: +与另一种常见曲线 `Curve.bounceIn` 对比一下: + A gif that shows a bounce-in curve @@ -148,6 +194,8 @@ alt="A gif that shows a bounce-in curve"> To change the `Curve` of this animation, update the code to the following: +要更改此动画的 `Curve`,请将代码更新为如下所示: + ```dart class Tile extends StatelessWidget { const Tile(required this.letter, required hitType, {super.key}); @@ -185,18 +233,30 @@ class Tile extends StatelessWidget { There are many different curves provided by the Flutter SDK, so feel free to try them out by passing different types to the `curve` parameter. +Flutter SDK 提供了许多不同的曲线, +你可以通过向 `curve` 参数传入不同的类型来自由尝试。 + Implicit animations like `AnimatedContainer` are powerful because you just tell the widget what the new state should be, and it handles the "how" of the animation. +像 `AnimatedContainer` 这样的隐式动画功能强大, +因为你只需告诉 widget 新状态是什么, +它就会自动处理动画的"过程"。 + For complex, custom animations, you can write your own animated widgets. If you're curious, try it out in the [animations tutorial][]. +对于复杂的自定义动画,你可以编写自己的动画 widget。 +如果感兴趣,可以在 [animations tutorial][](动画教程)中尝试。 + [`Curve`]: {{site.api}}/flutter/animation/Curves-class.html [animations tutorial]: /ui/animations/tutorial ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -232,6 +292,8 @@ items: ### Test yourself +### 自测 + - question: What widget can you use to automatically animate changes to properties like color, size, and decoration? options: diff --git a/src/content/learn/pathway/tutorial/index.md b/src/content/learn/pathway/tutorial/index.md index 7fb928bd3b..456e3983db 100644 --- a/src/content/learn/pathway/tutorial/index.md +++ b/src/content/learn/pathway/tutorial/index.md @@ -1,9 +1,13 @@ --- -title: Flutter tutorial -shortTitle: Tutorial +# title: Flutter tutorial +title: Flutter 教程 +# shortTitle: Tutorial +shortTitle: 教程 +# description: >- +# Learn how to use Flutter to build pixel-perfect applications from scratch that +# run on mobile, desktop, and web. description: >- - Learn how to use Flutter to build pixel-perfect applications from scratch that - run on mobile, desktop, and web. + 学习如何使用 Flutter 从零开始构建可在移动端、桌面端和 Web 端运行的精美应用。 layout: tutorial --- @@ -13,18 +17,30 @@ layout: tutorial Welcome to the Flutter tutorial! This tutorial teaches you how to build applications from scratch that run on mobile, desktop, and web. +欢迎来到 Flutter 教程!本教程将教你如何从零开始构建可在移动端、桌面端和 Web 端运行的应用。 + You'll start from the very beginning: creating a blank Flutter application. By the end, you'll have built a handful of small apps that demonstrate the critical features of Flutter development (and more!) +你将从最基础的地方起步:创建一个空白的 Flutter 应用。在教程结束时,你将完成若干个小型应用,它们将展示 Flutter 开发中的核心特性(以及更多内容!) + ### Before you begin +### 开始之前 + This tutorial is the third step in the Flutter learning pathway, and therefor assumes that you have: 1. A Flutter environment set up 2. Basic understanding of the Dart programming language +本教程是 Flutter 学习路径的第三步,因此默认你已经具备以下条件: +1. 已搭建好 Flutter 开发环境 +2. 对 Dart 编程语言有基本的了解 + If either of those aren't true, please start at the [Learning pathway page](/learn/pathway). +如果你还不满足以上条件,请先从 [学习路径页面](/learn/pathway) 开始。 +
@@ -33,6 +49,8 @@ If either of those aren't true, please start at the [Learning pathway page](/lea ## Contents +## 目录 + -Start learning +开始学习 diff --git a/src/content/learn/pathway/tutorial/layout.md b/src/content/learn/pathway/tutorial/layout.md index b4b14e8b71..42d987b644 100644 --- a/src/content/learn/pathway/tutorial/layout.md +++ b/src/content/learn/pathway/tutorial/layout.md @@ -1,11 +1,15 @@ --- -title: Layout -description: Learn about common layout widgets in Flutter. +# title: Layout +title: 布局 +# description: Learn about common layout widgets in Flutter. +description: 学习 Flutter 中常用的布局 widget。 layout: tutorial --- Learn how to build layouts with common widgets like Scaffold, AppBar, Column, and Row. +学习如何使用 Scaffold、AppBar、Column 和 Row 等常用 widget 构建布局。 + @@ -25,9 +29,14 @@ items: ### Introduction +### 简介 + Given that Flutter is a UI toolkit, you'll spend a lot of time creating layouts with Flutter widgets. +鉴于 Flutter 是一个 UI 工具包, +你将花费大量时间使用 Flutter widget 来创建布局。 + In this section, you'll learn how to build layouts with some of the most common layout widgets. This includes high-level widgets like @@ -35,6 +44,12 @@ This includes high-level widgets like as well as lower-level widgets like [`Column`][] or [`Row`][] that lay out widgets vertically or horizontally. +在本节中,你将学习如何使用一些最常用的布局 widget 来构建布局。 +这包括像 [`Scaffold`][] 和 [`AppBar`][] 这样的高级 widget, +它们用于构建页面的整体结构, +以及像 [`Column`][] 或 [`Row`][] 这样的低级 widget, +用于在垂直或水平方向上排列 widget。 + [`Scaffold`]: {{site.api}}/flutter/material/Scaffold-class.html [`AppBar`]: {{site.api}}/flutter/material/AppBar-class.html [`Column`]: {{site.api}}/flutter/widgets/Column-class.html @@ -45,23 +60,41 @@ lay out widgets vertically or horizontally. Mobile applications often have a bar at the top called an "app bar" that can display a title, navigation controls, and/or actions. +移动应用程序的顶部通常有一个称为 "app bar" 的栏, +可以显示标题、导航控件和/或操作按钮。 + A screenshot of a simple application with a bar across the top that has a title and settings button. The simplest way to add an app bar to your app is by using two widgets: `Scaffold` and `AppBar`. +向应用添加 app bar 的最简单方式是使用两个 widget: +`Scaffold` 和 `AppBar`。 + `Scaffold` is a convenience widget that provides a Material-style page layout, making it simple to add an app bar, drawer, navigation bar, and more to a page of your app. `AppBar` is, of course, the app bar. +`Scaffold` 是一个便捷 widget,它提供了 Material 风格的页面布局, +使你可以轻松地为应用页面添加 app bar、抽屉、导航栏等。 +`AppBar` 当然就是 app bar。 + The code generated from the `flutter create --empty` command already contains an `AppBar` widget and a `Scaffold` widget. The following code updates it to use an additional layout widget: [`Align`][]. This positions the title to the left, which would be centered by default. The `Text` widget contains the title itself. +通过 `flutter create --empty` 命令生成的代码 +已经包含了一个 `AppBar` widget 和一个 `Scaffold` widget。 +以下代码将其更新为使用一个额外的布局 widget:[`Align`][]。 +它将标题定位到左侧,默认情况下标题是居中的。 +`Text` widget 包含标题本身。 + Modify the `Scaffold` within your `MainApp` widget's `build` method: +修改 `MainApp` widget 的 `build` 方法中的 `Scaffold`: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -87,19 +120,31 @@ class MainApp extends StatelessWidget { #### An updated widget tree +#### 更新后的 widget 树 + Considering your app's widget tree gets more important as your app grows. At this point, there's a "branch" in the widget tree for the first time, and it now looks like the following figure: +随着应用的增长,关注应用的 widget 树变得越来越重要。 +此时,widget 树中首次出现了"分支", +现在它看起来如下图所示: + A screenshot that resembles the popular game Wordle. ### Create a widget for the game page layout +### 为游戏页面布局创建一个 widget + Add the following code for a new widget, called `GamePage`, to your `main.dart` file. This widget will eventually display the UI elements needed for the game itself. +将以下代码添加到你的 `main.dart` 文件中, +创建一个名为 `GamePage` 的新 widget。 +这个 widget 最终将显示游戏本身所需的 UI 元素。 + ```dart title="lib/main.dart" class GamePage extends StatelessWidget { GamePage({super.key}); @@ -117,8 +162,12 @@ class GamePage extends StatelessWidget { :::note Challenge - Display the `GamePage` rather than a `Tile`. +挑战 - 显示 `GamePage` 而不是 `Tile`。 + **Solution:** +**解决方案:** + ```dart title="solution.dart" collapsed class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -139,8 +188,12 @@ class MainApp extends StatelessWidget { ### Arrange widgets with `Column` and `Row` +### 使用 `Column` 和 `Row` 排列 widget + The `GamePage` layout contains the grid of tiles that display a user's guesses. +`GamePage` 的布局包含显示用户猜测的 tile 网格。 + A screenshot that resembles the popular game Wordle. There are a number of ways you can build this layout. @@ -150,9 +203,19 @@ with five rows total. So you'll need a single `Column` with five `Row` widgets as children, where each row contains five children. +有很多种方式可以构建这个布局。 +最简单的方式是使用 `Column` 和 `Row` widget。 +每行包含五个 tile,代表一次猜测中的五个字母, +总共五行。 +因此你需要一个 `Column`,其 children 为五个 `Row` widget, +每个 `Row` 包含五个 children。 + To get started, replace the `Container` in `GamePage.build` with a `Padding` widget with a `Column` widget as its child: +首先,将 `GamePage.build` 中的 `Container` 替换为 +一个 `Padding` widget,并以 `Column` widget 作为其 child: + ```dart class GamePage extends StatelessWidget { GamePage({super.key}); @@ -176,14 +239,24 @@ class GamePage extends StatelessWidget { The `spacing` property puts five pixels between each element on the main axis. +`spacing` 属性在主轴上的每个元素之间放置 5 像素的间距。 + Within `Column.children`, for each element in the `_game.guesses` list, add a `Row` widget as a child. +在 `Column.children` 中,为 `_game.guesses` 列表中的每个元素 +添加一个 `Row` widget 作为 child。 + :::note This `guesses` list is a **fixed-size** list, starting with five elements, one for each *potential* guess. The list will always contain exactly five elements, and therefore will always render five rows. + +这个 `guesses` 列表是一个**固定大小**的列表,初始包含五个元素, +每个元素对应一次*可能的*猜测。 +该列表将始终包含恰好五个元素, +因此将始终渲染五行。 ::: ```dart @@ -220,6 +293,11 @@ This syntactic sugar makes it easier for you to work with collections of widgets, providing a declarative alternative to the following: +`children` 列表中的 `for` 循环称为 [collection for 元素][collection for element], +这是一种 Dart 语法,允许你在运行时构建集合时迭代地向其中添加元素。 +这种语法糖使你更容易处理 widget 集合, +提供了以下写法的声明式替代方案: + ```dart [..._game.guesses.map((guess) => Row(/* ... */))], ``` @@ -227,13 +305,21 @@ providing a declarative alternative to the following: In this case, it adds five `Row` widgets to the column, one for each guess on the `Game` object. +在这种情况下,它向 column 中添加了五个 `Row` widget, +对应 `Game` 对象上的每个猜测。 + [collection for element]: {{site.dart-site}}/language/collections#for-element #### An updated widget tree +#### 更新后的 widget 树 + The widget tree for this app has expanded significantly in this lesson. Now, it looks more like the following (abridged) figure: +在本课中,这个应用的 widget 树已经显著扩展。 +现在,它看起来更像下面这个(简化的)图示: + A diagram showing a tree like structure with a node for each widget in the app. :::note Challenge @@ -242,8 +328,14 @@ Add a `Tile` to each row for each letter allowed in the guess. The `guess` variable in the loop is a [record][] with the type `({String char, HitType type})`. +为每一行中猜测允许的每个字母添加一个 `Tile`。 +循环中的 `guess` 变量是一个类型为 +`({String char, HitType type})` 的 [record][]。 + **Solution:** +**解决方案:** + ```dart class GamePage extends StatelessWidget { GamePage({super.key}); @@ -275,12 +367,16 @@ class GamePage extends StatelessWidget { When you reload your app, you should see a 5x5 grid of white squares. +当你热重载应用时,你应该会看到一个 5x5 的白色方块网格。 + A screenshot that resembles the popular game Wordle. [record]: {{site.dart-site}}/language/records ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -315,6 +411,8 @@ items: ### Test yourself +### 自测 + - question: What is the primary difference between a Column and a Row widget? options: diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 8da714f918..17157e3a2a 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -1,11 +1,15 @@ --- -title: Rebuild UI when state changes -description: Instructions on how to manage state with ChangeNotifiers. +#title: Rebuild UI when state changes +title: 当状态变化时重建 UI +#description: Instructions on how to manage state with ChangeNotifiers. +description: 关于如何使用 ChangeNotifier 管理状态的说明。 layout: tutorial --- Learn to use ListenableBuilder to automatically rebuild UI and handle all possible states with switch expressions. +学习使用 ListenableBuilder 自动重建 UI,并通过 switch 表达式处理所有可能的状态。 + title: What you'll accomplish items: @@ -21,6 +25,8 @@ items: ### Introduction +### 简介 + The view layer is your UI, and in Flutter, that refers to your app's widgets. As it pertains to this tutorial, the important part is @@ -29,15 +35,29 @@ wiring up your UI to respond to data changes from the ViewModel. [`ChangeNotifier`][], and automatically rebuilds when it's provided `ChangeNotifier` calls `notifyListeners()`. +视图层就是你的 UI,在 Flutter 中, +它指的是应用的 widget。 +在本教程中,重要的部分是 +将 UI 与 ViewModel 的数据变化连接起来。 +[`ListenableBuilder`][] 是一个可以"监听" +[`ChangeNotifier`][] 的 widget,当它所绑定的 +`ChangeNotifier` 调用 `notifyListeners()` 时会自动重建。 + [`ListenableBuilder`]: {{site.api}}/flutter/widgets/ListenableBuilder-class.html [`ChangeNotifier`]: {{site.api}}/flutter/foundation/ChangeNotifier-class.html ### Create the article view widget +### 创建文章视图 widget + Create the `ArticleView` widget that manages the overall page layout and state handling. Start with the basic class structure and widgets: +创建 `ArticleView` widget, +用于管理整体页面布局和状态处理。 +首先从基本的类结构和 widget 开始: + ```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -58,8 +78,12 @@ class ArticleView extends StatelessWidget { ### Create the article view model +### 创建文章视图模型 + Create the `ArticleViewModel` in this widget: +在这个 widget 中创建 `ArticleViewModel`: + ```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -82,10 +106,16 @@ class ArticleView extends StatelessWidget { ### Listen for state changes +### 监听状态变化 + Wrap your UI in a [`ListenableBuilder`][] to listen for state changes, and pass it a `ChangeNotifier` object. In this case, the `ArticleViewModel` extends `ChangeNotifier`. +将你的 UI 包裹在 [`ListenableBuilder`][] 中来监听状态变化, +并传入一个 `ChangeNotifier` 对象。 +在本例中,`ArticleViewModel` 继承自 `ChangeNotifier`。 + ```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -116,13 +146,24 @@ These widgets are flexible because you can perform operations within the callback, building different widgets based on the state. +`ListenableBuilder` 使用了 *builder* 模式, +它需要一个回调函数而非 `child` widget 来 +构建其下方的 widget 树。 +这类 widget 非常灵活,因为你可以 +在回调中执行操作, +根据不同的状态构建不同的 widget。 + [`ListenableBuilder`]: {{site.api}}/flutter/widgets/ListenableBuilder-class.html ### Handle possible view model states +### 处理视图模型的可能状态 + Recall the `ArticleViewModel`, which has three properties that the UI is interested in: +回顾一下 `ArticleViewModel`,它有三个 UI 关心的属性: + - `Summary? summary` - `bool loading` - `String? errorMessage` @@ -132,6 +173,11 @@ the UI can display different widgets. Use Dart's support for [switch expressions][] to handle all possible combinations in a clean, readable way: +根据这些属性的组合状态, +UI 可以展示不同的 widget。 +使用 Dart 的 [switch 表达式][switch expressions] +以简洁、可读的方式处理所有可能的组合: + ```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -179,16 +225,30 @@ a state changes demands it, but it doesn't manage any state or the process of updating itself. The business logic and rendering are completely separate from each other. +这是一个绝佳的例子,展示了 +Flutter 这样的声明式响应框架与 +MVVM 模式如何协同工作: +UI 基于状态进行渲染,并在状态变化需要时自动更新, +但它本身不管理任何状态,也不负责更新自身的过程。 +业务逻辑和渲染彼此完全分离。 + [switch expressions]: {{site.dart-site}}/language/branches#switch-expressions ### Complete the UI +### 完成 UI + The only thing remaining is to use the properties and methods provided by the view model to build the UI. +剩下要做的就是使用视图模型提供的属性和方法来构建 UI。 + Now create a `ArticlePage` widget that displays the actual article content. This reusable widget takes summary data and a callback function: +现在创建一个 `ArticlePage` widget 来展示实际的文章内容。 +这个可复用的 widget 接收摘要数据和一个回调函数: + ```dart class ArticlePage extends StatelessWidget { const ArticlePage({ @@ -209,8 +269,12 @@ class ArticlePage extends StatelessWidget { ### Add a scrollable layout +### 添加可滚动布局 + Replace the placeholder with a scrollable column layout: +将占位内容替换为可滚动的列布局: + ```dart class ArticlePage extends StatelessWidget { const ArticlePage({ @@ -237,8 +301,12 @@ class ArticlePage extends StatelessWidget { ### Add article content and button +### 添加文章内容和按钮 + Complete the layout with an article widget and navigation button: +使用文章 widget 和导航按钮完成布局: + ```dart class ArticlePage extends StatelessWidget { const ArticlePage({ @@ -271,13 +339,21 @@ class ArticlePage extends StatelessWidget { ### Create the `ArticleWidget` +### 创建 `ArticleWidget` + The `ArticleWidget` handles the display of the actual article content with proper styling and conditional rendering. +`ArticleWidget` 负责以合适的样式和条件渲染来展示实际的文章内容。 + #### Set up the basic article structure +#### 搭建基本的文章结构 + Start with the widget that accepts a `summary` parameter: +从接收 `summary` 参数的 widget 开始: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -293,8 +369,12 @@ class ArticleWidget extends StatelessWidget { #### Add padding and column layout +#### 添加内边距和列布局 + Wrap the content in proper padding and layout: +用合适的内边距和布局包裹内容: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -318,8 +398,12 @@ class ArticleWidget extends StatelessWidget { #### Add conditional image display +#### 添加条件图片展示 + Add the article image that only shows when available: +添加仅在可用时才显示的文章图片: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -347,9 +431,14 @@ class ArticleWidget extends StatelessWidget { #### Complete with styled text content +#### 完成带样式的文本内容 + Replace the placeholder text with a properly styled title, description, and extract: +将占位文本替换为 +带有合适样式的标题、描述和摘要: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -390,22 +479,39 @@ class ArticleWidget extends StatelessWidget { This widget demonstrates a few important UI concepts: +这个 widget 展示了几个重要的 UI 概念: + - **Conditional rendering**: The `if` statements show content only when available. + **条件渲染**: + `if` 语句仅在内容可用时才显示。 - **Text styling**: Different text styles create visual hierarchy using Flutter's theme system. + **文本样式**: + 不同的文本样式利用 Flutter 的主题系统创建视觉层次。 - **Proper spacing**: The `spacing` parameter provides consistent vertical spacing. + **合理间距**: + `spacing` 参数提供一致的垂直间距。 - **Overflow handling**: `TextOverflow.ellipsis` prevents text from breaking the layout. + **溢出处理**: + `TextOverflow.ellipsis` 防止文本破坏布局。 ### Update your app to include the article view +### 更新应用以包含文章视图 + Connect everything together by updating your `MainApp` to include your completed `ArticleView`. +通过更新 `MainApp` 来将所有内容连接在一起, +引入你已完成的 `ArticleView`。 + Replace your existing `MainApp` with this updated version: +将现有的 `MainApp` 替换为以下更新版本: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -422,22 +528,39 @@ class MainApp extends StatelessWidget { This change switches from the console-based test to the full UI experience with proper state management. +这个改动将应用从基于控制台的测试切换到具有完整状态管理的 UI 体验。 + ### Run the complete app +### 运行完整应用 + Hot reload your app one final time. You should now see: +最后一次热重载你的应用。你现在应该能看到: + 1. A loading spinner while the initial article loads. + 初始文章加载时显示一个加载旋转指示器。 1. The article's title, description, and summary extract. + 文章的标题、描述和摘要。 1. An image (if the article has one). + 一张图片(如果文章有的话)。 1. A button to load another random article. + 一个加载另一篇随机文章的按钮。 To see the reactive UI in action, click the **Next random article** button. The app shows a loading state, fetches new data, and updates the display automatically. +要查看响应式 UI 的实际效果, +请点击 **Next random article** 按钮。 +应用会显示加载状态,获取新数据, +并自动更新显示内容。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -476,6 +599,8 @@ items: ### Test yourself +### 自我测试 + - question: What is the purpose of ListenableBuilder in Flutter? options: diff --git a/src/content/learn/pathway/tutorial/navigation.md b/src/content/learn/pathway/tutorial/navigation.md index 7055fa4f4e..2c47853732 100644 --- a/src/content/learn/pathway/tutorial/navigation.md +++ b/src/content/learn/pathway/tutorial/navigation.md @@ -1,11 +1,15 @@ --- -title: Stack-based navigation -description: Learn how to navigate from one page to another in a Flutter app. +# title: Stack-based navigation +title: 基于栈的导航 +# description: Learn how to navigate from one page to another in a Flutter app. +description: 学习如何在 Flutter 应用中从一个页面导航到另一个页面。 layout: tutorial --- Learn to navigate between screens with Navigator.push and implement adaptive navigation patterns for different screen sizes. +学习使用 Navigator.push 在页面之间进行导航,并为不同屏幕尺寸实现自适应的导航模式。 + title: What you'll accomplish items: @@ -21,15 +25,25 @@ items: ### Introduction +### 简介 + Now that you understand slivers and scrolling, you can implement navigation between screens. In this lesson, you'll update the small-screen view such that when a contact group is tapped, it navigates to the contact list for that group. +现在你已经理解了 sliver 和滚动机制, +可以开始实现页面之间的导航了。 +在本课中, +你将更新小屏幕视图,使得当用户点击联系人分组时, +能够导航到该分组的联系人列表。 + First, revert changes in the adaptive layout widget so that it displays the `ContactGroupsPage` by default on small screens. +首先,还原自适应布局 widget 中的更改,使其在小屏幕上默认显示 `ContactGroupsPage`。 + ```dart title="lib/screens/adaptive_layout.dart" class _AdaptiveLayoutState extends State { int selectedListId = 0; @@ -59,14 +73,21 @@ class _AdaptiveLayoutState extends State { ### Add navigation to contact groups +### 为联系人分组添加导航 + The `ContactGroupsPage` already uses a `_ContactGroupsView` and provides it with a callback. That callback needs to be updated to navigate when a group is tapped, rather than printing the group to the console. +`ContactGroupsPage` 已经使用了 `_ContactGroupsView` 并为其提供了一个回调。 +该回调需要更新为在点击分组时执行导航,而不是将分组信息打印到控制台。 + Ensure that the `onListSelected` callback in `lib/screens/contact_groups.dart` is implemented as follows: +确保 `lib/screens/contact_groups.dart` 中的 `onListSelected` 回调实现如下: + ```dart title="lib/screens/contact_groups.dart" class ContactGroupsPage extends StatelessWidget { const ContactGroupsPage({super.key}); @@ -87,31 +108,53 @@ class ContactGroupsPage extends StatelessWidget { This small code block contains the most important new information on this page. +这段简短的代码包含了本页面最重要的新知识。 + `Navigator.of(context)` retrieves the nearest `Navigator` widget from the widget tree. The `push` method adds a new route to the navigator's stack, and displays the widget returned from the `builder` property. +`Navigator.of(context)` 从 widget 树中获取最近的 `Navigator` widget。 +`push` 方法会向 Navigator 的栈中添加一个新路由, +并显示 `builder` 属性返回的 widget。 + This is the most basic implementation of using stack-based navigation, where new screens are pushed on top of the current screen. To navigate back to the previous screen, you'd use the `Navigator.pop` method. +这是基于栈的导航最基本的实现方式, +新页面被 push 到当前页面之上。 +要返回上一个页面,可以使用 `Navigator.pop` 方法。 + `CupertinoPageRoute` creates iOS-style page transitions with the following features: +`CupertinoPageRoute` 创建具有以下特性的 iOS 风格页面过渡效果: + - A slide-in animation from the right. + 从右侧滑入的动画。 - Automatic back button support. + 自动支持返回按钮。 - Proper title handling. + 正确的标题处理。 - Swipe-to-go-back gesture support. + 支持滑动返回手势。 ### Create the sidebar component for large screens +### 为大屏幕创建侧边栏组件 + For large screens, you need a sidebar that doesn't navigate but instead updates the main content area. Thanks to the refactoring in the previous step, creating this component is more straightforward. Add this widget to the bottom of `lib/screens/contact_groups.dart`: +对于大屏幕,你需要一个不进行导航而是更新主内容区域的侧边栏。 +得益于上一步的重构,创建这个组件会更加简单。 +将以下 widget 添加到 `lib/screens/contact_groups.dart` 文件底部: + ```dart title="lib/screens/contact_groups.dart" // ... @@ -142,13 +185,24 @@ it calls `onListSelected` with the ID of the tapped list. It also passes the `selectedListId` to `_ContactGroupsView` so that the selected item can be highlighted. +这个侧边栏组件复用了 `_ContactGroupsView` 并提供了不同的回调。 +它不会进行导航,而是使用被点击列表的 ID 调用 `onListSelected`。 +同时它还将 `selectedListId` 传递给 `_ContactGroupsView`, +以便高亮显示选中的项目。 + ### Create the detail view for large screens +### 为大屏幕创建详情视图 + For the large screen layout, you need a detail view that doesn't show navigation controls. Just like the sidebar, this can be recreated by reusing the `_ContactListView`. Add this widget to the bottom of your `contacts.dart` file: +对于大屏幕布局,你需要一个不显示导航控件的详情视图。 +与侧边栏类似,可以通过复用 `_ContactListView` 来创建。 +将以下 widget 添加到 `contacts.dart` 文件底部: + ```dart title="lib/screens/contacts.dart" // ... @@ -172,12 +226,21 @@ The detail view reuses `_ContactListView` and sets the `automaticallyImplyLeading` parameter to `false` to hide the back button, as navigation is handled by the sidebar. +详情视图复用了 `_ContactListView`,并将 +`automaticallyImplyLeading` 参数设置为 `false` 以隐藏返回按钮, +因为导航由侧边栏处理。 + ### Connect the sidebar to the adaptive layout +### 将侧边栏连接到自适应布局 + Now, connect the sidebar to your adaptive layout. Update your `adaptive_layout.dart` file to import the necessary files and update the large screen layout: +现在,将侧边栏连接到你的自适应布局中。 +更新 `adaptive_layout.dart` 文件,导入必要的文件并更新大屏幕布局: + ```dart title="lib/screens/adaptive_layout.dart" import 'package:flutter/cupertino.dart'; import 'package:rolodex/screens/contact_groups.dart'; @@ -186,6 +249,8 @@ import 'package:rolodex/screens/contacts.dart'; Then update the `_buildLargeScreenLayout` method: +然后更新 `_buildLargeScreenLayout` 方法: + ```dart title="lib/screens/adaptive_layout.dart" Widget _buildLargeScreenLayout() { return CupertinoPageScaffold( @@ -217,28 +282,48 @@ Widget _buildLargeScreenLayout() { This code creates the classic menu-detail layout where the sidebar controls the content of the detail area. +这段代码创建了经典的菜单-详情布局,其中侧边栏控制详情区域的内容。 + ### Test the adaptive navigation behavior +### 测试自适应导航行为 + Hot reload your app and test the navigation: +热重载你的应用并测试导航功能: + **Small screens (<600px width):** +**小屏幕(宽度 <600px):** + - Tap contact groups to navigate to contact details. + 点击联系人分组以导航到联系人详情。 - Use the back button or a swipe gesture to return. + 使用返回按钮或滑动手势返回。 - This is a classic stack-based navigation flow. + 这是经典的基于栈的导航流程。 **Large screens (>600px width):** +**大屏幕(宽度 >600px):** + - Click contact groups in the sidebar to update the detail view. + 点击侧边栏中的联系人分组以更新详情视图。 - There is no navigation stack. The selection updates the content area. + 没有导航栈。选择操作会更新内容区域。 - This is a master-detail interface pattern. + 这是主从界面模式。 The app automatically chooses the appropriate navigation pattern based on screen size. This provides an optimal experience on both phones and tablets. +应用会根据屏幕尺寸自动选择合适的导航模式。 +这为手机和平板电脑都提供了最佳体验。 + ### Review +### 回顾 title: What you accomplished @@ -276,6 +361,8 @@ items: ### Test yourself +### 自测 + - question: "What does `Navigator.of(context).push` do?" options: diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 93e39ba4d2..0fc8e31bf1 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -1,11 +1,15 @@ --- -title: Set up your project -description: Instructions on how to create a new Flutter app. +#title: Set up your project +title: 设置你的项目 +#description: Instructions on how to create a new Flutter app. +description: 关于如何创建新 Flutter 应用的说明。 layout: tutorial --- Preview the Wikipedia reader app you'll build and set up the initial project with required packages. +预览你将构建的 Wikipedia 阅读器应用,并设置初始项目及所需的依赖包。 + title: What you'll accomplish items: @@ -21,10 +25,14 @@ items: ### Introduction +### 简介 + In the next few lessons, you'll learn how to work with data in a Flutter app. You'll build an app that fetches and displays article summaries from the [Wikipedia API][]. +在接下来的几节课中,你将学习如何在 Flutter 应用中处理数据。你将构建一个从 [Wikipedia API][] 获取并展示文章摘要的应用。 + A screenshot of the completed
 Wikipedia reader app showing an article with image, title,
@@ -32,16 +40,24 @@ description, and extract text. These lessons explore: +本系列课程将探索以下内容: + - Making HTTP requests in Flutter. + 在 Flutter 中发起 HTTP 请求。 - Managing application state with `ChangeNotifier`. + 使用 `ChangeNotifier` 管理应用状态。 - Using the MVVM architecture pattern. + 使用 MVVM 架构模式。 - Creating responsive user interfaces that update automatically when data changes. + 创建能在数据变化时自动更新的响应式用户界面。 This tutorial assumes you've completed the [Getting started with Dart][] and the [Introduction to Flutter UI][] tutorials, and therefore doesn't explain concepts like HTTP, JSON, or widget basics. +本教程假设你已完成 [Getting started with Dart][](Dart 入门)和 [Introduction to Flutter UI][](Flutter UI 入门)教程,因此不会再解释 HTTP、JSON 或 widget 基础等概念。 + :::recommend Support Wikipedia [Wikipedia][] is a valuable resource, providing free @@ -50,6 +66,8 @@ collaboratively by volunteers worldwide. Consider [donating to Wikipedia][] to help keep this incredible resource free and accessible to everyone. +[Wikipedia][] 是一个宝贵的资源,通过全球志愿者协作编写的数百万篇文章,免费提供人类知识。请考虑 [donating to Wikipedia][](向 Wikipedia 捐款),帮助这一卓越资源保持免费且人人可用。 + ::: [Wikipedia API]: https://en.wikipedia.org/api/rest_v1/ @@ -60,10 +78,14 @@ free and accessible to everyone. ### Create a new Flutter project +### 创建新的 Flutter 项目 + Create a new Flutter project using the [Flutter CLI][]. In your preferred terminal, run the following command to create a minimal Flutter app: +使用 [Flutter CLI][] 创建一个新的 Flutter 项目。在你常用的终端中,运行以下命令来创建一个精简的 Flutter 应用: + ```console $ flutter create wikipedia_reader --empty ``` @@ -72,9 +94,13 @@ $ flutter create wikipedia_reader --empty ### Add required dependencies +### 添加所需的依赖 + Your app needs the [`http` package][] to make HTTP requests. Add it to your project: +你的应用需要 [`http` package][](`http` 包)来发起 HTTP 请求。将它添加到你的项目中: + ```console $ cd wikipedia_reader && flutter pub add http ``` @@ -83,11 +109,15 @@ $ cd wikipedia_reader && flutter pub add http ### Examine the starter code +### 检查初始代码 + First, create a new file `lib/summary.dart` to define the data model for Wikipedia article summaries. This file has no special logic, and is simply a collection of classes that represent the data returned by the Wikipedia API. Its sufficient to copy the code below into the file and then ignore it. If you aren't comfortable basic Dart classes, you should read the [Dart Getting Started][] tutorial first. +首先,创建一个新文件 `lib/summary.dart` 来定义 Wikipedia 文章摘要的数据模型。这个文件没有特殊逻辑,只是一组表示 Wikipedia API 返回数据的类。将下面的代码复制到文件中即可,之后无需再关注它。如果你对基本的 Dart 类还不够熟悉,建议先阅读 [Dart Getting Started][](Dart 入门)教程。 + ```dart title="lib/summary.dart" collapsed class Summary { /// Returns a new [Summary] instance. @@ -380,6 +410,8 @@ const acceptableImageFormats = ['png', 'jpg', 'jpeg']; Then, open `lib/main.dart` and replace the existing code with this basic structure, which adds required imports that the app uses: +然后,打开 `lib/main.dart`,用以下基本结构替换现有代码,其中包含了应用所需的导入: + ```dart title="lib/main.dart" import 'dart:convert'; import 'dart:io'; @@ -417,12 +449,18 @@ a title bar and placeholder content. The imports at the top include everything you need for HTTP requests, JSON parsing, and the Wikipedia data model. +这段代码提供了一个基本的应用结构,包含标题栏和占位内容。顶部的导入语句涵盖了 HTTP 请求、JSON 解析和 Wikipedia 数据模型所需的一切。 + [Dart Getting Started]: {{site.dart-site}}/tutorial ### Run your app +### 运行你的应用 + Test that everything works by running your app: +运行你的应用来测试一切是否正常: + ```console $ flutter run -d chrome ``` @@ -430,8 +468,12 @@ $ flutter run -d chrome You should see a simple app with "Wikipedia Flutter" in the app bar and "Loading..." in the center of the screen. +你应该能看到一个简单的应用,应用栏中显示 "Wikipedia Flutter",屏幕中央显示 "Loading..."。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -460,6 +502,8 @@ items: ### Test yourself +### 自测 + - question: "What does the `--empty` flag do when running `flutter create`?" options: diff --git a/src/content/learn/pathway/tutorial/slivers.md b/src/content/learn/pathway/tutorial/slivers.md index 28a705048d..b69a30eab9 100644 --- a/src/content/learn/pathway/tutorial/slivers.md +++ b/src/content/learn/pathway/tutorial/slivers.md @@ -1,6 +1,8 @@ --- -title: Advanced scrolling and slivers -description: Learn how to implement performant scrolling with slivers. +#title: Advanced scrolling and slivers +title: 高级滚动与 sliver +#description: Learn how to implement performant scrolling with slivers. +description: 学习如何使用 sliver 实现高性能滚动。 layout: tutorial --- @@ -13,6 +15,13 @@ By the end of this section, you'll understand how to use `CustomScrollView`, create navigation bars that collapse, and organize content in scrollable sections. +在本课程中,你将学习 sliver—— +一种能够充分利用 Flutter 强大且可组合的滚动系统的特殊 widget。 +Sliver 使你能够创建复杂的滚动效果, +包括可折叠的头部、搜索集成以及自定义滚动行为。 +在本节结束时,你将了解如何使用 `CustomScrollView`、 +创建可折叠的导航栏,以及在可滚动区域中组织内容。 + title: What you'll accomplish items: @@ -30,39 +39,69 @@ items: ### Slivers and widgets +### Sliver 和 widget + Slivers are scrollable areas that can be composed together in a `CustomScrollView` or other scroll views. Think of slivers as building blocks that each contribute a portion of the overall scrollable content. +Sliver 是可以在 `CustomScrollView` 或其他滚动视图中组合使用的可滚动区域。 +可以将 sliver 理解为构建块,每个 sliver 都贡献了整体可滚动内容的一部分。 + While slivers and widgets are both fundamental Flutter concepts, they serve different purposes and aren't interchangeable. +虽然 sliver 和 widget 都是 Flutter 的基本概念, +但它们有着不同的用途,且不可互换。 + - **Widgets** are general UI building blocks that can be used anywhere in your widget tree. + **Widget** 是通用的 UI 构建块, + 可以在 widget 树的任何位置使用。 - **Slivers** are specialized widgets designed specifically for scrollable layouts and have some constraints: + **Sliver** 是专门为可滚动布局设计的特殊 widget, + 并且有一些约束: - Slivers can **only** be direct children of scroll views, such as `CustomScrollView` and `NestedScrollView`. + Sliver **只能**作为滚动视图的直接子级,例如 + `CustomScrollView` 和 `NestedScrollView`。 - Some scroll views **only** accept slivers as children. You can't pass regular widgets to `CustomScrollView.slivers`. + 某些滚动视图**只**接受 sliver 作为子级。 + 你不能将普通 widget 传递给 `CustomScrollView.slivers`。 - To use regular widgets within a sliver context, wrap them in `SliverToBoxAdapter` or `SliverFillRemaining`. + 要在 sliver 上下文中使用普通 widget, + 请将它们包裹在 `SliverToBoxAdapter` 或 `SliverFillRemaining` 中。 This architectural separation allows Flutter to optimize scrolling performance while it maintains clear boundaries between different types of UI components. +这种架构上的分离使 Flutter 能够优化滚动性能, +同时在不同类型的 UI 组件之间保持清晰的边界。 + ### Add a basic sliver structure to contact groups +### 为联系人分组添加基本的 sliver 结构 + First, replace the placeholder content in your contact groups page. To avoid duplicating code between the phone layout and the tablet sidebar, you can create a private, reusable widget. +首先,替换联系人分组页面中的占位内容。 +为了避免在手机布局和平板侧边栏之间重复代码, +你可以创建一个私有的、可复用的 widget。 + Update `lib/screens/contact_groups.dart` by adding `_ContactGroupsView` to the bottom of the file. +更新 `lib/screens/contact_groups.dart`, +在文件底部添加 `_ContactGroupsView`。 + ```dart title="lib/screens/contact_groups.dart" // New imports import 'package:rolodex/data/contact_group.dart'; @@ -118,21 +157,39 @@ displaying the list of contact groups. On small screens, it will be used as a page, and on large screens it will be used to fill the left column. +这个私有 widget 包含了用于显示联系人分组列表的共享 UI。 +在小屏幕上,它将作为一个页面使用; +在大屏幕上,它将用于填充左侧栏。 + This widget introduces several slivers: +这个 widget 引入了几种 sliver: + - `CupertinoSliverNavigationBar`: An opinionated navigation bar that collapses as the page scrolls. + `CupertinoSliverNavigationBar`: + 一个自带样式的导航栏,会随着页面滚动而折叠。 - `SliverList`: A scrollable list of items. + `SliverList`: + 一个可滚动的列表项。 - `SliverFillRemaining`: A sliver that takes up the remaining space in the scroll area, and whose child is a non-sliver widget. + `SliverFillRemaining`: + 一个占据滚动区域剩余空间的 sliver, + 其子级是非 sliver widget。 It accepts a callback function, `onListSelected`, to handle taps, which makes it adaptable for both navigation and sidebar selection. +它接受一个回调函数 `onListSelected` 来处理点击事件, +这使它既适用于导航,也适用于侧边栏选择。 + Now, update `ContactGroupsPage` to use your new `_ContactGroupsView` widget: +现在,更新 `ContactGroupsPage` 以使用新的 `_ContactGroupsView` widget: + ```dart title="lib/screens/contact_groups.dart" class ContactGroupsPage extends StatelessWidget { const ContactGroupsPage({super.key}); @@ -155,11 +212,20 @@ This structure keeps the `ContactGroupsPage` clean and focused on its primary responsibility: navigation, which you'll learn about in the next section of this tutorial. +这种结构使 `ContactGroupsPage` 保持简洁, +并专注于其主要职责:导航—— +你将在本教程的下一节中学习相关内容。 + ### Enhance the list with icons and visual elements +### 使用图标和视觉元素增强列表 + Now, add icons and contact counts to make the list more informative. Add this `_buildTrailing` helper method to your `_ContactGroupsView` class: +现在,添加图标和联系人计数,使列表更具信息量。 +将以下 `_buildTrailing` 辅助方法添加到 `_ContactGroupsView` 类中: + ```dart title="lib/screens/contact_groups.dart" // Inside _ContactGroupsView: @@ -185,10 +251,17 @@ Widget _buildTrailing(List contacts, BuildContext context) { This helper creates the trailing content for each list item. It shows the contact count and a forward arrow. +这个辅助方法为每个列表项创建尾部内容, +显示联系人数量和一个前进箭头。 + Now, update the `CupertinoListSection` in `_ContactGroupsView` to use icons and the trailing helper. Update the code within the `ListenableBuilder.builder` callback in the `build` method: +现在,更新 `_ContactGroupsView` 中的 `CupertinoListSection`, +使用图标和尾部辅助方法。更新 `build` 方法中 +`ListenableBuilder.builder` 回调内的代码: + ```dart title="lib/screens/contact_groups.dart" import 'package:flutter/cupertino.dart'; import 'package:rolodex/data/contact.dart'; @@ -276,15 +349,27 @@ The updated code now shows icons that differentiate between the main "All iPhone" group and user-created groups, along with contact counts and navigation indicators. +更新后的代码现在会显示图标来区分主要的"All iPhone"分组 +和用户创建的分组,同时还显示联系人数量和导航指示器。 + ### Create advanced scrolling for contacts +### 为联系人创建高级滚动效果 + Now, work on the contacts page. Just like before, you'll create a private, reusable view to avoid code duplication. +现在,开始处理联系人页面。和之前一样, +你将创建一个私有的、可复用的视图以避免代码重复。 + In the next lesson, you'll implement navigation for small screens. To see your progress on the contacts list page in the meantime, update `AdaptiveLayout` to display the contacts list page: +在下一课中,你将实现小屏幕的导航功能。 +在此期间,为了查看联系人列表页面的进展, +请更新 `AdaptiveLayout` 以显示联系人列表页面: + ```dart title="lib/screens/adaptive_layout.dart" class _AdaptiveLayoutState extends State { int selectedListId = 0; @@ -315,6 +400,9 @@ class _AdaptiveLayoutState extends State { Update `lib/screens/contacts.dart` by adding `_ContactListView` to the bottom of the file: +更新 `lib/screens/contacts.dart`, +在文件底部添加 `_ContactListView`: + ```dart title="lib/screens/contacts.dart" class _ContactListView extends StatelessWidget { const _ContactListView({ @@ -357,6 +445,8 @@ class _ContactListView extends StatelessWidget { Now, update `ContactListsPage` to use this view: +现在,更新 `ContactListsPage` 以使用此视图: + ```dart title="lib/screens/contacts.dart" import 'package:flutter/cupertino.dart'; import 'package:rolodex/data/contact_group.dart'; @@ -379,13 +469,22 @@ class ContactListsPage extends StatelessWidget { This basic implementation demonstrates how to use slivers with dynamic data in a reusable component. +这个基本实现展示了如何在可复用的组件中将 sliver 与动态数据结合使用。 + ### Add search integration with slivers +### 为 sliver 添加搜索集成 + Now, enhance the contacts page with integrated search functionality UI. Update the `CustomScrollView` in `_ContactListView` to use the `CupertinoSliverNavigationBar.search` constructor instead of the default `CupertinoSliverNavigationBar` constructor: +现在,通过集成搜索功能 UI 来增强联系人页面。 +更新 `_ContactListView` 中的 `CustomScrollView`,使用 +`CupertinoSliverNavigationBar.search` 构造函数代替 +默认的 `CupertinoSliverNavigationBar` 构造函数: + ```dart class _ContactListView extends StatelessWidget { const _ContactListView({ @@ -435,13 +534,24 @@ The `CupertinoSliverNavigationBar.search` constructor provides integrated search functionality. As you scroll down, the search field smoothly transitions into the collapsed navigation bar. +`CupertinoSliverNavigationBar.search` 构造函数提供了 +集成的搜索功能。当你向下滚动时, +搜索框会平滑地过渡到折叠的导航栏中。 + ### Create alphabetized contact sections +### 创建按字母排序的联系人分区 + Real-world contact apps organize contacts alphabetically. To do this, create sections for each letter. Add the following widget to the bottom of your `contacts.dart` file. This widget doesn't contain any slivers. +现实中的联系人应用会按字母顺序组织联系人。 +为此,需要为每个字母创建分区。 +将以下 widget 添加到 `contacts.dart` 文件的底部。 +这个 widget 不包含任何 sliver。 + ```dart title="lib/screens/contacts.dart" // ... @@ -496,11 +606,17 @@ class ContactListSection extends StatelessWidget { This widget creates the familiar alphabetized sections that you see in the iOS Contacts app. +这个 widget 创建了你在 iOS 通讯录应用中看到的熟悉的字母分区样式。 + ### Use `SliverList` for the alphabetized sections +### 使用 `SliverList` 展示按字母排序的分区 + Now, replace the placeholder content in `_ContactListView` with the alphabetized sections: +现在,用按字母排序的分区替换 `_ContactListView` 中的占位内容: + ```dart title="lib/screens/contacts.dart" // Inside _ContactListView's builder: @@ -535,12 +651,21 @@ return CustomScrollView( become part of the scrollable content. This is the simplest way to add a list of normal widgets to a scrollable sliver area. +`SliverList.list` 允许你提供一组 widget, +使其成为可滚动内容的一部分。这是将普通 widget 列表 +添加到可滚动 sliver 区域的最简单方式。 + In the next lesson, you'll learn about stack-based navigation and update the UI on small screens to navigate between the contacts list view and the contacts view. +在下一课中,你将学习基于栈的导航, +并更新小屏幕上的 UI,以便在联系人列表视图和联系人视图之间导航。 + ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -574,6 +699,8 @@ items: ### Test yourself +### 自测 + - question: What is the key difference between slivers and regular widgets? options: diff --git a/src/content/learn/pathway/tutorial/stateful-widget.md b/src/content/learn/pathway/tutorial/stateful-widget.md index f6e01b6240..1ac34350e9 100644 --- a/src/content/learn/pathway/tutorial/stateful-widget.md +++ b/src/content/learn/pathway/tutorial/stateful-widget.md @@ -1,11 +1,15 @@ --- -title: Stateful widgets -description: Learn about StatefulWidgets and rebuilding Flutter UI. +# title: Stateful widgets +title: 有状态 widget +# description: Learn about StatefulWidgets and rebuilding Flutter UI. +description: 了解 StatefulWidget 以及如何重新构建 Flutter UI。 layout: tutorial --- Learn when widgets need to be stateful and how to trigger UI updates with setState. +了解 widget 何时需要是有状态的,以及如何使用 setState 触发 UI 更新。 + @@ -23,23 +27,39 @@ items: ### Introduction +### 简介 + So far, your app displays a grid and an input field, but the grid doesn't yet update to reflect the user's guesses. When this app is complete, each tile in the next unfilled row should update after each submitted user guess by: +到目前为止,你的应用显示了一个网格和一个输入框, +但网格尚未更新以反映用户的猜测。 +当应用完成后,每次用户提交猜测时, +下一个未填充行中的每个方块应通过以下方式更新: + - Displaying the correct letter. + 显示正确的字母。 - Changing color to reflect whether the letter is correct (green), is in the word but at an incorrect position (yellow), or doesn't appear in the word at all (grey). + 改变颜色以反映该字母是否正确(绿色)、 + 在单词中但位置不对(黄色),还是 + 完全不在单词中(灰色)。 To handle this dynamic behavior, you need to convert `GamePage` from a `StatelessWidget` to a [`StatefulWidget`][]. +要处理这种动态行为,你需要将 `GamePage` 从 +`StatelessWidget` 转换为 [`StatefulWidget`][]。 + [`StatefulWidget`]: {{site.api}}/flutter/widgets/StatefulWidget-class.html ### Why stateful widgets? +### 为什么需要有状态 widget? + When a widget's appearance or data needs to change during its lifetime, you need a `StatefulWidget` and a companion `State` object. While the `StatefulWidget` itself is still immutable (its properties @@ -47,14 +67,26 @@ can't change after creation), the `State` object is long-lived, can hold mutable data, and can be rebuilt when that data changes, causing the UI to update. +当一个 widget 的外观或数据需要在其生命周期内发生变化时, +你需要一个 `StatefulWidget` 及其伴随的 `State` 对象。 +虽然 `StatefulWidget` 本身仍然是不可变的(其属性在创建后不能更改), +但 `State` 对象是长期存在的,可以持有可变的数据, +并且可以在数据变化时重新构建,从而使 UI 更新。 + For example, the following widget tree imagines a simple app that uses a stateful widget with a counter that increases when the button is pressed. +例如,下面的 widget 树展示了一个简单的应用, +它使用了一个有状态 widget,其中有一个 +在按下按钮时会增加的计数器。 + A diagram of a widget tree with a stateful widget and state object. Here is the basic `StatefulWidget` structure (doesn't do anything yet): +以下是基本的 `StatefulWidget` 结构(还不执行任何操作): + ```dart class ExampleWidget extends StatefulWidget { ExampleWidget({super.key}); @@ -73,16 +105,28 @@ class _ExampleWidgetState extends State { ### Convert `GamePage` to a stateful widget +### 将 `GamePage` 转换为有状态 widget + To convert the `GamePage` (or any other) widget from a stateless widget to a stateful widget, do the following steps: +要将 `GamePage`(或任何其他)widget 从 +无状态 widget 转换为有状态 widget,请执行以下步骤: + 1. Change `GamePage` to extend `StatefulWidget` instead of `StatelessWidget`. + 将 `GamePage` 改为继承 `StatefulWidget` 而不是 `StatelessWidget`。 1. Create a new class named `_GamePageState`, that extends `State`. This new class will hold the mutable state and the `build` method. Move the `build` method and all properties *instantiated on the widget* from `GamePage` to the state object. + 创建一个名为 `_GamePageState` 的新类,继承 `State`。 + 这个新类将持有可变的状态和 `build` 方法。 + 将 `build` 方法和所有*在 widget 上实例化的*属性 + 从 `GamePage` 移动到 State 对象中。 1. Implement the `createState()` method in `GamePage`, which returns an instance of `_GamePageState`. + 在 `GamePage` 中实现 createState() 方法, + 该方法返回一个 `_GamePageState` 实例。 :::tip Quick assists @@ -90,10 +134,15 @@ You don't have to manually do this work, as the Flutter plugins for VS Code and IntelliJ provide ["quick assists"][] that can do this conversion for you. +你不必手动完成这些工作,因为 VS Code 和 IntelliJ 的 Flutter 插件 +提供了["quick assists"][](快速辅助)功能,可以自动完成这个转换。 + ::: Your modified code should look like this: +修改后的代码应如下所示: + ```dart class GamePage extends StatefulWidget { GamePage({super.key}); @@ -139,29 +188,50 @@ class _GamePageState extends State { ### Updating the UI with `setState` +### 使用 `setState` 更新 UI + Whenever you mutate a `State` object, you must call [`setState`][] to signal the framework to update the user interface and call the `build` method again. +每当你修改 `State` 对象时, +必须调用 [`setState`][] 来通知框架 +更新用户界面并再次调用 `build` 方法。 + In this app, when a user makes a guess, the word they guessed is saved on the `Game` object, which is a property on the `GamePage` class, and therefore is state that might change and require the UI to update. When this state is mutated, the grid should be re-drawn to show the user's guess. +在这个应用中,当用户进行猜测时,他们猜测的单词会 +保存在 `Game` 对象上,该对象是 `GamePage` 类的一个属性, +因此它是可能会变化并需要 UI 更新的状态。 +当这个状态被修改时,网格应该被 +重新绘制以显示用户的猜测。 + To implement this, update the callback function passed to `GuessInput`. The function needs to call `setState` and, within `setState`, it needs to execute the logic to determine whether the users guess was correct. +要实现这一点,请更新传递给 `GuessInput` 的回调函数。 +该函数需要调用 setState,并在 setState 内部 +执行判断用户猜测是否正确的逻辑。 + :::note The game logic is abstracted away into the `Game` object, and outside the scope of this tutorial. +游戏逻辑被抽象到了 `Game` 对象中, +不在本教程的讨论范围内。 + ::: Update your code: +更新你的代码: + ```dart class GamePage extends StatefulWidget { GamePage({super.key}); @@ -210,10 +280,18 @@ If you were to call `_game.guess(guess)` *without* a calling `setState`, the internal game data would change, but Flutter wouldn't know it needs to repaint the screen, and the user wouldn't see any updates. +现在,当你在 `TextInput` 中输入一个合法的猜测并提交时, +应用将会反映用户的猜测。 +如果你调用 `_game.guess(guess)` 时*没有*调用 setState, +内部游戏数据会改变,但 Flutter 不会知道 +它需要重新绘制屏幕,用户也不会看到任何更新。 + [`setState`]: {{site.api}}/flutter/widgets/State/setState.html ### Review +### 回顾 + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -244,6 +322,8 @@ items: ### Test yourself +### 自测 + - question: When should you use a StatefulWidget instead of a StatelessWidget? options: diff --git a/src/content/learn/pathway/tutorial/user-input.md b/src/content/learn/pathway/tutorial/user-input.md index 1976189b82..611d48d8ec 100644 --- a/src/content/learn/pathway/tutorial/user-input.md +++ b/src/content/learn/pathway/tutorial/user-input.md @@ -1,11 +1,15 @@ --- -title: User input -description: Accept input from the user with buttons and text fields. +#title: User input +title: 用户输入 +#description: Accept input from the user with buttons and text fields. +description: 通过按钮和文本输入框接收用户输入。 layout: tutorial --- Learn to build text inputs, manage text with controllers, and handle user actions with buttons. +学习如何构建文本输入框、使用控制器管理文本,以及通过按钮处理用户操作。 + title: What you'll accomplish items: @@ -23,24 +27,41 @@ items: ### Introduction +### 简介 + The app will display the user's guesses in the `Tile` widgets, but it needs a way for the user to input those guesses. In this lesson, build that functionality with two interaction widgets: [`TextField`][] and [`IconButton`][]. +应用会在 `Tile` widget 中显示用户的猜测结果, +但还需要一种方式让用户输入这些猜测。 +在本课中,你将使用两个交互 widget 来构建该功能: +[`TextField`][] 和 [`IconButton`][]。 + [`TextField`]: {{site.api}}/flutter/material/TextField-class.html [`IconButton`]: {{site.api}}/flutter/material/IconButton-class.html ### Implement callback functions +### 实现回调函数 + To allow users to type in their guesses, you'll create a dedicated widget named `GuessInput`. First, create the basic structure for your `GuessInput` widget that requires a callback function as an argument. Name the callback function `onSubmitGuess`. +为了让用户能够输入猜测内容, +你需要创建一个名为 `GuessInput` 的专用 widget。 +首先,创建 `GuessInput` widget 的基本结构, +它需要一个回调函数作为参数。 +将该回调函数命名为 `onSubmitGuess`。 + Add the following code to your `main.dart` file. +将以下代码添加到你的 `main.dart` 文件中。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -61,26 +82,49 @@ that has the type `void Function(String)`. This function takes a single `String` argument (the user's guess) and doesn't return any value (denoted by `void`). +`final void Function(String) onSubmitGuess;` 这行代码 +声明了一个名为 `onSubmitGuess` 的 `final` 类成员, +其类型为 `void Function(String)`。 +该函数接受一个 `String` 参数(用户的猜测)并且 +不返回任何值(用 `void` 表示)。 + This callback tells us that the logic that actually handles the user's guess will be written elsewhere. It's a good practice for interactive widgets to use callback functions to keep the widget that handles interactions reusable and decoupled from any specific functionality. +这个回调告诉我们,实际处理用户猜测的逻辑将在其他地方编写。 +对于交互式 widget 来说,使用回调函数是一种好的实践, +它能让处理交互的 widget 保持可复用, +并与特定的业务逻辑解耦。 + By the end of this lesson, the passed-in `onSubmitGuess` function is called when a user enters a guess. First, you'll need to build the visual parts of this widget. This is what the widget will look like. +在本课结束时,当用户输入猜测后, +传入的 `onSubmitGuess` 函数将被调用。 +首先,你需要构建这个 widget 的可视化部分。 +下图展示了该 widget 的最终效果。 + A screenshot of the Flutter property editor tool. ### The `TextField` widget +### `TextField` widget + Given that the text field and button are displayed side-by-side, create them as a `Row` widget. Replace the `Container` placeholder in your `build` method with a `Row` containing an `Expanded` `TextField`: +由于文本输入框和按钮需要并排显示, +使用 `Row` widget 来创建它们。 +将 `build` 方法中的 `Container` 占位符替换为 +包含 `Expanded` `TextField` 的 `Row`: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -119,30 +163,53 @@ hasn't been taken by other children. This makes the `TextField` stretch to take up all the space *except* what's taken by other widgets in the row. +你在之前的课程中已经见过其中一些 widget: +`Row` 和 `Padding`。不过 [`Expanded`][] widget 是新出现的。 +当 `Row`(或 `Column`)的子 widget 被 `Expanded` 包裹时, +它会让该子 widget 沿主轴方向填满所有可用空间 +(`Row` 中为水平方向,`Column` 中为垂直方向), +即其他子 widget 未占用的空间。 +这使得 `TextField` 会拉伸以占据行中*除*其他 widget 之外的所有空间。 + :::tip `Expanded` is often the solution to "[unbounded width/height][]" exceptions. + +`Expanded` 通常是解决"[unbounded width/height][](无限宽度/高度)"异常的方案。 ::: The `TextField` widget is also new in this lesson and is the star of the show. This is the basic Flutter widget for text input. +`TextField` widget 也是本课新出现的,是本课的重点。 +这是 Flutter 中用于文本输入的基础 widget。 + Thus far, `TextField` has the following configuration. +到目前为止,`TextField` 的配置如下: + - It's decorated with a rounded border. Notice that the decoration configuration is very similar to how a `Container` and boxes are decorated. + 它具有圆角边框装饰。 + 注意,这里的装饰配置与 `Container` 和盒子模型的装饰方式非常相似。 - Its `maxLength` property is set to 5 because the game only allows guesses of 5-letter words. + 它的 `maxLength` 属性设置为 5,因为游戏只允许猜测 5 个字母的单词。 [`Expanded`]: {{site.api}}/flutter/widgets/Expanded-class.html [unbounded width/height]: https://www.youtube.com/watch?v=jckqXR5CrPI ### Handle text with `TextEditingController` +### 使用 `TextEditingController` 处理文本 + Next, you need a way to manage the text that the user types into the input field. For this, use a [`TextEditingController`][]. +接下来,你需要一种方式来管理用户在输入框中键入的文本。 +为此,请使用 [`TextEditingController`][]。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -180,6 +247,9 @@ A `TextEditingController` is used to read, clear, and modify the text in a `TextField`. To use it, pass it into the `TextField`. +`TextEditingController` 用于读取、清除和修改 `TextField` 中的文本。 +要使用它,需要将其传入 `TextField`。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -220,9 +290,17 @@ using the `TextField.onSubmitted` argument. This argument accepts a callback, and the callback is triggered whenever the user presses the "Enter" key on the keyboard while the text field has focus. +现在,当用户输入文本时,你可以通过 `_textEditingController` 捕获文本, +但你需要知道_何时_去捕获它。 +响应输入最简单的方式是使用 `TextField.onSubmitted` 参数。 +该参数接受一个回调,当文本输入框获得焦点时, +用户按下键盘上的"Enter"键就会触发这个回调。 + For now, ensure that this works by adding the following callback to `TextField.onSubmitted`: +现在,通过向 `TextField.onSubmitted` 添加以下回调来验证它是否正常工作: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -263,6 +341,11 @@ you could print the `input` passed to the `onSubmitted` callback directly, but a better user experience clears the text after each guess: You need a `TextEditingController` to do that. Update the code as follows: +在这种情况下, +你可以直接打印传递给 `onSubmitted` 回调的 `input`, +但更好的用户体验是在每次猜测后清除文本: +你需要一个 `TextEditingController` 来实现这一点。按如下方式更新代码: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -303,6 +386,10 @@ class GuessInput extends StatelessWidget { In Dart, it's good practice to use the `_` [wildcard][] to hide the input to a function that'll never be used. The preceding example does so. + +在 Dart 中,使用 `_` [wildcard][](通配符)来 +隐藏不会被使用的函数参数是一种好的实践。 +前面的示例就是这样做的。 ::: [`TextEditingController`]: {{site.api}}/flutter/widgets/TextEditingController-class.html @@ -310,6 +397,8 @@ The preceding example does so. ### Gain input focus +### 获取输入焦点 + Often, you want a specific input or widget to automatically gain focus without the user taking action. In this app, for example, the only thing a user can do is enter a guess, @@ -317,9 +406,19 @@ so the `TextField` should be focused automatically when the app launches. And after the user enters a guess, the focus should stay in the `TextField` so they can enter their next guess. +通常,你希望某个特定的输入框或 widget +无需用户操作就能自动获得焦点。 +例如,在这个应用中,用户唯一能做的事就是输入猜测, +因此 `TextField` 应该在应用启动时自动获得焦点。 +而且在用户输入猜测之后,焦点应该保留在 `TextField` 中, +以便用户可以继续输入下一次猜测。 + To resolve the first focus issue, set up the `autofocus` property on the `TextField`. +要解决第一个焦点问题, +请在 `TextField` 上设置 `autofocus` 属性。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -363,8 +462,15 @@ You can use `FocusNode` to request that a `TextField` gain focus, (making the keyboard appear on mobile), or to know when a field has focus. +第二个问题需要你使用 [`FocusNode`][] 来管理键盘焦点。 +你可以使用 `FocusNode` 来请求 `TextField` 获得焦点 +(在移动端使键盘弹出), +或者检测某个输入框是否拥有焦点。 + First, create a `FocusNode` in the `GuessInput` class: +首先,在 `GuessInput` 类中创建一个 `FocusNode`: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -385,6 +491,9 @@ class GuessInput extends StatelessWidget { Then, use the `FocusNode` to request focus whenever the `TextField` is submitted after the controller is cleared: +然后,在控制器清除文本之后, +使用 `FocusNode` 在每次 `TextField` 提交时重新请求焦点: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -429,16 +538,27 @@ class GuessInput extends StatelessWidget { Now, when you press Enter after inputting text, you can continue typing. +现在,当你输入文本后按下 Enter, +就可以继续输入了。 + [`FocusNode`]: {{site.api}}/flutter/widgets/FocusNode-class.html ### Use the input +### 使用输入内容 + Finally, you need to handle the text that the user enters. Recall that the constructor for `GuessInput` requires a callback called `onSubmitGuess`. In `GuessInput`, you need to use that callback. Replace the `print` statement with a call to that function. +最后,你需要处理用户输入的文本。 +回想一下,`GuessInput` 的构造函数需要一个 +名为 `onSubmitGuess` 的回调。 +在 `GuessInput` 中,你需要使用这个回调。 +将 `print` 语句替换为对该函数的调用。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -483,6 +603,9 @@ class GuessInput extends StatelessWidget { :::note The `trim` function prevents whitespace from being entered; otherwise, the user could enter a four-letter word plus a space character. + +`trim` 函数用于去除空白字符; +否则用户可能会输入一个四字母单词加一个空格字符。 ::: The remaining functionality is handled in the parent widget, `GamePage`. @@ -490,6 +613,11 @@ In the `build` method of that class, under the `Row` widgets in the `Column` widget's children, add the `GuessInput` widget: +剩余的功能由父 widget `GamePage` 处理。 +在该类的 `build` 方法中, +在 `Column` widget 的 children 中的 `Row` widget 下方, +添加 `GuessInput` widget: + ```dart class GamePage extends StatelessWidget { final Game _game = Game(); @@ -526,18 +654,34 @@ prove that it's wired up correctly. Submitting the guess requires using the functionality of a `StatefulWidget`, which you'll do in the next lesson. +目前,这段代码只是打印猜测内容以证明连接正确。 +提交猜测需要使用有状态 widget (`StatefulWidget`) 的功能, +你将在下一课中实现。 + ### Buttons +### 按钮 + To improve the UX on mobile and reflect well-known UI practices, there should also be a button that can submit the guess. +为了改善移动端的用户体验并遵循常见的 UI 实践, +还应该提供一个按钮来提交猜测。 + There are many button widgets built into Flutter, like [`TextButton`][], [`ElevatedButton`][], and the button you'll use now: [`IconButton`][]. All of these buttons (and many other interaction widgets) require two arguments (in addition to their optional arguments): +Flutter 内置了许多按钮 widget,如 [`TextButton`][]、 +[`ElevatedButton`][],以及你现在要用的 [`IconButton`][]。 +所有这些按钮(以及许多其他交互 widget)都需要两个参数 +(除了它们的可选参数之外): + - A callback function passed to `onPressed`. + 传递给 `onPressed` 的回调函数。 - A widget that makes up the content of the button (often `Text` or an `Icon`). + 构成按钮内容的 widget(通常是 `Text` 或 `Icon`)。 Add an icon button to the row widget's children list in the `GuessInput` widget, and give it an [`Icon`][] widget to display. @@ -546,6 +690,12 @@ the `padding` property sets the padding between the edge of the button and the icon it wraps to zero. This removes the default padding and makes the button smaller. +在 `GuessInput` widget 的 Row widget 的 children 列表中添加一个图标按钮, +并为其提供一个 [`Icon`][] widget 来显示。 +`Icon` widget 需要进行配置;在这里, +`padding` 属性将按钮边缘与其包裹的图标之间的内边距设置为零。 +这会移除默认的内边距,使按钮更小。 + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -572,6 +722,8 @@ class GuessInput extends StatelessWidget { The `IconButton.onPressed` callback should look familiar: +`IconButton.onPressed` 回调应该看起来很熟悉: + ```dart class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -603,6 +755,8 @@ class GuessInput extends StatelessWidget { This method does the same as the `onSubmitted` callback on the `TextField`. +这个方法与 `TextField` 上的 `onSubmitted` 回调做了相同的事情。 + [`Icon`]: {{site.api}}/flutter/material/Icons-class.html [`TextButton`]: {{site.api}}/flutter/material/TextButton-class.html [`ElevatedButton`]: {{site.api}}/flutter/material/ElevatedButton-class.html @@ -616,10 +770,20 @@ You could, and as your app grows in complexity, you probably should. That said, the callbacks `IconButton.onPressed` and `TextField.onSubmitted` have different signatures, so it's not completely straight-forward. +你可能会想:"难道不应该把这些方法抽象成一个函数, +然后传递给两个输入控件吗?" +确实可以,而且随着应用复杂度的增长,你可能也应该这么做。 +不过,`IconButton.onPressed` 和 `TextField.onSubmitted` 的回调 +具有不同的函数签名,所以这并不是完全直截了当的。 + Refactor the code such that the logic inside this method isn't repeated. +重构代码,使这个方法内部的逻辑不再重复。 + **Solution:** +**解决方案:** + ```dart title="solution.dart" collapsed class GuessInput extends StatelessWidget { GuessInput({super.key, required this.onSubmitGuess}); @@ -674,6 +838,7 @@ class GuessInput extends StatelessWidget { ### Review +### 回顾 title: What you accomplished @@ -709,6 +874,8 @@ items: ### Test yourself +### 自测 + - question: How do you programmatically read or clear the text in a TextField? options: diff --git a/src/content/learn/pathway/tutorial/widget-fundamentals.md b/src/content/learn/pathway/tutorial/widget-fundamentals.md index a425d2adc3..16ee6a6cae 100644 --- a/src/content/learn/pathway/tutorial/widget-fundamentals.md +++ b/src/content/learn/pathway/tutorial/widget-fundamentals.md @@ -1,12 +1,16 @@ --- -title: Create widgets -description: Learn about stateless widgets and how to build your own. +# title: Create widgets +title: 创建 widget +# description: Learn about stateless widgets and how to build your own. +description: 了解无状态 widget 以及如何构建你自己的 widget。 layout: tutorial --- Learn to create custom widgets and use the most common SDK widgets like Container, Center, and Text. +学习如何创建自定义 widget,并使用最常见的 SDK widget,如 Container、Center 和 Text。 + @@ -24,14 +28,25 @@ items: ### Before you start +### 开始之前 + This app relies on a bit of game logic that isn't UI-related, and thus is outside the scope of this tutorial. Before you move on, you need to add this logic to your app. +这个应用依赖一些与 UI 无关的游戏逻辑, +因此超出了本教程的范围。 +在继续之前,你需要将这些逻辑添加到你的应用中。 + 1. Download the file below and save it as `lib/game.dart` in your project directory. + + 下载下面的文件,并将其保存为项目目录中的 `lib/game.dart`。 + 1. Import the file in your `lib/main.dart` file. + 在你的 `lib/main.dart` 文件中导入该文件。 + :::note Game logic note @@ -42,22 +57,38 @@ The full lists combined have over 10,000 words and were omitted for brevity. You don't need the full lists to continue the tutorial. When you're testing your app, make sure to use the words from those lists. +你可能会注意到 `legalGuesses` 和 `legalWords` 列表只包含少量单词。 +完整的列表合计超过 10,000 个单词,为简洁起见已省略。 +你不需要完整的列表即可继续本教程。 +在测试应用时,请确保使用这些列表中的单词。 + Alternatively, you can find the full lists in [this GitHub repository][full-words], as well as instructions to import it into your project. +或者,你可以在[这个 GitHub 仓库][full-words]中找到完整的列表, +以及将其导入到项目中的说明。 + ::: [full-words]: https://github.com/ericwindmill/legal_wordle_words ### Anatomy of a stateless widget +### 无状态 widget 的结构 + A `Widget` is a Dart class that extends one of the Flutter widget classes, in this case [`StatelessWidget`][]. +`Widget` 是一个 Dart 类,它继承了 Flutter widget 类之一, +在本例中是 [`StatelessWidget`][]。 + Open your `main.dart` file and add this code below the `MainApp` class, which defines a new widget called `Tile`. +打开你的 `main.dart` 文件,在 `MainApp` 类下方添加以下代码, +它定义了一个名为 `Tile` 的新 widget。 + ```dart class Tile extends StatelessWidget { const Tile(this.letter, this.hitType, {super.key}); @@ -73,25 +104,45 @@ class Tile extends StatelessWidget { #### Constructor +#### 构造函数 + The `Tile` class has a [constructor][] that defines what data needs to be passed into the widget to render the widget. In this case, the constructor accepts two parameters: +`Tile` 类有一个[构造函数][constructor],它定义了 +渲染该 widget 时需要传入哪些数据。 +在本例中,构造函数接受两个参数: + - A `String` representing the guessed letter of the tile. + + 一个 `String`,表示方块中猜测的字母。 + - A `HitType` [enum value][] represent the guess result and used to determine the color of the tile. For example, `HitType.hit` results in a green tile. + 一个 `HitType` [枚举值][enum value],表示猜测结果, + 用于确定方块的颜色。 + 例如,`HitType.hit` 会产生一个绿色方块。 + Passing data into widget constructors is at the core of making widgets reusable. +通过构造函数向 widget 传递数据是实现 widget 可复用的核心。 + [constructor]: {{site.dart-site}}/language/constructors [enum value]: {{site.dart-site}}/language/enums #### Build method +#### `build` 方法 + Finally, there's the all important `build` method, which must be defined on every widget, and will always return another widget. +最后是至关重要的 `build` 方法,它必须在每个 widget 上定义, +并且始终返回另一个 widget。 + ```dart class Tile extends StatelessWidget { const Tile(this.letter, this.hitType, {super.key}); @@ -109,11 +160,17 @@ class Tile extends StatelessWidget { ### Use the custom widget +### 使用自定义 widget + When the app is finished, there will be 25 instances of this widget on the screen. For now, though, display just one so you can see the updates as they're made. In the `MainApp.build` method, replace the `Text` widget with the following: +当应用完成后,屏幕上将有 25 个这样的 widget 实例。 +不过现在,只显示一个,这样你可以在修改时看到更新效果。 +在 `MainApp.build` 方法中,将 `Text` widget 替换为以下内容: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -135,19 +192,36 @@ At the moment, your app will be blank, because the `Tile` widget returns an empty `Container`, which doesn't display anything by default. +此时,你的应用将是空白的, +因为 `Tile` widget 返回了一个空的 `Container`, +默认情况下它不会显示任何内容。 + ### The `Container` widget +### `Container` widget + The `Tile` widget consists of three of the most common core widgets: `Container`, `Center`, and `Text`. [`Container`][] is a convenience widget that wraps several core styling widgets, such as [`Padding`][], [`ColoredBox`][], [`SizedBox`][], and [`DecoratedBox`][]. +`Tile` widget 由三个最常用的核心 widget 组成: +`Container`、`Center` 和 `Text`。 +[`Container`][] 是一个便捷 widget,它封装了多个核心样式 widget, +例如 [`Padding`][]、[`ColoredBox`][]、[`SizedBox`][] 和 [`DecoratedBox`][]。 + Because the finished UI contains 25 `Tile` widgets in neat columns and rows, it should have an explicit size. Set the width and height properties on the `Container`. (You could also do this with a `SizedBox` widget, but you'll use more properties of the `Container` next.) +因为最终的 UI 包含 25 个整齐排列成行和列的 `Tile` widget, +所以它应该有一个明确的尺寸。 +在 `Container` 上设置 width 和 height 属性。 +(你也可以使用 `SizedBox` widget 来实现,但接下来你会用到 +`Container` 的更多属性。) + ```dart class Tile extends StatelessWidget { const Tile(this.letter, this.hitType, {super.key}); @@ -177,6 +251,8 @@ class Tile extends StatelessWidget { Next, add a [`Border`][] to the box with the following code: +接下来,使用以下代码为方框添加一个 [`Border`][]: + ```dart class Tile extends StatelessWidget { const Tile(this.letter, this.hitType, {super.key}); @@ -206,20 +282,36 @@ In this case, you've added a border. When you hot reload, there should be a lightly colored border around the white square. +`BoxDecoration` 是一个能够为 widget 添加各种装饰的对象, +包括背景颜色、边框、阴影等等。 +在本例中,你添加了一个边框。 +当你热重载后,白色方块周围应该会出现一个浅色边框。 + When this game is complete, the color of the tile will depend on the user's guess. The tile will be green when the user has guessed correctly, yellow when the letter is correct but the position is incorrect, and gray if the guess is wrong in both respects. +当这个游戏完成后, +方块的颜色将取决于用户的猜测。 +当用户猜对时方块为绿色, +当字母正确但位置不对时为黄色, +当两者都错时为灰色。 + The following figure shows all three possibilities. +下图展示了所有三种可能的情况。 + A screenshot of a green, yellow, and grey tile. To achieve this in UI, use a [switch expression][] to set the `color` of the `BoxDecoration`. +要在 UI 中实现这一效果,请使用 [switch 表达式][switch expression] +来设置 `BoxDecoration` 的 `color`。 + ```dart class Tile extends StatelessWidget { const Tile(required this.letter, required hitType, {super.key}); @@ -252,13 +344,21 @@ class Tile extends StatelessWidget { ### Child widgets +### 子 widget + Finally, add the `Center` and `Text` widgets to the `Container.child` property. +最后,将 `Center` 和 `Text` widget 添加到 `Container.child` 属性中。 + Most widgets in the Flutter SDK have a `child` or `children` property that's meant to be passed a widget or a list of widgets, respectively. It's the best practice to use the same naming convention in your own custom widgets. +Flutter SDK 中的大多数 widget 都有 `child` 或 `children` 属性, +分别用于传入一个 widget 或一个 widget 列表。 +在你自己的自定义 widget 中使用相同的命名约定是最佳实践。 + ```dart class Tile extends StatelessWidget { const Tile(this.letter, this.hitType, {super.key}); @@ -294,6 +394,9 @@ class Tile extends StatelessWidget { Hot reload and a green box appears. To toggle the color, update and hot reload the `HitType` passed into the `Tile` you created: +热重载后会出现一个绿色方块。要切换颜色, +请更新传入你创建的 `Tile` 的 `HitType` 并热重载: + ```dart // main.dart line ~16 // green @@ -307,8 +410,12 @@ child: Tile('A', HitType.partial) Soon, this small box will be one of many widgets on the screen. In the next lesson, you'll start building the game grid itself. +很快,这个小方块将成为屏幕上众多 widget 之一。在下一课中, +你将开始构建游戏网格本身。 + ### Review +### 回顾 title: What you accomplished @@ -340,6 +447,8 @@ items: ### Test yourself +### 测试一下 + - question: "What must every Flutter widget's `build` method return?" options: From e1d5a5c1dc1be72243eb3d41b44678830daa2314 Mon Sep 17 00:00:00 2001 From: XinLei Date: Sun, 8 Mar 2026 11:29:56 -0700 Subject: [PATCH 2/9] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20sr?= =?UTF-8?q?c/content/learn/=20=E5=AD=A6=E4=B9=A0=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E9=A1=B5=E9=9D=A2=20(4=E7=AF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 翻译 Learn 路径入口页面: - learn/index.md - 学习 Flutter 首页 - learn/pathway/index.md - 学习路径概览 - learn/pathway/quick-install.md - 快速安装 - learn/pathway/how-flutter-works.md - Flutter 的工作原理 Co-Authored-By: Claude Opus 4.6 --- src/content/learn/index.md | 29 ++++++++++-- .../learn/pathway/how-flutter-works.md | 45 +++++++++++++++--- src/content/learn/pathway/index.md | 46 +++++++++++++++++-- src/content/learn/pathway/quick-install.md | 18 ++++++-- 4 files changed, 122 insertions(+), 16 deletions(-) diff --git a/src/content/learn/index.md b/src/content/learn/index.md index b867c0b251..c2c491dda6 100644 --- a/src/content/learn/index.md +++ b/src/content/learn/index.md @@ -1,41 +1,64 @@ --- -title: Learn Flutter +# title: Learn Flutter +title: 学习 Flutter breadcrumb: Learn -description: Find everything you need to start building Flutter apps. +# description: Find everything you need to start building Flutter apps. +description: 找到开始构建 Flutter 应用所需的一切。 showToc: false --- -An image of Dash in front of a blackboard. ## For beginners +## 适合初学者 + Start here for an overview of how to get set up, build Flutter apps, write Dart code, and understand how Flutter works. +从这里开始,了解如何进行环境配置、构建 Flutter 应用、编写 Dart 代码以及理解 Flutter 的工作原理。 +

This is the **recommended** set of resources to start learning Flutter! Learn how to write Dart code in the first tutorial, then build a series of apps using Flutter in the second. You'll learn the essential best practices for Flutter app development, app architecture, fetching data from the network, and more. Great for beginners with minimal programming experience.

+

+ + 这是开始学习 Flutter 的**推荐**资源集!在第一个教程中学习如何编写 Dart 代码,然后在第二个教程中使用 Flutter 构建一系列应用。你将学习 Flutter 应用开发的基本最佳实践、应用架构、从网络获取数据等内容。非常适合编程经验较少的初学者。 +

Start learning + 开始学习
## For experienced developers +## 适合有经验的开发者 + Dive deeper into more specific topics in Flutter development with sample code, YouTube videos, codelabs, and more. +通过示例代码、YouTube 视频、Codelab 等内容,深入了解 Flutter 开发中更具体的主题。 +

Find sample code, YouTube videos, codelabs, and more written by the experts on the Flutter team at Google. This index contains the resources you need to learn any aspect of Flutter.

+

+ 查找由 Google Flutter 团队专家编写的示例代码、YouTube 视频、Codelab 等。此索引包含你学习 Flutter 各个方面所需的资源。 +

Explore all resources + 探索所有资源

Learn the recommended app architecture to build a Flutter app. This guide contains everything you need to know to understand and implement MVVM architecture in a Flutter app.

+

+ 学习构建 Flutter 应用的推荐应用架构。本指南包含你理解和实现 Flutter 应用中 MVVM 架构所需的一切知识。 +

Start learning + 开始学习
diff --git a/src/content/learn/pathway/how-flutter-works.md b/src/content/learn/pathway/how-flutter-works.md index 13412aad5f..873177e4b4 100644 --- a/src/content/learn/pathway/how-flutter-works.md +++ b/src/content/learn/pathway/how-flutter-works.md @@ -1,7 +1,10 @@ --- -title: How Flutter Works -breadcrumb: How Flutter Works -description: This pages walks you through the quick install process for Flutter. +# title: How Flutter Works +title: Flutter 的工作原理 +# breadcrumb: How Flutter Works +breadcrumb: Flutter 的工作原理 +# description: This pages walks you through the quick install process for Flutter. +description: 本页面通过视频系列带你了解 Flutter 的工作原理。 layout: tutorial --- @@ -9,6 +12,8 @@ layout: tutorial ## Flutter's architecture +## Flutter 的架构 +
Welcome to the first episode of "How Flutter Works," a six-part @@ -19,8 +24,12 @@ multi-platform frameworks, and the role of Dart in Flutter's architecture. This video is perfect for those beginning to research Flutter and who want to understand the big picture. +欢迎来到"Flutter 的工作原理"的第一集,这是一个由六部分组成的系列,旨在探索你的 Dart 代码进入 Flutter 后会发生什么。在本集中,我们将从高层次审视 Flutter 的架构。了解声明式代码、多平台框架以及 Dart 在 Flutter 架构中的作用。本视频非常适合刚开始研究 Flutter 并想要了解全貌的开发者。 + ## The three trees +## 三棵树 +
Dive into Flutter's architecture by exploring its three primary @@ -29,9 +38,13 @@ the declarative API for Flutter developers and see how Elements glue widgets to the rendering layer. Follow along as we also cover the role of RenderObjects in translating widget values into painting calls. +深入了解 Flutter 的架构,探索它的三棵主要的树:widget 树、Element 树和渲染树。了解 widget 如何为 Flutter 开发者提供声明式 API,以及 Element 如何将 widget 与渲染层连接起来。跟随我们一起了解 RenderObject 在将 widget 的值转化为绘制调用中所扮演的角色。 + ## The state class +## State 类 +
In Episode 3 of How Flutter Works, we dive deep into the State @@ -45,6 +58,8 @@ end of the episode, you'll understand how State objects track, respond to, and manage changes in your Flutter apps—and how the State life cycle enables efficient UI updates. +在"Flutter 的工作原理"第 3 集中,我们深入探讨了 State 类——每个 StatefulWidget 背后的关键部分。我们将完整走过一个 State 对象的生命周期——从初始化资源的 initState,到清理资源的 dispose。在此过程中,我们还会探索重要的方法,如 didChangeDependencies(由 MediaQuery 等 inherited widget 触发)、didUpdateWidget(用于响应祖先驱动的变化)以及最重要的 build 方法。在本集结束时,你将理解 State 对象如何在 Flutter 应用中跟踪、响应和管理变化——以及 State 的生命周期如何实现高效的 UI 更新。 + We also peel back the curtain on how Flutter recurses down the widget tree after a setState call, building out only the parts of your app that need to change. You'll learn why const constructors matter for @@ -54,9 +69,13 @@ If you're curious how Flutter keeps apps fast and responsive, or you just want to really understand what happens behind the scenes, this episode is packed with the essential foundations. +我们还揭示了 Flutter 在 setState 调用后如何沿 widget 树递归,只重建应用中需要变化的部分。你将了解为什么 const 构造函数对性能很重要,为什么 setState 的闭包必须是同步的,以及 Element(而非 Widget 本身)如何管理实际的重建过程。如果你好奇 Flutter 如何保持应用的快速和流畅,或者你只是想真正了解幕后发生了什么,这一集包含了所有必要的基础知识。 + ## The RenderObjectWidget +## RenderObjectWidget +
Ever wonder how your Flutter app actually renders to the screen? @@ -65,13 +84,19 @@ widget in Flutter that creates something visual. While StatelessWidgets and StatefulWidgets help structure your app, it’s RenderObjectWidgets that turn your UI code into real pixels. -You'll learn how Flutter builds the Widget, Element, and RenderObject +是否想过你的 Flutter 应用实际上是如何渲染到屏幕上的?在本视频中,我们深入了解 RenderObjectWidget——Flutter 中唯一能创建可视化内容的 widget 类型。虽然 StatelessWidget 和 StatefulWidget 帮助构建应用的结构,但真正将 UI 代码转化为实际像素的是 RenderObjectWidget。 + +You’ll learn how Flutter builds the Widget, Element, and RenderObject trees, why many common widgets don’t directly render anything, and how RenderObjectWidgets create and update RenderObjects that power your UI. +你将了解 Flutter 如何构建 widget 树、Element 树和渲染树,为什么许多常见的 widget 并不直接渲染任何内容,以及 RenderObjectWidget 如何创建和更新驱动 UI 的 RenderObject。 + ## The RenderObject +## RenderObject +
In Episode 5 of "How Flutter Works," Craig walks through a full @@ -84,9 +109,13 @@ He also breaks down key methods like layout, paint, and describeSemanticsConfiguration, showing how they fit together to keep your UI responsive and accurate. +在"Flutter 的工作原理"第 5 集中,Craig 带你走过一个 RenderObject 的完整生命历程。在第 4 集概念的基础上,本视频讲解了 RenderObject 的核心职责:布局、绘制、命中测试和无障碍。Craig 揭开了约束如何沿渲染树向下传递、尺寸如何向上回传,以及父 RenderObject 如何设置子节点位置的神秘面纱。他还拆解了 layout、paint 和 describeSemanticsConfiguration 等关键方法,展示了它们如何协同工作以保持 UI 的响应性和准确性。 + ## The Flutter Engine and Embedders +## Flutter 引擎与嵌入器 +
In Episode 6 of "How Flutter Works," Craig takes us beneath the @@ -97,14 +126,18 @@ to the host platform, and how embedders facilitate communication between the two. Craig also highlights the structure of a newly generated Flutter project, dives into how threads are managed in a Flutter app, and explains the roles of PlatformChannels and the Pigeon -package. +package. + +在"Flutter 的工作原理"第 6 集中,Craig 带我们深入 Dart 代码之下,探索 Flutter 引擎和嵌入器。本集讲解了 Flutter 移动应用如何依赖原生 Android 和 iOS 代码来启动和运行,Flutter 引擎如何将你的 Dart 代码连接到宿主平台,以及嵌入器如何促进两者之间的通信。Craig 还重点介绍了新生成的 Flutter 项目的结构,深入探讨了 Flutter 应用中的线程管理方式,并讲解了平台通道和 Pigeon 包的作用。 You’ll also learn why the Flutter engine is written in C++ rather than Dart, how it evolved from a fork of Chrome, and how it uses Skia or Impeller to render each frame. The episode wraps up by looking ahead -to Flutter's future architecture improvements, which aim to simplify +to Flutter’s future architecture improvements, which aim to simplify native interop even further. If you want a clear mental model of how Flutter apps work under the hood, this is the perfect way to connect all the layers together. +你还将了解为什么 Flutter 引擎是用 C++ 而非 Dart 编写的,它如何从 Chrome 的一个分支演变而来,以及它如何使用 Skia 或 Impeller 来渲染每一帧。本集最后展望了 Flutter 未来的架构改进,这些改进旨在进一步简化原生互操作。如果你想对 Flutter 应用在底层的工作方式建立清晰的心智模型,这是将所有层次串联起来的完美方式。 + diff --git a/src/content/learn/pathway/index.md b/src/content/learn/pathway/index.md index d3b560e7a7..3cbd46bcea 100644 --- a/src/content/learn/pathway/index.md +++ b/src/content/learn/pathway/index.md @@ -1,7 +1,10 @@ --- -title: Overview -breadcrumb: Learning pathway -description: This learning pathway walks you through the basics of both Dart and Flutter. +# title: Overview +title: 概览 +# breadcrumb: Learning pathway +breadcrumb: 学习路径 +# description: This learning pathway walks you through the basics of both Dart and Flutter. +description: 本学习路径将带你了解 Dart 和 Flutter 的基础知识。 layout: tutorial --- @@ -9,37 +12,67 @@ layout: tutorial Welcome to the Dart and Flutter Getting Started pathway. In it, you'll set up your development environment, learn how to write Dart code, and build three small Flutter apps step-by-step. This learning pathway spans both the Dart and Flutter websites, YouTube, and your IDE. By the end, you'll have a solid foundation in both Dart and Flutter. +欢迎来到 Dart 和 Flutter 入门学习路径。在这个学习路径中,你将搭建开发环境、学习如何编写 Dart 代码,并逐步构建三个小型 Flutter 应用。本学习路径涵盖 Dart 和 Flutter 网站、YouTube 以及你的 IDE。完成后,你将在 Dart 和 Flutter 两方面打下坚实的基础。 + ### Set up your development environment +### 搭建开发环境 + Before you can start building Flutter apps, you'll need to set up your development environment. Follow the installation guide to get Flutter and all required dependencies installed on your machine. +在开始构建 Flutter 应用之前,你需要搭建开发环境。请按照安装指南在你的设备上安装 Flutter 及所有必需的依赖项。 + [Get Started →](/learn/pathway/quick-install) +[开始 →](/learn/pathway/quick-install) + ### Complete the Dart Getting Started tutorial +### 完成 Dart 入门教程 + Flutter uses the Dart programming language. If you're new to Dart, complete this interactive tutorial to learn the fundamentals—variables, functions, classes, and more. +Flutter 使用 Dart 编程语言。如果你是 Dart 新手,请完成这个交互式教程来学习基础知识——变量、函数、类等等。 + If you're in a hurry and already familiar with modern, object-oriented programming languages, you can safely skip this step. +如果你时间紧迫且已经熟悉现代面向对象编程语言,可以放心跳过这一步。 + Dart Getting Started tutorial → + +Dart 入门教程 → + + ### Complete the Flutter Getting Started tutorial +### 完成 Flutter 入门教程 + Now that you have Dart skills under your belt, dive into Flutter! This hands-on tutorial walks you through building three small Flutter apps step-by-step. +现在你已经掌握了 Dart 技能,让我们开始学习 Flutter 吧!这个实践教程将逐步带你构建三个小型 Flutter 应用。 + [Flutter Getting Started tutorial →](/learn/pathway/tutorial) +[Flutter 入门教程 →](/learn/pathway/tutorial) + ### Understand how Flutter works +### 了解 Flutter 的工作原理 + Deepen your understanding of how Flutter works under the hood by watching this video series. Learn about the widget tree, rendering pipeline, and what makes Flutter unique. This series provides the knowledge you need to know to take full advantage of the framework. +通过观看这个视频系列,深入了解 Flutter 的底层工作原理。学习 widget 树、渲染管线,以及 Flutter 的独特之处。本系列提供了你充分利用该框架所需的知识。 + [Watch: How Flutter Works →](/learn/pathway/how-flutter-works) +[观看:Flutter 的工作原理 →](/learn/pathway/how-flutter-works) + Dash with question marks around her; looking curious. @@ -50,8 +83,13 @@ Deepen your understanding of how Flutter works under the hood by watching this v

We're always looking for ways to improve the Getting Started experience. Moving forward, we plan to add more resources to this section of the website. We'd love to know what you think and what you'd like to see. - + + 我们一直在寻求改善入门体验的方法。未来,我们计划在网站的这个部分添加更多资源。我们很想知道你的想法和期望。 + Please take a moment to share your feedback with us. + + 请花一点时间与我们分享你的反馈。

Leave feedback + 提交反馈 \ No newline at end of file diff --git a/src/content/learn/pathway/quick-install.md b/src/content/learn/pathway/quick-install.md index 91f7870764..822871f0c1 100644 --- a/src/content/learn/pathway/quick-install.md +++ b/src/content/learn/pathway/quick-install.md @@ -1,7 +1,10 @@ --- -title: Quick install -breadcrumb: Install -description: This pages walks you through the quick install process for Flutter. +# title: Quick install +title: 快速安装 +# breadcrumb: Install +breadcrumb: 安装 +# description: This pages walks you through the quick install process for Flutter. +description: 本页面将引导你完成 Flutter 的快速安装流程。 layout: tutorial --- @@ -10,15 +13,24 @@ layout: tutorial ## Start learning! +## 开始学习吧! + Installing the Flutter SDK automatically installs Dart as well. You can now start building Flutter apps! +安装 Flutter SDK 时会自动安装 Dart。你现在可以开始构建 Flutter 应用了! +

If you're new to programming, start with the Dart Getting Started tutorial to learn the fundamentals of the Dart programming language. This tutorial was designed to lead into the Flutter Getting Started tutorial. +如果你是编程新手,请先从 Dart 入门教程开始,学习 Dart 编程语言的基础知识。该教程旨在为 Flutter 入门教程做铺垫。 +

Start learning + 开始学习
If you're already familiar with programming, you can skip the Dart tutorial and jump straight into the first Flutter lesson. + +如果你已经熟悉编程,可以跳过 Dart 教程,直接进入第一个 Flutter 课程。 From 93051fdd49a94b36a4f8af07f566e0dec04a5471 Mon Sep 17 00:00:00 2001 From: XinLei Date: Sun, 8 Mar 2026 11:30:08 -0700 Subject: [PATCH 3/9] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20sr?= =?UTF-8?q?c/content/app-architecture/=20=E6=9E=B6=E6=9E=84=E6=8C=87?= =?UTF-8?q?=E5=8D=97=E7=B3=BB=E5=88=97=20(4=E7=AF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 翻译应用架构指南系列: - app-architecture/index.md - 架构概述 - app-architecture/concepts.md - 常见架构概念 - app-architecture/guide.md - 应用架构指南 - app-architecture/recommendations.md - 架构建议与资源 Co-Authored-By: Claude Opus 4.6 --- src/content/app-architecture/concepts.md | 149 ++++++++- src/content/app-architecture/guide.md | 286 +++++++++++++++++- src/content/app-architecture/index.md | 69 ++++- .../app-architecture/recommendations.md | 92 +++++- 4 files changed, 565 insertions(+), 31 deletions(-) diff --git a/src/content/app-architecture/concepts.md b/src/content/app-architecture/concepts.md index 5939b49070..928fc3f881 100644 --- a/src/content/app-architecture/concepts.md +++ b/src/content/app-architecture/concepts.md @@ -1,15 +1,22 @@ --- -title: Common architecture concepts -shortTitle: Architecture concepts +# title: Common architecture concepts +title: 常见架构概念 +# shortTitle: Architecture concepts +shortTitle: 架构概念 +# description: > +# Learn about common architecture concepts in application design, +# and how they apply to Flutter. description: > - Learn about common architecture concepts in application design, - and how they apply to Flutter. + 了解应用设计中的常见架构概念, + 以及它们如何应用于 Flutter。 prev: - title: Architecting Flutter apps - path: /app-architecture + # title: Architecting Flutter apps + title: 构建 Flutter 应用架构 + path: /app-architecture next: - title: Guide to app architecture - path: /app-architecture/guide + # title: Guide to app architecture + title: 应用架构指南 + path: /app-architecture/guide --- In this section, you'll find tried and true principles that guide architectural @@ -19,8 +26,15 @@ It's a gentle introduction to vocabulary and concepts related to the recommended architecture and best practices, so they can be explored in more detail throughout this guide. +在本节中,你将了解在应用开发领域中经过实践检验的架构决策指导原则, +以及它们如何具体应用于 Flutter。 +这是对推荐架构和最佳实践相关术语和概念的简要介绍, +以便在本指南中进行更详细的探讨。 + ## Separation of concerns +## 关注点分离 + [Separation-of-concerns][] is a core principle in app development that promotes modularity and maintainability by dividing an application's functionality into distinct, self-contained units. From a high-level, @@ -30,23 +44,38 @@ Within each layer, you should further separate your application by feature or functionality. For example, your application's authentication logic should be in a different class than the search logic. +[关注点分离][Separation-of-concerns]是应用开发中的核心原则, +它通过将应用的功能划分为独立的、自包含的单元来促进模块化和可维护性。 +从高层次来看,这意味着将 UI 逻辑与业务逻辑分离。 +这通常被描述为*分层*架构。 +在每一层中,你还应该按功能或特性进一步拆分你的应用。 +例如,你的应用的身份验证逻辑应该与搜索逻辑位于不同的类中。 + In Flutter, this applies to [widgets](/resources/glossary#widget) in the UI layer as well. You should write reusable, lean widgets that hold as little logic as possible. +在 Flutter 中,这同样适用于 UI 层中的 [widget](/resources/glossary#widget)。你应该编写可复用的、精简的 widget,使其包含尽可能少的逻辑。 + ## Layered architecture +## 分层架构 + Flutter applications should be written in *layers*. Layered architecture is a software design pattern that organizes an application into distinct layers, each with specific roles and responsibilities. Typically, applications are separated into 2 to 3 layers, depending on complexity. +Flutter 应用应该以*分层*的方式编写。分层架构是一种软件设计模式, +它将应用组织成不同的层,每一层都有特定的角色和职责。 +通常,应用根据复杂程度被分为 2 到 3 层。 + The three common layers of app architecture, the UI layer, logic layer, and data layer. * **UI layer** - Displays data to the user that is exposed by the business logic layer, and handles user interaction. This is also commonly referred to as the - ‘presentation layer'. + 'presentation layer'. * **Logic layer** - Implements core business logic, and facilitates interaction - between the data layer and UI layer. Commonly known as the ‘domain layer'. + between the data layer and UI layer. Commonly known as the 'domain layer'. The logic layer is optional, and only needs to be implemented if your application has complex business logic that happens on the client. Many apps are only concerned with presenting data to a user and @@ -55,24 +84,47 @@ into 2 to 3 layers, depending on complexity. * **Data layer** - Manages interactions with data sources, such as databases or platform plugins. Exposes data and methods to the business logic layer. -These are called ‘layers' because each layer can only communicate with the +* **UI 层** - 向用户展示由业务逻辑层暴露的数据,并处理用户交互。这通常也被称为"展示层"。 +* **逻辑层** - 实现核心业务逻辑,并协调数据层和 UI 层之间的交互。通常也被称为"领域层"。 + 逻辑层是可选的,只有当你的应用在客户端存在复杂的业务逻辑时才需要实现。 + 许多应用只关心向用户展示数据并允许用户修改数据(俗称 CRUD 应用)。 + 这些应用可能不需要这个可选层。 +* **数据层** - 管理与数据源的交互,例如数据库或平台插件。向业务逻辑层暴露数据和方法。 + +These are called 'layers' because each layer can only communicate with the layers directly below or above it. The UI layer shouldn't know that the data layer exists, and vice versa. +之所以称为"层",是因为每一层只能与直接相邻的上下层通信。 +UI 层不应该知道数据层的存在,反之亦然。 + ## Single source of truth +## 单一数据源 + Every data type in your app should have a [single source of truth][] (SSOT). The source of truth is responsible for representing local or remote state. If the data can be modified in the app, the SSOT class should be the only class that can do so. +你的应用中的每种数据类型都应该有一个[单一数据源][single source of truth](SSOT)。 +数据源负责表示本地或远程状态。 +如果数据可以在应用中被修改, +SSOT 类应该是唯一能够执行此操作的类。 + This can dramatically reduce the number of bugs in your application, and it can simplify code because you'll only ever have one copy of the same data. +这可以大幅减少应用中的 bug 数量, +并且可以简化代码,因为同一份数据始终只有一个副本。 + Generally, the source of truth for any given type of data in your application is held in a class called a **Repository**, which is part of the data layer. There is typically one repository class for each type of data in your app. +通常,应用中任何给定类型数据的数据源都保存在一个名为 **Repository** 的类中, +它是数据层的一部分。通常每种数据类型对应一个 Repository 类。 + This principle can be applied across layers and components in your application as well as within individual classes. For example, a Dart class might use [getters][] to derive values from an SSOT field @@ -80,8 +132,16 @@ a Dart class might use [getters][] to derive values from an SSOT field or a list of [records][] to group related values (instead of parallel lists whose indices might get out of sync). +这一原则可以应用于应用中的各个层和组件,也可以应用于单个类内部。 +例如,一个 Dart 类可以使用 [getter][getters] 从 SSOT 字段派生值 +(而不是使用多个需要独立更新的字段), +或者使用 [record][records] 列表来分组相关值 +(而不是使用索引可能不同步的并行列表)。 + ## Unidirectional data flow +## 单向数据流 + [Unidirectional data flow][] (UDF) refers to a design pattern that helps decouple state from the UI that displays that state. In the simplest terms, state flows from the data layer through the logic layer and eventually to the @@ -89,21 +149,43 @@ widgets in the UI layer. Events from user-interaction flow the opposite direction, from the presentation layer back through the logic layer and to the data layer. +[单向数据流][Unidirectional data flow](UDF)是一种设计模式, +它有助于将状态与显示该状态的 UI 解耦。 +简单来说,状态从数据层流经逻辑层,最终到达 UI 层的 widget。 +用户交互产生的事件则沿相反方向流动, +从展示层经过逻辑层回到数据层。 + The three common layers of app architecture, the UI layer, logic layer, and data layer, and the flow of state from the data layer to the UI layer. In UDF, the update loop from user interaction to re-rendering the UI looks like this: +在 UDF 中,从用户交互到重新渲染 UI 的更新循环如下所示: + 1. [UI layer] An event occurs due to user interaction, such as a button being clicked. The widget's event handler callback invokes a method exposed by a class in the logic layer. + + [UI 层] 由于用户交互(例如点击按钮)产生一个事件。widget 的事件处理回调调用逻辑层中某个类暴露的方法。 + 2. [Logic layer] The logic class calls methods exposed by a repository that know how to mutate the data. + + [逻辑层] 逻辑类调用 Repository 暴露的方法来修改数据。 + 3. [Data layer] The repository updates data (if necessary) and then provides the new data to the logic class. + + [数据层] Repository 更新数据(如果需要),然后将新数据提供给逻辑类。 + 4. [Logic layer] The logic class saves its new state, which it sends to the UI. + + [逻辑层] 逻辑类保存其新状态,并将其发送到 UI。 + 5. [UI layer] The UI displays the new state of the view model. + [UI 层] UI 显示视图模型的新状态。 + New data can also start at the data layer. For example, a repository might poll an HTTP server for new data. In this case, the data flow only makes the second half of the journey. @@ -112,15 +194,28 @@ in the [SSOT][], which is the data layer. This makes your code easier to understand, less error prone, and prevents malformed or unexpected data from being created. +新数据也可以从数据层开始。 +例如,Repository 可能会轮询 HTTP 服务器以获取新数据。 +在这种情况下,数据流只经历后半段旅程。 +最重要的理念是,数据变更始终发生在[单一数据源][SSOT]中,即数据层。 +这使你的代码更容易理解、更不容易出错,并防止产生格式错误或意外的数据。 + ## UI is a function of (immutable) state +## UI 是(不可变)状态的函数 + Flutter is declarative, meaning that it builds its UI to reflect the current state of your app. When state changes, your app should trigger a rebuild of the UI that depends on that state. In Flutter, you'll often hear this described as "UI is a function of state". +Flutter 是声明式的, +这意味着它构建 UI 以反映应用的当前状态。 +当状态发生变化时,你的应用应该触发依赖于该状态的 UI 的重建。 +在 Flutter 中,你经常会听到这样的描述:"UI 是状态的函数"。 + UI is a function of state. It's crucial that your data drive your UI, and not the other way around. @@ -129,33 +224,60 @@ and views should contain as little logic as possible. This minimizes the possibility of data being lost when an app is closed, and makes your app more testable and resilient to bugs. +至关重要的是,你的数据应该驱动 UI,而不是反过来。 +数据应该是不可变的和持久化的,视图应该包含尽可能少的逻辑。 +这可以最大限度地减少应用关闭时数据丢失的可能性, +并使你的应用更具可测试性和对 bug 的抵抗力。 + ## Extensibility +## 可扩展性 + Each piece of architecture should have a well defined list of inputs and outputs. For example, a view model in the logic layer should only take in data sources as inputs, such as repositories, and should only expose commands and data formatted for views. +每个架构组件都应该有一组明确定义的输入和输出。 +例如,逻辑层中的视图模型应该只接收数据源作为输入(如 Repository), +并且只暴露命令和为视图格式化的数据。 + Using clean interfaces in this way allows you to swap out concrete implementations of your classes without needing to change any of the code that consumes the interface. +以这种方式使用清晰的接口,允许你替换类的具体实现, +而无需更改任何使用该接口的代码。 + ## Testability +## 可测试性 + The principles that make software extensible also make software easier to test. For example, you can test the self-contained logic of a view model by mocking a repository. The view model tests don't require you to mock other parts of your application, and you can test your UI logic separate from Flutter widgets themselves. +使软件具有可扩展性的原则同样也使软件更容易测试。 +例如,你可以通过模拟 Repository 来测试视图模型的独立逻辑。 +视图模型的测试不需要你模拟应用的其他部分, +并且你可以独立于 Flutter widget 本身来测试 UI 逻辑。 + Your app will also be more flexible. It will be straightforward and low risk to add new logic and new UI. For example, adding a new view model cannot break any logic from the data or business logic layers. +你的应用也将更加灵活。 +添加新逻辑和新 UI 将是简单且低风险的。 +例如,添加一个新的视图模型不会破坏数据层或业务逻辑层的任何逻辑。 + The next section explains the idea of inputs and outputs for any given component in your application's architecture. +下一节将解释应用架构中任何给定组件的输入和输出的概念。 + [Separation-of-concerns]: https://en.wikipedia.org/wiki/Separation_of_concerns [single source of truth]: https://en.wikipedia.org/wiki/Single_source_of_truth [SSOT]: https://en.wikipedia.org/wiki/Single_source_of_truth @@ -165,7 +287,12 @@ in your application's architecture. ## Feedback +## 反馈 + As this section of the website is evolving, we [welcome your feedback][]! +由于网站的这一部分仍在不断完善中, +我们[欢迎你的反馈][welcome your feedback]! + [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="concepts" diff --git a/src/content/app-architecture/guide.md b/src/content/app-architecture/guide.md index 5c220119d4..afae57b70d 100644 --- a/src/content/app-architecture/guide.md +++ b/src/content/app-architecture/guide.md @@ -1,13 +1,19 @@ --- -title: Guide to app architecture -shortTitle: Architecture guide +# title: Guide to app architecture +title: 应用架构指南 +# shortTitle: Architecture guide +shortTitle: 架构指南 +# description: > +# The recommended way to architect a Flutter app. description: > - The recommended way to architect a Flutter app. + 构建 Flutter 应用的推荐架构方式。 prev: - title: Common architecture concepts + # title: Common architecture concepts + title: 通用架构概念 path: /app-architecture/concepts next: - title: Architecture case study + # title: Architecture case study + title: 架构案例研究 path: /app-architecture/case-study --- @@ -17,29 +23,57 @@ making them easier to scale, test, and maintain. However, they're guidelines, not steadfast rules, and you should adapt them to your unique requirements. +以下页面演示了如何使用最佳实践构建应用。 +本指南中的建议适用于大多数应用, +使它们更易于扩展、测试和维护。 +然而,这些只是指导方针,而非一成不变的规则, +你应该根据自己的具体需求进行调整。 + This section provides a high-level overview of how Flutter applications can be architected. It explains the layers of an application, along with the classes that exist within each layer. The section after this provides concrete code samples and walks through a Flutter application that's implemented these recommendations. +本节提供了 Flutter 应用架构的高层概述。 +它解释了应用的各个层级, +以及每个层级中存在的类。 +下一节将提供具体的代码示例, +并详细介绍一个实现了这些建议的 Flutter 应用。 + ## Overview of project structure +## 项目结构概述 + [Separation-of-concerns][] is the most important principle to follow when designing your Flutter app. Your Flutter application should split into two broad layers, the UI layer and the Data layer. +[关注点分离][Separation-of-concerns]是设计 Flutter 应用时应遵循的最重要原则。 +你的 Flutter 应用应该分为两个大的层级, +即 UI 层和数据层。 + Each layer is further split into different components, each of which has distinct responsibilities, a well-defined interface, boundaries and dependencies. This guide recommends you split your application into the following components: +每个层级又进一步划分为不同的组件, +每个组件都有明确的职责、定义良好的接口、 +边界和依赖关系。 +本指南建议你将应用拆分为以下组件: + * Views * View models * Repositories * Services +* 视图(Views) +* 视图模型(View models) +* Repositories +* Services + ### MVVM If you've encountered the [Model-View-ViewModel architectural pattern][] (MVVM), @@ -52,6 +86,15 @@ Repositories and services represent the data of an application, or the model layer of MVVM. Each of these components is defined in the next section. +如果你了解过 [Model-View-ViewModel 架构模式][Model-View-ViewModel architectural pattern](MVVM), +对此会比较熟悉。 +MVVM 是一种将应用功能分为三个部分的架构模式: +`Model`、`ViewModel` 和 `View`。 +视图和视图模型构成了应用的 UI 层。 +Repository 和 Service 代表应用的数据, +即 MVVM 中的模型层。 +每个组件将在下一节中定义。 + MVVM architectural pattern Every feature in an application will contain one view to describe the UI and @@ -60,14 +103,24 @@ one or more repositories as the sources of truth for your application data, and zero or more services that interact with external APIs, like client servers and platform plugins. +应用中的每个功能都包含一个用于描述 UI 的视图和一个用于处理逻辑的视图模型, +一个或多个作为应用数据单一数据源的 Repository, +以及零个或多个与外部 API 交互的 Service, +例如客户端服务器和平台插件。 + A single feature of an application might require all of the following objects: +应用的单个功能可能需要以下所有对象: + An example of the Dart objects that might exist in one feature using the architecture described on page. Each of these objects and the arrows that connect them will be explained thoroughly by the end of this page. Throughout this guide, the following simplified version of that diagram will be used as an anchor. +在本页结束时,这些对象及连接它们的箭头都将被详细解释。 +在整个指南中,以下简化版的图表将作为参考。 + A simplified diagram of the architecture described on this page. :::note @@ -75,25 +128,45 @@ Apps with complex logic might also have a logic layer that sits in between the UI layer and data layer. This logic layer is commonly called the *domain layer.* The domain layer contains additional components often called *interactors* or *use-cases*. The domain layer is covered later in this guide. + +具有复杂逻辑的应用可能还有一个位于 UI 层和数据层之间的逻辑层。 +这个逻辑层通常被称为*领域层*。 +领域层包含通常被称为*交互器*或*用例*的额外组件。 +领域层将在本指南的后面部分介绍。 ::: [Model-View-ViewModel architectural pattern]: https://en.wikipedia.org/wiki/Model–view–viewmodel ## UI layer +## UI 层 + An application's UI layer is responsible for interacting with the user. It displays an application's data to the user and receives user input, such as tap events and form inputs. +应用的 UI 层负责与用户交互。 +它向用户展示应用数据并接收用户输入, +例如点击事件和表单输入。 + The UI reacts to data changes or user input. When the UI receives new data from a Repository, it should re-render to display that new data. When the user interacts with the UI, it should change to reflect that interaction. +UI 对数据变化或用户输入做出响应。 +当 UI 从 Repository 接收到新数据时, +它应该重新渲染以显示新数据。 +当用户与 UI 交互时, +它应该发生变化以反映该交互。 + The UI layer is made up of two architectural components, based on the MVVM design pattern: +UI 层由两个架构组件组成, +基于 MVVM 设计模式: + * **Views** describe how to present application data to the user. Specifically, they refer to *compositions of widgets* that make a feature. For instance, a view is often (but not always) a screen that @@ -101,14 +174,25 @@ based on the MVVM design pattern: all of the widgets below it in the widget tree. Views are also responsible for passing events to the view model in response to user interactions. + **视图**描述了如何向用户展示应用数据。 + 具体来说,它们指的是构成一个功能的 *widget 组合*。 + 例如,视图通常(但不总是)是一个包含 `Scaffold` widget + 以及 widget 树中其下所有 widget 的屏幕。 + 视图还负责将事件传递给视图模型以响应用户交互。 * **View models** contain the logic that converts app data into *UI State*, because data from repositories is often formatted differently from the data that needs to be displayed. For example, you might need to combine data from multiple repositories, or you might want to filter a list of data records. + **视图模型**包含将应用数据转换为 *UI 状态*的逻辑, + 因为来自 Repository 的数据格式通常与需要显示的数据格式不同。 + 例如,你可能需要合并来自多个 Repository 的数据, + 或者你可能想要过滤数据记录列表。 Views and view models should have a one-to-one relationship. +视图和视图模型应该是一对一的关系。 + A simplified diagram of the architecture described on this page with the view and view model objects highlighted. In the simplest terms, @@ -117,11 +201,22 @@ Using views and view models, your UI layer can maintain state during configuration changes (such as screen rotations), and you can test the logic of your UI independently of Flutter widgets. +简单来说, +视图模型管理 UI 状态,视图则显示该状态。 +通过使用视图和视图模型,你的 UI 层可以在 +配置更改(例如屏幕旋转)期间保持状态, +并且你可以独立于 Flutter widget 来测试 UI 逻辑。 + :::note 'View' is an abstract term, and one view doesn't equal one widget. Widgets are composable, and several can be combined to create one view. Therefore, view models don't have a one-to-one relationship with widgets, but rather a one-to-one relationship with a *collection* of widgets. + +"视图"是一个抽象术语,一个视图并不等于一个 widget。 +Widget 是可组合的,多个 widget 可以组合成一个视图。 +因此,视图模型与 widget 不是一对一的关系, +而是与一组 widget 的一对一关系。 ::: A feature of an application is user centric, @@ -134,6 +229,16 @@ only purpose is to provide the user with a way to log in. In the application code, the login screen would be made up of a `LoginViewModel` class and a `LoginView` class. +应用的功能以用户为中心, +因此由 UI 层定义。 +每一对*视图*和*视图模型*的实例定义了应用中的一个功能。 +这通常是应用中的一个屏幕,但不一定如此。 +例如,考虑登录和退出的场景。 +登录通常在一个专门的屏幕上完成, +该屏幕的唯一目的是为用户提供登录方式。 +在应用代码中,登录屏幕由 +`LoginViewModel` 类和 `LoginView` 类组成。 + On the other hand, logging out of an app is generally not done on a dedicated screen. The ability to log out is generally presented to the user as a button in @@ -142,45 +247,84 @@ It's often presented in multiple locations. In such scenarios, you might have a `LogoutViewModel` and a `LogoutView` which only contains a single button that can be dropped into other widgets. +另一方面, +退出应用通常不在专门的屏幕上完成。 +退出功能通常以按钮的形式呈现给用户, +可能在菜单、用户账户屏幕或其他任意位置。 +它经常出现在多个位置。 +在这种场景下,你可能有一个 `LogoutViewModel` 和一个 `LogoutView`, +其中只包含一个可以放入其他 widget 中的按钮。 + ### Views +### 视图 + In Flutter, views are the widget classes of your application. Views are the primary method of rendering UI, and shouldn't contain any business logic. They should be passed all data they need to render from the view model. +在 Flutter 中,视图是应用的 widget 类。 +视图是渲染 UI 的主要方式, +不应该包含任何业务逻辑。 +它们应该从视图模型获取渲染所需的所有数据。 + A simplified diagram of the architecture described on this page with the view object highlighted. The only logic a view should contain is: +视图应该包含的唯一逻辑是: + * Simple if-statements to show and hide widgets based on a flag or nullable field in the view model + 简单的 if 语句,根据视图模型中的标志或可空字段来显示和隐藏 widget * Animation logic + 动画逻辑 * Layout logic based on device information, like screen size or orientation. + 基于设备信息的布局逻辑,如屏幕尺寸或方向。 * Simple routing logic + 简单的路由逻辑 All logic related to data should be handled in the view model. +所有与数据相关的逻辑都应该在视图模型中处理。 + ### View models +### 视图模型 + A view model exposes the application data necessary to render a view. In the architecture design described on this page, most of the logic in your Flutter application lives in view models. +视图模型暴露渲染视图所需的应用数据。 +在本页描述的架构设计中, +Flutter 应用中的大部分逻辑都在视图模型中。 + A simplified diagram of the architecture described on this page with the view model object highlighted. A view model's main responsibilities include: +视图模型的主要职责包括: + * Retrieving application data from repositories and transforming it into a format suitable for presentation in the view. For example, it might filter, sort or aggregate data. + 从 Repository 获取应用数据并将其转换为适合在视图中展示的格式。 + 例如,它可能会过滤、排序或聚合数据。 * Maintaining the current state needed in the view, so that the view can rebuild without losing data. For example, it might contain boolean flags to conditionally render widgets in the view, or a field that tracks which section of a carousel is active on screen. + 维护视图所需的当前状态, + 以便视图可以在不丢失数据的情况下重建。 + 例如,它可能包含用于在视图中有条件地渲染 widget 的布尔标志, + 或者跟踪屏幕上轮播图哪个部分处于活动状态的字段。 * Exposes callbacks (called **commands**) to the view that can be attached to an event handler, like a button press or form submission. + 向视图暴露回调(称为**命令**), + 这些回调可以附加到事件处理器上,如按钮点击或表单提交。 Commands are named for the [command pattern][], and are Dart functions that allow views to @@ -188,27 +332,46 @@ execute complex logic without knowledge of its implementation. Commands are written as members of the view model class to be called by the gesture handlers in the view class. +命令以[命令模式][command pattern]命名, +是允许视图在不了解其实现细节的情况下执行复杂逻辑的 Dart 函数。 +命令作为视图模型类的成员编写, +由视图类中的手势处理器调用。 + You can find examples of views, view models, and commands on the [UI layer][] portion of the [App architecture case study][]. +你可以在[架构案例研究][App architecture case study]的 [UI 层][UI layer]部分找到视图、视图模型和命令的示例。 + For a gentle introduction to MVVM in Flutter, check out the [state management fundamentals][]. +如需了解 Flutter 中 MVVM 的入门介绍, +请查看[状态管理基础][state management fundamentals]。 + [UI layer]: /app-architecture/case-study/ui-layer [App architecture case study]: /app-architecture/case-study [state management fundamentals]: /data-and-backend/state-mgmt/intro ## Data layer +## 数据层 + The data layer of an app handles your business data and logic. Two pieces of architecture make up the data layer: services and repositories. These pieces should have well-defined inputs and outputs to simplify their reusability and testability. +应用的数据层处理业务数据和逻辑。 +数据层由两部分架构组成:Service 和 Repository。 +这些部分应该有定义良好的输入和输出, +以简化其可复用性和可测试性。 + A simplified diagram of the architecture described on this page with the Data layer highlighted. Using MVVM language, services and repositories make up your *model layer*. +使用 MVVM 的术语来说,Service 和 Repository 构成了你的*模型层*。 + ### Repositories [Repository][] classes are the source of truth for your model data. @@ -219,14 +382,29 @@ formatted in a way that your view model classes can consume. There should be a repository class for each different type of data handled in your app. +[Repository][] 类是模型数据的单一数据源。 +它们负责从 Service 轮询数据, +并将原始数据转换为**领域模型**。 +领域模型表示应用所需的数据, +以视图模型类可以使用的格式进行格式化。 +应用中处理的每种不同类型的数据都应该有一个对应的 Repository 类。 + Repositories handle the business logic associated with services, such as: +Repository 处理与 Service 相关的业务逻辑,例如: + * Caching + 缓存 * Error handling + 错误处理 * Retry logic + 重试逻辑 * Refreshing data + 刷新数据 * Polling services for new data + 轮询 Service 获取新数据 * Refreshing data based on user actions + 根据用户操作刷新数据 A simplified diagram of the architecture described on this page with the Repository object highlighted. @@ -235,16 +413,31 @@ For example, a social media app might have a `UserProfileRepository` class that exposes a `Stream`, which emits a new value whenever the user signs in or out. +Repository 以领域模型的形式输出应用数据。 +例如,一个社交媒体应用可能有一个 +`UserProfileRepository` 类,它暴露一个 `Stream`, +每当用户登录或退出时都会发出新值。 + The models output by repositories are consumed by view models. Repositories and view models have a many-to-many relationship. A view model can use many repositories to get the data it needs, and a repository can be used by many view models. +Repository 输出的模型由视图模型使用。 +Repository 和视图模型是多对多的关系。 +一个视图模型可以使用多个 Repository 来获取所需的数据, +一个 Repository 也可以被多个视图模型使用。 + Repositories should never be aware of each other. If your application has business logic that needs data from two repositories, you should combine the data in the view model or in the domain layer, especially if your repository-to-view-model relationship is complex. +Repository 之间不应该相互感知。 +如果你的应用有需要来自两个 Repository 数据的业务逻辑, +你应该在视图模型或领域层中合并数据, +尤其是当 Repository 与视图模型的关系比较复杂时。 + ### Services Services are in the lowest layer of your application. @@ -254,55 +447,94 @@ They're only used to isolate data-loading, and they hold no state. Your app should have one service class per data source. Examples of endpoints that services might wrap include: +Service 处于应用的最底层。 +它们封装 API 端点并暴露异步响应对象, +如 `Future` 和 `Stream` 对象。 +它们仅用于隔离数据加载,不持有任何状态。 +你的应用应该为每个数据源创建一个 Service 类。 +Service 可能封装的端点示例包括: + * The underlying platform, like iOS and Android APIs + 底层平台,如 iOS 和 Android API * REST endpoints + REST 端点 * Local files + 本地文件 As a rule of thumb, services are most helpful when the necessary data lives outside of your application's Dart code - which is true of each of the preceding examples. +根据经验,当所需数据存在于应用的 Dart 代码之外时, +Service 最为有用——上述每个示例都是如此。 + Services and repositories have a many-to-many relationship. A single Repository can use several services, and a service can be used by multiple repositories. +Service 和 Repository 是多对多的关系。 +一个 Repository 可以使用多个 Service, +一个 Service 也可以被多个 Repository 使用。 + A simplified diagram of the architecture described on this page with the Service object highlighted. ## Optional: Domain layer +## 可选:领域层 + As your app grows and adds features, you may need to abstract away logic that adds too much complexity to your view models. These classes are often called interactors or **use-cases**. +随着应用的增长和功能的增加,你可能需要将为视图模型带来过多复杂性的逻辑抽象出来。 +这些类通常被称为交互器或**用例**。 + Use-cases are responsible for making interactions between the UI and Data layers simpler and more reusable. They take data from repositories and make it suitable for the UI layer. +用例负责使 UI 层和数据层之间的交互更简单、更可复用。 +它们从 Repository 获取数据并使其适合 UI 层使用。 + MVVM design pattern with an added domain layer object Use-cases are primarily used to encapsulate business logic that would otherwise live in the view model and meets one or more of the following conditions: +用例主要用于封装原本存在于视图模型中的业务逻辑, +这些逻辑满足以下一个或多个条件: + 1. Requires merging data from multiple repositories + 需要合并来自多个 Repository 的数据 2. Is exceedingly complex + 逻辑极其复杂 3. The logic will be reused by different view models + 该逻辑将被不同的视图模型复用 This layer is optional because not all applications or features within an application have these requirements. If you suspect your application would benefit from this additional layer, consider the pros and cons: +这一层是可选的,因为并非所有应用或应用中的所有功能都有这些需求。 +如果你认为你的应用可以从这个额外的层中受益,请考虑其优缺点: + -| Pros | Cons | +| Pros | Cons | |--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| ✅ Avoid code duplication in view models | ❌ Increases complexity of your architecture, adding more classes and higher cognitive load | -| ✅ Improve testability by separating complex business logic from UI logic | ❌ Testing requires additional mocks | -| ✅ Improve code readability in view models | ❌ Adds additional boilerplate to your code | +| ✅ Avoid code duplication in view models | ❌ Increases complexity of your architecture, adding more classes and higher cognitive load | +| ✅ 避免视图模型中的代码重复 | ❌ 增加架构复杂性,添加更多类并提高认知负荷 | +| ✅ Improve testability by separating complex business logic from UI logic | ❌ Testing requires additional mocks | +| ✅ 通过将复杂的业务逻辑与 UI 逻辑分离来提高可测试性 | ❌ 测试需要额外的 mock 对象 | +| ✅ Improve code readability in view models | ❌ Adds additional boilerplate to your code | +| ✅ 提高视图模型中的代码可读性 | ❌ 为代码增加额外的样板代码 | {:.table .table-striped} ### Data access with use-cases +### 通过用例访问数据 + Another consideration when adding a Domain layer is whether view models will continue to have access to repository data directly, or if you'll enforce view models to go through use-cases to get their data. Put another way, @@ -311,11 +543,23 @@ Perhaps when you notice repeated logic in your view models? Or, will you create a use-case each time a view model needs data, even if the logic in the use-case is simple? +添加领域层时的另一个考虑因素是,视图模型是否仍然可以直接访问 Repository 数据, +还是强制视图模型通过用例来获取数据。换句话说, +你是在需要时才添加用例? +也许是当你注意到视图模型中有重复逻辑时? +还是每次视图模型需要数据时都创建用例, +即使用例中的逻辑很简单? + If you choose to do the latter, it intensifies the earlier outlined pros and cons. Your application code will be extremely modular and testable, but it also adds a significant amount of unnecessary overhead. +如果你选择后者, +它会加剧前面概述的优缺点。 +你的应用代码将非常模块化且可测试, +但也会增加大量不必要的开销。 + A good approach is to add use-cases only when needed. If you find that your view models are accessing data through use-cases most of the time, @@ -324,14 +568,26 @@ The example app used later in this guide has use-cases for some features, but also has view models that interact with repositories directly. A complex feature may ultimately end up looking like this: +一个好的方法是仅在需要时添加用例。 +如果你发现视图模型大部分时间都通过用例访问数据, +你可以随时重构代码以完全使用用例。 +本指南后面使用的示例应用对某些功能使用了用例, +但也有直接与 Repository 交互的视图模型。 +一个复杂的功能最终可能看起来像这样: + A simplified diagram of the architecture described on this page with a use case object. This method of adding use-cases is defined by the following rules: +这种添加用例的方法由以下规则定义: + * Use-cases depend on repositories + 用例依赖于 Repository * Use-cases and repositories have a many-to-many relationship + 用例和 Repository 是多对多的关系 * View models depend on one or more use-cases *and* one or more repositories + 视图模型依赖于一个或多个用例*以及*一个或多个 Repository This method of using use-cases ends up looking less like a layered lasagna, and more like a plated dinner with @@ -340,13 +596,25 @@ Use-cases are just utility classes that have well-defined inputs and outputs. This approach is flexible and extendable, but it requires greater diligence to maintain order. +这种使用用例的方法看起来 +不像是分层的千层面,而更像是一道有 +两个主菜(UI 层和数据层)和一个配菜(领域层)的晚餐。 +用例只是具有定义良好的输入和输出的工具类。 +这种方法灵活且可扩展, +但需要更加谨慎地维护秩序。 + [Separation-of-concerns]: https://en.wikipedia.org/wiki/Separation_of_concerns [command pattern]: https://en.wikipedia.org/wiki/Command_pattern [Repository]: https://martinfowler.com/eaaCatalog/repository.html ## Feedback +## 反馈 + As this section of the website is evolving, we [welcome your feedback][]! +由于网站的这个部分仍在不断完善中, +我们[欢迎你的反馈][welcome your feedback]! + [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="guide" diff --git a/src/content/app-architecture/index.md b/src/content/app-architecture/index.md index 8a6d829a8f..983b34cf0e 100644 --- a/src/content/app-architecture/index.md +++ b/src/content/app-architecture/index.md @@ -1,11 +1,16 @@ --- -title: Architecting Flutter apps -shortTitle: Architecture +# title: Architecting Flutter apps +title: 构建 Flutter 应用的架构 +# shortTitle: Architecture +shortTitle: 架构 +# description: > +# Learn how to structure Flutter apps. description: > - Learn how to structure Flutter apps. + 学习如何组织 Flutter 应用的结构。 showToc: false next: - title: Architecture concepts + # title: Architecture concepts + title: 架构概念 path: /app-architecture/concepts --- @@ -17,12 +22,20 @@ maintainable, resilient, and scalable Flutter app. In this guide, you'll learn app architecture principles and best practices for building Flutter apps. +架构是构建可维护、可靠且可扩展的 Flutter 应用的重要组成部分。 +在本指南中,你将学习应用架构原则以及构建 Flutter 应用的最佳实践。 + 'Architecture' is a word that's hard to define. It's a broad term and can refer to any number of topics depending on the context. In this guide, 'architecture' refers to how to structure, organize, and design your Flutter app in order to scale as your project requirements and team grow. +"架构"是一个很难定义的词。 +它是一个广泛的术语,根据上下文可以指代许多不同的主题。 +在本指南中,"架构"是指如何组织、规划和设计你的 Flutter 应用, +以便随着项目需求和团队的增长而进行扩展。 +
Hero image @@ -32,6 +45,8 @@ your Flutter app in order to scale as your project requirements and team grow. ## What you'll learn +## 你将学到什么 + * Benefits of intentional architecture * Common architectural principles * The Flutter team's recommended app architecture @@ -39,35 +54,67 @@ your Flutter app in order to scale as your project requirements and team grow. * Dependency injection * Common design patterns for writing robust Flutter applications +* 有意识地进行架构设计的好处 +* 常见的架构原则 +* Flutter 团队推荐的应用架构 +* MVVM 和状态管理 +* 依赖注入 +* 编写健壮的 Flutter 应用的常见设计模式 + {% comment %} TODO @ewindmill complete this list as pages land, add links. {% endcomment %} ## Benefits of intentional architecture +## 有意识地进行架构设计的好处 + Good app architecture provides a number of benefits to engineering teams and their end users. +良好的应用架构为工程团队及其最终用户提供了许多好处。 + * Maintainability - App architecture makes it easier to modify, update, and fix issues over time. + + 可维护性——应用架构使得随着时间的推移,修改、更新和修复问题变得更加容易。 + * Scalability - A well thought out application allows more people to contribute to the same codebase concurrently, with minimal code conflicts. + + 可扩展性——经过深思熟虑的应用允许更多人同时为同一代码库做出贡献,并且代码冲突最少。 + * Testability - Applications with intentional architecture generally have simpler classes with well-defined inputs and outputs, which makes them easier to mock and test. + + 可测试性——有意识地进行架构设计的应用通常拥有更简单的类,具有明确定义的输入和输出,这使得它们更容易进行模拟和测试。 + * Lower cognitive load - Developers who are new to the project will be more productive in a shorter amount of time, and code reviews are generally less time-consuming when code is easier to understand. + + 更低的认知负担——新加入项目的开发者可以在更短的时间内提高生产力,当代码更容易理解时,代码审查通常也不那么耗时。 + * A better user experience - Features can ship faster and with fewer bugs. + 更好的用户体验——功能可以更快地交付,并且 bug 更少。 + ## How to use this guide +## 如何使用本指南 + This is a guide for building scalable Flutter applications and was written for teams that have multiple developers contributing to the same code base, who're building a feature-rich application. If you're writing a Flutter app that has a *growing team and codebase*, this guidance is for you. +这是一份用于构建可扩展 Flutter 应用的指南,专为多个开发者在同一代码库中协作、 +构建功能丰富的应用的团队而编写。 +如果你正在编写一个拥有*不断增长的团队和代码库*的 Flutter 应用, +本指南就是为你准备的。 + Along with general architectural advice, this guide gives concrete examples of best practices and includes specific recommendations. Some libraries can be swapped out, and very large teams with unique complexity @@ -75,6 +122,11 @@ might find that some parts don't apply. In either case, the ideas remain sound. This is the recommended way to build a Flutter app. +除了一般的架构建议外,本指南还提供了最佳实践的具体示例,并包含特定的建议。 +某些库可以替换,而拥有独特复杂性的大型团队可能会发现某些部分并不适用。 +无论哪种情况,这些理念都是合理的。 +这是构建 Flutter 应用的推荐方式。 + In the first part of this guide, you'll learn about common architectural principles from a high level. In the second part, the guide walks through specific and @@ -82,6 +134,10 @@ concrete recommendations of architecting Flutter apps. Finally, at the end of the guide, you'll find a list of design patterns and sample code that shows the recommendations in action. +在本指南的第一部分,你将从较高的层面了解常见的架构原则。 +在第二部分,本指南将介绍构建 Flutter 应用架构的具体建议。 +最后,在指南的末尾,你将找到一系列设计模式和示例代码,展示这些建议的实际应用。 + [Common architectural principles]: /app-architecture/concepts [recommended app architecture]: /app-architecture/guide [MVVM]: /app-architecture/guide#mvvm @@ -89,7 +145,12 @@ sample code that shows the recommendations in action. ## Feedback +## 反馈 + As this section of the website is evolving, we [welcome your feedback][]! +由于网站的这一部分仍在不断完善中, +我们 [欢迎你的反馈][welcome your feedback]! + [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="index" diff --git a/src/content/app-architecture/recommendations.md b/src/content/app-architecture/recommendations.md index 0d5ad368c4..4c4751ecdc 100644 --- a/src/content/app-architecture/recommendations.md +++ b/src/content/app-architecture/recommendations.md @@ -1,13 +1,19 @@ --- -title: Architecture recommendations and resources -shortTitle: Architecture recommendations +# title: Architecture recommendations and resources +title: 架构推荐与资源 +# shortTitle: Architecture recommendations +shortTitle: 架构推荐 +# description: > +# Recommendations for building scalable Flutter applications. description: > - Recommendations for building scalable Flutter applications. + 构建可扩展 Flutter 应用的推荐方案。 prev: - title: Architecture case study + # title: Architecture case study + title: 架构案例研究 path: /app-architecture/case-study next: - title: Design patterns + # title: Design patterns + title: 设计模式 path: /app-architecture/design-patterns --- @@ -17,47 +23,87 @@ You should treat these recommendations as recommendations, and not steadfast rules, and you should adapt them to your app's unique requirements. +本页介绍了架构最佳实践、它们的重要性, +以及我们是否推荐你在 Flutter 应用中采用它们。 +你应该将这些推荐视为建议, +而非一成不变的规则, +并根据应用的独特需求进行调整。 + The best practices on this page have a priority, which reflects how strongly the Flutter team recommends it. +本页的最佳实践都有一个优先级, +反映了 Flutter 团队的推荐程度。 + * **Strongly recommend:** You should always implement this recommendation if you're starting to build a new application. You should strongly consider refactoring an existing app to implement this practice unless doing so would fundamentally clash with your current approach. + + **强烈推荐:** 如果你正在构建一个新应用,应该始终采用此推荐。 + 对于现有应用,你也应该认真考虑通过重构来实施此实践, + 除非这样做会与你当前的方案产生根本性冲突。 + * **Recommend**: This practice will likely improve your app. + + **推荐:** 此实践可能会改善你的应用。 + * **Conditional**: This practice can improve your app in certain circumstances. + **视情况而定:** 此实践在某些情况下可以改善你的应用。 + ## Separation of concerns -You should separate your app into a UI layer and a data layer. Within those layers, +## 关注点分离 + +You should separate your app into a UI layer and a data layer. Within those layers, you should further separate logic into classes by responsibility. +你应该将应用分为 UI 层和数据层。在每一层中, +你还应该按职责将逻辑进一步拆分到不同的类中。 + ## Handling data +## 数据处理 + Handling data with care makes your code easier to understand, less error prone, and prevents malformed or unexpected data from being created. +谨慎地处理数据可以使你的代码更易于理解、更不容易出错, +并防止创建格式错误或意外的数据。 + ## App structure +## 应用结构 + Well organized code benefits both the health of the app itself, and the team working on the code. +良好的代码组织既有利于应用本身的健康发展,也有利于团队的协作开发。 + ## Testing -Good testing practices makes your app flexible. +## 测试 + +Good testing practices makes your app flexible. It also makes it straightforward and low risk to add new logic and new UI. +良好的测试实践使你的应用更具灵活性, +也使添加新逻辑和新 UI 变得简单且低风险。 + ## Recommended resources {:#resources} +## 推荐资源 {:#resources} + * Code and templates * [Compass app source code][] - Source code of a full-featured, robust Flutter application that @@ -66,6 +112,16 @@ It also makes it straightforward and low risk to add new logic and new UI. A Flutter application template made by the Flutter experts Very Good Ventures. This template generates a similar app structure. + +* 代码和模板 + * [Compass app source code][] - + 一个功能完整、健壮的 Flutter 应用的源代码, + 实现了本页中的许多推荐方案。 + * [very_good_cli][] - + 由 Flutter 专家团队 Very Good Ventures 制作的 + Flutter 应用模板。 + 该模板生成类似的应用结构。 + * Documentation * [Very Good Engineering architecture documentation][] - Very Good Engineering is a documentation site by VGV that has @@ -74,6 +130,16 @@ It also makes it straightforward and low risk to add new logic and new UI. * [State Management with ChangeNotifier walkthrough][] - A gentle introduction into using the primitives in the Flutter SDK for your state management. + +* 文档 + * [Very Good Engineering architecture documentation][] - + Very Good Engineering 是 VGV 的一个文档网站,包含技术文章、 + 演示和开源项目。 + 其中包括 Flutter 应用架构的相关文档。 + * [State Management with ChangeNotifier walkthrough][] - + 一个关于使用 Flutter SDK 中的基本构件 + 进行状态管理的入门介绍。 + * Tooling * [Flutter developer tools][] - DevTools is a suite of performance and debugging tools for Dart and Flutter. @@ -82,6 +148,13 @@ It also makes it straightforward and low risk to add new logic and new UI. Flutter apps recommended by the Flutter team. Use this package to encourage good coding practices across a team. +* 工具 + * [Flutter developer tools][] - + DevTools 是一套用于 Dart 和 Flutter 的性能和调试工具。 + * [flutter_lints][] - + 一个包含 Flutter 团队推荐的 Flutter 应用 lint 规则的 package。 + 使用此 package 可以在团队中推广良好的编码实践。 + [Compass app source code]: https://github.com/flutter/samples/tree/main/compass_app [very_good_cli]: https://cli.vgv.dev/ @@ -92,7 +165,12 @@ It also makes it straightforward and low risk to add new logic and new UI. ## Feedback +## 反馈 + As this section of the website is evolving, we [welcome your feedback][]! +由于网站的这一部分仍在不断完善中, +我们 [欢迎你的反馈][welcome your feedback]! + [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="recommendations" From 99a0e34a777d11f023bdb2718aa63a070d986368 Mon Sep 17 00:00:00 2001 From: XinLei Date: Sun, 8 Mar 2026 11:30:22 -0700 Subject: [PATCH 4/9] =?UTF-8?q?[=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91]=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F=E4=B8=8E=20UI=20=E8=87=AA?= =?UTF-8?q?=E9=80=82=E5=BA=94=20(3=E7=AF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 翻译设计模式与 UI 相关文档: - app-architecture/design-patterns/optimistic-state.md - 乐观状态 - app-architecture/design-patterns/offline-first.md - 离线优先 - ui/adaptive-responsive/safearea-mediaquery.md - SafeArea 与 MediaQuery Co-Authored-By: Claude Opus 4.6 --- .../design-patterns/offline-first.md | 262 +++++++++++++++++- .../design-patterns/optimistic-state.md | 226 +++++++++++++-- .../safearea-mediaquery.md | 100 ++++++- 3 files changed, 558 insertions(+), 30 deletions(-) diff --git a/src/content/app-architecture/design-patterns/offline-first.md b/src/content/app-architecture/design-patterns/offline-first.md index 1cd16fec9c..d8703d2f16 100644 --- a/src/content/app-architecture/design-patterns/offline-first.md +++ b/src/content/app-architecture/design-patterns/offline-first.md @@ -1,6 +1,8 @@ --- -title: "Offline-first support" -description: Implement offline-first support for one feature in an application. +# title: "Offline-first support" +title: "离线优先支持" +# description: Implement offline-first support for one feature in an application. +description: 在应用中为某个功能实现离线优先支持。 contentTags: - data - user experience @@ -17,6 +19,11 @@ Offline-first applications usually rely on stored data to offer users temporary access to data that would otherwise only be available online. +离线优先应用是一种即使在断开互联网连接的情况下, +也能提供大部分或全部功能的应用。 +离线优先应用通常依赖于本地存储的数据, +为用户提供对原本只能在线获取的数据的临时访问。 + Some offline-first applications combine local and remote data seamlessly, while other applications inform the user when the application is using cached data. @@ -24,15 +31,28 @@ In the same way, some applications synchronize data in the background while others require the user to explicitly synchronize it. It all depends on the application requirements and the functionality it offers, -and it’s up to the developer to decide which implementation fits their needs. +and it's up to the developer to decide which implementation fits their needs. + +一些离线优先应用无缝地结合了本地和远程数据, +而另一些应用则会在使用缓存数据时通知用户。 +同样地,一些应用在后台同步数据, +而另一些则需要用户手动触发同步。 +这一切取决于应用的需求和它所提供的功能, +开发者需要自行决定哪种实现方式最适合自己的需求。 In this guide, you will learn how to implement different approaches to offline-first applications in Flutter, following the [Flutter Architecture guidelines][]. +在本指南中, +你将学习如何按照 [Flutter 架构指南][Flutter Architecture guidelines], +在 Flutter 中实现离线优先应用的不同方法。 + ## Offline-first architecture +## 离线优先架构 + As explained in the common architecture concepts guide, repositories act as the single source of truth. They are responsible for presenting local or remote data, @@ -42,17 +62,35 @@ repositories combine different local and remote data sources to present data in a single access point, independently of the connectivity state of the device. +正如通用架构概念指南中所解释的, +Repository 充当单一数据源的角色。 +它们负责提供本地或远程数据, +并且应该是唯一可以修改数据的地方。 +在离线优先应用中, +Repository 将不同的本地和远程数据源组合在一起, +通过单一访问点提供数据, +而不依赖于设备的网络连接状态。 + This example uses the `UserProfileRepository`, a repository that allows you to obtain and store `UserProfile` objects with offline-first support. +本示例使用了 `UserProfileRepository`, +这是一个允许你获取和存储 `UserProfile` 对象并支持离线优先的 Repository。 + The `UserProfileRepository` uses two different data services: one works with remote data, and the other works with a local database. +`UserProfileRepository` 使用了两种不同的数据服务: +一个用于处理远程数据, +另一个用于处理本地数据库。 + The API client,`ApiClientService`, connects to a remote service using HTTP REST calls. +API 客户端 `ApiClientService` 通过 HTTP REST 调用连接到远程服务。 + ```dart class ApiClientService { @@ -71,6 +109,9 @@ class ApiClientService { The database service, `DatabaseService`, stores data using SQL, similar to the one found in the [Persistent Storage Architecture: SQL][] recipe. +数据库服务 `DatabaseService` 使用 SQL 存储数据, +类似于 [持久化存储架构:SQL][Persistent Storage Architecture: SQL] 示例中的实现。 + ```dart class DatabaseService { @@ -90,6 +131,8 @@ class DatabaseService { This example also uses the `UserProfile` data class that has been created using the [`freezed`][] package. +本示例还使用了通过 [`freezed`][] 包创建的 `UserProfile` 数据类。 + ```dart @freezed @@ -112,10 +155,24 @@ and then `UserProfile` for the UI data model class. The `UserProfileRepository` would take care of converting from one to the other when necessary. +在数据结构复杂的应用中, +例如当远程数据包含的字段多于 UI 所需时, +你可能需要为 API 和数据库服务使用一个数据类, +为 UI 使用另一个数据类。 +例如, +`UserProfileLocal` 用于数据库实体, +`UserProfileRemote` 用于 API 响应对象, +然后 `UserProfile` 用于 UI 数据模型类。 +`UserProfileRepository` 会在需要时负责在不同数据类之间进行转换。 + This example also includes the `UserProfileViewModel`, a view model that uses the `UserProfileRepository` to display the `UserProfile` on a widget. +本示例还包含 `UserProfileViewModel`, +这是一个使用 `UserProfileRepository` +在 widget 上显示 `UserProfile` 的视图模型。 + ```dart class UserProfileViewModel extends ChangeNotifier { @@ -139,31 +196,56 @@ class UserProfileViewModel extends ChangeNotifier { ## Reading data +## 读取数据 + Reading data is a fundamental part of any application that relies on remote API services. +读取数据是任何依赖远程 API 服务的应用的基本功能。 + In offline-first applications, you want to ensure that the access to this data is as fast as possible, -and that it doesn’t depend on the device being online +and that it doesn't depend on the device being online to provide data to the user. This is similar to the [Optimistic State design pattern][]. +在离线优先应用中, +你需要确保对数据的访问尽可能快, +并且不依赖于设备在线才能向用户提供数据。 +这与 [乐观状态设计模式][Optimistic State design pattern] 类似。 + In this section, you will learn two different approaches, one that uses the database as a fallback, and one that combines local and remote data using a `Stream`. +在本节中, +你将学习两种不同的方法, +一种是将数据库作为备选方案, +另一种是使用 `Stream` 将本地和远程数据结合起来。 + ### Using local data as a fallback +### 将本地数据作为备选方案 + As a first approach, you can implement offline support by having a fallback mechanism for when the user is offline or a network call fails. +作为第一种方法, +你可以通过设置备选机制来实现离线支持, +用于处理用户离线或网络调用失败的情况。 + In this case, the `UserProfileRepository` attempts to obtain the `UserProfile` from the remote API server using the `ApiClientService`. If this request fails, then returns the locally stored `UserProfile` from the `DatabaseService`. +在这种情况下,`UserProfileRepository` 尝试通过 `ApiClientService` +从远程 API 服务器获取 `UserProfile`。 +如果请求失败, +则从 `DatabaseService` 返回本地存储的 `UserProfile`。 + ```dart Future getUserProfile() async { @@ -193,19 +275,34 @@ Future getUserProfile() async { ### Using a Stream +### 使用 Stream + A better alternative presents the data using a `Stream`. In the best case scenario, the `Stream` emits two values, the locally stored data, and the data from the server. +更好的替代方案是使用 `Stream` 来提供数据。 +在最理想的情况下, +`Stream` 会发出两个值, +分别是本地存储的数据和来自服务器的数据。 + First, the stream emits the locally stored data using the `DatabaseService`. This call is generally faster and less error prone than a network call, and by doing it first the view model can already display data to the user. +首先,Stream 使用 `DatabaseService` 发出本地存储的数据。 +这个调用通常比网络调用更快、更不容易出错, +通过优先执行这个调用,视图模型可以先向用户展示数据。 + If the database does not contain any cached data, then the `Stream` relies completely on the network call, emitting only one value. +如果数据库中没有任何缓存数据, +则 `Stream` 完全依赖于网络调用, +只发出一个值。 + Then, the method performs the network call using the `ApiClientService` to obtain up-to-date data. If the request was successful, @@ -213,6 +310,13 @@ it updates the database with the newly obtained data, and then yields the value to the view model, so it can be displayed to the user. +然后,该方法通过 `ApiClientService` 执行网络调用 +以获取最新数据。 +如果请求成功, +它会用新获取的数据更新数据库, +然后将值传递给视图模型, +以便向用户展示。 + ```dart Stream getUserProfile() async* { @@ -240,10 +344,17 @@ The view model must subscribe to this `Stream` and wait until it has completed. For that, call `asFuture()` with the `Subscription` object and await the result. +视图模型必须订阅这个 `Stream` 并等待其完成。 +为此,需要对 `Subscription` 对象调用 `asFuture()` 并等待结果。 + For each obtained value, update the view model data and call `notifyListeners()` so the UI shows the latest data. +对于每个获取到的值, +更新视图模型的数据并调用 `notifyListeners()`, +以便 UI 显示最新的数据。 + ```dart Future load() async { @@ -261,13 +372,20 @@ Future load() async { .asFuture(); } ``` + ### Using only local data +### 仅使用本地数据 + Another possible approach uses locally stored data for read operations. This approach requires that the data has been preloaded at some point into the database, and requires a synchronization mechanism that can keep the data up to date. +另一种可行的方法是仅使用本地存储的数据进行读取操作。 +这种方法要求数据在某个时间点已经预加载到数据库中, +并且需要一个同步机制来保持数据的更新。 + ```dart @@ -297,42 +415,73 @@ Future sync() async { ``` This approach can be useful for applications -that don’t require data to be in sync with the server at all times. +that don't require data to be in sync with the server at all times. For example, a weather application where the weather data is only updated once a day. +这种方法适用于不需要数据始终与服务器保持同步的应用。 +例如,一个天气应用, +其天气数据每天只更新一次。 + Synchronization could be done manually by the user, for example, a pull-to-refresh action that then calls the `sync()` method, or done periodically by a `Timer` or a background process. You can learn how to implement a synchronization task in the section about synchronizing state. +同步可以由用户手动完成, +例如通过下拉刷新操作调用 `sync()` 方法, +也可以通过 `Timer` 或后台进程定期完成。 +你可以在关于同步状态的章节中学习如何实现同步任务。 + ## Writing data +## 写入数据 + Writing data in offline-first applications depends fundamentally on the application use case. +在离线优先应用中写入数据, +从根本上取决于应用的使用场景。 + Some applications might require the user input data to be immediately available on the server side, while other applications might be more flexible and allow data to be out-of-sync temporarily. +一些应用可能要求用户输入的数据立即在服务器端可用, +而另一些应用可能更灵活, +允许数据暂时不同步。 + This section explains two different approaches for implementing writing data in offline-first applications. +本节介绍了在离线优先应用中实现数据写入的两种不同方法。 + ### Online-only writing +### 仅在线写入 + One approach for writing data in offline-first applications is to enforce being online to write data. While this might sound counterintuitive, this ensures that the data the user has modified is fully synchronized with the server, -and the application doesn’t have a different state than the server. +and the application doesn't have a different state than the server. + +在离线优先应用中写入数据的一种方法是强制要求在线才能写入数据。 +虽然这听起来可能有些矛盾, +但这确保了用户修改的数据与服务器完全同步, +应用不会与服务器存在不同的状态。 In this case, you first attempt to send the data to the API service, and if the request succeeds, then store the data in the database. +在这种情况下,你首先尝试将数据发送到 API 服务, +如果请求成功, +则将数据存储到数据库中。 + ```dart Future updateUserProfile(UserProfile userProfile) async { @@ -353,13 +502,23 @@ The disadvantage in this case is that the offline-first functionality is only available for read operations, but not for write operations, as those require the user being online. +这种情况下的缺点是离线优先功能仅适用于读取操作, +而不适用于写入操作,因为写入操作需要用户处于在线状态。 + ### Offline-first writing +### 离线优先写入 + The second approach works the other way around. Instead of performing the network call first, the application first stores the new data in the database, and then attempts to send it to the API service once it has been stored locally. +第二种方法的工作方式恰好相反。 +应用不是先执行网络调用, +而是先将新数据存储到数据库中, +然后在本地存储完成后尝试将其发送到 API 服务。 + ```dart Future updateUserProfile(UserProfile userProfile) async { @@ -383,8 +542,16 @@ In the next section, you will learn different approaches to handle synchronization between local and remote data. +这种方法允许用户在应用离线时也能在本地存储数据, +但是,如果网络调用失败, +本地数据库和 API 服务将不再同步。 +在下一节中, +你将学习处理本地和远程数据之间同步的不同方法。 + ## Synchronizing state +## 同步状态 + Keeping the local and remote data in sync is an important part of offline-first applications, as the changes that have been done locally @@ -392,16 +559,28 @@ need to be copied to the remote service. The app must also ensure that, when the user goes back to the application, the locally stored data is the same as in the remote service. +保持本地和远程数据的同步是离线优先应用的重要组成部分, +因为在本地所做的更改需要复制到远程服务。 +应用还必须确保当用户返回应用时, +本地存储的数据与远程服务中的数据一致。 + ### Writing a synchronization task +### 编写同步任务 + There are different approaches for implementing synchronization in a background task. +在后台任务中实现同步有多种不同的方法。 + A simple solution is to create a `Timer` in the `UserProfileRepository` that runs periodically, for example every five minutes. +一个简单的解决方案是在 `UserProfileRepository` 中创建一个定期运行的 `Timer`, +例如每五分钟运行一次。 + ```dart Timer.periodic(const Duration(minutes: 5), (timer) => sync()); @@ -410,6 +589,9 @@ Timer.periodic(const Duration(minutes: 5), (timer) => sync()); The `sync()` method then fetches the `UserProfile` from the database, and if it requires synchronization, it is then sent to the API service. +`sync()` 方法从数据库中获取 `UserProfile`, +如果需要同步,则将其发送到 API 服务。 + ```dart Future sync() async { @@ -440,34 +622,61 @@ like the [`workmanager`][] plugin. This allows your application to run the synchronization process in the background even when the application is not running. +更复杂的解决方案使用后台进程, +例如 [`workmanager`][] 插件。 +这允许你的应用在后台运行同步过程, +即使应用没有在运行也可以。 + :::note Running background operations continuously can drain the device battery dramatically, and some devices limit the background processing capabilities, so this approach needs to be tuned to the application requirements and one solution might not fit all cases. + +持续运行后台操作会大量消耗设备电池, +并且一些设备会限制后台处理能力, +因此这种方法需要根据应用需求进行调整, +一种解决方案可能无法适用于所有情况。 ::: -It’s also recommended to only perform the synchronization task +It's also recommended to only perform the synchronization task when the network is available. For example, you can use the [`connectivity_plus`][] plugin to check if the device is connected to WiFi. You can also use [`battery_plus`][] to verify that the device is not running low on battery. +还建议仅在网络可用时执行同步任务。 +例如,你可以使用 [`connectivity_plus`][] 插件检查设备是否连接到 WiFi。 +你还可以使用 [`battery_plus`][] 来验证设备电量是否充足。 + In the previous example, the synchronization task runs every 5 minutes. In some cases, that might be excessive, while in others it might not be frequent enough. The actual synchronization period time for your application -depends on your application needs and it’s something you will have to decide. +depends on your application needs and it's something you will have to decide. + +在前面的示例中,同步任务每 5 分钟运行一次。 +在某些情况下,这可能过于频繁, +而在另一些情况下,可能不够频繁。 +你的应用的实际同步周期时间取决于应用需求, +这是你需要自行决定的。 ### Storing a synchronization flag +### 存储同步标志 + To know if the data requires synchronization, add a flag to the data class indicating if the changes need to be synchronized. +要知道数据是否需要同步, +可以在数据类中添加一个标志来指示更改是否需要同步。 + For example, `bool synchronized`: +例如,`bool synchronized`: + ```dart @freezed @@ -485,34 +694,62 @@ to send it to the API service only when the `synchronized` flag is `false`. If the request is successful, then change it to `true`. +你的同步逻辑应该仅在 `synchronized` 标志为 `false` 时 +尝试将数据发送到 API 服务。 +如果请求成功,则将其更改为 `true`。 + ### Pushing data from server +### 从服务器推送数据 + A different approach for synchronization is to use a push service to provide up-to-date data to the application. In this case, the server notifies the application when data has changed, instead of being the application asking for updates. +另一种同步方法是使用推送服务向应用提供最新数据。 +在这种情况下,当数据发生变化时由服务器通知应用, +而不是由应用主动请求更新。 + For example, you can use [Firebase messaging][], to push small payloads of data to the device, as well as trigger synchronization tasks remotely using background messages. +例如,你可以使用 [Firebase messaging][] +向设备推送小量数据, +以及通过后台消息远程触发同步任务。 + Instead of having a synchronization task running in the background, the server notifies the application when the stored data needs to be updated with a push notification. +服务器通过推送通知在存储的数据需要更新时通知应用, +而不是让同步任务在后台持续运行。 + You can combine both approaches together, having a background synchronization task and using background push messages, to keep the application database synchronized with the server. +你可以将两种方法结合使用, +既有后台同步任务,又使用后台推送消息, +以保持应用数据库与服务器的同步。 + ## Putting it all together +## 总结 + Writing an offline-first application requires making decisions regarding the way read, write and sync operations are implemented, which depend on the requirements from the application you are developing. +编写离线优先应用需要就读取、写入和同步操作的实现方式做出决策, +这些决策取决于你正在开发的应用的需求。 + The key takeaways are: +关键要点如下: + - When reading data, you can use a `Stream` to combine locally stored data with remote data. - When writing data, @@ -522,6 +759,15 @@ and if you need synchronizing data later or not. take into account the device status and your application needs, as different applications may have different requirements. +- 读取数据时, + 你可以使用 `Stream` 将本地存储的数据与远程数据结合起来。 +- 写入数据时, + 决定是否需要在线或离线操作, + 以及是否需要稍后同步数据。 +- 实现后台同步任务时, + 需要考虑设备状态和应用需求, + 因为不同的应用可能有不同的要求。 + [Flutter Architecture guidelines]:/app-architecture [Persistent Storage Architecture: SQL]:/app-architecture/design-patterns/sql [`freezed`]:{{site.pub}}/packages/freezed diff --git a/src/content/app-architecture/design-patterns/optimistic-state.md b/src/content/app-architecture/design-patterns/optimistic-state.md index f1204081b6..ee068c2ada 100644 --- a/src/content/app-architecture/design-patterns/optimistic-state.md +++ b/src/content/app-architecture/design-patterns/optimistic-state.md @@ -1,6 +1,8 @@ --- -title: Optimistic state -description: "Improve the perception of responsiveness of an application by implementing optimistic state." +# title: Optimistic state +title: 乐观状态 +# description: "Improve the perception of responsiveness of an application by implementing optimistic state." +description: "通过实现乐观状态来提升应用的响应感知体验。" contentTags: - user experience - asynchronous dart @@ -13,28 +15,49 @@ order: 0 When building user experiences, the perception of performance is sometimes just as important as the actual performance of the code. -In general, users don’t like waiting for an action to finish to see the result, -and anything that takes more than a few milliseconds could be considered “slow” -or “unresponsive” from the user’s perspective. +In general, users don't like waiting for an action to finish to see the result, +and anything that takes more than a few milliseconds could be considered "slow" +or "unresponsive" from the user's perspective. + +在构建用户体验时, +性能的感知有时与代码的实际性能同样重要。 +通常,用户不喜欢等待操作完成才能看到结果, +从用户的角度来看,任何超过几毫秒的操作都可能被认为是"慢"或"无响应"的。 Developers can help mitigate this negative perception by presenting a successful UI state before the background task is fully completed. -An example of this would be tapping a “Subscribe” button, -and seeing it change to “Subscribed” instantly, +An example of this would be tapping a "Subscribe" button, +and seeing it change to "Subscribed" instantly, even if the background call to the subscription API is still running. +开发者可以通过在后台任务完全完成之前 +就展示成功的 UI 状态来帮助缓解这种负面感知。 +例如,点击"Subscribe"按钮后, +即使后台对订阅 API 的调用仍在运行, +按钮也会立即变为"Subscribed"。 + This technique is known as Optimistic State, Optimistic UI or Optimistic User Experience. In this recipe, you will implement an application feature using Optimistic State and following the [Flutter architecture guidelines][]. +这种技术被称为乐观状态(Optimistic State)、乐观 UI 或乐观用户体验。 +在本教程中, +你将按照 [Flutter 架构指南][Flutter architecture guidelines] +使用乐观状态来实现一个应用功能。 + ## Example feature: a subscribe button +## 示例功能:订阅按钮 + This example implements a subscribe button similar to the one you could find in a video streaming application or a newsletter. +本示例实现了一个订阅按钮, +类似于你在视频流媒体应用或新闻订阅中看到的那种。 + Application with subscribe button @@ -46,31 +69,59 @@ For demo purposes, you will not implement the actual backend code, instead you will replace this call with a fake action that will simulate a network request. +当按钮被点击时,应用会调用一个外部 API, +执行订阅操作, +例如在数据库中记录用户已加入订阅列表。 +出于演示目的,你不会实现实际的后端代码, +而是用一个模拟网络请求的假操作来替代。 + In the case that the call is successful, -the button text will change from “Subscribe” to “Subscribed”. +the button text will change from "Subscribe" to "Subscribed". The button background color will change as well. +如果调用成功, +按钮文本将从"Subscribe"变为"Subscribed"。 +按钮的背景颜色也会随之改变。 + On the contrary, if the call fails, -the button text should revert back to “Subscribe”, +the button text should revert back to "Subscribe", and the UI should show an error message to the user, for example using a Snackbar. +相反,如果调用失败, +按钮文本应恢复为"Subscribe", +并且 UI 应向用户显示错误消息, +例如使用 Snackbar。 + Following the Optimistic State idea, -the button should instantly change to “Subscribed” once it is tapped, -and only change back to “Subscribe” if the request failed. +the button should instantly change to "Subscribed" once it is tapped, +and only change back to "Subscribe" if the request failed. + +遵循乐观状态的理念, +按钮在被点击后应立即变为"Subscribed", +仅在请求失败时才恢复为"Subscribe"。 Animation of application with subscribe button ## Feature architecture +## 功能架构 + Start by defining the feature architecture. Following the architecture guidelines, create these Dart classes in a Flutter project: +首先定义功能架构。 +按照架构指南, +在 Flutter 项目中创建以下 Dart 类: + - A `StatefulWidget` named `SubscribeButton` + 一个名为 `SubscribeButton` 的 `StatefulWidget` - A class named `SubscribeButtonViewModel` extending `ChangeNotifier` + 一个名为 `SubscribeButtonViewModel` 的类,继承自 `ChangeNotifier` - A class named `SubscriptionRepository` + 一个名为 `SubscriptionRepository` 的类 ```dart @@ -96,19 +147,32 @@ class SubscriptionRepository {} The `SubscribeButton` widget and the `SubscribeButtonViewModel` represent the presentation layer of this solution. The widget is going to display a button -that will show the text “Subscribe” or “Subscribed” +that will show the text "Subscribe" or "Subscribed" depending on the subscription state. The view model will contain the subscription state. When the button is tapped, the widget will call the view model to perform the action. +`SubscribeButton` widget 和 `SubscribeButtonViewModel` 代表了该方案的展示层。 +该 widget 将显示一个按钮, +根据订阅状态显示文本"Subscribe"或"Subscribed"。 +视图模型将包含订阅状态。 +当按钮被点击时, +widget 将调用视图模型来执行操作。 + The `SubscriptionRepository` will implement a subscribe method that will throw an exception when the action fails. The view model will call this method when performing the subscription action. +`SubscriptionRepository` 将实现一个订阅方法, +当操作失败时会抛出异常。 +视图模型在执行订阅操作时将调用此方法。 + Next, connect them together by adding the `SubscriptionRepository` to the `SubscribeButtonViewModel`: +接下来,通过将 `SubscriptionRepository` 添加到 `SubscribeButtonViewModel` 来将它们连接在一起: + ```dart class SubscribeButtonViewModel extends ChangeNotifier { @@ -120,6 +184,8 @@ class SubscribeButtonViewModel extends ChangeNotifier { And add the `SubscribeButtonViewModel` to the `SubscribeButton` widget: +然后将 `SubscribeButtonViewModel` 添加到 `SubscribeButton` widget 中: + ```dart class SubscribeButton extends StatefulWidget { @@ -136,6 +202,9 @@ class SubscribeButton extends StatefulWidget { Now that you have created the basic solution architecture, you can create the `SubscribeButton` widget the following way: +现在你已经创建了基本的方案架构, +可以按以下方式创建 `SubscribeButton` widget: + ```dart SubscribeButton( @@ -144,11 +213,16 @@ SubscribeButton( ), ) ``` + ### Implement the `SubscriptionRepository` +### 实现 `SubscriptionRepository` + Add a new asynchronous method named `subscribe()` to the `SubscriptionRepository` with the following code: +向 `SubscriptionRepository` 添加一个名为 `subscribe()` 的异步方法,代码如下: + ```dart class SubscriptionRepository { @@ -166,16 +240,28 @@ The call to `await Future.delayed()` with a duration of one second has been added to simulate a long running request. The method execution will pause for a second, and then it will continue running. +调用 `await Future.delayed()` 并设置一秒的持续时间是为了模拟一个长时间运行的请求。 +方法执行将暂停一秒,然后继续运行。 + In order to simulate a request failing, the subscribe method throws an exception at the end. This will be used later on to show how to recover from a failed request when implementing Optimistic State. +为了模拟请求失败, +subscribe 方法在最后抛出了一个异常。 +这将在后面用于展示在实现乐观状态时如何从失败的请求中恢复。 + ### Implement the `SubscribeButtonViewModel` +### 实现 `SubscribeButtonViewModel` + To represented the subscription state, as well a possible error state, add the following public members to the `SubscribeButtonViewModel`: +为了表示订阅状态以及可能的错误状态, +向 `SubscribeButtonViewModel` 添加以下公共成员: + ```dart // Whether the user is subscribed @@ -187,17 +273,29 @@ bool error = false; Both are set to `false` on start. +两者在初始时都设置为 `false`。 + Following the ideas of Optimistic State, the `subscribed` state will change to `true` as soon as the user taps the subscribe button. And will only change back to `false` if the action fails. +遵循乐观状态的理念, +一旦用户点击订阅按钮,`subscribed` 状态将变为 `true`。 +仅在操作失败时才会恢复为 `false`。 + The `error` state will change to `true` when the action fails, indicating the `SubscribeButton` widget to show an error message to the user. The variable should go back to `false` once the error has been displayed. +当操作失败时,`error` 状态将变为 `true`, +通知 `SubscribeButton` widget 向用户显示错误消息。 +一旦错误消息已显示,该变量应恢复为 `false`。 + Next, implement an asynchronous `subscribe()` method: +接下来,实现一个异步的 `subscribe()` 方法: + ```dart // Subscription action @@ -230,7 +328,12 @@ Future subscribe() async { As described previously, first the method sets the `subscribed` state to `true` and then calls to `notifyListeners()`. This forces the UI to update and the button changes its appearance, -showing the text “Subscribed” to the user. +showing the text "Subscribed" to the user. + +如前所述,该方法首先将 `subscribed` 状态设置为 `true`, +然后调用 `notifyListeners()`。 +这会强制 UI 更新,按钮改变外观, +向用户显示文本"Subscribed"。 Then the method performs the actual call to the repository. This call is wrapped by a `try-catch` @@ -238,13 +341,25 @@ in order to catch any exceptions it may throw. In case an exception is caught, the `subscribed` state is set back to `false`, and the `error` state is set to `true`. A final call to `notifyListeners()` is done -to change the UI back to ‘Subscribe’. +to change the UI back to 'Subscribe'. + +然后该方法执行对 Repository 的实际调用。 +此调用被 `try-catch` 包裹, +以捕获可能抛出的任何异常。 +如果捕获到异常,`subscribed` 状态将被设回 `false`, +`error` 状态将被设为 `true`。 +最后调用 `notifyListeners()` 将 UI 恢复为"Subscribe"。 If there is no exception, the process is complete because the UI is already reflecting the success state. +如果没有异常,则流程已完成, +因为 UI 已经反映了成功状态。 + The complete `SubscribeButtonViewModel` should look like this: +完整的 `SubscribeButtonViewModel` 应如下所示: + ```dart /// Subscribe button View Model. @@ -291,12 +406,20 @@ class SubscribeButtonViewModel extends ChangeNotifier { ### Implement the `SubscribeButton` +### 实现 `SubscribeButton` + In this step, you will first implement the build method of the `SubscribeButton`, -and then implement the feature’s error handling. +and then implement the feature's error handling. + +在这一步中, +你将首先实现 `SubscribeButton` 的 build 方法, +然后实现该功能的错误处理。 Add the following code to the build method: +将以下代码添加到 build 方法中: + ```dart @override @@ -327,10 +450,22 @@ The button style will also change depending on this state. As well, when the button is tapped, it runs the `subscribe()` method from the view model. +此 build 方法包含一个 `ListenableBuilder`, +用于监听视图模型的变化。 +builder 会创建一个 `FilledButton`, +根据视图模型的状态显示文本"Subscribed"或"Subscribe"。 +按钮样式也会根据此状态变化。 +同时,当按钮被点击时, +它会运行视图模型中的 `subscribe()` 方法。 + The `SubscribeButtonStyle` can be found here. Add this class next to the `SubscribeButton`. Feel free to modify the `ButtonStyle`. +`SubscribeButtonStyle` 可以在这里找到。 +将此类添加到 `SubscribeButton` 旁边。 +你可以随意修改 `ButtonStyle`。 + ```dart class SubscribeButtonStyle { @@ -348,12 +483,22 @@ If you run the application now, you will see how the button changes when you press it, however it will change back to the original state without showing an error. +如果你现在运行应用, +你将看到按钮在按下时如何变化, +但它会恢复到原始状态而不显示错误。 + ### Handling errors +### 处理错误 + To handle errors, add the `initState()` and `dispose()` methods to the `SubscribeButtonState`, and then add the `_onViewModelChange()` method. +为了处理错误, +向 `SubscribeButtonState` 添加 `initState()` 和 `dispose()` 方法, +然后添加 `_onViewModelChange()` 方法。 + ```dart @override @@ -387,9 +532,14 @@ void _onViewModelChange() { The `addListener()` call registers the `_onViewModelChange()` method to be called when the view model notifies listeners. -It’s important to call `removeListener()` when the widget is disposed of, +It's important to call `removeListener()` when the widget is disposed of, in order to avoid errors. +`addListener()` 调用注册了 `_onViewModelChange()` 方法, +使其在视图模型通知监听器时被调用。 +在 widget 被销毁时调用 `removeListener()` 非常重要, +以避免错误。 + The `_onViewModelChange()` method checks the `error` state, and if it is `true`, displays a `Snackbar` to the user showing an error message. @@ -397,39 +547,77 @@ As well, the `error` state is set back to `false`, to avoid displaying the error message multiple times if `notifyListeners()` is called again in the view model. +`_onViewModelChange()` 方法检查 `error` 状态, +如果为 `true`, +则向用户显示一个带有错误消息的 Snackbar。 +同时,`error` 状态被设回 `false`, +以避免在视图模型中再次调用 `notifyListeners()` 时多次显示错误消息。 + ## Advanced Optimistic State +## 进阶乐观状态 + In this tutorial, -you’ve learned how to implement an Optimistic State with a single binary state, +you've learned how to implement an Optimistic State with a single binary state, but you can use this technique to create a more advanced solution by incorporating a third temporal state that indicates that the action is still running. +在本教程中, +你已经学习了如何使用单一的二元状态来实现乐观状态, +但你可以通过引入第三个临时状态来创建更高级的方案, +该临时状态用于指示操作仍在运行中。 + For example, in a chat application when the user sends a new message, the application will display the new chat message in the chat window, but with an icon indicating that the message is still pending to be delivered. When the message is delivered, that icon would be removed. +例如,在聊天应用中,当用户发送新消息时, +应用会在聊天窗口中显示新的聊天消息, +但会带有一个图标表示消息仍在等待发送。 +当消息发送成功后,该图标会被移除。 + In the subscribe button example, you could add another flag in the view model indicating that the `subscribe()` method is still running, or use the Command pattern running state, then modify the button style slightly to show that the operation is running. +在订阅按钮的示例中, +你可以在视图模型中添加另一个标志, +表示 `subscribe()` 方法仍在运行, +或使用命令模式的运行状态, +然后稍微修改按钮样式以显示操作正在运行。 + ## Interactive example +## 交互示例 + This example shows the `SubscribeButton` widget together with the `SubscribeButtonViewModel` and `SubscriptionRepository`, which implement a subscribe tap action with Optimistic State. +此示例展示了 `SubscribeButton` widget +以及 `SubscribeButtonViewModel` +和 `SubscriptionRepository`, +它们使用乐观状态实现了一个订阅点击操作。 + When you tap the button, -the button text changes from “Subscribe” to “Subscribed”. After a second, +the button text changes from "Subscribe" to "Subscribed". After a second, the repository throws an exception, which gets captured by the view model, -and the button reverts back to showing “Subscribe”, +and the button reverts back to showing "Subscribe", while also displaying a Snackbar with an error message. +当你点击按钮时, +按钮文本从"Subscribe"变为"Subscribed"。一秒后, +Repository 抛出一个异常, +该异常被视图模型捕获, +按钮恢复为显示"Subscribe", +同时显示一个带有错误消息的 Snackbar。 + ```dartpad title="Flutter Optimistic State example in DartPad" run="true" // ignore_for_file: avoid_print diff --git a/src/content/ui/adaptive-responsive/safearea-mediaquery.md b/src/content/ui/adaptive-responsive/safearea-mediaquery.md index e3c18bea86..ee2ec933c7 100644 --- a/src/content/ui/adaptive-responsive/safearea-mediaquery.md +++ b/src/content/ui/adaptive-responsive/safearea-mediaquery.md @@ -1,13 +1,22 @@ --- -title: SafeArea & MediaQuery +# title: SafeArea & MediaQuery +title: SafeArea 与 MediaQuery +# description: >- +# Learn how to use SafeArea and MediaQuery +# to create an adaptive app. description: >- - Learn how to use SafeArea and MediaQuery - to create an adaptive app. + 了解如何使用 SafeArea 和 MediaQuery + 来创建自适应应用。 --- This page discusses how and when to use the `SafeArea` and `MediaQuery` widgets. +本页面讨论了如何以及何时使用 +`SafeArea` 和 `MediaQuery` widget。 + +## SafeArea + ## SafeArea When running your app on the latest devices, @@ -20,39 +29,77 @@ as well as operating system UI (such as the status bar on Android), or by rounded corners of the physical display. +当你在最新的设备上运行应用时, +可能会遇到部分 UI 被设备屏幕上的缺口遮挡的情况。 +你可以使用 [`SafeArea`][] widget 来解决这个问题, +它会对子 widget 进行内缩以避免被遮挡 +(例如刘海和摄像头挖孔), +同时也会避开操作系统 UI +(例如 Android 上的状态栏), +以及物理显示屏的圆角区域。 + If you don't want this behavior, the `SafeArea` widget allows you to disable padding on any of its four sides. By default, all four sides are enabled. +如果你不需要这种行为, +`SafeArea` widget 允许你 +禁用其四个方向中任意一侧的内边距。 +默认情况下,四个方向都是启用的。 + It's generally recommended to wrap the body of a `Scaffold` widget in `SafeArea` as a good place to start, but you don't always need to put it this high in the `Widget` tree. +通常建议将 `Scaffold` widget 的 `body` 包裹在 +`SafeArea` 中作为一个良好的起点, +但你并不总是需要将它放在 widget 树中这么高的位置。 + For example, if you purposefully want your app to stretch under the cutouts, you can move the `SafeArea` to wrap whatever content makes sense, and let the rest of the app take up the full screen. +例如,如果你有意让应用延伸到屏幕缺口下方, +你可以将 `SafeArea` 移到需要保护的内容外层进行包裹, +让应用的其他部分占据整个屏幕。 + Using `SafeArea` ensures that your app content won't be cut off by physical display features or operating system UI, and sets your app up for success even as new devices with different shapes and styles of cutouts enter the market. +使用 `SafeArea` 可以确保你的应用内容不会被 +物理显示特性或操作系统 UI 所遮挡, +并且即使市场上出现具有不同形状和样式缺口的新设备, +你的应用也能从容应对。 + How does `SafeArea` do so much in a small amount of code? Behind the scenes it uses the `MediaQuery` object. +`SafeArea` 是如何用这么少的代码做到这些的呢? +在幕后,它使用了 `MediaQuery` 对象。 + [`SafeArea`]: {{site.api}}/flutter/widgets/SafeArea-class.html ## MediaQuery +## MediaQuery + As discussed in the [SafeArea](#safearea) section, `MediaQuery` is a powerful widget for creating adaptive apps. Sometimes you'll use `MediaQuery` directly, and sometimes you'll use `SafeArea`, which uses `MediaQuery` behind the scenes. +如 [SafeArea](#safearea) 部分所述, +`MediaQuery` 是一个功能强大的 widget, +用于创建自适应应用。有时你会直接使用 `MediaQuery`, +有时你会使用 `SafeArea`, +而 `SafeArea` 在幕后使用了 `MediaQuery`。 + `MediaQuery` provides lots of information, including the app's current window size. It exposes accessibility settings like high contrast mode @@ -61,14 +108,30 @@ service like TalkBack or VoiceOver. `MediaQuery` also contains info about the features of your device's display, such as having a hinge or a fold. +`MediaQuery` 提供了大量信息, +包括应用当前的窗口尺寸。 +它还提供了无障碍设置,如高对比度模式 +和文字缩放,以及用户是否正在使用 +TalkBack 或 VoiceOver 等无障碍服务。 +`MediaQuery` 还包含有关设备显示屏特性的信息, +例如是否有铰链或折叠屏。 + `SafeArea` uses the data from `MediaQuery` to figure out how much to inset its child `Widget`. Specifically, it uses the `MediaQuery` padding property, which is basically the amount of the display that's partially obscured by system UI, display notches, or status bar. +`SafeArea` 使用来自 `MediaQuery` 的数据来确定 +其子 Widget 需要内缩多少。 +具体来说,它使用 `MediaQuery` 的 padding 属性, +该属性本质上表示显示屏被系统 UI、刘海或状态栏 +部分遮挡的区域大小。 + So, why not use `MediaQuery` directly? +那么,为什么不直接使用 `MediaQuery` 呢? + The answer is that `SafeArea` does one clever thing that makes it beneficial to use over just raw `MediaQueryData`. Specifically, it modifies the `MediaQuery` exposed @@ -78,12 +141,26 @@ This means that you can nest `SafeArea`s, and only the topmost one will apply the padding needed to avoid the notches as system UI. +答案是 `SafeArea` 做了一件巧妙的事情, +使得它比直接使用原始的 `MediaQueryData` 更有优势。 +具体来说,它会修改暴露给 `SafeArea` 子 Widget 的 `MediaQuery`, +使其看起来好像 `SafeArea` 添加的内边距并不存在。 +这意味着你可以嵌套多个 `SafeArea`, +而只有最外层的那个会应用所需的内边距 +来避开刘海和系统 UI。 + As your app grows and you move widgets around, you don't have to worry about having too much padding applied if you have multiple `SafeArea`s, whereas you would have issues if using `MediaQueryData.padding` directly. +随着应用的增长和 widget 的移动, +如果你使用了多个 `SafeArea`, +不必担心会应用过多的内边距, +而如果直接使用 `MediaQueryData.padding` +则会遇到这个问题。 + You _can_ wrap the body of a `Scaffold` widget with a `SafeArea`, but you don't _have_ to put it this high in the widget tree. @@ -91,6 +168,11 @@ The `SafeArea` just needs to wrap the contents that would cause information loss if cut off by the hardware features mentioned earlier. +你*可以*将 `Scaffold` widget 的 `body` 用 `SafeArea` 包裹, +但你并*不一定*要把它放在 widget 树中这么高的位置。 +`SafeArea` 只需要包裹那些如果被前面提到的硬件特性遮挡 +就会导致信息丢失的内容即可。 + For example, if you purposefully want your app to stretch under the cutouts, you can move the `SafeArea` to wrap whatever content makes sense, @@ -101,7 +183,19 @@ system status bar. This is also why wrapping the body of a `Scaffold` in a `SafeArea` is recommended, instead of wrapping the whole `Scaffold` itself. +例如,如果你有意让应用延伸到屏幕缺口下方, +你可以将 `SafeArea` 移到需要保护的内容外层进行包裹, +让应用的其他部分占据整个屏幕。 +附带说明一下,这正是 `AppBar` widget 默认的行为方式, +这也是它能够延伸到系统状态栏下方的原因。 +这也是为什么建议将 `Scaffold` 的 `body` 包裹在 `SafeArea` 中, +而不是包裹整个 `Scaffold` 本身的原因。 + `SafeArea` ensures that your app content won't be cut off in a generic way and sets your app up for success even as new devices with different shapes and styles of cutouts enter the market. + +`SafeArea` 以一种通用的方式确保你的应用内容不会被遮挡, +并且即使市场上出现具有不同形状和样式缺口的新设备, +你的应用也能从容应对。 From 3fbf65eed36ef61d7d49525b238367e08733de5c Mon Sep 17 00:00:00 2001 From: XinLei Date: Sun, 8 Mar 2026 14:10:55 -0700 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=AD=E5=88=97=E8=A1=A8=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复列表中英文和中文翻译未正确交替排列的问题: - 将分块列表(英文全部在前、中文全部在后)改为交替格式 - 在列表项的英文和中文之间添加必要的空行分隔 - 删除 devtools.md 中重复的 SummaryCard 条目 Co-Authored-By: Claude Opus 4.6 --- src/content/app-architecture/concepts.md | 14 +++-- .../design-patterns/offline-first.md | 16 ++++-- .../design-patterns/optimistic-state.md | 5 ++ src/content/app-architecture/guide.md | 55 +++++++++++++++++-- src/content/app-architecture/index.md | 22 ++++++-- .../learn/pathway/tutorial/adaptive-layout.md | 11 ++++ .../learn/pathway/tutorial/advanced-ui.md | 11 ++++ .../learn/pathway/tutorial/change-notifier.md | 5 ++ .../learn/pathway/tutorial/devtools.md | 8 --- .../learn/pathway/tutorial/http-requests.md | 5 ++ src/content/learn/pathway/tutorial/index.md | 7 ++- .../pathway/tutorial/listenable-builder.md | 7 +++ .../learn/pathway/tutorial/navigation.md | 17 ++++++ .../pathway/tutorial/set-up-state-project.md | 7 +++ src/content/learn/pathway/tutorial/slivers.md | 13 +++++ .../learn/pathway/tutorial/stateful-widget.md | 8 +++ .../learn/pathway/tutorial/user-input.md | 6 ++ 17 files changed, 185 insertions(+), 32 deletions(-) diff --git a/src/content/app-architecture/concepts.md b/src/content/app-architecture/concepts.md index 928fc3f881..0407fdd55c 100644 --- a/src/content/app-architecture/concepts.md +++ b/src/content/app-architecture/concepts.md @@ -74,6 +74,9 @@ Flutter 应用应该以*分层*的方式编写。分层架构是一种软件设 * **UI layer** - Displays data to the user that is exposed by the business logic layer, and handles user interaction. This is also commonly referred to as the 'presentation layer'. + + **UI 层** - 向用户展示由业务逻辑层暴露的数据,并处理用户交互。这通常也被称为"展示层"。 + * **Logic layer** - Implements core business logic, and facilitates interaction between the data layer and UI layer. Commonly known as the 'domain layer'. The logic layer is optional, and only needs to be implemented if your @@ -81,15 +84,16 @@ Flutter 应用应该以*分层*的方式编写。分层架构是一种软件设 Many apps are only concerned with presenting data to a user and allowing the user to change that data (colloquially known as CRUD apps). These apps might not need this optional layer. -* **Data layer** - Manages interactions with data sources, such as databases or - platform plugins. Exposes data and methods to the business logic layer. -* **UI 层** - 向用户展示由业务逻辑层暴露的数据,并处理用户交互。这通常也被称为"展示层"。 -* **逻辑层** - 实现核心业务逻辑,并协调数据层和 UI 层之间的交互。通常也被称为"领域层"。 + **逻辑层** - 实现核心业务逻辑,并协调数据层和 UI 层之间的交互。通常也被称为"领域层"。 逻辑层是可选的,只有当你的应用在客户端存在复杂的业务逻辑时才需要实现。 许多应用只关心向用户展示数据并允许用户修改数据(俗称 CRUD 应用)。 这些应用可能不需要这个可选层。 -* **数据层** - 管理与数据源的交互,例如数据库或平台插件。向业务逻辑层暴露数据和方法。 + +* **Data layer** - Manages interactions with data sources, such as databases or + platform plugins. Exposes data and methods to the business logic layer. + + **数据层** - 管理与数据源的交互,例如数据库或平台插件。向业务逻辑层暴露数据和方法。 These are called 'layers' because each layer can only communicate with the layers directly below or above it. The UI layer shouldn't know that the data diff --git a/src/content/app-architecture/design-patterns/offline-first.md b/src/content/app-architecture/design-patterns/offline-first.md index d8703d2f16..455eac80e7 100644 --- a/src/content/app-architecture/design-patterns/offline-first.md +++ b/src/content/app-architecture/design-patterns/offline-first.md @@ -752,19 +752,23 @@ The key takeaways are: - When reading data, you can use a `Stream` to combine locally stored data with remote data. + + 读取数据时, + 你可以使用 `Stream` 将本地存储的数据与远程数据结合起来。 + - When writing data, decide if you need to be online or offline, and if you need synchronizing data later or not. + + 写入数据时, + 决定是否需要在线或离线操作, + 以及是否需要稍后同步数据。 + - When implementing a background sync task, take into account the device status and your application needs, as different applications may have different requirements. -- 读取数据时, - 你可以使用 `Stream` 将本地存储的数据与远程数据结合起来。 -- 写入数据时, - 决定是否需要在线或离线操作, - 以及是否需要稍后同步数据。 -- 实现后台同步任务时, + 实现后台同步任务时, 需要考虑设备状态和应用需求, 因为不同的应用可能有不同的要求。 diff --git a/src/content/app-architecture/design-patterns/optimistic-state.md b/src/content/app-architecture/design-patterns/optimistic-state.md index ee068c2ada..37b345e83e 100644 --- a/src/content/app-architecture/design-patterns/optimistic-state.md +++ b/src/content/app-architecture/design-patterns/optimistic-state.md @@ -117,10 +117,15 @@ create these Dart classes in a Flutter project: 在 Flutter 项目中创建以下 Dart 类: - A `StatefulWidget` named `SubscribeButton` + 一个名为 `SubscribeButton` 的 `StatefulWidget` + - A class named `SubscribeButtonViewModel` extending `ChangeNotifier` + 一个名为 `SubscribeButtonViewModel` 的类,继承自 `ChangeNotifier` + - A class named `SubscriptionRepository` + 一个名为 `SubscriptionRepository` 的类 diff --git a/src/content/app-architecture/guide.md b/src/content/app-architecture/guide.md index afae57b70d..ec97ffcb2a 100644 --- a/src/content/app-architecture/guide.md +++ b/src/content/app-architecture/guide.md @@ -65,15 +65,21 @@ This guide recommends you split your application into the following components: 本指南建议你将应用拆分为以下组件: * Views + + 视图(Views) + * View models -* Repositories -* Services -* 视图(Views) -* 视图模型(View models) + 视图模型(View models) + * Repositories + + Repositories + * Services + Services + ### MVVM If you've encountered the [Model-View-ViewModel architectural pattern][] (MVVM), @@ -174,16 +180,19 @@ UI 层由两个架构组件组成, all of the widgets below it in the widget tree. Views are also responsible for passing events to the view model in response to user interactions. + **视图**描述了如何向用户展示应用数据。 具体来说,它们指的是构成一个功能的 *widget 组合*。 例如,视图通常(但不总是)是一个包含 `Scaffold` widget 以及 widget 树中其下所有 widget 的屏幕。 视图还负责将事件传递给视图模型以响应用户交互。 + * **View models** contain the logic that converts app data into *UI State*, because data from repositories is often formatted differently from the data that needs to be displayed. For example, you might need to combine data from multiple repositories, or you might want to filter a list of data records. + **视图模型**包含将应用数据转换为 *UI 状态*的逻辑, 因为来自 Repository 的数据格式通常与需要显示的数据格式不同。 例如,你可能需要合并来自多个 Repository 的数据, @@ -277,12 +286,19 @@ The only logic a view should contain is: * Simple if-statements to show and hide widgets based on a flag or nullable field in the view model + 简单的 if 语句,根据视图模型中的标志或可空字段来显示和隐藏 widget + * Animation logic + 动画逻辑 + * Layout logic based on device information, like screen size or orientation. + 基于设备信息的布局逻辑,如屏幕尺寸或方向。 + * Simple routing logic + 简单的路由逻辑 All logic related to data should be handled in the view model. @@ -310,19 +326,24 @@ A view model's main responsibilities include: * Retrieving application data from repositories and transforming it into a format suitable for presentation in the view. For example, it might filter, sort or aggregate data. + 从 Repository 获取应用数据并将其转换为适合在视图中展示的格式。 例如,它可能会过滤、排序或聚合数据。 + * Maintaining the current state needed in the view, so that the view can rebuild without losing data. For example, it might contain boolean flags to conditionally render widgets in the view, or a field that tracks which section of a carousel is active on screen. + 维护视图所需的当前状态, 以便视图可以在不丢失数据的情况下重建。 例如,它可能包含用于在视图中有条件地渲染 widget 的布尔标志, 或者跟踪屏幕上轮播图哪个部分处于活动状态的字段。 + * Exposes callbacks (called **commands**) to the view that can be attached to an event handler, like a button press or form submission. + 向视图暴露回调(称为**命令**), 这些回调可以附加到事件处理器上,如按钮点击或表单提交。 @@ -394,16 +415,27 @@ Repositories handle the business logic associated with services, such as: Repository 处理与 Service 相关的业务逻辑,例如: * Caching + 缓存 + * Error handling + 错误处理 + * Retry logic + 重试逻辑 + * Refreshing data + 刷新数据 + * Polling services for new data + 轮询 Service 获取新数据 + * Refreshing data based on user actions + 根据用户操作刷新数据 A simplified diagram of the architecture described on this page with the Repository object highlighted. @@ -455,10 +487,15 @@ Service 处于应用的最底层。 Service 可能封装的端点示例包括: * The underlying platform, like iOS and Android APIs + 底层平台,如 iOS 和 Android API + * REST endpoints + REST 端点 + * Local files + 本地文件 As a rule of thumb, services are most helpful when @@ -505,10 +542,15 @@ live in the view model and meets one or more of the following conditions: 这些逻辑满足以下一个或多个条件: 1. Requires merging data from multiple repositories + 需要合并来自多个 Repository 的数据 + 2. Is exceedingly complex + 逻辑极其复杂 + 3. The logic will be reused by different view models + 该逻辑将被不同的视图模型复用 This layer is optional because not all applications or features within an @@ -583,10 +625,15 @@ This method of adding use-cases is defined by the following rules: 这种添加用例的方法由以下规则定义: * Use-cases depend on repositories + 用例依赖于 Repository + * Use-cases and repositories have a many-to-many relationship + 用例和 Repository 是多对多的关系 + * View models depend on one or more use-cases *and* one or more repositories + 视图模型依赖于一个或多个用例*以及*一个或多个 Repository This method of using use-cases ends up looking diff --git a/src/content/app-architecture/index.md b/src/content/app-architecture/index.md index 983b34cf0e..6114377f9c 100644 --- a/src/content/app-architecture/index.md +++ b/src/content/app-architecture/index.md @@ -48,18 +48,28 @@ your Flutter app in order to scale as your project requirements and team grow. ## 你将学到什么 * Benefits of intentional architecture + + 有意识地进行架构设计的好处 + * Common architectural principles + + 常见的架构原则 + * The Flutter team's recommended app architecture + + Flutter 团队推荐的应用架构 + * MVVM and state management + + MVVM 和状态管理 + * Dependency injection + + 依赖注入 + * Common design patterns for writing robust Flutter applications -* 有意识地进行架构设计的好处 -* 常见的架构原则 -* Flutter 团队推荐的应用架构 -* MVVM 和状态管理 -* 依赖注入 -* 编写健壮的 Flutter 应用的常见设计模式 + 编写健壮的 Flutter 应用的常见设计模式 {% comment %} TODO @ewindmill complete this list as pages land, add links. diff --git a/src/content/learn/pathway/tutorial/adaptive-layout.md b/src/content/learn/pathway/tutorial/adaptive-layout.md index 03c45c512a..46bf5ee930 100644 --- a/src/content/learn/pathway/tutorial/adaptive-layout.md +++ b/src/content/learn/pathway/tutorial/adaptive-layout.md @@ -38,10 +38,13 @@ Specifically, this app handles two screen sizes: - **Large screens (tablets, desktop)**: Shows contact groups and contact details side-by-side. + **大屏幕(平板、桌面端)**: 并排显示联系人分组和联系人详情。 + - **Small screens (phones)**: Uses navigation to move between contact groups and details. + **小屏幕(手机)**: 使用导航在联系人分组和详情之间切换。 @@ -407,10 +410,15 @@ This layout creates the following: 这个布局创建了以下内容: - A fixed-width sidebar (320 pixels) for contact groups. + 一个固定宽度的侧边栏(320 像素),用于显示联系人分组。 + - A 1-pixel divider between the panels. + 面板之间的 1 像素分隔线。 + - A details panel that uses an `Expanded` widget to take the remaining space. + 一个使用 `Expanded` widget 占据剩余空间的详情面板。 ### Test the adaptive layout @@ -425,10 +433,13 @@ see the layout change: - **Wide window (> 600px)**: Shows placeholder text for the sidebar and details side-by-side. + **宽窗口(> 600px)**: 并排显示侧边栏和详情的占位文本。 + - **Narrow window (< 600px)**: Shows only the contact groups page. + **窄窗口(< 600px)**: 仅显示联系人分组页面。 diff --git a/src/content/learn/pathway/tutorial/advanced-ui.md b/src/content/learn/pathway/tutorial/advanced-ui.md index e8683ccea5..4507687ec0 100644 --- a/src/content/learn/pathway/tutorial/advanced-ui.md +++ b/src/content/learn/pathway/tutorial/advanced-ui.md @@ -60,16 +60,27 @@ This tutorial explores the following topics: 本教程将探讨以下主题: * Building responsive layouts with `LayoutBuilder`. + 使用 `LayoutBuilder` 构建响应式布局。 + * Using advanced scrolling with slivers and search. + 使用 sliver 和搜索实现高级滚动。 + * Implementing stack-based navigation patterns. + 实现基于栈的导航模式。 + * Creating comprehensive themes with `CupertinoThemeData`. + 使用 `CupertinoThemeData` 创建全面的主题。 + * Supporting both light and dark themes. + 同时支持浅色和深色主题。 + * Creating an iOS-style UI using Cupertino widgets. + 使用 Cupertino widget 创建 iOS 风格的 UI。 This tutorial assumes that you've completed the previous Flutter tutorials diff --git a/src/content/learn/pathway/tutorial/change-notifier.md b/src/content/learn/pathway/tutorial/change-notifier.md index 1254ab2542..d1ed4914c0 100644 --- a/src/content/learn/pathway/tutorial/change-notifier.md +++ b/src/content/learn/pathway/tutorial/change-notifier.md @@ -82,10 +82,15 @@ The `ArticleViewModel` holds three pieces of state: `ArticleViewModel` 持有三个状态: - `summary`: The current Wikipedia article data. + `summary`:当前的 Wikipedia 文章数据。 + - `errorMessage`: Any error that occurred during data fetching. + `errorMessage`:数据获取过程中发生的错误信息。 + - `loading`: A flag to show progress indicators. + `loading`:用于显示加载指示器的标志位。 ### Add constructor initialization diff --git a/src/content/learn/pathway/tutorial/devtools.md b/src/content/learn/pathway/tutorial/devtools.md index efb5604225..136a6389fd 100644 --- a/src/content/learn/pathway/tutorial/devtools.md +++ b/src/content/learn/pathway/tutorial/devtools.md @@ -274,14 +274,6 @@ items: This happens when widgets like `Row`, `Column`, or `ListView` receive infinite constraints. Now you can recognize and fix these issues when they occur. - - title: Learned about common layout issues - icon: bug_report - details: >- - You learned about **unbounded constraints**, - one of the most common errors hit in Flutter development. - This happens when widgets like - `Row`, `Column`, or `ListView` receive infinite constraints. - Now you can recognize and fix these issues when they occur. - title: Experimented with properties in real-time icon: tune details: >- diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index fe59f840f9..722ba91aa9 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -38,10 +38,15 @@ MVVM 是一种用于客户端应用的[架构模式][architectural pattern], 它将你的应用分为三层: - **Model**: Handles data operations. + **Model**:处理数据操作。 + - **View**: Displays the UI. + **View**:展示用户界面。 + - **ViewModel**: Manages state and connects the two. + **ViewModel**:管理状态并连接上述两层。 The core tenet of MVVM (and many other patterns) is *separation of concerns*. diff --git a/src/content/learn/pathway/tutorial/index.md b/src/content/learn/pathway/tutorial/index.md index 456e3983db..3635b3224c 100644 --- a/src/content/learn/pathway/tutorial/index.md +++ b/src/content/learn/pathway/tutorial/index.md @@ -31,11 +31,12 @@ the critical features of Flutter development (and more!) This tutorial is the third step in the Flutter learning pathway, and therefor assumes that you have: 1. A Flutter environment set up + + 已搭建好 Flutter 开发环境 + 2. Basic understanding of the Dart programming language -本教程是 Flutter 学习路径的第三步,因此默认你已经具备以下条件: -1. 已搭建好 Flutter 开发环境 -2. 对 Dart 编程语言有基本的了解 + 对 Dart 编程语言有基本的了解 If either of those aren't true, please start at the [Learning pathway page](/learn/pathway). diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 17157e3a2a..a7248b0258 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -483,18 +483,25 @@ This widget demonstrates a few important UI concepts: - **Conditional rendering**: The `if` statements show content only when available. + **条件渲染**: `if` 语句仅在内容可用时才显示。 + - **Text styling**: Different text styles create visual hierarchy using Flutter's theme system. + **文本样式**: 不同的文本样式利用 Flutter 的主题系统创建视觉层次。 + - **Proper spacing**: The `spacing` parameter provides consistent vertical spacing. + **合理间距**: `spacing` 参数提供一致的垂直间距。 + - **Overflow handling**: `TextOverflow.ellipsis` prevents text from breaking the layout. + **溢出处理**: `TextOverflow.ellipsis` 防止文本破坏布局。 diff --git a/src/content/learn/pathway/tutorial/navigation.md b/src/content/learn/pathway/tutorial/navigation.md index 2c47853732..6ceb96cf49 100644 --- a/src/content/learn/pathway/tutorial/navigation.md +++ b/src/content/learn/pathway/tutorial/navigation.md @@ -133,12 +133,19 @@ the following features: `CupertinoPageRoute` 创建具有以下特性的 iOS 风格页面过渡效果: - A slide-in animation from the right. + 从右侧滑入的动画。 + - Automatic back button support. + 自动支持返回按钮。 + - Proper title handling. + 正确的标题处理。 + - Swipe-to-go-back gesture support. + 支持滑动返回手势。 ### Create the sidebar component for large screens @@ -297,10 +304,15 @@ Hot reload your app and test the navigation: **小屏幕(宽度 <600px):** - Tap contact groups to navigate to contact details. + 点击联系人分组以导航到联系人详情。 + - Use the back button or a swipe gesture to return. + 使用返回按钮或滑动手势返回。 + - This is a classic stack-based navigation flow. + 这是经典的基于栈的导航流程。 **Large screens (>600px width):** @@ -308,10 +320,15 @@ Hot reload your app and test the navigation: **大屏幕(宽度 >600px):** - Click contact groups in the sidebar to update the detail view. + 点击侧边栏中的联系人分组以更新详情视图。 + - There is no navigation stack. The selection updates the content area. + 没有导航栈。选择操作会更新内容区域。 + - This is a master-detail interface pattern. + 这是主从界面模式。 The app automatically chooses the diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 0fc8e31bf1..62d6b31bb8 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -43,13 +43,20 @@ These lessons explore: 本系列课程将探索以下内容: - Making HTTP requests in Flutter. + 在 Flutter 中发起 HTTP 请求。 + - Managing application state with `ChangeNotifier`. + 使用 `ChangeNotifier` 管理应用状态。 + - Using the MVVM architecture pattern. + 使用 MVVM 架构模式。 + - Creating responsive user interfaces that update automatically when data changes. + 创建能在数据变化时自动更新的响应式用户界面。 This tutorial assumes you've completed the diff --git a/src/content/learn/pathway/tutorial/slivers.md b/src/content/learn/pathway/tutorial/slivers.md index b69a30eab9..6658bcb726 100644 --- a/src/content/learn/pathway/tutorial/slivers.md +++ b/src/content/learn/pathway/tutorial/slivers.md @@ -57,23 +57,31 @@ they serve different purposes and aren't interchangeable. - **Widgets** are general UI building blocks that can be used anywhere in your widget tree. + **Widget** 是通用的 UI 构建块, 可以在 widget 树的任何位置使用。 + - **Slivers** are specialized widgets designed specifically for scrollable layouts and have some constraints: + **Sliver** 是专门为可滚动布局设计的特殊 widget, 并且有一些约束: - Slivers can **only** be direct children of scroll views, such as `CustomScrollView` and `NestedScrollView`. + Sliver **只能**作为滚动视图的直接子级,例如 `CustomScrollView` 和 `NestedScrollView`。 + - Some scroll views **only** accept slivers as children. You can't pass regular widgets to `CustomScrollView.slivers`. + 某些滚动视图**只**接受 sliver 作为子级。 你不能将普通 widget 传递给 `CustomScrollView.slivers`。 + - To use regular widgets within a sliver context, wrap them in `SliverToBoxAdapter` or `SliverFillRemaining`. + 要在 sliver 上下文中使用普通 widget, 请将它们包裹在 `SliverToBoxAdapter` 或 `SliverFillRemaining` 中。 @@ -167,15 +175,20 @@ This widget introduces several slivers: - `CupertinoSliverNavigationBar`: An opinionated navigation bar that collapses as the page scrolls. + `CupertinoSliverNavigationBar`: 一个自带样式的导航栏,会随着页面滚动而折叠。 + - `SliverList`: A scrollable list of items. + `SliverList`: 一个可滚动的列表项。 + - `SliverFillRemaining`: A sliver that takes up the remaining space in the scroll area, and whose child is a non-sliver widget. + `SliverFillRemaining`: 一个占据滚动区域剩余空间的 sliver, 其子级是非 sliver widget。 diff --git a/src/content/learn/pathway/tutorial/stateful-widget.md b/src/content/learn/pathway/tutorial/stateful-widget.md index 1ac34350e9..bc19db1c57 100644 --- a/src/content/learn/pathway/tutorial/stateful-widget.md +++ b/src/content/learn/pathway/tutorial/stateful-widget.md @@ -40,10 +40,13 @@ update after each submitted user guess by: 下一个未填充行中的每个方块应通过以下方式更新: - Displaying the correct letter. + 显示正确的字母。 + - Changing color to reflect whether the letter is correct (green), is in the word but at an incorrect position (yellow), or doesn't appear in the word at all (grey). + 改变颜色以反映该字母是否正确(绿色)、 在单词中但位置不对(黄色),还是 完全不在单词中(灰色)。 @@ -114,17 +117,22 @@ a stateless widget to a stateful widget, do the following steps: 无状态 widget 转换为有状态 widget,请执行以下步骤: 1. Change `GamePage` to extend `StatefulWidget` instead of `StatelessWidget`. + 将 `GamePage` 改为继承 `StatefulWidget` 而不是 `StatelessWidget`。 + 1. Create a new class named `_GamePageState`, that extends `State`. This new class will hold the mutable state and the `build` method. Move the `build` method and all properties *instantiated on the widget* from `GamePage` to the state object. + 创建一个名为 `_GamePageState` 的新类,继承 `State`。 这个新类将持有可变的状态和 `build` 方法。 将 `build` 方法和所有*在 widget 上实例化的*属性 从 `GamePage` 移动到 State 对象中。 + 1. Implement the `createState()` method in `GamePage`, which returns an instance of `_GamePageState`. + 在 `GamePage` 中实现 createState() 方法, 该方法返回一个 `_GamePageState` 实例。 diff --git a/src/content/learn/pathway/tutorial/user-input.md b/src/content/learn/pathway/tutorial/user-input.md index 611d48d8ec..1d3939a208 100644 --- a/src/content/learn/pathway/tutorial/user-input.md +++ b/src/content/learn/pathway/tutorial/user-input.md @@ -190,10 +190,13 @@ Thus far, `TextField` has the following configuration. - It's decorated with a rounded border. Notice that the decoration configuration is very similar to how a `Container` and boxes are decorated. + 它具有圆角边框装饰。 注意,这里的装饰配置与 `Container` 和盒子模型的装饰方式非常相似。 + - Its `maxLength` property is set to 5 because the game only allows guesses of 5-letter words. + 它的 `maxLength` 属性设置为 5,因为游戏只允许猜测 5 个字母的单词。 [`Expanded`]: {{site.api}}/flutter/widgets/Expanded-class.html @@ -679,8 +682,11 @@ Flutter 内置了许多按钮 widget,如 [`TextButton`][]、 (除了它们的可选参数之外): - A callback function passed to `onPressed`. + 传递给 `onPressed` 的回调函数。 + - A widget that makes up the content of the button (often `Text` or an `Icon`). + 构成按钮内容的 widget(通常是 `Text` 或 `Icon`)。 Add an icon button to the row widget's children list in the `GuessInput` widget, From 96f274a17fb732f009a0b24e4bc2093210bb03ff Mon Sep 17 00:00:00 2001 From: XinLei Date: Tue, 10 Mar 2026 09:16:11 -0700 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=AD=E7=9A=84=E6=8E=92=E7=89=88=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据 docs/style-guide.md 排版规范修复以下问题: - 链接前后添加空格(中文与链接之间) - 将斜体强调改为加粗(_text_ → **text**) - 加粗前后添加空格(中文与加粗之间) 修改范围: - src/content/learn/pathway/tutorial/ (11 个文件) - src/content/app-architecture/ (3 个文件) - src/content/ui/adaptive-responsive/ (1 个文件) Co-Authored-By: Claude Sonnet 4.5 --- src/content/app-architecture/concepts.md | 12 +++---- src/content/app-architecture/guide.md | 32 +++++++++---------- src/content/app-architecture/index.md | 2 +- .../learn/pathway/tutorial/advanced-ui.md | 2 +- .../learn/pathway/tutorial/create-an-app.md | 4 +-- .../learn/pathway/tutorial/devtools.md | 4 +-- .../learn/pathway/tutorial/http-requests.md | 6 ++-- .../pathway/tutorial/implicit-animations.md | 2 +- src/content/learn/pathway/tutorial/layout.md | 4 +-- .../pathway/tutorial/listenable-builder.md | 2 +- src/content/learn/pathway/tutorial/slivers.md | 4 +-- .../learn/pathway/tutorial/stateful-widget.md | 6 ++-- .../learn/pathway/tutorial/user-input.md | 4 +-- .../pathway/tutorial/widget-fundamentals.md | 4 +-- .../safearea-mediaquery.md | 4 +-- 15 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/content/app-architecture/concepts.md b/src/content/app-architecture/concepts.md index 0407fdd55c..b9dac36324 100644 --- a/src/content/app-architecture/concepts.md +++ b/src/content/app-architecture/concepts.md @@ -44,10 +44,10 @@ Within each layer, you should further separate your application by feature or functionality. For example, your application's authentication logic should be in a different class than the search logic. -[关注点分离][Separation-of-concerns]是应用开发中的核心原则, +[关注点分离][Separation-of-concerns] 是应用开发中的核心原则, 它通过将应用的功能划分为独立的、自包含的单元来促进模块化和可维护性。 从高层次来看,这意味着将 UI 逻辑与业务逻辑分离。 -这通常被描述为*分层*架构。 +这通常被描述为 **分层** 架构。 在每一层中,你还应该按功能或特性进一步拆分你的应用。 例如,你的应用的身份验证逻辑应该与搜索逻辑位于不同的类中。 @@ -65,7 +65,7 @@ software design pattern that organizes an application into distinct layers, each with specific roles and responsibilities. Typically, applications are separated into 2 to 3 layers, depending on complexity. -Flutter 应用应该以*分层*的方式编写。分层架构是一种软件设计模式, +Flutter 应用应该以 **分层** 的方式编写。分层架构是一种软件设计模式, 它将应用组织成不同的层,每一层都有特定的角色和职责。 通常,应用根据复杂程度被分为 2 到 3 层。 @@ -111,7 +111,7 @@ The source of truth is responsible for representing local or remote state. If the data can be modified in the app, the SSOT class should be the only class that can do so. -你的应用中的每种数据类型都应该有一个[单一数据源][single source of truth](SSOT)。 +你的应用中的每种数据类型都应该有一个 [单一数据源][single source of truth](SSOT)。 数据源负责表示本地或远程状态。 如果数据可以在应用中被修改, SSOT 类应该是唯一能够执行此操作的类。 @@ -201,7 +201,7 @@ prevents malformed or unexpected data from being created. 新数据也可以从数据层开始。 例如,Repository 可能会轮询 HTTP 服务器以获取新数据。 在这种情况下,数据流只经历后半段旅程。 -最重要的理念是,数据变更始终发生在[单一数据源][SSOT]中,即数据层。 +最重要的理念是,数据变更始终发生在 [单一数据源][SSOT] 中,即数据层。 这使你的代码更容易理解、更不容易出错,并防止产生格式错误或意外的数据。 @@ -297,6 +297,6 @@ As this section of the website is evolving, we [welcome your feedback][]! 由于网站的这一部分仍在不断完善中, -我们[欢迎你的反馈][welcome your feedback]! +我们 [欢迎你的反馈][welcome your feedback]! [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="concepts" diff --git a/src/content/app-architecture/guide.md b/src/content/app-architecture/guide.md index ec97ffcb2a..826e49fac6 100644 --- a/src/content/app-architecture/guide.md +++ b/src/content/app-architecture/guide.md @@ -50,7 +50,7 @@ designing your Flutter app. Your Flutter application should split into two broad layers, the UI layer and the Data layer. -[关注点分离][Separation-of-concerns]是设计 Flutter 应用时应遵循的最重要原则。 +[关注点分离][Separation-of-concerns] 是设计 Flutter 应用时应遵循的最重要原则。 你的 Flutter 应用应该分为两个大的层级, 即 UI 层和数据层。 @@ -136,8 +136,8 @@ The domain layer contains additional components often called *interactors* or *use-cases*. The domain layer is covered later in this guide. 具有复杂逻辑的应用可能还有一个位于 UI 层和数据层之间的逻辑层。 -这个逻辑层通常被称为*领域层*。 -领域层包含通常被称为*交互器*或*用例*的额外组件。 +这个逻辑层通常被称为 **领域层**。 +领域层包含通常被称为 **交互器** 或 **用例** 的额外组件。 领域层将在本指南的后面部分介绍。 ::: @@ -181,8 +181,8 @@ UI 层由两个架构组件组成, Views are also responsible for passing events to the view model in response to user interactions. - **视图**描述了如何向用户展示应用数据。 - 具体来说,它们指的是构成一个功能的 *widget 组合*。 + **视图** 描述了如何向用户展示应用数据。 + 具体来说,它们指的是构成一个功能的 **widget 组合**。 例如,视图通常(但不总是)是一个包含 `Scaffold` widget 以及 widget 树中其下所有 widget 的屏幕。 视图还负责将事件传递给视图模型以响应用户交互。 @@ -193,7 +193,7 @@ UI 层由两个架构组件组成, For example, you might need to combine data from multiple repositories, or you might want to filter a list of data records. - **视图模型**包含将应用数据转换为 *UI 状态*的逻辑, + **视图模型** 包含将应用数据转换为 **UI 状态** 的逻辑, 因为来自 Repository 的数据格式通常与需要显示的数据格式不同。 例如,你可能需要合并来自多个 Repository 的数据, 或者你可能想要过滤数据记录列表。 @@ -240,7 +240,7 @@ made up of a `LoginViewModel` class and a `LoginView` class. 应用的功能以用户为中心, 因此由 UI 层定义。 -每一对*视图*和*视图模型*的实例定义了应用中的一个功能。 +每一对 **视图** 和 **视图模型** 的实例定义了应用中的一个功能。 这通常是应用中的一个屏幕,但不一定如此。 例如,考虑登录和退出的场景。 登录通常在一个专门的屏幕上完成, @@ -344,7 +344,7 @@ A view model's main responsibilities include: * Exposes callbacks (called **commands**) to the view that can be attached to an event handler, like a button press or form submission. - 向视图暴露回调(称为**命令**), + 向视图暴露回调(称为 **命令**), 这些回调可以附加到事件处理器上,如按钮点击或表单提交。 Commands are named for the [command pattern][], @@ -353,7 +353,7 @@ execute complex logic without knowledge of its implementation. Commands are written as members of the view model class to be called by the gesture handlers in the view class. -命令以[命令模式][command pattern]命名, +命令以 [命令模式][command pattern] 命名, 是允许视图在不了解其实现细节的情况下执行复杂逻辑的 Dart 函数。 命令作为视图模型类的成员编写, 由视图类中的手势处理器调用。 @@ -361,13 +361,13 @@ be called by the gesture handlers in the view class. You can find examples of views, view models, and commands on the [UI layer][] portion of the [App architecture case study][]. -你可以在[架构案例研究][App architecture case study]的 [UI 层][UI layer]部分找到视图、视图模型和命令的示例。 +你可以在 [架构案例研究][App architecture case study] 的 [UI 层][UI layer] 部分找到视图、视图模型和命令的示例。 For a gentle introduction to MVVM in Flutter, check out the [state management fundamentals][]. 如需了解 Flutter 中 MVVM 的入门介绍, -请查看[状态管理基础][state management fundamentals]。 +请查看 [状态管理基础][state management fundamentals]。 [UI layer]: /app-architecture/case-study/ui-layer [App architecture case study]: /app-architecture/case-study @@ -391,7 +391,7 @@ to simplify their reusability and testability. Using MVVM language, services and repositories make up your *model layer*. -使用 MVVM 的术语来说,Service 和 Repository 构成了你的*模型层*。 +使用 MVVM 的术语来说,Service 和 Repository 构成了你的 **模型层**。 ### Repositories @@ -405,7 +405,7 @@ each different type of data handled in your app. [Repository][] 类是模型数据的单一数据源。 它们负责从 Service 轮询数据, -并将原始数据转换为**领域模型**。 +并将原始数据转换为 **领域模型**。 领域模型表示应用所需的数据, 以视图模型类可以使用的格式进行格式化。 应用中处理的每种不同类型的数据都应该有一个对应的 Repository 类。 @@ -524,7 +524,7 @@ adds too much complexity to your view models. These classes are often called interactors or **use-cases**. 随着应用的增长和功能的增加,你可能需要将为视图模型带来过多复杂性的逻辑抽象出来。 -这些类通常被称为交互器或**用例**。 +这些类通常被称为交互器或 **用例**。 Use-cases are responsible for making interactions between the UI and Data layers simpler and more reusable. @@ -634,7 +634,7 @@ This method of adding use-cases is defined by the following rules: * View models depend on one or more use-cases *and* one or more repositories - 视图模型依赖于一个或多个用例*以及*一个或多个 Repository + 视图模型依赖于一个或多个用例 **以及** 一个或多个 Repository This method of using use-cases ends up looking less like a layered lasagna, and more like a plated dinner with @@ -662,6 +662,6 @@ As this section of the website is evolving, we [welcome your feedback][]! 由于网站的这个部分仍在不断完善中, -我们[欢迎你的反馈][welcome your feedback]! +我们 [欢迎你的反馈][welcome your feedback]! [welcome your feedback]: https://google.qualtrics.com/jfe/form/SV_4T0XuR9Ts29acw6?page="guide" diff --git a/src/content/app-architecture/index.md b/src/content/app-architecture/index.md index 6114377f9c..a0f66c7132 100644 --- a/src/content/app-architecture/index.md +++ b/src/content/app-architecture/index.md @@ -122,7 +122,7 @@ this guidance is for you. 这是一份用于构建可扩展 Flutter 应用的指南,专为多个开发者在同一代码库中协作、 构建功能丰富的应用的团队而编写。 -如果你正在编写一个拥有*不断增长的团队和代码库*的 Flutter 应用, +如果你正在编写一个拥有 **不断增长的团队和代码库** 的 Flutter 应用, 本指南就是为你准备的。 Along with general architectural advice, this guide gives concrete examples of diff --git a/src/content/learn/pathway/tutorial/advanced-ui.md b/src/content/learn/pathway/tutorial/advanced-ui.md index 4507687ec0..eb17a1b915 100644 --- a/src/content/learn/pathway/tutorial/advanced-ui.md +++ b/src/content/learn/pathway/tutorial/advanced-ui.md @@ -610,7 +610,7 @@ you should complete the [previous tutorial covering state][] before continuing, which covers state management. 如果你不熟悉 `ValueNotifier`, -你应该先完成[前一个关于状态管理的教程][previous tutorial covering state], +你应该先完成 [前一个关于状态管理的教程][previous tutorial covering state], 该教程涵盖了状态管理的内容。 [previous tutorial covering state]: /learn/pathway/tutorial/set-up-state-project diff --git a/src/content/learn/pathway/tutorial/create-an-app.md b/src/content/learn/pathway/tutorial/create-an-app.md index 8269791677..0e363fb8fc 100644 --- a/src/content/learn/pathway/tutorial/create-an-app.md +++ b/src/content/learn/pathway/tutorial/create-an-app.md @@ -58,7 +58,7 @@ You create new apps with the [Flutter CLI tool][], installed as part of the Flutter SDK. 构建 Flutter 应用的第一步是创建一个新项目。 -你可以使用 [Flutter CLI 工具][Flutter CLI tool]来创建新应用, +你可以使用 [Flutter CLI 工具][Flutter CLI tool] 来创建新应用, 它作为 Flutter SDK 的一部分被安装。 Open your terminal or command prompt and run @@ -137,7 +137,7 @@ Essentially, this is what a Flutter app is: a composition of widgets that make up a tree structure called the **widget tree.** -`MainApp` 是**根 widget**, +`MainApp` 是 **根 widget**, 因为它是传递给 `runApp` 的那个 widget。 在这个 widget 内部,有一个 `build` 方法, 它返回另一个名为 `MaterialApp` 的 widget。 diff --git a/src/content/learn/pathway/tutorial/devtools.md b/src/content/learn/pathway/tutorial/devtools.md index 136a6389fd..66e1c0b0d4 100644 --- a/src/content/learn/pathway/tutorial/devtools.md +++ b/src/content/learn/pathway/tutorial/devtools.md @@ -34,7 +34,7 @@ The [Dart and Flutter DevTools][] provide you with two particularly useful features: the **widget inspector** and the **property editor**. -随着 Flutter 应用复杂度的增加,理解每个 widget 属性如何影响 UI 变得越来越重要。[Dart and Flutter DevTools][] 为你提供了两个特别实用的功能:**widget 检查器**和**属性编辑器**。 +随着 Flutter 应用复杂度的增加,理解每个 widget 属性如何影响 UI 变得越来越重要。[Dart and Flutter DevTools][] 为你提供了两个特别实用的功能:**widget 检查器** 和 **属性编辑器**。 First, launch DevTools by running the following commands while your app is running in debug mode: @@ -149,7 +149,7 @@ the maximum width or the maximum height is set to [`double.infinity`][]. A widget that tries to be as big as possible won't function usefully when given an unbounded constraint and, in debug mode, throws an exception. -在某些情况下,widget 的[约束][constraints]是无界的,即无限大。这意味着最大宽度或最大高度被设置为 [`double.infinity`][]。当一个试图尽可能大的 widget 遇到无界约束时,它将无法正常工作,并在调试模式下抛出异常。 +在某些情况下,widget 的 [约束][constraints] 是无界的,即无限大。这意味着最大宽度或最大高度被设置为 [`double.infinity`][]。当一个试图尽可能大的 widget 遇到无界约束时,它将无法正常工作,并在调试模式下抛出异常。 The most common case where a render box ends up with an unbounded constraint is within a flex box widget ([`Row`][] or [`Column`][]), diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index 722ba91aa9..75e4aeb05b 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -33,8 +33,8 @@ MVVM is an [architectural pattern][] used in client apps that separates your app into three layers: 本教程所实现的总体模式被称为 -_Model-View-ViewModel_,即 _MVVM_。 -MVVM 是一种用于客户端应用的[架构模式][architectural pattern], +**Model-View-ViewModel**,即 **MVVM**。 +MVVM 是一种用于客户端应用的 [架构模式][architectural pattern], 它将你的应用分为三层: - **Model**: Handles data operations. @@ -53,7 +53,7 @@ The core tenet of MVVM (and many other patterns) is *separation of concerns*. Managing state in separate classes (outside your UI widgets) makes your code more testable, reusable, and easier to maintain. -MVVM(以及许多其他模式)的核心原则是*关注点分离*。 +MVVM(以及许多其他模式)的核心原则是 **关注点分离**。 在独立的类中管理状态(在 UI widget 之外)可以让你的代码 更易于测试、复用和维护。 diff --git a/src/content/learn/pathway/tutorial/implicit-animations.md b/src/content/learn/pathway/tutorial/implicit-animations.md index 498fdcc4dd..1b4b8c9005 100644 --- a/src/content/learn/pathway/tutorial/implicit-animations.md +++ b/src/content/learn/pathway/tutorial/implicit-animations.md @@ -12,7 +12,7 @@ start using them is with **implicit animations**. automatically animate changes to their properties without you needing to manage any intermediate behavior. -Flutter 提供了丰富的动画 API,而使用它们最简单的方式就是**隐式动画**。 +Flutter 提供了丰富的动画 API,而使用它们最简单的方式就是 **隐式动画**。 "隐式动画"是指一组能够自动对属性变化进行动画处理的 widget, 你无需手动管理任何中间状态行为。 diff --git a/src/content/learn/pathway/tutorial/layout.md b/src/content/learn/pathway/tutorial/layout.md index 42d987b644..fee57f8da5 100644 --- a/src/content/learn/pathway/tutorial/layout.md +++ b/src/content/learn/pathway/tutorial/layout.md @@ -253,8 +253,8 @@ elements, one for each *potential* guess. The list will always contain exactly five elements, and therefore will always render five rows. -这个 `guesses` 列表是一个**固定大小**的列表,初始包含五个元素, -每个元素对应一次*可能的*猜测。 +这个 `guesses` 列表是一个 **固定大小** 的列表,初始包含五个元素, +每个元素对应一次 **可能的** 猜测。 该列表将始终包含恰好五个元素, 因此将始终渲染五行。 ::: diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index a7248b0258..b80ccc89f9 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -146,7 +146,7 @@ These widgets are flexible because you can perform operations within the callback, building different widgets based on the state. -`ListenableBuilder` 使用了 *builder* 模式, +`ListenableBuilder` 使用了 **builder** 模式, 它需要一个回调函数而非 `child` widget 来 构建其下方的 widget 树。 这类 widget 非常灵活,因为你可以 diff --git a/src/content/learn/pathway/tutorial/slivers.md b/src/content/learn/pathway/tutorial/slivers.md index 6658bcb726..41d5402ffd 100644 --- a/src/content/learn/pathway/tutorial/slivers.md +++ b/src/content/learn/pathway/tutorial/slivers.md @@ -70,13 +70,13 @@ they serve different purposes and aren't interchangeable. - Slivers can **only** be direct children of scroll views, such as `CustomScrollView` and `NestedScrollView`. - Sliver **只能**作为滚动视图的直接子级,例如 + Sliver **只能** 作为滚动视图的直接子级,例如 `CustomScrollView` 和 `NestedScrollView`。 - Some scroll views **only** accept slivers as children. You can't pass regular widgets to `CustomScrollView.slivers`. - 某些滚动视图**只**接受 sliver 作为子级。 + 某些滚动视图 **只** 接受 sliver 作为子级。 你不能将普通 widget 传递给 `CustomScrollView.slivers`。 - To use regular widgets within a sliver context, diff --git a/src/content/learn/pathway/tutorial/stateful-widget.md b/src/content/learn/pathway/tutorial/stateful-widget.md index bc19db1c57..5d305ee6b6 100644 --- a/src/content/learn/pathway/tutorial/stateful-widget.md +++ b/src/content/learn/pathway/tutorial/stateful-widget.md @@ -127,7 +127,7 @@ a stateless widget to a stateful widget, do the following steps: 创建一个名为 `_GamePageState` 的新类,继承 `State`。 这个新类将持有可变的状态和 `build` 方法。 - 将 `build` 方法和所有*在 widget 上实例化的*属性 + 将 `build` 方法和所有 **在 widget 上实例化的** 属性 从 `GamePage` 移动到 State 对象中。 1. Implement the `createState()` method in `GamePage`, which @@ -143,7 +143,7 @@ VS Code and IntelliJ provide ["quick assists"][] that can do this conversion for you. 你不必手动完成这些工作,因为 VS Code 和 IntelliJ 的 Flutter 插件 -提供了["quick assists"][](快速辅助)功能,可以自动完成这个转换。 +提供了 ["quick assists"][](快速辅助)功能,可以自动完成这个转换。 ::: @@ -290,7 +290,7 @@ needs to repaint the screen, and the user wouldn't see any updates. 现在,当你在 `TextInput` 中输入一个合法的猜测并提交时, 应用将会反映用户的猜测。 -如果你调用 `_game.guess(guess)` 时*没有*调用 setState, +如果你调用 `_game.guess(guess)` 时 **没有** 调用 setState, 内部游戏数据会改变,但 Flutter 不会知道 它需要重新绘制屏幕,用户也不会看到任何更新。 diff --git a/src/content/learn/pathway/tutorial/user-input.md b/src/content/learn/pathway/tutorial/user-input.md index 1d3939a208..c6908c8381 100644 --- a/src/content/learn/pathway/tutorial/user-input.md +++ b/src/content/learn/pathway/tutorial/user-input.md @@ -169,7 +169,7 @@ what's taken by other widgets in the row. 它会让该子 widget 沿主轴方向填满所有可用空间 (`Row` 中为水平方向,`Column` 中为垂直方向), 即其他子 widget 未占用的空间。 -这使得 `TextField` 会拉伸以占据行中*除*其他 widget 之外的所有空间。 +这使得 `TextField` 会拉伸以占据行中 **除** 其他 widget 之外的所有空间。 :::tip `Expanded` is often the solution to "[unbounded width/height][]" exceptions. @@ -294,7 +294,7 @@ This argument accepts a callback, and the callback is triggered whenever the user presses the "Enter" key on the keyboard while the text field has focus. 现在,当用户输入文本时,你可以通过 `_textEditingController` 捕获文本, -但你需要知道_何时_去捕获它。 +但你需要知道 **何时** 去捕获它。 响应输入最简单的方式是使用 `TextField.onSubmitted` 参数。 该参数接受一个回调,当文本输入框获得焦点时, 用户按下键盘上的"Enter"键就会触发这个回调。 diff --git a/src/content/learn/pathway/tutorial/widget-fundamentals.md b/src/content/learn/pathway/tutorial/widget-fundamentals.md index 16ee6a6cae..4c9f9c3eb2 100644 --- a/src/content/learn/pathway/tutorial/widget-fundamentals.md +++ b/src/content/learn/pathway/tutorial/widget-fundamentals.md @@ -66,7 +66,7 @@ Alternatively, you can find the full lists in [this GitHub repository][full-words], as well as instructions to import it into your project. -或者,你可以在[这个 GitHub 仓库][full-words]中找到完整的列表, +或者,你可以在 [这个 GitHub 仓库][full-words] 中找到完整的列表, 以及将其导入到项目中的说明。 ::: @@ -110,7 +110,7 @@ The `Tile` class has a [constructor][] that defines what data needs to be passed into the widget to render the widget. In this case, the constructor accepts two parameters: -`Tile` 类有一个[构造函数][constructor],它定义了 +`Tile` 类有一个 [构造函数][constructor],它定义了 渲染该 widget 时需要传入哪些数据。 在本例中,构造函数接受两个参数: diff --git a/src/content/ui/adaptive-responsive/safearea-mediaquery.md b/src/content/ui/adaptive-responsive/safearea-mediaquery.md index ee2ec933c7..3ea17d5e6c 100644 --- a/src/content/ui/adaptive-responsive/safearea-mediaquery.md +++ b/src/content/ui/adaptive-responsive/safearea-mediaquery.md @@ -168,8 +168,8 @@ The `SafeArea` just needs to wrap the contents that would cause information loss if cut off by the hardware features mentioned earlier. -你*可以*将 `Scaffold` widget 的 `body` 用 `SafeArea` 包裹, -但你并*不一定*要把它放在 widget 树中这么高的位置。 +你 **可以** 将 `Scaffold` widget 的 `body` 用 `SafeArea` 包裹, +但你并 **不一定** 要把它放在 widget 树中这么高的位置。 `SafeArea` 只需要包裹那些如果被前面提到的硬件特性遮挡 就会导致信息丢失的内容即可。 From 00510548429f831b483eb361456c69703c8950ce Mon Sep 17 00:00:00 2001 From: XinLei Date: Tue, 10 Mar 2026 09:16:26 -0700 Subject: [PATCH 7/9] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=96=87=E6=A1=A3=E6=8E=92=E7=89=88=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创建 docs/style-guide.md,规定: - 链接前后加空格 - 使用加粗代替斜体表示强调 - 加粗前后加空格 - 中英文混排规则 Co-Authored-By: Claude Sonnet 4.5 --- docs/style-guide.md | 113 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/style-guide.md diff --git a/docs/style-guide.md b/docs/style-guide.md new file mode 100644 index 0000000000..f49e50ef4f --- /dev/null +++ b/docs/style-guide.md @@ -0,0 +1,113 @@ +# Flutter 中文文档排版指南 + +> 本指南规定了 Flutter 中文文档的排版规范,所有翻译和修改必须遵循这些规则。 + +--- + +## 核心排版规则 + +### 1. 链接格式 + +**规则**:链接前后必须加空格(中文与链接之间) + +❌ **错误示例**: +```markdown +请先从[学习路径页面](/learn/pathway)开始。 +``` + +✅ **正确示例**: +```markdown +请先从 [学习路径页面](/learn/pathway) 开始。 +``` + +### 2. 强调格式 + +**规则**:使用加粗(`**text**`)代替斜体(`_text_`)来表示强调 + +❌ **错误示例**: +```markdown +_Model-View-ViewModel_,即 _MVVM_。 +``` + +✅ **正确示例**: +```markdown +**Model-View-ViewModel**,即 **MVVM**。 +``` + +**原因**:斜体在中文排版中不够明显,加粗更符合中文阅读习惯。 + +### 3. 加粗前后空格 + +**规则**:加粗文本前后需要加空格(中文与加粗之间) + +❌ **错误示例**: +```markdown +这是一个**重要**的概念。 +``` + +✅ **正确示例**: +```markdown +这是一个 **重要** 的概念。 +``` + +**例外**: +- 如果加粗文本在句首或句尾,则不需要在边界处加空格 +- 如果加粗文本与标点符号相邻,标点符号一侧不加空格 + +示例: +```markdown +**重要提示**:请注意以下内容。 +这个概念非常 **重要**。 +``` + +### 4. 中英文混排 + +**规则**:中文与英文/数字之间加空格 + +✅ **正确示例**: +```markdown +了解 Flutter 的基本构成要素 +第 3 步 +使用 Dart 语言 +``` + +### 5. 代码与反引号 + +**规则**:反引号内的代码不翻译,反引号前后加空格(中文与代码之间) + +✅ **正确示例**: +```markdown +使用 `setState()` 方法更新状态 +创建一个 `StatefulWidget` 类 +``` + +--- + +## 检查清单 + +在提交翻译前,请确认: + +- [ ] 所有链接前后都有空格 +- [ ] 没有使用斜体(`_text_`)表示强调 +- [ ] 所有强调都使用加粗(`**text**`) +- [ ] 加粗文本前后有空格(除非在句首/句尾或与标点相邻) +- [ ] 中英文之间有空格 +- [ ] 中文与数字之间有空格 +- [ ] 代码反引号前后有空格 + +--- + +## 自动化检查 + +可以使用以下命令检查常见格式问题: + +```bash +# 检查链接前后是否缺少空格(中文字符紧邻链接) +grep -rn '[一-龥]\[.*\]([^)]*)[一-龥]' src/content/ + +# 检查是否使用了斜体 +grep -rn '_[^_]*_' src/content/ | grep -v '://' | grep -v 'ignore:' + +# 检查加粗前后是否缺少空格 +grep -rn '[一-龥]\*\*[^*]*\*\*[一-龥]' src/content/ +``` From 15cfb631419854c8c6dbcb66dd470ed3b3ed6398 Mon Sep 17 00:00:00 2001 From: XinLei Date: Tue, 10 Mar 2026 09:19:55 -0700 Subject: [PATCH 8/9] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20auto-pilot=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E4=BB=B6=E5=88=B0=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index f0a56959a8..87510a1fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ tmp/ # Temporary as site-shared used to be a submodule. site-shared/ + +# Auto-pilot related files (project management, not source code) +docs/operations/ +docs/todo/ +scripts/ +test-flows/ From 888ac85e6ca639d643ebaea74599923ae86954e3 Mon Sep 17 00:00:00 2001 From: XinLei Date: Tue, 10 Mar 2026 09:21:31 -0700 Subject: [PATCH 9/9] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Flutter=20?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E7=BF=BB=E8=AF=91=E6=9C=AF=E8=AF=AD=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.5 --- docs/glossary.md | 206 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/glossary.md diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000000..15d98b343b --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,206 @@ +# Flutter 中文翻译术语表 + +> 翻译时必须参照本表,确保术语统一。 +> 规则:类名/API 名保持英文不翻译,概念性术语翻译为中文。 + +--- + +## 不翻译(保持英文) + +以下术语在中文语境中直接使用英文,不翻译: + +### 框架与平台名 +| 英文 | 说明 | +|------|------| +| Flutter | 框架名 | +| Dart | 语言名 | +| Material / Material Design / Material 3 | 设计语言 | +| Cupertino | iOS 风格设计语言 | +| Impeller | 渲染引擎名 | +| Skia | 渲染引擎名 | +| pub / pub.dev | 包管理器 | + +### Widget 类名(所有类名不翻译) +| 英文 | 说明 | +|------|------| +| Widget | 核心概念,不译为"组件" | +| StatelessWidget | 无状态 widget 类 | +| StatefulWidget | 有状态 widget 类 | +| Container | widget 类名 | +| Scaffold | widget 类名 | +| AppBar | widget 类名 | +| Row / Column | widget 类名 | +| Text | widget 类名 | +| Icon | widget 类名 | +| ListView / GridView | widget 类名 | +| SizedBox | widget 类名 | +| Expanded / Flexible | widget 类名 | +| Center | widget 类名 | +| Stack / Positioned | widget 类名 | +| InheritedWidget | widget 类名 | +| AnimatedContainer | widget 类名(所有 Animated* 同理)| + +### 方法与属性名 +| 英文 | 说明 | +|------|------| +| build() | 构建方法 | +| setState() | 状态更新方法 | +| initState() | 生命周期方法 | +| dispose() | 生命周期方法 | +| didUpdateWidget() | 生命周期方法 | +| runApp() | 入口方法 | +| Navigator.push() / pop() | 导航方法 | + +### 工具与包名 +| 英文 | 说明 | +|------|------| +| DevTools | 开发者工具 | +| hot reload | 可译为"热重载" | +| hot restart | 可译为"热重启" | +| Provider | 状态管理包名 | +| Riverpod | 状态管理包名 | +| Bloc | 状态管理包名 | +| pubspec.yaml | 配置文件名 | + +--- + +## 翻译对照表 + +以下术语有统一的中文翻译: + +### 核心概念 +| 英文 | 中文 | 备注 | +|------|------|------| +| widget | widget | **不译为"组件"**,直接用英文小写 | +| widget tree | widget 树 | | +| element tree | element 树 | | +| render tree | 渲染树 | | +| state | 状态 | | +| stateful widget | 有状态的 widget | 描述性用法 | +| stateless widget | 无状态的 widget | 描述性用法 | +| build method | build 方法 | | +| context / BuildContext | context / 上下文 | 视语境选用 | +| key | key / 键值 | 视语境选用 | +| constructor | 构造函数 | | +| callback | 回调 / 回调函数 | | +| mixin | mixin | 不翻译 | + +### 布局相关 +| 英文 | 中文 | 备注 | +|------|------|------| +| layout | 布局 | | +| constraints | 约束 | | +| padding | 内边距 | 或直接用 padding | +| margin | 外边距 | | +| alignment | 对齐 | | +| flex | 弹性 | | +| scrolling | 滚动 | | +| sliver | sliver | **不翻译** | +| viewport | 视口 | | +| overflow | 溢出 | | +| responsive | 响应式 | | +| adaptive | 自适应 | | + +### 状态管理 +| 英文 | 中文 | 备注 | +|------|------|------| +| state management | 状态管理 | | +| ephemeral state | 短时状态 / 临时状态 | | +| app state | 应用状态 | | +| declarative | 声明式 | | +| imperative | 命令式 | | +| immutable | 不可变的 | | +| reactive | 响应式的 | | + +### 导航 +| 英文 | 中文 | 备注 | +|------|------|------| +| navigation | 导航 | | +| route | 路由 | | +| push | push / 推入 | 视语境选用 | +| pop | pop / 弹出 | 视语境选用 | +| deep linking | 深层链接 | | +| named route | 命名路由 | | + +### 动画 +| 英文 | 中文 | 备注 | +|------|------|------| +| animation | 动画 | | +| implicit animation | 隐式动画 | | +| explicit animation | 显式动画 | | +| tween | 补间 | | +| curve | 曲线 | | +| animation controller | 动画控制器 | | +| staggered animation | 交织动画 | | +| hero animation | Hero 动画 | | +| transition | 过渡 / 转场 | 视语境选用 | + +### 构建与工具 +| 英文 | 中文 | 备注 | +|------|------|------| +| build | 构建 | | +| compile | 编译 | | +| debug mode | 调试模式 | | +| release mode | 发布模式 | | +| profile mode | 性能分析模式 | | +| hot reload | 热重载 | | +| hot restart | 热重启 | | +| tree shaking | tree shaking / 摇树优化 | | + +### 性能 +| 英文 | 中文 | 备注 | +|------|------|------| +| jank | 卡顿 | | +| frame | 帧 | | +| rasterization | 光栅化 | | +| rendering | 渲染 | | +| performance | 性能 | | + +### 测试 +| 英文 | 中文 | 备注 | +|------|------|------| +| unit test | 单元测试 | | +| widget test | widget 测试 | | +| integration test | 集成测试 | | +| golden test | 黄金测试 / golden 测试 | | + +### 国际化 +| 英文 | 中文 | 备注 | +|------|------|------| +| internationalization (i18n) | 国际化 | | +| localization (l10n) | 本地化 | | +| locale | 语言区域 | | + +### 平台集成 +| 英文 | 中文 | 备注 | +|------|------|------| +| platform channel | 平台通道 | | +| method channel | 方法通道 | | +| event channel | 事件通道 | | +| plugin | 插件 | | +| package | package / 软件包 | 视语境选用 | +| embedding | 嵌入 | | +| add-to-app | 混合开发 / add-to-app | | + +### 其他常用 +| 英文 | 中文 | 备注 | +|------|------|------| +| null safety | 空安全 | | +| sound null safety | 健全的空安全 | | +| type system | 类型系统 | | +| future | Future | 类名不译 | +| stream | Stream / 流 | 视语境选用 | +| async / await | async / await | 不翻译 | +| framework | 框架 | | +| library | 库 | | +| dependency | 依赖 | | + +--- + +## 混排格式规则 + +1. **中英文之间加空格**:`了解 Flutter 的基本构成要素` +2. **中文与数字之间加空格**:`第 3 步` +3. **中文标点使用中文全角**:`,。!?:;()` +4. **英文标点保持英文半角**:代码中的标点不变 +5. **反引号内容不翻译**:`` `setState()` `` 保持原样