Flutter初体验(二)—— 创建第一个Flutter APP

2,147 阅读14分钟

Flutter初体验(二)--- 创建第一个Flutter APP

在第一篇文章 Flutter初体验(一)---Mac 安装配置,学习了配置 Flutter 开发环境,并运行了Demo项目,本篇根据官方教程,学习创建自己的第一个Flutter APP。


  参考文档 https://flutter.io/get-started/codelab/

项目需求

您将实施一个简单的移动应用程序,为一家创业公司生成建议名称。用户可以选择和取消选择名称,保存最好的名称。该代码一次生成十个名称。当用户滚动时,会生成新批次的名称。用户可以点击应用栏右上方的列表图标以移动到仅列出收藏名称的新路线

  • 下面的GIF动画显示我们将要完成应用程序

学习成果:
  • Flutter应用程序的基本结构
  • 学习查找和使用包来扩展功能
  • 使用热重载加快开发周期
  • 如何实现状态化组件
  • 如何创建一个无限的,延迟加载的列表
  • 如何创建并导航到第二个屏幕
  • 如何使用主题更改应用程序的外观

第1步 :创建一个运行的Flutter 应用程序

在第一篇文章中已经讲述了如何配置Flutter开发环境,并且创建一个示例Flutter项目,现在将会修改这个示例程序来完成我们的项目。

本次操作,主要编辑Dart代码中的lib/main.dart

提示: 将代码粘贴到您的应用中时,缩进可能会变形。您可以使用Flutter工具自动修复此问题

  • Android Studio / IntelliJ IDEA:右键单击Dart代码,然后选择使用dartfmt重新格式化代码。
  • VS代码:右键单击并选择格式化文档
  • Terminal: 运行 flutter format

1.替换 lib/main.dart. 删除lib / main.dart中的所有代码。替换为下面的代码,它在屏幕的中心显示“Hello World”

  import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          child: new Text('Hello World'),
        ),
      ),
    );
  }
}

2.运行工程,将会看到下面的屏幕截图

总结

  • 本示例创建一个Material应用程序。 Material是一种在移动设备和web端是统一标准的视觉设计语言。 Flutter提供了一套丰富的Material小部件

  • 主要方法指定箭头(=>)表示法,它是用于单行函数或方法的简写

  • 该应用程序扩展了使应用程序本身成为小部件的StatelessWidget。在Flutter中,几乎所有东西都是一个小部件,包括对齐,填充和布局

  • 小部件的主要工作是提供一个build()方法,该方法描述如何根据其他较低级别的小部件显示小部件

  • 此示例的小部件树由包含Text小部件的中心小部件组成。中心小部件将其小部件子树对齐到屏幕中心


第2步:使用外部包

在这一步中,您将开始使用名为english_words的开源软件包,其中包含数千个最常用的英文单词以及一些实用功能 1.pubspec文件管理Flutter应用程序的资源。在pubspec.yaml中,将english_words(3.1.0或更高版本)添加到依赖项列表。新行强调如下:

  dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0

2.在Android Studio的编辑器视图,单击右上角Packages get。将该包引入您的项目。将会在控制台中看到以下内容

flutter packages get
Running "flutter packages get" in flutter_app...
Process finished with exit code 0

3.在lib / main.dart中,将english_words导入,如下所示:

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

在输入过程中,Android Studio会为您提供有关库导入的建议。然后它将呈现灰色的导入字符串,让您知道导入的库未使用(到目前为止)

4.使用English words 包来生成文本取代字符串“Hello World” 进行以下更改:

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

  void main() => runApp(new MyApp());

  class MyApp extends StatelessWidget {
    @override
   Widget build(BuildContext context) {
     final wordPair = new WordPair.random();
     return new MaterialApp(
        title: 'Welcome to Flutter',
       home: new Scaffold(
         appBar: new AppBar(
           title: new Text('Welcome to Flutter'),
          ),
         body: new Center(
           //child: new Text('Hello World'), // Replace the   highlighted text...
           child: new Text(wordPair.asPascalCase),  // With this  highlighted text.
          ),
        ),
      );
   }
  }

5.如果应用程序正在运行,使用热重载按钮(⚡️)更新正在运行的应用程序,每次点击热重载或者保存项目时,都会在正在运行的应用程序中随机显示一个不同的单词,这是单词配对是在构建方法内部生成的,每次MaterialApp需要渲染时都会运行它,或在检查器中切换平台时。


第3步:添加一个有状态的组件

无状态组件是不可变的,意味着他们的属性不能被改变--所有的值都是最终值

有状态的小部件保持在小部件的生命周期中可能改变的状态,实现一个有状态的小部件至少需要两个类:1)一个StatefulWidget类,它创建一个取代 2)一个State类的实例。 StatefulWidget类本身是不可变的,但State类在整个构件的生命周期中保持不变

在这一步中,您将添加一个有状态的小部件RandomWords,它创建其状态类RandomWordsState 1.将有状态的RandomWords小部件添加到main.dart,它可以在MyApp之外的文件中的任何位置使用,但解决方案将它放在文件的底部。 RandomWords小部件除了创建其状态类之外无其他作用

    class RandomWords extends StatefulWidget {
      @override
      createState() => new RandomWordsState();
    }

2.添加RandomWordsState类。该应用程序的大部分代码都驻留在该类中,该类保持RandomWords小部件的状态,这个类将保存随着用户滚动而无限增长的生成的单词对,以及最喜欢的单词对,因为用户通过切换红心图标来将它们从列表中添加或删除。

你会一点一点地建立这个类。首先,通过添加突出显示的文本创建一个最小类:

   class RandomWordsState extends State<RandomWords> {
}

3.在添加状态类后,IDE会抱怨该类缺少构建方法,接下来,您将添加一个基本构建方法,该方法通过将单词生成代码从MyApp移动到RandomWordsState来生成单词对

将构建方法添加到RandomWordState中,如突出显示的文本所示:

    class RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        final wordPair = new WordPair.random();
        return new Text(wordPair.asPascalCase);
        }
      }

4.通过下面突出显示的更改从MyApp中删除单词生成代码:

  class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          //child: new Text(wordPair.asPascalCase), // Change the highlighted text to...
          child: new RandomWords(), // ... this highlighted text
        ),
      ),
    );
  }
}

重新启动应用程序。如果您尝试重新加载热点,则可能会看到警告:

  Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.

这可能是误报,但考虑重新启动以确保您的更改反映在应用的用户界面中。

应用程序应该像以前一样运行,每次热重新加载或保存应用程序时都会显示一个字对。


第4步:创建一个无限的滚动ListView

在这一步中,您将展开RandomWordsState以生成并显示单词配对列表。当用户滚动时,ListView小部件中显示的列表将无限增长。 ListView的构建器工厂构造函数允许您根据需要懒惰地构建列表视图。

1.将一个_suggestions列表添加到RandomWordsState类以保存建议的词对。该变量以下划线(_)开头 - 在下划线前加上一个带有下划线的标识符可以强化Dart语言的隐私。

另外,添加一个largerFont变量来使字体变大。

  class RandomWordsState extends State<RandomWords> {
  ...
  final _suggestions = <WordPair>[];
  
  final _biggerFont = const TextStyle(fontSize: 18.0);
}

2.将一个_buildSuggestions()函数添加到RandomWordsState类。此方法构建显示建议词对的ListView。

ListView类提供了一个构建器属性itemBuilder,一个指定为匿名函数的工厂构建器和回调函数,两个参数传递给函数 - BuildContext和行迭代器.迭代器从0开始,每次调用该函数时递增,每次建议的单词配对一次.该模型允许建议的列表在用户滚动时无限增长。


class RandomWordsState extends State<RandomWords> {
  ...
  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // The itemBuilder callback is called once per suggested word pairing,
      // and places each suggestion into a ListTile row.
      // For even rows, the function adds a ListTile row for the word pairing.
      // For odd rows, the function adds a Divider widget to visually
      // separate the entries. Note that the divider may be difficult
      // to see on smaller devices.
      itemBuilder: (context, i) {
        // Add a one-pixel-high divider widget before each row in theListView.
        if (i.isOdd) return new Divider();

        // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
        // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
        // This calculates the actual number of word pairings in the ListView,
        // minus the divider widgets.
        final index = i ~/ 2;
        // If you've reached the end of the available word pairings...
        if (index >= _suggestions.length) {
          // ...then generate 10 more and add them to the suggestions list.
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }
}

3._buildSuggestions函数每个字对调用_buildRow一次。这个函数在ListTile中显示每个新对,这允许您在下一步中使行更具吸引力

RandomWordsState添加_buildRow函数:

class RandomWordsState extends State<RandomWords> {
  ...

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}

4.更新RandomWordsState的构建方法以使用_buildSuggestions(),而不是直接调用单词生成库。进行突出显示的更改:

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {

    //final wordPair = new WordPair.random(); // Delete these two lines.
    //return new Text(wordPair.asPascalCase);
    
    return new Scaffold (
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),  
    );
  }
  ...
}

5.更新MyApp的构建方法。从MyApp中删除Scaffold和AppBar实例。这些将由RandomWordsState管理,当用户在下一步中从一个屏幕导航到另一个屏幕时,可以更轻松地更改应用栏中的路由名称。

用下面突出显示的构建方法替换原始方法:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      home: new RandomWords(),
    );
  }
}

重新启动应用程序。你应该看到一个单词配对清单。尽可能向下滚动,您将继续看到新的单词配对。


第5步:添加交互性

在这一步中,您将为每一行添加可点击的心形图标。当用户点击列表中的条目,切换其“收藏”状态时,该单词配对被添加到一组保存的收藏夹中或从中删除

1.将一个_saved集合添加到RandomWordsState。这个集合存储用户最喜欢的单词配对。 Set比List更受欢迎,因为正确实施的Set不允许重复输入。

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _saved = new Set<WordPair>();  //添加 saved 集合

  final _biggerFont = const TextStyle(fontSize: 18.0);
  
}

2.在_buildRow函数中,添加alreadySaved检查以确保单词配对尚未添加到收藏夹.

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  ...
}

3.同样在_buildRow()中,将心形图标添加到ListTiles以启用收藏。稍后,您将添加与心形图标进行交互的功能。

添加下面突出显示的行:

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    //添加心形图标以添加收藏功能
    trailing: new Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
  );
}

4.重新启动应用程序。你现在应该在每一行看到开放的心,但它们还没有互动。

5.在_buildRow函数中让心形图标可点击。如果单词条目已经添加到收藏夹中,再次点击它将其从收藏夹中删除。当心形图标被轻敲时,函数调用setState()来通知框架状态已经改变。

添加下列代码:

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
    //修改心形图标的状态
    onTap: () {
      setState(() {
        if (alreadySaved) {
          _saved.remove(pair);
        } else {
          _saved.add(pair);
        }
      });
    },
  );
}

热重载应用程序。你应该能够点击任何一行以获得最喜欢的,或不适合的条目。注意,点击一行会生成从心形图标散发的涟漪动画。


第6步:导航到新的屏幕

在这一步中,您将添加一个显示收藏夹的新屏幕(在Flutter中称为路线)。您将学习如何在主路线和新路线之间导航。

在Flutter中,导航器管理包含应用程序路线的堆栈。将路线推入导航器的堆栈,将显示更新为该推入的路线。从导航器的堆栈中弹出路径,将显示返回到上一个路线。

1.向RandomWordsState的构建方法中的AppBar添加列表图标。当用户点击列表图标时,包含收藏夹项目的新路线被推送到导航器,显示该图标。

提示:某些小部件属性采用单个小部件(子级),而其他属性(如操作)则采用小部件(子级)数组,如方括号([])所示。

将该图标及其相应的操作添加到构建方法中:

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        //添加actions
        actions: <Widget>[
          new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

2.将一个_pushSaved()函数添加到RandomWordsState类。

class RandomWordsState extends State<RandomWords> {
  ...
  void _pushSaved() {
  }
}

热重新加载应用程序。列表图标出现在应用程序栏中。点击它什么也没做,因为_pushSaved函数是空的。

3.当用户点击应用栏中的列表图标时,建立一条路线并将其推送到导航器的堆栈。此操作会更改屏幕以显示新路线。

新页面的内容是使用匿名函数在MaterialPageRoute的构建器属性中构建的。

将呼叫添加到Navigator.push,如突出显示的代码所示,将路线推送到导航器的堆栈。

void _pushSaved() {
  Navigator.of(context).push(
  );
}

4.添加MaterialPageRoute及其构建器。现在,添加生成ListTile行的代码。ListTile的divideTiles()方法在每个ListTile之间添加水平间距。divided的变量保存最后的行,通过便利函数toList()转换为列表。

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute(
      builder: (context) {
        final tiles = _saved.map(
          (pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();
      },
    ),
  );
}

5.构建器属性返回一个Scaffold,其中包含名为“Saved Suggestions”的新路线的应用程序栏。新路由的主体由包含ListTiles行的ListView组成;每行由一个分隔符分隔。

添加下面突出显示的代码:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute(
      builder: (context) {
        final tiles = _saved.map(
          (pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();

        //添加 Scaffold 名为 Saved Suggestions
      
        return new Scaffold(
          appBar: new AppBar(
            title: new Text('Saved Suggestions'),
          ),
          body: new ListView(children: divided),
        );
      },
    ),
  );
}

6.热重载应用程序。最喜欢的一些选择,并点击应用栏中的列表图标。新路线显示包含收藏夹。请注意,导航器会在应用栏中添加一个“返回”按钮。你不必显式实现Navigator.pop。点击后退按钮返回到主页路线。


第7步:使用主题更改UI

在最后一步中,您将使用该应用的主题,主题控制你的应用的外观和感觉。您可以使用默认主题,该主题取决于物理设备或模拟器,也可以自定义主题以反映您的品牌。

1.您可以通过配置ThemeData类轻松更改应用程序的主题。您的应用程序目前使用默认主题,但您将更改主要颜色为白色。

将突出显示的代码添加到MyApp,将应用程序的主题更改为蓝色:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      //修改主题颜色
      theme: new ThemeData(
        primaryColor: Colors.blue,
      ),
      home: new RandomWords(),
    );
  }
}

2.热重新加载应用程序。请注意,整个背景是白色的,甚至是应用栏。

3.作为读者的练习,使用ThemeData来改变用户界面的其他方面。材质库中的Colors类提供了许多可以使用的颜色常量,而热重载使得用户界面的试验变得快速而简单。


搞定!

我们编写了一个在iOS和Android上运行的交互式Flutter应用程序。在这个codelab中,你有:

  • 从头开始创建一个Flutter应用程序。
  • 书写Dart代码。
  • 利用外部的第三方库。
  • 使用热重载加快开发周期。
  • 实现一个有状态的小部件,为你的应用增加交互性。
  • 用ListView和ListTiles创建一个延迟加载的无限滚动列表。
  • 创建了一条路线并添加了在主路线和新路线之间移动的逻辑。
  • 了解如何使用主题更改应用UI的外观。

第一篇文章: Flutter初体验(一)---Mac 安装配置