0x02.编写第一个Flutter应用-[Flutter]

Flutter官网上Get started里的教程写的还是挺好的,因此本教程基于Flutter官网教程【Write Your First Flutter App】翻译撰写。

本指南将带领你完成第一Flutter应用。阅读本文不需要具备Dart或移动端开发经验,但需要了解变量、循环、分支等基本的编程概念。

  1. 创建可运行的Flutter应用
  2. 使用扩展包(external package)
  3. 添加一个有状态组件(Stateful widget)
  4. 创建支持无限滚动的列表视图
  5. 添加交互
  6. 导航到新的页面
  7. 使用主题改变UI样式
  8. 搞定

下面的动画展示了应用完成后的效果。

应用完成后的效果

通过本教程,将能够掌握以下知识

  • Flutter应用的基本结构
  • 查找、使用包扩展功能
  • 使用热重载(hot reload)进行快速开发
  • 如何实现一个有状态组件
  • 如何创建一个无限的懒加载列表
  • 如何创建并导航到另一页面
  • 如何使用主题改变应用外观

在开始旅程前,请确保您已正确安装了Flutter SDK(含Dart SDK)、Android SDK及IDE(VS Code、IntelliJ IDEs)。

一、创建Flutter应用

我一般都是用idea,装好插件后,按步骤创建新的Flutter project就可以了,如有疑问可参考官方文档Getting Started with your first Flutter app,文档里详细写了android studio、VS Code和命令行三种方式下新建项目的方法。

好了,下面进入正题。

  1. 修改lib/main.dart文件为以下内容

    // 导入flutter/material包
    import 'package:flutter/material.dart';
          
    // 定义main函数,执行runApp
    void main() => runApp(new MyApp());
          
    // 定义继承自无状态组件的组件类
    class MyApp extends StatelessWidget {
           
      // 重载build方法
      @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. 点击运行,可以在模拟器/真机中看到下图的效果。 效果

现在我们的第一个Flutter应用已经可以成功运行了。

知识点:

二、使用扩展包

在本步骤中,我们将导入一个开源扩展包english_words,这个包中包含了数千个常用的英文单词及相关的函数。在dart官方包管理网站可以找到更多的关于dart和flutter的开源包(可能需要科学上网。。。)

  1. Flutter通过pubspec文件对项目配置及资源进行管理。修改pubspec.yaml,在其中添加对于包english_words的依赖(3.0或更高版本)。

    dependencies:
      flutter:
        sdk: flutter
    
      cupertino_icons: ^0.1.0
      english_words: ^3.1.0
    
  2. 右键点击pubspec.yaml,选择Flutter->Package get,在控制台中将看到类似如下的信息

    flutter packages get
    Running "flutter packages get" in startup_namer...
    Process finished with exit code 0
    
  3. lib/main.dart中导入english_words包

    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';
    
  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'), // 注释掉这行
              child: new Text(wordPair.asPascalCase),  // 使用这部分替换
            ),
          ),
        );
      }
    }
    
  5. 点击IDE中的【热重载(hot reload)】图标,不需要重新部署应用就可以看到刚刚的修改结果。通常情况下,我们再修改代码后都只需要热重载,而不需要重新部署应用。

wordPair.asPascalCase会随机取得英文单词,而组件的build方法会在每次需要刷新页面时被调用,所以每次刷新都会显示不同的单词。

效果图

出问题了?请再次仔细检查是否正确修改点,如果仍然不行,可以直接使用下面文件的内容替换工程中的文件。出于学习的目的考虑不建议直接替换文件,更好的方式是比较下面文件的内容与你写的有和差异并进行修改。

三、添加有状态组件(Stateful widget)

无状态组件的不可变性意味着,组件一旦创建,其属性就无法改变。

有状态组件可以在其生命周期中修改状态值。实现一个有状态组件至少需要两个类:

  1. 一个有状态组件类(继承自StatefulWidget)
  2. 一个状态类(继承自State)

有状态组件类自身不可变,而是通过自身持有的状态类(State class)修改组件状态。

在本步骤中我们将添加一个有状态组件RandomWords,这个组件将创建一个状态类RandomWordsState。这个状态类将维护组件中显示的建议和喜欢单词对。

  1. 添加一个有状态组件类RandomWords到main.dart。可以在类MyApp的作用域外的任何地方添加。在这个示例中,我们将其添加到MyApp类之后。

    class RandomWords extends StatefulWidget {
      @override
      createState() => new RandomWordsState();
    }
    
  2. 添加一个状态类RandomWordsState。包括构建组件、维持状态的主要代码都位于这个类中。这个类将保存随着用户滚动而无限增长的生成的单词对,以及最喜欢的单词对,因为用户通过切换心脏图标来将它们从列表中添加或删除。 我们将逐步构建这个类,首先创建类。

    class RandomWordsState extends State<RandomWords> {
    }
    
  3. 接下来实现build方法。如果使用IDE,会看到IDE的提示。

    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) {
        // 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(wordPair.asPascalCase), // 修改本行内容为下面的内容
              child: new RandomWords(), // 新的内容
            ),
          ),
        );
      }
    }
    

重启APP,如果热重载应用可能会收到以下警告。

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

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

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

实际使用过程中确实发现过热重载机制无法将应用刷新到最新状态的情况,如果开发过程中明明修改了却死活不起效,重新部署一下试试吧。(具体什么情况下需要重新部署,我目前还不清楚)

效果图

有问题?

四、创建无限滚动列表视图

经过了上面的三步、我们虽然创建了一个可以运行的应用,但是应用的就够和功能上似乎没有太大的变化。在本步骤中,我们将利用列表视图的构造器工厂方法,为应用添加列表视图。通过构造器方法,可以实现列表滚动时的懒加载。

  1. 再RandomWordsState类中添加数组变量_suggestions来保存建议单词对。这个变量以下划线【_】开头,在Dart语言中通常以此表示一个私有变量。 紧接着添加一个变量biggerFont来保存文字大小。

    class RandomWordsState extends State<RandomWords> {
     final _suggestions = <WordPair>[];
       
     final _biggerFont = const TextStyle(fontSize: 18.0);
     ...
    }
    
  2. 在类RandomWordsState中添加一个方法_buildSuggestions()。这个方法构造一个列表视图来显示建议单词对。 列表视图类提供了构造属性itemBuilder,我们需要提供一个匿名函数,作为这个工厂构造方法的回调函数。匿名方法接收两个参数:构造上下文(BuildContext)和行迭代i。迭代从0开始,每次调用函数自增,每次向列表添加一个单词对,所有单词只生成一次。这个模型可以在用户滚动时生成一个无限增长的列表。 修改代码为如下内容

    class RandomWordsState extends State<RandomWords> {
     ...
     Widget _buildSuggestions() {
       return new ListView.builder(
         padding: const EdgeInsets.all(16.0),
         // 回调函数每次生成一个单词对添加到列表项中
         // 偶数行时添加一个单词对,奇数行时添加
         // 注意:分隔符的样式在模拟器中看上去可能不同
         itemBuilder: (context, i) {
           // 添加1像素的分隔符
           if (i.isOdd) return new Divider();
       
           // 由于奇数时生成分割线,这里用i整除2,将i换算成单词对数组的实际下标
           final index = i ~/ 2;
           // 滚动到底部时,生成新的单词对
           if (index >= _suggestions.length) {
             // 每次生成10个单词
             _suggestions.addAll(generateWordPairs().take(10));
           }
           return _buildRow(_suggestions[index]);
         }
       );
     }
    }
    
  3. _buildSuggestions方法为每个单词对调用_buildRow,这个方法为每个新的单词对生成列表项。 添加_buildRow方法

    class RandomWordsState extends State<RandomWords> {
     ...
       
     Widget _buildRow(WordPair pair) {
       return new ListTile(
         title: new Text(
           pair.asPascalCase,
           style: _biggerFont,
         ),
       );
     }
    }
    
  4. 修改_buildSuggestions()方法,将body部分替换为我们新增加的方法_buildSuggestions

    class RandomWordsState extends State<RandomWords> {
     ...
     @override
     Widget build(BuildContext context) {
       // 删除下面两行
       // final wordPair = new WordPair.random();
       // return new Text(wordPair.asPascalCase);
       // 修改为下面的内容
       return new Scaffold (
         appBar: new AppBar(
           title: new Text('Startup Name Generator'),
         ),
         body: _buildSuggestions(),
       );
     }
     ...
    }
    
  5. 修改MyApp类的build方法,删除Scaffold和AppBar。这些将放到RandomWordsState类中进行监管,在后续步骤中我们将导航到其他页面中,这样的方式,可以让我们更方便的替换标题等的内容。

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

重启引用,可以看到下面的效果了。 效果图

有问题?

五、添加交互事件

在本步骤中,我们将在每行中添加一个可点击的心形图标,通过点击心形图标可以将选定的单词对添加到收藏夹或从中删除。

  1. 添加一个集合(Set)变量_saved。使用集合而非列表(List),是应为我们的需求不允许添加重复记录,集合的实现正好满足我们的需求。

    class RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
        
      final _saved = new Set<WordPair>();
        
      final _biggerFont = const TextStyle(fontSize: 18.0);
      ...
    }
    
  2. _buildRow方法中添加变量alreadySaved,用来保存单词对是否已经被添加到收藏列表。

    Widget _buildRow(WordPair pair) {
     final alreadySaved = _saved.contains(pair);
     ...
    }
    
  3. 修改_buildRow方法,在列表项中添加心形图标。

    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. 现在来实现添加收藏的交互功能。当点击单词对时,如果没有添加到收藏中则添加、如添加到收藏中则删除。 在组件中为onTab方法指定匿名函数,响应用户点击事件,并在其中使用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);
           }
         });
       },
     );
    }
    

热重载一下,我们的应用可以响应点击事件了 效果图

有问题?

六、导航到新页面

一步中,将添加一个新的页面用来显示收藏的单体对。同时还将添加首页与收藏页之间的导航处理。 在Flutter中,导航(Navigator)管理管理着包含应用路由(routes)的堆栈(stack),屏幕始终显示在栈顶的页面,将新的路由压入栈中后,屏幕显示刚刚入栈的页面,将顶层页面弹出栈后,屏幕返回到上一个页面。 1. 在标题栏中添加一个图标,当用户点击图标是,跳转到收藏页。

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

    class RandomWordsState extends State<RandomWords> {
     ...
     void _pushSaved() {
     }
    }
    
  2. 当用户点击标题栏中的图标时,建造一个新的页面,并压入导航堆栈中,屏幕将显示新的页面。 新页面将使用MaterialPageRoutebuilder属性指定的匿名函数进行创建。 添加对Navigator.push的调用,将路由压入导航堆栈。

    void _pushSaved() {
     Navigator.of(context).push(
     );
    }
    
  3. 下面的代码将使用一些函数时编程的内容。 首先迭代保存收藏单词对的集合,并根据每一项生成列表项,然后将生成项目重新组合成列表项。 接下来调用ListTile.divideTiles方法,向列表项中增加分隔符,并通过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();
         },
       ),
     );
    }
    
  4. builer属性的匿名函数最终为路由返回一个Scaffold,其包含一个标题栏和根据收藏单词对生成的列表页。

    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();
       
           // 增加以下代码
           return new Scaffold(
             appBar: new AppBar(
               title: new Text('Saved Suggestions'),
             ),
             body: new ListView(children: divided),
           );
         },
       ),
     );
    }
    
  5. 热重载应用,现在可以跳转到收藏页面了。 在收藏页的标题栏中,框架自动添加了返回按钮【<】,并实现了Navigator.pop功能。 效果图效果图

七、修改应用主题 截止到上一步为止,我们的应用已经实现了教程开始设定的所有功能性目标,这一步中,我们通过使用主题来修改应用的整体观感。 1. 通过下面的代码,我们将主题颜色修改为白色

   class MyApp extends StatelessWidget {
     @override
     Widget build(BuildContext context) {
       return new MaterialApp(
         title: 'Startup Name Generator',
   
         // 添加以下三行内容
         theme: new ThemeData(
           primaryColor: Colors.white,
         ),
         home: new RandomWords(),
       );
     }
   }
  1. 热重载应用,看看最终效果吧。 效果图

有问题?

总结 通过这个教程我们学习到了 - 创建一个Flutter - 编写Dart代码 - 使用第三方扩展包简化开发 - 使用热重载功能加速开发 - 实现一个有状态组件 - 创建一个懒加载的无限循环列表 - 创建新的页面并通过路由-导航机制跳转 - 使用主题修改应用的观感