[Flutter学徒] 6 - 交互部件

760 阅读40分钟

本文由 简悦 SimpRead 转码, 原文地址 www.raywenderlich.com

学习如何结合控件和输入小部件,通过使用小部件(如t......)来触发更新。

在上一章中,你学会了如何用可滚动的小部件捕捉大量的数据。但你如何使一个应用程序更有吸引力?你如何收集用户的输入和反馈?

在本章中,你将探索交互式部件。特别是,你将学习如何创建。

  • 基于手势的小工具
  • 时间和日期选择器小工具
  • 输入和选择小工具
  • 解除的小工具

你将继续在Fooderlich上工作,建立最后的标签。要买的东西。这个标签允许用户创建一个待购物品的杂货清单,对其进行修改,并在完成后从他们的TODO清单中勾选。他们将能够添加、删除和更新清单上的物品。

你还会得到一个关于Provider的快速介绍,这个包可以帮助你管理状态并通知组件有更新的数据要显示。

你将从建立一个空屏幕开始。如果没有可用的食品杂货项目,用户有两个选择。

  1. 点击浏览菜谱,查看其他菜谱。
  2. 点击 "+"按钮,添加一个新的食品杂货项目。

当用户点击 "+"按钮时,应用程序将呈现一个屏幕,供用户创建一个项目。

该屏幕由以下数据属性组成。

  • 项目的名称。
  • 一个显示项目重要性级别的标签。
  • 你想购买这个物品的日期和时间。
  • 你想给这个物品贴上的颜色。
  • 物品的数量。

此外,当你创建项目时,应用程序将显示项目本身的预览!这有多酷?这多酷啊?

当你创建你的第一个项目时,杂货清单会取代空的屏幕。

用户将能够在这个新屏幕上采取四项行动。

  1. 点一个杂货店项目来更新一些信息。
  2. 点选复选框,将一个项目标记为完成。
  3. 轻扫该项目以删除它。
  4. 创建并添加另一个项目到列表中。

在本章结束时,你将为用户建立一个功能性的TODO列表,以管理他们的杂货项目。你甚至会增加对明暗模式的支持!

现在是时候开始了。

开始吧

在Android Studio中打开启动项目,如有必要,运行flutter pub get。然后,运行该应用程序。

你会看到上一章中的Fooderlich应用程序。当你点击要购买标签时,你会看到一个蓝屏。别担心,很快你就会添加一个图片,这样你的用户就不会认为有问题了。

在assets/fooderlich_assets里面,你会发现一个新图片。

当列表中没有任何项目时,你会显示 empty_list.png。

现在,是时候添加一些代码了!

创建杂货店项目模型

首先,你要为你要保存的物品信息设置模型。在models目录下,创建一个名为grocery_item.dart的新文件,然后添加以下代码。

import 'package:flutter/painting.dart';

// 1
enum Importance { low, medium, high }

class GroceryItem {
  // 2
  final String id;
  // 3
  final String name;
  final Importance importance;
  final Color color;
  final int quantity;
  final DateTime date;
  final bool isComplete;

  GroceryItem(
      {this.id,
      this.name,
      this.importance,
      this.color,
      this.quantity,
      this.date,
      this.isComplete = false});

  // 4
  GroceryItem copyWith(
      {String id,
      String name,
      Importance importance,
      Color color,
      int quantity,
      DateTime date,
      bool isComplete}) {
    return GroceryItem(
        id: id ?? this.id,
        name: name ?? this.name,
        importance: importance ?? this.importance,
        color: color ?? this.color,
        quantity: quantity ?? this.quantity,
        date: date ?? this.date,
        isComplete: isComplete ?? this.isComplete);
  }
}


花点时间来探索grocery_item.dart包含的内容。

Importance是一个枚举,你将用它来标记一个项目的重要性:低、中或高。 每个GroceryItem都必须有一个唯一的id,以区别于其他项目。 用户可以设置名称、重要程度、颜色标签、数量和日期,以及标记完成的项目。 copyWith复制并创建一个全新的GroceryItem的实例。这在以后你管理物品的状态时将很有用。

接下来,你需要把这段新代码添加到桶状文件中。

打开models/models.dart桶状文件,添加以下内容。

export 'grocery_item.dart'

注意:记住,桶状文件的目的是将常见的Dart文件组合在一起。这允许你把常用的类放在一起,所以你只需要导入一个文件--在这种情况下,models.dart。

创建杂货店屏幕

现在你已经建立了你的模型,现在是时候创建杂货店屏幕了。这个屏幕将显示两个视图中的一个。

EmptyGroceryScreen将在没有物品时显示。在用户添加了至少一个项目后,你会显示GroceryListScreen来代替。

建立GroceryScreen

你需要做的第一件事是创建GroceryScreen,它决定是显示空屏幕还是列表屏幕。

在屏幕目录下,创建一个新的Dart文件,名为grocery_screen.dart。然后,添加以下代码。

import 'package:flutter/material.dart';

class GroceryScreen extends StatelessWidget {
  const GroceryScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 2: Replace with EmptyGroceryScreen
    return Container(color: Colors.green);
  }
}


现在,这只是显示一个绿色的屏幕。以后你会用更多的信息来取代它。

显示杂货店的屏幕

你的下一个任务是给你的用户提供一个看到新页面的方法--当它准备好的时候,就是这样。当你点击To Buy标签时,它需要显示GroceryScreen,而不是纯色的。

通过打开home.dart并添加以下导入来做到这一点。

import 'screens/grocery_screen.dart';


然后,找到// TODO 1: 用grocery screen替换,用下面的内容替换Container。

const GroceryScreen()


这样就用新的屏幕替换了现有的空容器。

在应用程序运行的情况下,执行热重启,然后点击要购买标签。现在你会看到一个绿色的屏幕,而不是一个蓝色的。

很好! 这就是进步,对吗?嗯,在你创建屏幕后会变得更好。你现在就开始做,用杂货店屏幕,当清单是空的时候就会显示。

创建空的杂货店屏幕

在屏幕目录下,创建一个新的Dart文件,名为empty_grocery_screen.dart,并添加以下内容。

import 'package:flutter/material.dart';

class EmptyGroceryScreen extends StatelessWidget {
  const EmptyGroceryScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 3: Replace and add layout widgets
    return Container(color: Colors.purple);
  }
}


这就是EmptyGroceryScreen的第一个简单实现。你很快就会用其他部件替换占位符Container。

添加空屏幕

在你继续构建EmptyGroceryScreen之前,你需要设置小部件的热重载,这样你就可以看到你的实时更新。

打开grocery_screen.dart并添加以下导入。

import 'empty_grocery_screen.dart';


找到 // TODO 2: 用 EmptyGroceryScreen 替换,用以下内容替换 Container。

// TODO 4: Add a scaffold widget
return const EmptyGroceryScreen();


现在,你会看到下面的紫色屏幕。

太好了! 现在,你就可以在编码后看到你的变化了。

添加布局小部件

接下来,你将通过添加处理空杂货店屏幕布局的部件,为页面的最终外观奠定基础。

打开 empty_grocery_screen.dart 并找到 // TODO 3: 替换并添加布局部件。用以下内容替换它下面的一行。

// 1
return Padding(
  padding: const EdgeInsets.all(30.0),
  // 2
  child: Center(
      // 3
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // TODO 4: Add empty image
          // TODO 5: Add empty screen title
          // TODO 6: Add empty screen subtitle
          // TODO 7: Add browse recipes button
        ],),),
);


下面是你添加的内容。

填充。在所有边上增加30像素。 中心。将所有其他的部件放在中间。 纵队。处理其他小部件的垂直布局。

现在,该应用程序将看起来像这样。

现在,是时候让你的屏幕变得更有趣了!

添加视觉碎片

最后,是时候超越多彩的屏幕,添加一些文字和图片了。

定位 // TODO 4: 添加空的图像,并用下面的内容代替它。

AspectRatio(
  aspectRatio: 1 / 1,
  child: Image.asset('assets/fooderlich_assets/empty_list.png'),),


AspectRatio将其子代的尺寸设定为指定的aspectRatio。虽然aspectRatio是一个双数,但Flutter文档建议将其写成宽度/高度,而不是计算结果。在这种情况下,你想要一个1/1的方形长宽比,而不是1.0。

注意:Dart为你做了计算,提供了双数。如果你想要一个16:9的比例呢?你会填上16 / 9而不是1.5。

接下来,把// TODO 5: 添加空的屏幕标题替换成以下内容。

const SizedBox(height: 8.0),
const Text('No Groceries', style: TextStyle(fontSize: 21.0),),


这里,你用Text添加了一个标题,然后在它和上面的图片之间放置了一个8像素的盒子。

接下来,找到//TODO 6:添加空的屏幕字幕,用下面的内容代替。

const SizedBox(height: 16.0),
const Text(
  'Shopping for ingredients?\n'
  'Tap the + button to write them down!',
  textAlign: TextAlign.center,
),


正如你刚才对标题所做的那样,你添加了一个副标题,在它和上面的标题之间有一个16像素的框。

最后,将//TODO 7:添加浏览食谱按钮替换为以下内容。

MaterialButton(
  textColor: Colors.white,
  child: const Text('Browse Recipes'),
  shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(30.0),),
  color: Colors.green,
  onPressed: () {
    // TODO 8: Go to Recipes Tab
},),


现在,你已经添加了一个圆形的绿色MaterialButton。

注意:还有很多其他风格的按钮可以选择!更多的选择见。参见flutter.dev/docs/develo…

现在,检查一下你的应用程序。它将看起来像这样。

很好! 现在,用户将知道他们的食品杂货清单是空的。接下来,你将添加一个处理程序,以便点击浏览食谱按钮将你带到食谱标签。

切换标签

你有两个选择来实现切换到Recipes标签。

标准回调。将一个回调从父小组件沿着小组件树一路传递到一个子小组件。当准备好时,它在树上的每一级触发回调,并调用setState()来重建主页小组件的子树。

这种方法对于小项目来说是可以的,但是当你的项目变大时,它就会变得很烦人,很复杂。想象一下,必须在widget树的每一层都传递一个回调。这很难维护,而且会使你的小部件彼此之间过度耦合。

提供者。围绕着继承的widget。这个包允许你为后代的widget提供数据和状态信息。

这种方法更好。它允许子树中的后代小部件访问状态信息。

你不需要回调,只需要把消费者包在你的小部件周围。每当一个状态改变时,这些消费者就会重建它下面的子树。

这是提供者的时间!

提供者概述

Provider是一种方便的方式,可以将状态传递到widget树下,并在发生变化时重建你的用户界面。接下来你将把它添加到你的项目中。

然而,在这之前,你需要理解四个概念。

ChangeNotifier由一个类扩展,以向其监听器提供变化通知。 ChangeNotifierProvider监听ChangeNotifier的变化。它下面的widgets可以访问状态对象并监听状态变化。 消费者包裹着widget树的一部分。当它所监听的状态发生变化时,它会重建子树的一部分。 Provider.of允许后代的widget访问状态对象。如果你只需要访问状态对象而不需要监听变化,请使用这个

注意:这是对Provider如何工作的一个快速概述。你会在第13章 "状态管理 "中了解更多关于使用Provider进行状态管理的信息。

添加Provider

打开pubspec.yaml,在依赖项下添加以下包。

provider: ^5.0.0


保存该文件并运行 flutter pub get。

创建TabManager

在models目录下,创建一个名为tab_manager.dart的新文件并添加以下代码。

import 'package:flutter/material.dart';

// 1
class TabManager extends ChangeNotifier {
  // 2
  int selectedTab = 0;

  // 3
  void goToTab(index) {
    // 4
    selectedTab = index;
    // 5
    notifyListeners();
  }

  // 6
  void goToRecipes() {
    selectedTab = 1;
    // 7
    notifyListeners();
  }
}


TabManager管理着用户点击的标签索引。这段代码做了以下工作。

TabManager扩展了ChangeNotifier。这允许该对象向其监听器提供变化通知。 selectedTab记录了用户点击的标签。 goToTab是一个简单的函数,它修改了当前标签的索引。 存储用户点击的新标签的索引。 调用notifyListeners()来通知所有监听此状态的部件。 goToRecipes()是一个特定的函数,它将selectedTab设置为Recipes标签,它的索引是1。 通知所有监听TabManager的widgets,Recipes是选定的标签。

接下来,在models.dart中添加TabManager。

export 'tab_manager.dart';


棒极了! 现在,你有一个简单的标签管理器来管理当前的标签索引。现在是时候把它提供给你的小部件了。

管理标签状态

那么你将如何使用Provider呢?这里有一个蓝图。

这些是你将采取的步骤。

在Fooderlich的根层提供TabManager变更通知器。这允许子孙小部件访问标签管理器。 在Home周围包裹一个消费者。这可以确保Home在TabManager的标签索引改变时显示正确的屏幕。 因为Provider是一个围绕继承部件的包装器,EmptyGroceryScreen可以通过其上下文访问TabManager状态对象。 goToRecipes()改变了标签索引,通知了Consumer,然后Consumer用正确的标签来重建Home。

听起来很简单,对吗?好吧,那么,现在是时候跳进去了。

提供TabManager

首先,你需要在Fooderlich的顶层提供TabManager,以便让后代的widgets访问状态对象。

打开main.dart,添加以下导入。

import 'package:provider/provider.dart';
import 'models/models.dart';


然后,找到// TODO 8: 用MultiProvider替换Home,并用以下内容替换Home。

// 1
home: MultiProvider(
  providers: [
    // 2
    ChangeNotifierProvider(create: (context) => TabManager(),),
    // TODO 10: Add GroceryManager Provider
  ],
  child: const Home(),),


下面是代码的工作方式。

你把MultiProvider指定为Home的一个属性。这接受了一个供Home的后代部件访问的提供者列表。 ChangeNotifierProvider创建一个TabManager的实例,它监听标签索引的变化并通知其监听者。

注意:当你需要为一个widget树提供一个以上的提供者时,请使用MultiProvider。稍后,你还会添加一个GroceryManager状态对象来管理项目列表。

添加一个TabManager消费者

现在,是时候设置消费者了,这样应用程序就可以收听TabManager广播的变化。

打开home.dart并添加以下导入。

import 'package:provider/provider.dart';
import 'models/models.dart';


接下来,找到// TODO 9: Wrap inside a Consumer Widget,并将build()的整个主体替换为以下内容。

// 1
return Consumer<TabManager>(builder: (context, tabManager, child) {
  return Scaffold(
      appBar: AppBar(
          title: Text('Fooderlich',
              style: Theme.of(context).textTheme.headline6),),
      // 2
      body: pages[tabManager.selectedTab],
      bottomNavigationBar: BottomNavigationBar(
          selectedItemColor: Theme.of(context)
            .textSelectionTheme.selectionColor,
          // 3
          currentIndex: tabManager.selectedTab,
          onTap: (index) {
            // 4
            tabManager.goToTab(index);
          },
          items: <BottomNavigationBarItem>[
            const BottomNavigationBarItem(
                icon: Icon(Icons.explore), label: 'Explore',),
            const BottomNavigationBarItem(
                icon: Icon(Icons.book), label: 'Recipes',),
            const BottomNavigationBarItem(
                icon: Icon(Icons.list), label: 'To Buy',),
          ],),);
},);


以下是代码的工作方式。

把所有的小部件都包裹在Consumer里面。当TabManager改变时,它下面的widget将重建。 根据当前的标签索引,显示正确的页面小部件。 设置BottomNavigationBar的当前索引。 当用户点击不同的标签时,调用manager.goToTab(),以通知其他widget索引的改变。

现在,由于你已经添加了Consumer<TabManager>,你不再需要_onItemTapped()。找到这个方法并删除它,以及int _selectedIndex = 0; 声明。

切换到菜谱标签

还有最后一步来实现在标签间切换的能力。

打开 empty_grocery_screen.dart 并添加以下导入。

import 'package:provider/provider.dart';
import '../models/models.dart';


然后,找到//TODO 8:进入Recipes Tab,用下面的内容替换它。

Provider.of<TabManager>(context, listen: false).goToRecipes();


这里,你使用Provider.of()来访问模型对象TabManager。 goToRecipes()将索引设置为Recipes标签。这就通知了消费者用正确的标签索引重建主页。

在To Buy标签上,点击Browse Recipes按钮。现在它将导航到菜谱屏幕。

很好! 现在是时候创建一些食品杂货项目了。

管理杂货店项目

在你显示或创建杂货店项目之前,你需要一种方法来管理它们。

在models目录下,创建一个名为grocery_manager.dart的新文件。添加以下内容。

import 'package:flutter/material.dart';
import 'grocery_item.dart';

class GroceryManager extends ChangeNotifier {
  // 1
  final _groceryItems = <GroceryItem>[];

  // 2
  List<GroceryItem> get groceryItems => List.unmodifiable(_groceryItems);

  // 3
  void deleteItem(int index) {
    _groceryItems.removeAt(index);
    notifyListeners();
  }

  // 4
  void addItem(GroceryItem item) {
    _groceryItems.add(item);
    notifyListeners();
  }

  // 5
  void updateItem(GroceryItem item, int index) {
    _groceryItems[index] = item;
    notifyListeners();
  }

  // 6
  void completeItem(int index, bool change) {
    final item = _groceryItems[index];
    _groceryItems[index] = item.copyWith(isComplete: change);
    notifyListeners();
  }
}


GroceryManager扩展了ChangeNotifier,以通知其监听器关于状态的变化。下面是GroceryManager的工作原理。

这个管理器持有一个_groceryItems的私有数组。只有管理器可以改变和更新杂货店项目。 为groceryItems提供一个公共的getter方法,它是不可修改的。外部实体只能读取杂货店项目的列表。 deleteItem()在一个特定的索引中删除一个项目。 addItem() 在列表的末尾添加一个新的食品杂货项目。 updateItem() 用一个新的项目替换给定索引处的旧项目。 completeItem()切换isComplete标志的开启和关闭。

这些方法中的每一个都调用 notifyListeners()。这通知了GroceryManager中需要重建的小部件的变化。

接下来,打开桶状文件models.dart,并添加以下内容。

export 'grocery_manager.dart';


这可以让其他类使用新的管理器。

添加GroceryManager作为一个提供者

就像你对TabManager所做的那样,你现在要添加GroceryManager作为一个提供者。

打开main.dart并找到//TODO 10: 添加GroceryManager提供者。把它替换成。

ChangeNotifierProvider(create: (context) => GroceryManager(),),


Fooderlich的所有后代部件现在都可以监听或访问GroceryManager!

消耗变化

待购屏幕如何对杂货清单的变化做出反应?到目前为止,它还没有,但你现在已经准备好将新的管理器与显示食品杂货项目的视图挂钩了。 :]

打开grocery_screen.dart,添加以下导入。

import 'package:provider/provider.dart';
import '../models/models.dart';


就在build()下面,添加以下代码。

Widget buildGroceryScreen() {
  // 1
  return Consumer<GroceryManager>(
    // 2
    builder: (context, manager, child) {
    // 3
    if (manager.groceryItems.isNotEmpty) {
      // TODO 25: Add GroceryListScreen
      return Container();
    } else {
      // 4
      return const EmptyGroceryScreen();
    }
  },);
}


buildGroceryScreen()是一个辅助函数,用于决定构建哪种widget树。它的工作原理是这样的。

你把你的widget包在一个Consumer里面,它监听GroceryManager的状态变化。 当杂货店经理的项目发生变化时,Consumer会重建自己下面的widget。 如果列表中有杂货店项目,就显示GroceryListScreen。你将很快创建这个屏幕。 如果没有杂货店项目,显示EmptyGroceryScreen。

提示:你应该只把消费者包在需要它的部件周围。例如,在顶层包裹一个消费者小组件会迫使它重建整个树!

接下来,找到// TODO 4:添加一个脚手架部件,并将其下面的一行替换为以下内容。

// 5
return Scaffold(
  // 6
  floatingActionButton: FloatingActionButton(
      child: const Icon(Icons.add),
      onPressed: () {
        // TODO 11: Present GroceryItemScreen
      },),
  // 7
  body: buildGroceryScreen(),);


下面是代码的工作原理。

GroceryScreen的主要布局结构是一个脚手架。 添加一个带有 "+"图标的浮动动作按钮。点击该按钮,就会出现创建或添加物品的屏幕。你将在后面构建这个屏幕。 构建杂货店屏幕的其余子树。这是下一步要做的!

GroceryScreen已经全部设置好了,可以在空屏幕和列表屏幕之间切换。现在,是时候创建杂货店项目了!

添加新的包裹

在继续前进之前,你需要添加三个新的包。打开pubspec.yaml,在依赖项下添加以下内容。

flutter_colorpicker: ^0.4.0
intl: ^0.17.0
uuid: ^3.0.4


下面是它们各自的作用。

flutter_colorpicker。为您的应用程序提供一个材料颜色选择器。 intl。提供国际化和本地化的工具。你会用它来格式化日期。 uuid。为每个杂货店项目生成唯一的键。这有助于你知道要添加、更新或删除哪个项目。

不要忘记在更新pubspec.yaml条目后运行flutter pub get。

你要创建的下一个屏幕是GroceryItemScreen,它为用户提供了编辑或创建新杂货项目的方法。

创建GroceryItemScreen

在屏幕目录中,创建一个名为grocery_item_screen.dart的新文件并添加以下内容。

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import '../models/models.dart';

class GroceryItemScreen extends StatefulWidget {
  // 1
  final Function(GroceryItem) onCreate;
  // 2
  final Function(GroceryItem) onUpdate;
  // 3
  final GroceryItem originalItem;
  // 4
  final bool isUpdating;

  const GroceryItemScreen(
      {Key key, this.onCreate, this.onUpdate, this.originalItem,})
      : isUpdating = (originalItem != null),
        super(key: key);

  @override
  _GroceryItemScreenState createState() => _GroceryItemScreenState();
}

class _GroceryItemScreenState extends State<GroceryItemScreen> {

  // TODO: Add grocery item screen state properties

  @override
  Widget build(BuildContext context) {
    // TODO 12: Add GroceryItemScreen Scaffold
    return Container(color: Colors.orange);
  }
}


花点时间来理解你添加的属性。

onCreate是一个回调,让你知道什么时候创建了一个新项目。 onUpdate是一个回调,用于返回更新的食品杂货项目。 用户点击的杂货项目。 isUpdating决定了用户是在创建还是编辑一个项目。

在你继续构建GroceryItemScreen之前,你需要呈现这个小部件。

呈现GroceryItemScreen

打开grocery_screen.dart并添加以下导入。

import 'grocery_item_screen.dart';


然后,找到//TODO 11: Present GroceryItemScreen,用下面的代码替换它。

// 1
final manager = Provider.of<GroceryManager>(
  context,
  listen: false);
// 2
Navigator.push(
  context,
  // 3
  MaterialPageRoute(
    // 4
    builder: (context) => GroceryItemScreen(
      // 5
      onCreate: (item) {
        // 6
        manager.addItem(item);
        // 7
        Navigator.pop(context);
      },
    ),),);


下面是代码的工作原理。

返回树中可用的GroceryManager。

Navigator.push()在路由栈中添加一个新的路由。

MaterialPageRoute用一个特定平台的过渡来替换整个屏幕。例如,在Android中,它向上滑动并淡入。在iOS中,它从右边滑入。

在路由的构建器回调中创建一个新的GroceryItemScreen。

onCreate定义了如何处理创建的项目。

addItem()将这个新项目添加到项目列表中。

一旦该项目被添加到列表中,pop就会移除顶部的导航路线项目GroceryItemScreen,以显示杂货店项目的列表。

注意:现在,你只需要知道Navigator.push()呈现了一个新的屏幕,Navigator.pop又将其删除。你将在下一章中更深入地了解Navigator。

在你正在运行的应用程序上点击 "+"按钮,你会看到以下内容。

继续前进!

添加GroceryItemScreen的状态属性

现在,是时候给杂货店项目一些属性,使它们更有用。

首先打开grocery_item_screen.dart并找到//TODO: 添加杂货店项目屏幕的状态属性。用下面的内容替换它。

final _nameController = TextEditingController();
String _name = '';
Importance _importance = Importance.low;
DateTime _dueDate = DateTime.now();
TimeOfDay _timeOfDay = TimeOfDay.now();
Color _currentColor = Colors.green;
int _currentSliderValue = 0;


忽略任何棕褐色的皱纹--当你添加更多的代码时,这些皱纹会消失。

_GroceryItemScreenState管理着六个不同的值。

_nameController是一个TextEditingController。这个控制器监听文本的变化。它控制显示在一个文本字段中的值。 _name存储项目的名称。 _importance存储重要性级别。 _dueDate存储当前的日期和时间。 _timeOfDay 存储当前的时间。 _currentColor 存储颜色标签。 _currentSliderValue存储物品的数量。

这些属性将创建最终的GroceryItem。

初始化GroceryItemScreen的属性

接下来,在_GroceryItemScreenState中,在build()之前添加以下代码。

@override
void initState() {
  // 1
  if (widget.originalItem != null) {
    _nameController.text = widget.originalItem.name;
    _name = widget.originalItem.name;
    _currentSliderValue = widget.originalItem.quantity;
    _importance = widget.originalItem.importance;
    _currentColor = widget.originalItem.color;
    final date = widget.originalItem.date;
    _timeOfDay = TimeOfDay(hour: date.hour, minute: date.minute);
    _dueDate = date;
  }

  // 2
  _nameController.addListener(() {
    setState(() {
      _name = _nameController.text;
    });
  });

  super.initState();
}


在一个widget建立之前,initState()在使用前初始化其属性。

当originalItem不是空时,用户正在编辑一个现有的项目。在这种情况下,你必须配置小组件以显示该项目的值。 添加一个监听器来监听文本字段的变化。当文本发生变化时,你设置_name。

这个新添加的代码使那些棕褐色的斜线消失了。 :]

很快,你就会添加一些布局小部件,使项目在屏幕上正确对齐。然而,在这之前,你还有一些代码需要添加,以清理一切。

最后,在initState()之后添加以下方法。

@override
void dispose() {
  _nameController.dispose();
  super.dispose();
}


当你不再需要TextEditingController的时候,这将会处置它。

现在你已经完成了你的配置并清理了它,是时候添加一些布局部件了!

添加GroceryItemScreen的布局部件

还是在grocery_item_screen.dart中,找到//TODO 12: Add GroceryItemScreen Scaffold并将其下面的一行替换为以下内容。

// 1
return Scaffold(
  // 2
  appBar: AppBar(
    actions: [
      IconButton(
          icon: const Icon(Icons.check),
          onPressed: () {
            // TODO 24: Add callback handler
          },)
    ],
    // 3
    elevation: 0.0,
    // 4
    title: Text('Grocery Item',
        style: GoogleFonts.lato(fontWeight: FontWeight.w600),),),
  // 5
  body: Container(
    padding: const EdgeInsets.all(16.0),
    child: ListView(
      children: [
      // TODO 13: Add name TextField
      // TODO 14: Add Importance selection
      // TODO 15: Add date picker
      // TODO 16: Add time picker
      // TODO 17: Add color picker
      // TODO 18: Add slider
      // TODO: 19: Add Grocery Tile
    ],),),
  );


以下是代码的工作方式。

Scaffold定义了整个屏幕的主要布局和结构。 包括一个带有一个行动按钮的应用栏。当用户完成创建一个项目时,他们会点击这个按钮。 设置海拔高度为0.0,移除应用栏下的阴影。 设置应用栏的标题。 在脚手架的主体中显示一个列表视图,每边填充16像素。你很快就会在这个列表视图中加入一堆交互式部件。

你的屏幕现在看起来像这样。

这有点光秃秃的,但时间不长。现在你有了主要的布局结构,是时候添加交互式小部件了

添加输入杂货店名称的文本字段

你要创建的第一个输入部件是TextField,当你需要用户输入一些文本时,它是一个有用的部件。在这种情况下,它将捕获杂货店的名称。

有两种方法可以监听文本的变化。你可以选择。

实现一个onChanged回调处理程序。 为TextField提供TextEditingController,并为文本变化添加一个监听器。这种方法允许对你的文本字段进行更精细的控制,例如根据某种逻辑改变文本字段的值。

你将使用第二种方法。TextEditingController允许你设置初始值。

注意:你可以在这里找到更多关于文本字段的信息:flutter.dev/docs/cookbo…

还是在grocery_item_screen.dart中,在build()下面添加以下代码。

Widget buildNameField() {
  // 1
  return Column(
    // 2
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 3
      Text('Item Name', style: GoogleFonts.lato(fontSize: 28.0),),
      // 4
      TextField(
        // 5
        controller: _nameController,
        // 6
        cursorColor: _currentColor,
        // 7
        decoration: InputDecoration(
          // 8
          hintText: 'E.g. Apples, Banana, 1 Bag of salt',
          // 9
          enabledBorder: const UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.white),),
          focusedBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: _currentColor),),
          border: UnderlineInputBorder(
              borderSide: BorderSide(color: _currentColor),),
        ),),
  ],);
}


这就是上面发生的事情。

创建一个柱子来垂直布置元素。 将该列中的所有部件向左对齐。 添加一个使用GoogleFonts样式的文本。 添加一个TextField。 设置TextField的TextEditingController。 设置光标颜色。 使用InputDecoration对文本字段进行造型。 包括一个提示,给用户一个写什么的例子。 定制文本字段的边框颜色。

接下来,定位//TODO 13:添加名称TextField并替换为。

buildNameField(),


运行该应用程序,它将看起来像这样。

进入下一个输入小部件!

构建重要性小组件

你的下一步是让用户选择一件杂货的重要程度。你将使用一个Chip来做这件事。这个小部件代表了关于一个实体的信息。你可以提出一个筹码的集合让用户选择。

了解筹码

有四种不同类型的筹码部件。

图片来自:material.io/components/…

InputChip。将输入转换为芯片。一个例子是用户偏好的UI流程,例如询问他们喜欢的新媒体的类型。 ChoiceChip。允许用户在一组选项中做出单一选择。 筛选筹码。允许用户在一组选项中选择多个答案。 行动筹码(ActionChip)。一个按钮,其样式像一个芯片。

注:关于芯片的更多信息,请查看:material.io/components/…

在这里,你将使用一个ChoiceChip来让用户选择一个杂货店项目的重要程度。

继续在grocery_item_screen.dart中,在buildNameField()下面添加以下代码。

Widget buildImportanceField() {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Text('Importance', style: GoogleFonts.lato(fontSize: 28.0),),
      // 3
      Wrap(
        spacing: 10.0,
        children: [
          // 4
          ChoiceChip(
            // 5
            selectedColor: Colors.black,
            // 6
            selected: _importance == Importance.low,
            label: const Text(
              'low',
              style: TextStyle(color: Colors.white),),
            // 7
            onSelected: (selected) {
              setState(() => _importance = Importance.low);
            },),
          ChoiceChip(
            selectedColor: Colors.black,
            selected: _importance == Importance.medium,
            label: const Text(
              'medium',
              style: TextStyle(color: Colors.white),),
            onSelected: (selected) {
              setState(() => _importance = Importance.medium);
            },),
          ChoiceChip(
            selectedColor: Colors.black,
            selected: _importance == Importance.high,
            label: const Text(
              'high',
              style: TextStyle(color: Colors.white),),
            onSelected: (selected) {
              setState(() => _importance = Importance.high);
            },),
        ],)
    ],);
}


在上面的代码中,你。

使用一个柱子来垂直布置小部件。 添加文本。 添加Wrap,将每个子部件间隔10像素。Wrap将子部件水平地排列起来。当没有更多的空间时,它将包裹到下一行。 创建一个ChoiceChip,让用户选择低优先级。 将选择芯片的背景颜色设置为黑色。 检查用户是否选择了这个ChoiceChip。 如果用户选择了这个选择,则更新_importance。

然后你再为中等和高优先级的ChoiceChip重复步骤四到七两次。

注意:关于更多信息,请看这个关于Wrap如何工作的动画视频!youtu.be/z5iw2SeFx2M

接下来,定位//TODO 14:添加重要性选择并替换为。

buildImportanceField(),


运行该应用程序。它目前看起来会是这样的。

现在,是时候添加用户需要得到这个项目的日期了

建立日期小组件

当你需要用户输入一个日期时,DatePicker是一个有用的部件。你将在这里使用它,让用户设定一个购买杂货的最后期限。

要使用它,请在buildImportanceField()下面添加以下代码。

Widget buildDateField(BuildContext context) {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Row(
        // 3
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          // 4
          Text('Date', style: GoogleFonts.lato(fontSize: 28.0),),
          // 5
          TextButton(
            child: const Text('Select'),
            // 6
            onPressed: () async {
              final currentDate = DateTime.now();
              // 7
              final selectedDate = await showDatePicker(
                context: context,
                initialDate: currentDate,
                firstDate: currentDate,
                lastDate: DateTime(currentDate.year + 5),
              );
              // 8
              setState(() {
                if (selectedDate != null) {
                  _dueDate = selectedDate;
                }
              });
          },),
        ],),
      // 9
      if (_dueDate != null)
        Text('${DateFormat('yyyy-MM-dd').format(_dueDate)}'),
  ],);
}


下面是代码的工作原理。

添加一个列,以垂直排列元素。 添加一个行,以水平方向排列元素。 在行中的元素之间添加一个空格。 添加日期文本。 添加一个TextButton。 当用户按下按钮时获取当前日期。 显示日期选择器。你限制了日期选择器,只允许用户选择从今天到未来5年的日期。 在用户选择日期后设置_dueDate。 如果_dueDate不是空的,则格式化当前日期并以Text显示。

接下来,定位 // TODO 15: 添加日期选择器并替换为。

buildDateField(context),


运行该应用程序。现在它看起来会像这样。

接下来--时间选择器!

建立时间小部件

现在,用户可以设置他们想要购买物品的日期,你也可以让他们设置时间。为了做到这一点,你将使用TimePicker--一个在你需要用户输入时间时很有用的小部件。

要做到这一点,请在buildDateField()下面添加以下代码。

Widget buildTimeField(BuildContext context) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
        Text('Time of Day', style: GoogleFonts.lato(fontSize: 28.0),),
        TextButton(
          child: const Text('Select'),
          onPressed: () async {
            // 1
            final timeOfDay = await showTimePicker(
              // 2
              initialTime: TimeOfDay.now(),
              context: context,
            );

            // 3
            setState(() {
              if (timeOfDay != null) {
                _timeOfDay = timeOfDay;
              }
            });
          },),
      ]),
    if (_timeOfDay != null)
      Text('${_timeOfDay.format(context)}'),
  ],);
}


这与buildDateField()的设置相同。下面是时间选择器的代码如何工作。

当用户点击选择按钮时显示时间选择器。 将时间选择器中显示的初始时间设置为当前时间。 一旦用户选择了时间小部件,返回一个TimeOfDay对象,然后更新_timeOfDay。

接下来,定位 // TODO 16: 添加时间选择器,并将其替换为。

buildTimeField(context),


运行该应用程序,可以看到以下情况。

构建颜色选择器小组件

现在,你已经准备好让用户选择一种颜色来标记杂货店的物品。为此,你将使用一个第三方小部件ColorPicker,它为用户提供一个调色板。

首先,在buildTimeField()下面添加以下代码。

Widget buildColorPicker(BuildContext context) {
  // 1
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      // 2
      Row(
        children: [
          Container(height: 50.0, width: 10.0, color: _currentColor,),
          const SizedBox(width: 8.0),
          Text('Color', style: GoogleFonts.lato(fontSize: 28.0),),
        ],
      ),
      // 3
      TextButton(
          child: const Text('Select'),
          onPressed: () {
            // 4
            showDialog(
              context: context,
              builder: (context) {
                // 5
                return AlertDialog(
                  content: BlockPicker(
                      pickerColor: Colors.white,
                      // 6
                      onColorChanged: (color) {
                        setState(() => _currentColor = color);
                      },),
                  actions: [
                    // 7
                    TextButton(
                      child: const Text('Save'),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ],
                );
            },);
          })
  ],);
}


下面是它的工作原理。

添加一个行部件,在水平方向上布局颜色选择器部分。 创建一个子行,并将以下小部件分组。

一个容器来显示所选的颜色。 一个8像素宽的SizedBox。 一个文本,显示颜色选择器的标题。

添加一个TextButton。 当用户点击按钮时显示一个弹出式对话框。 将BlockPicker包裹在AlertDialog中。 当用户选择一个颜色时,更新_currentColor。 在对话框中添加一个动作按钮。当用户点选 "保存 "时,它就会解散对话框。

注意:关于AlertDialog的更多信息,请查看:api.flutter.dev/flutter/mat…

接下来,将颜色选择器添加到应用程序中。定位 // TODO 17: 添加颜色选择器并将其替换为。

const SizedBox(height: 10.0),
buildColorPicker(context),


运行该应用程序后,它将看起来像这样。

很好! 现在,用户可以用颜色标记他们的杂货清单项目,使它们更容易识别。

建立一个数量小部件

对于你的下一步,你将让用户指出他们需要多少任何特定的项目。为此,你将使用一个小部件,它对捕捉数量或金额很有用。滑块。

在buildColorPicker()下面添加以下代码。

Widget buildQuantityField() {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Row(
          crossAxisAlignment: CrossAxisAlignment.baseline,
          textBaseline: TextBaseline.alphabetic,
          children: [
            Text(
              'Quantity',
              style: GoogleFonts.lato(fontSize: 28.0),),
            const SizedBox(width: 16.0),
            Text(
              _currentSliderValue.toInt().toString(),
              style: GoogleFonts.lato(fontSize: 18.0),),
          ],
        ),
      // 3
      Slider(
        // 4
        inactiveColor: _currentColor.withOpacity(0.5),
        activeColor: _currentColor,
        // 5
        value: _currentSliderValue.toDouble(),
        // 6
        min: 0.0,
        max: 100.0,
        // 7
        divisions: 100,
        // 8
        label: _currentSliderValue.toInt().toString(),
        // 9
        onChanged: (double value) {
          setState(() {
            _currentSliderValue = value.toInt();
          },);
        },
      ),
    ],);
}


在上面的代码中,你。

使用一个列,垂直地布置你的小部件。 通过创建一个包含两个文本的行,为数量部分添加一个标题和数量标签。你使用一个SizedBox来分隔文本。 添加一个滑块。 设置活动和非活动的颜色。 设置当前滑块的值。 设置滑块的最小值和最大值。 设置你希望滑块如何递增。 设置滑块上方的标签。这里,你想在滑块上方显示当前值。 当值发生变化时,更新_currentSliderValue。

现在,你已经准备好使用这个滑块了。定位 // TODO 18: 添加滑块并替换为。

const SizedBox(height: 10.0),
buildQuantityField(),


运行应用程序,看看。

现在,所有的输入小部件都完成了。棒极了! 但要保持grocery_item_screen.dart的开放,你很快就会有更多的更新。

如果你能在创建杂货店项目时预览它的样子,那不是很好吗?这就是你的下一个任务。

创建GroceryTile

从创建GroceryTile开始。下面是它的样子。

在lib/components中,创建一个名为grocery_tile.dart的新文件。然后添加以下代码。

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../models/grocery_item.dart';

class GroceryTile extends StatelessWidget {
  // 1
  final GroceryItem item;
  // 2
  final Function(bool) onComplete;
  // 3
  final TextDecoration textDecoration;

  // 4
  GroceryTile({
    Key key,
    this.item,
    this.onComplete}) :
    textDecoration =
    item.isComplete ? TextDecoration.lineThrough : TextDecoration.none,
    super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 21: Change this Widget
    return Container(
      height: 100.0,
      // TODO 20: Replace this color
      color: Colors.red,
    );
  }
}


这里是GroceryTile的设置方式。

你包括一个GroceryItem来配置瓦片。 onComplete是一个回调处理程序,让你知道用户是否打开或关闭了复选框。 textDecoration帮助设计所有文本的样式。 当你初始化一个GroceryTile时,你检查该项目,看用户是否将其标记为完成。如果是的话,你就在文本中显示一个划线。否则,你将显示正常的文本。

现在你已经完成了GroceryTile的初始设置,是时候给屏幕添加更多的功能了。因为你很快就会再次使用它,所以保持grocery_tile.dart的开放。

使用GroceryTile

切换回grocery_item_screen.dart并添加以下导入。

import '../components/grocery_tile.dart';


然后找到// TODO: 19: 添加Grocery Tile并替换成以下内容。

const SizedBox(height: 16),
GroceryTile(
  item: GroceryItem(
    name: _name,
    importance: _importance,
    color: _currentColor,
    quantity: _currentSliderValue,
    date: DateTime(_dueDate.year, _dueDate.month, _dueDate.day,
        _timeOfDay.hour, _timeOfDay.minute,),),),


这段代码使用小组件的所有状态属性来创建一个GroceryItem,然后将其传递给GroceryTile来配置自己。

你的应用程序看起来应该与此类似。

现在,是时候把那个红色的盒子变成一个杂货店的物品了!

建造杂货店瓷砖

现在你已经设置了实时更新,是时候为你的杂货店瓷砖添加更多细节了。切换回grocery_tile.dart。

显示重要性标签

到目前为止,你已经让用户为每个杂货店项目选择了一个重要性,但你没有显示这个信息。为了解决这个问题,在build()下面添加以下代码。

Widget buildImportance() {
  if (item.importance == Importance.low) {
    return Text(
      'Low',
      style: GoogleFonts.lato(decoration: textDecoration));
  } else if (item.importance == Importance.medium) {
    return Text(
      'Medium',
      style: GoogleFonts.lato(
        fontWeight: FontWeight.w800,
        decoration: textDecoration));
  } else if (item.importance == Importance.high) {
    return Text(
      'High',
      style: GoogleFonts.lato(
        color: Colors.red,
        fontWeight: FontWeight.w900,
        decoration: textDecoration,),);
  } else {
    throw Exception('This importance type does not exist');
  }
}


在这里,你已经创建了一个辅助方法来构建重要性标签。你检查项目的重要性并显示正确的Text。

显示选定的日期

现在,你需要解决购买杂货的日期方面的同样问题。要做到这一点,在buildImportance()下面添加以下代码。

Widget buildDate() {
  final dateFormatter = DateFormat('MMMM dd h:mm a');
  final dateString = dateFormatter.format(item.date);
  return Text(
    dateString,
    style: TextStyle(
      decoration: textDecoration),);
}


在这里,你创建了一个辅助方法来格式化并将DateTime转换成dateString格式。

显示复选框

同样地,你已经添加了让用户标记项目为完成的功能,但没有在任何地方显示复选框。通过在buildDate()下面添加以下代码来解决这个问题。

Widget buildCheckbox() {
  return Checkbox(
    // 1
    value: item.isComplete,
    // 2
    onChanged: onComplete,);
}


这里,你创建了Checkbox,它。

根据item.isComplete来切换复选框的开或关。 当用户点击复选框时触发onComplete回调。

现在你已经设置了所有的辅助部件,是时候把所有的部件放在一起了

完成GroceryTile

在这一点上,你已经准备好把所有的元素放在一起,完成GroceryTile小部件的构建。

定位 // TODO 20: 替换这个颜色并替换TODO信息和颜色:Colors.red, line with:

child: Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    // TODO 22: Add Row to group (name, date, importance)
    // TODO 23: Add Row to group (quantity, checkbox)
  ],),


在这里,你正在准备让行来承载项目的所有元素。

现在,找到// TODO 21: 改变这个Widget,注意Container下面有一个斜线。在Dart分析选项卡中,也有一个信息警告信息。SizedBox for whitespace。那是由于lint规则。Container()比SizedBox()需要更多的处理。

因为你只需要定义盒子的高度,你不需要一个容器;你只需要一些东西给你一些空间。为了解决这个问题,把Container这个词改为SizedBox。你会注意到Dart分析标签中的斜线和相关信息现在已经消失了。]

由于它不再适用,请删除//TODO 21:把这个Widget也改掉。

接下来,你将把元素分组到两个独立的行中,以创造以下效果。

第1行:将颜色标签和包含名称、日期和重要性的一列分组。 第2行:对数量和复选框进行分组。

添加第一行

找到 // TODO 22: 添加行到组(名称、日期、重要性),并替换为以下内容。

// 1
Row(
  children: [
    // 2
    Container(width: 5.0, color: item.color),
    // 3
    const SizedBox(width: 16.0),
    // 4
    Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 5
          Text(item.name,
              style: GoogleFonts.lato(
                  decoration: textDecoration,
                  fontSize: 21.0,
                  fontWeight: FontWeight.bold),),
          const SizedBox(height: 4.0),
          buildDate(),
          const SizedBox(height: 4.0),
          buildImportance(),
        ],),
  ],
),


在这段代码中,你。

添加一个行,在水平方向上布置元素。 添加一个带有项目颜色的容器小部件。这有助于对项目进行颜色编码。 将元素间隔16像素。 添加一个列,在垂直方向上布置元素。 按以下顺序排列间隔为4像素的元素:项目名称、日期和重要性。

注意:如果你在添加这些新功能时没有看到你的应用程序更新,你可能需要热重新启动,导航到待购屏幕并添加一个新的杂货项目。

当你对屏幕选项进行修改并添加更多代码时,你会在新添加的磁贴中看到这些更新。下面是一个例子。

显示复选框

接下来,找到//TODO 23: 添加行到组(数量,复选框),并将其替换为以下内容。

// 6
Row(
  children: [
    // 7
    Text(item.quantity.toString(),
        style:
            GoogleFonts.lato(
              decoration: textDecoration,
              fontSize: 21.0),),
    // 8
    buildCheckbox(),
],),


下面是代码的工作原理。

添加一个行,在水平方向上布置元素。 然后添加一个Text来显示项目的数量。 最后,添加复选框。

你的应用程序看起来应该类似于这样。

改变你的一些选择,如名称或数量,并看到瓷砖自动更新。用户现在可以在创建杂货项目的同时预览它们

保存用户的工作

为了达到画龙点睛的效果,用户需要能够保存该项目。

切换回grocery_item_screen.dart,找到 // TODO 24: 添加回调处理程序,并将其替换为以下内容。

// 1
final groceryItem = GroceryItem(
    id: widget.originalItem?.id ?? const Uuid().v1(),
    name: _nameController.text,
    importance: _importance,
    color: _currentColor,
    quantity: _currentSliderValue,
    date: DateTime(_dueDate.year, _dueDate.month,
        _dueDate.day, _timeOfDay.hour, _timeOfDay.minute,),);

if (widget.isUpdating) {
  // 2
  widget.onUpdate(groceryItem);
} else {
  // 3
  widget.onCreate(groceryItem);
}


这就是上面发生的事情。

当用户点击 "保存 "时,你就会获取所有的状态属性并创建一个GroceryItem。 如果用户正在更新一个现有的项目,就调用onUpdate。 如果用户正在创建一个新的项目,请调用onCreate。

这几乎是你创建一个项目所需要的全部内容了。最后,是显示项目列表的时候了。

创建GroceryListScreen

在屏幕中,创建一个名为grocery_list_screen.dart的新文件。

添加以下代码。

import 'package:flutter/material.dart';
import '../components/grocery_tile.dart';
import '../models/models.dart';
import 'grocery_item_screen.dart';

class GroceryListScreen extends StatelessWidget {
  final GroceryManager manager;

  const GroceryListScreen({Key key, this.manager}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 26: Replace with ListView
    return Container();
  }
}


这就是GroceryListScreen的初始设置。它需要一个GroceryManager,这样它就可以得到要在列表中显示的杂货项目的清单。

添加项目到杂货店屏幕

打开grocery_screen.dart并添加以下导入。

import 'grocery_list_screen.dart';


然后定位//TODO 25:添加GroceryListScreen,并将其下面的一行替换为。

return GroceryListScreen(manager: manager);


如果用户的列表中有物品,杂货清单就会显示。现在,是时候创建这个列表了

创建一个GroceryList视图

打开grocery_list_screen.dart,然后找到// TODO 26: 替换为ListView,并将其下面一行替换为以下内容。

// 1
final groceryItems = manager.groceryItems;
// 2
return Padding(
  padding: const EdgeInsets.all(16.0),
  // 3
  child: ListView.separated(
      // 4
      itemCount: groceryItems.length,
      itemBuilder: (context, index) {
        final item = groceryItems[index];
        // TODO 28: Wrap in a Dismissable
        // TODO 27: Wrap in an InkWell
        // 5
        return GroceryTile(
          key: Key(item.id),
          item: item,
          // 6
          onComplete: (change) {
            // 7
            manager.completeItem(index, change);
        },);
      },
      // 8
      separatorBuilder: (context, index) {
        return const SizedBox(height: 16.0);
      },),
);


在上面的代码中,你。

从经理那里获得杂货店项目的清单。 在这个屏幕周围应用16像素的padding。 添加ListView。 设置列表中的项目数。 对于列表中的每个项目,获取当前项目并构建一个GroceryTile。 当用户点击复选框时返回onComplete。 更新项目的isComplete状态。 将每个杂货店项目间隔16像素。

热重启,在瓦片中添加一个项目,并点击右上角的复选标记。这将把你带到这个屏幕。

很好,现在你可以查看杂货店的物品清单,并标记一个物品的完成情况!但是,你如何点选一个现有的物品?但是,你如何点击一个现有的项目来更新它呢?你猜怎么着,这是你的下一步。]

添加手势

在你添加手势之前,这里有一个简单的概述!

基于手势的小部件可以检测到不同的用户触摸行为。你把手势小部件包在需要触摸行为的其他小部件周围。

手势部件试图识别用户执行的手势类型--例如,如果他们点击、双击、长按或平移。

两个最常见的手势小工具是。

GestureDetector。提供其他控制,如拖动。 InkWell。提供动画的波纹反馈。例如,当用户点击一个UI元素时,你用它来显示一个飞溅动画,如下图所示。

手势行为

手势部件需要注意的另一件事是HitTestBehavior,它控制手势在撞击测试中的行为。

有三种类型的行为。

deferToChild。将触摸事件向下传递给小组件树。这是GestureDetector的默认行为。 opaque(不透明)。防止后台的小部件接收触摸事件。 半透明的。允许后台的小部件接收触摸事件。

这些手势部件支持轻击、双击、长按、平移和许多其他手势。欲了解更多信息,请查看:flutter.dev/docs/develo…

现在,是时候添加一个手势了!

添加一个InkWell

打开grocery_list_screen.dart,找到//TODO 27: 包裹在InkWell中,用下面的代码替换现有的return GroceryTile()。

// 1
return InkWell(
  child: GroceryTile(
    key: Key(item.id),
    item: item,
    onComplete: (change) {
      manager.completeItem(index, change);
  }),
  // 2
  onTap: () {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => GroceryItemScreen(
              originalItem: item,
              // 3
              onUpdate: (item) {
                // 4
                manager.updateItem(item, index);
                // 5
                Navigator.pop(context);
              },
            ),),);
  },
);


下面是代码的工作方式。

你把GroceryTile包裹在InkWell里面。 当手势识别出一个轻击,它就会呈现GroceryItemScreen,让用户编辑当前的项目。 当用户更新一个项目时,GroceryItemScreen会调用onUpdate。 GroceryManager在特定的索引处更新项目。 Dismisses GroceryItemScreen。

随着应用程序的运行,你现在可以点击一个杂货店项目并进行更新。

干得好! 你可以创建和更新一个项目。但如果删除你不再需要的物品呢?

用扫一扫的方式删除物品

接下来,你将学习如何从列表中解雇或删除物品。你将使用Dismissible,一个当用户向左或向右滑动时从列表中清除项目的小部件。它甚至支持垂直方向上的滑动。

在grocery_list_screen.dart中,找到//TODO 28:用dismissable包裹。用另一个widget包裹一个widget听起来很复杂,但其实并不复杂。你可以手动完成,但为什么不使用IDE的内置功能呢?尤其是该功能还包括关闭的);所以你不必手动添加它。

把你的光标放在你想包装的部件上--在本例中是Inkwell--就会出现一个灯泡。

当你点击灯泡的时候,会出现一个列表。下面的图片是用Android Studio制作的,但VSCode也有一个列表,尽管它的顺序可能不同。

点击Wrap with widget...将一个新的widget包在你现有的widget周围,包括添加结尾的);。)

在正常情况下,你会输入你想要的小部件。在这种情况下,只要高亮显示widget( 子项。

并将突出显示的代码替换成这样。

Dismissible(
  // 6
  key: Key(item.id),
  // 7
  direction: DismissDirection.endToStart,
  // 8
  background: Container(
    color: Colors.red,
    alignment: Alignment.centerRight,
    child: const Icon(Icons.delete_forever,
      color: Colors.white, size: 50.0)),
  // 9
  onDismissed: (direction) {
    // 10
    manager.deleteItem(index);
    // 11
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('${item.name} dismissed')));
  },
  child:


你现在已经把InkWell包裹在一个Dismissible里面了。下面是它的工作原理。

Dismissible widget包括一个Key。Flutter需要这个来找到并移除树中的正确元素。 设置用户可以滑动的方向来解除。 选择背景小部件,在你滑动的小部件后面显示。在这个例子中,背景部件是红色的,有一个白色的垃圾桶Icon,排列在容器的中间和右边。 onDismissed让你知道用户何时刷走了一个GroceryTile。 让杂货店经理处理删除物品,给定一个索引。 显示一个小吃店部件,让用户知道他们删除了哪件物品。

全部完成! 试试通过从右到左的滑动来删除一个项目。真棒,对吗?]

缓存你的页面选择

你几乎已经完成了,但还有最后一件事需要调整 当你切换标签时,你有没有发现任何问题?

有两个问题需要解决。

每次你切换标签时都会出现一个旋转器。这表明每次都会重新加载数据。 当你切换到另一个标签时,该应用程序没有保留滚动位置。

现在是解决这些问题的时候了!

打开home.dart,在build()中,将body参数替换为以下内容。

body: IndexedStack(index: tabManager.selectedTab, children: pages),


IndexedStack允许你在你的应用程序中轻松切换部件。它一次只显示一个子部件,但它保留了所有子部件的状态。是的,有一个widget可以做到这一点!

现在,滚动并切换到不同的标签。你会注意到,该应用程序现在保留了所有孩子的所有状态。干的好!

关键点

你可以用回调或提供者包来传递数据。 如果你需要向上传递数据,请使用回调。 如果你需要在widget树的深处传递数据,就使用提供者。 提供者是一个状态管理助手,它作为继承的widget的一个包装器。 提供者帮助将状态模型对象暴露给它下面的widget。 消费者监听值的变化并重建它下面的部件。 按屏幕分割你的widget,以保持代码的模块化和组织化。 创建管理器对象,在一个地方管理功能和状态变化。 手势部件识别并确定触摸事件的类型。它们提供回调,对onTap或onDrag等事件做出反应。 你可以使用可拆卸的部件来扫除列表中的项目。

何去何从?

有很多方法可以让你的用户参与并收集数据。你已经学会了使用回调和提供者来传递数据。你学会了创建不同的输入小部件。你还学会了应用触摸事件来导航到你的应用程序的各个部分。

这是很重要的,但你只触及到了表面! 还有大量的widget在那里。你可以在pub.dev/ 探索其他软件包,在这里你可以找到由Flutter社区创建的最流行的widget!

在下一节中,您将深入了解导航。


www.deepl.com 翻译