使用 Flutter 开发知识小集 iOS/Android 客户端

阅读原文️前端

Flutter 目前仍是 Beta 3 版本,1.0 版本还在路上。不过它在 React Native/weex等跨平台方案以外,又为咱们提供了一种跨平台的方案。并且其自身的许多特性,也为咱们扩展了新的视野。若是 Fuchsia 系统最终能和 iOS、Android 成三足鼎立之式,甚至于取代 Android,那么 Flutter 就能为咱们带来更多的可能。因此如今了解一下仍是有必要的。 本文将经过一个简单的实例(知识小集 Flutter 版本客户端,咱们后期会慢慢优化),同时半翻译半参考 Raywenderlich 上的 Getting Started with Flutter 这篇文章,来一步步了解如何使用 Flutter 构建 App。android

在这个 App 的开发过程当中,咱们将学习如下关于 Flutter 的内容:ios

  • 设置开发环境
  • 建立新工程
  • Hot Reload
  • 导入文件
  • 使用 Widget 及自定义 Widget
  • 网络请求
  • 在列表中展现信息
  • 为 App 添加主题

在这个过程当中,咱们将同时学习一些 Dart 相关的知识。项目的完整代码在 Github 上能够找到。git

设置开发环境

咱们能够在 macOSLinux 或者 Windows 上开发 Flutter 应用。目前 Flutter 团队为一些 IDE 开发了相应的插件,这些 IDE 包括 IntelliJ IDEAAndroid StudioVisual Studio Code。个人开发环境主要为 macOS + Visual Studio Code,因此本文主要基这二者来进行描述。github

实际的配置过程能够参考官方文档 Get Started: Install on macOS。具体的步骤各个平台稍有不一样,但主要是如下几步:macos

  1. 拷贝 Flutter 的 git 库;
  2. 添加 Flutter bin 目录到咱们指定的目录;
  3. 运行 flutter doctor 命令,这个命令将告诉咱们缺乏哪些依赖;
  4. 安装缺失的依赖;
  5. 在 IDE 中安装 Flutter 插件/扩展;
  6. 测试

须要注意的是,若是想在 iOS 模拟器或 iOS 设备上构建和测试应用,咱们须要使用 macOS 系统,同时须要安装 Xcode 9.0+json

建立新工程

在安装了 Flutter 插件的 VS Code 中,咱们能够经过 View > Command Palette... 或者快捷键 cmd+shift+p 来打开 命令面板(command palette),而后输入 Flutter:New Project 并回车:小程序

为工程取名为 awesome_tips_flutter,并回车。选择一个目录来存储工程,而后等待 Flutter 配置好工程。配置的过程主要有几个步骤:api

  1. 建立工程所须要的模板文件,包括对应的 iOS 和 Android 工程;
  2. 运行 flutter packages get 命令来获取依赖包;
  3. 运行 flutter doctor 命令来检测依赖包;

如图是构建过程的部分信息:xcode

工程建立完成后,IDE 会默认打开 lib 目录下的 main.dart 文件,这也是咱们 App 的入口。

注意:从 Flutter Beta 3 开始,建立 Widget 时,new 关键字是可选的。目前我这生成的模板代码部分仍是带 new 关键字的。

在左侧的工程目录中,咱们能够看到 iosandroidlib 这些目录,lib 目录下的代码将应用于两个平台,目前咱们也主要是在这个目录下工做。

为了构建咱们本身的应用,先删除 main.dart 中现有的代码,并用以下代码替代:

import 'package:flutter/material.dart';

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

class AwesomeTips extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Awesome Tips',
      home: Scaffold(
        appBar: AppBar(title: Text('Awesome Tips')),
        body: Center(
          child: Text('Awesome Tips'),
        )
      )
    );
  }
}
复制代码

顶部的 main() 函数使用 => 操做符来指定单行函数的函数体(相似于 ES6 中的箭头函数),并运行 App。runApp的参数是咱们的 AwesomeTipsApp 类(根 Widget)。

在这里,咱们的 AwesomeTipsApp 类继承自 StatelessWidget。Flutter 中大部分实体都是 Widget,或者是无状态的(stateless),或者是有状态的(stateful)。咱们重写 Widget 的 build() 方法来构建自定义的 App Widget。

咱们先来运行一下这个 App。首先启动 iOS 模拟器。选择菜单 Debug -> Start Debugging 构建并运行工程。能够看到 VS Code 打开了 Debug Console (调试控制台) 面板,同时 xcode-builder 开始构建并启动 App。初始效果以下图:

同时,咱们能够在 VS Code 顶部看到一个调试工具栏,咱们能够经过这个工具栏来中止或者从新加载 App。

Hot Reload

Flutter 开发最吸引人的一个方面就是当程序代码更改时,能够自动执行 Hot Reload 操做,来从新加载 App。咱们来试试这个特性,对咱们的程序作个小小的修改:

appBar: AppBar(title: Text('Awesome Tips for Test')),
复制代码

在咱们保存文件时,VS Code 会自动启动 Hot Reload 功能,加载完成后,模拟器会显示新的内容。固然咱们也能够手动点击调试工具栏上的 Hot Reload 按钮来启动热加载。来看看效果。

注:因为 Flutter 仍是 Beta 版,因此 Hot Reload 并不老是能正常工具。我就遇到了相似 Request to Dart VM Service timed out: _flutter.listViews({}) 这样的问题,解决方法是重启 Debug。

导入文件

一般咱们都不但愿在一个文件中放入大量的代码,而是将代码分散在不一样的文件中,并经过必定的方式将这些文件组织起来。而后若是一个文件须要用到其它文件的类或方法,只须要导入相关文件便可。在一个 Dart 文件中,咱们能够经过 import 关键字来实现这一目标。

好比上面代码中,咱们但愿将字符串统一放在一个文件中来管理,那么能够建立一个 strings.dart 文件。在 lib 目录处点击右键,会弹出菜单,选择 New File,并输入文件名。

string.dart 中添加如下代码:

class Strings {
  static String appTitle = "Awesome Tips";
}
复制代码

而后在 main.dart 中经过如下方式导入:

import 'strings.dart';
复制代码

如今就能够在 AwesomeTipsApp 中使用 appTitle 了:

class AwesomeTipsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: Scaffold(
        appBar: AppBar(title: Text(Strings.appTitle)),
        body: Center(
          child: Text(Strings.appTitle),
        )
      )
    );
  }
}
复制代码

Widgets

在 Flutter App 中,几乎全部的界面元素都是 Widget。Widget 被设计成是不可变的(immutable),由于这样可让 App 的 UI 轻量化。咱们可使用两种类型的 Widget:

  • Stateless:无状态 Widget,只依赖于自身的配置信息,例如一个 image view 的静态图片;
  • Stateful:有状态 Widget,须要处理动态信息,并与 State 对象交互。

两种类型的 Widget 都会在 Flutter App 的每一帧进行重绘,不一样的是 Stateful Widgets 会将其配置交给 State 对象来管理。关于 Flutter 界面开发,能够参考阿里闲鱼团队 的**《深刻了解Flutter界面开发》**一文。

咱们如今来建立一个 Widget 展现列表。在 lib 目录中新建文件 content_list.dart,在文件中加入以下代码:

import 'package:flutter/material.dart';

class ContentList extends StatefulWidget {
  @override
  createState() => _ContentListState();
}
复制代码

这里咱们建立了 StatefulWidget 的一个子类 ContentList 并重写了 createState() 方法,该方法返回 ContentList 对应的 State 对象。而后咱们在同一文件中添加如下代码:

class _ContentListState extends State<ContentList> {
}
复制代码

_ContentListState 继承自泛型参数为 ContentList 的 State 对象。在 _ContentListState 中,咱们的主要工做就是重写 build() 方法,这个方法在 Widget 被渲染到屏幕上时会调用。目前咱们尚未涉及到数据的处理,因此暂时和以前同样,在 ContentList 中显示一个简单的文本。在 build() 方法中添加如下代码:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(Strings.appTitle)),
      body: Text(Strings.appTitle),
    );
  }
复制代码

Scaffold 类是 Material Design Widgets 的容器。它一般做为 Widget 层级的根。

上面的代码咱们添加了一个 AppBar 和一个 body 到 Scaffold 中。接下来咱们用这个 ContentList Widget 替换 main.dart 中的 home 属性的内容:

import 'content_list.dart';

void main() => runApp(AwesomeTipsApp());

class AwesomeTipsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: ContentList(),				// 替换此处内容
    );
  }
}
复制代码

编译运行程序,获得的结果和上面差很少。

网络请求及数据转换

咱们最终要展现的是知识小集的内容清单,因此须要从服务器上获取到清单内容,并转换成咱们须要的 Dart 对象。这里咱们须要用到两个库:

  • package:http/http.dart:负责网络请求,从服务端获取数据;
  • dart:convert:将服务端返回的字符串转换成 JSON 对象;

咱们在 main.dart 中导入这两个模块:

import 'package:http/http.dart';
import 'dart:convert';
复制代码

须要注意的是:Dart 应用是单线程的,可是 Dart 支持代码运行在其它线程上,同时也支持使用 async/await 模式让代码异步执行,而不会阻塞 UI 线程。

接下来咱们须要经过异步网络调用来获取知识小集的内容列表。首先咱们在 _ContentListState 类的顶部添加一个空列表属性,用于保存内容清单:

var _items = [];
复制代码

Dart 语言中,若是属性/方法名是以_开头,则表示这个属性/方法是类私有的。

而后添加一个 _loadData() 方法,咱们在这作网络请求:

void _loadData() async {
    String dataURL =
        "https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
    http.Response response = await http.get(dataURL);

    // ...
  }
复制代码

这里咱们在 _loadData() 后面加上 async 关键字,用于告诉 Dart 这是一个异步方法,同时在 http.get 前使用 await 关键字,来阻塞后面的代码执行。当 HTTP 调用完成后,服务端返回的是一个 JSON 字符串,具体结构以下:

{
  "code": 0,
  "msg": "SUCCESS",
  "data": {}
}
复制代码

对于 feed/list 接口,其 data 中的结构以下:

"data": {
		"feeds": [{
			"fid": "96",
			"auther": "halohily",
			"title": "如何重写自定义对象的 hash 方法",
			"url": "https://weibo.com/3656155132/GfEGebnEN",
			"platform": "0",
			"postdate": "2018-05-08"
		}, {
			"fid": "95",
			"auther": "南峰子",
			"title": "微博一周推送",
			"url": "https://weibo.com/3321824014/GfviNzT3z",
			"platform": "0",
			"postdate": "2018-05-07"
		}]
	}
复制代码

在获取到 JSON 字符串后,咱们首先须要将其转换成 JSON 对象,而后根据 code 是否为 0 作处理。若是请求成功,则须要从 data 中取出 feeds 的数据。同时,咱们但愿将 feed 数据转换成一个 Dart 对象,因此咱们建立一个 feed.dart 文件,并添加以下代码:

class Feed {
  final String author;
  final String title;
  final String postdate;

  Feed(this.author, this.title, this.postdate);
}
复制代码

而后咱们就能够对返回的数据作处理,将每一条 feed 转换成一个 Feed 对象,并存储在 _items 中。完整的 _loadData() 代码以下所示:

void _loadData() async {
    String dataURL =
        "https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
    http.Response response = await http.get(dataURL);

    final body = JSON.decode(response.body);
    final int code = body["code"];
    if (code == 0) {
      final feeds = body["data"]["feeds"];
      var items = [];
      feeds.forEach((item) =>
          items.add(Feed(item["author"], item["title"], item["postdate"])));

      setState(() {
        _items = items;
      });
    }
  }
复制代码

若是咱们但愿在状态改变时,触发界面从新渲染,则须要调用 setState() 方法来设置咱们的属性值。

有了加载数据的方法,咱们就须要在合适的位置来调用。咱们暂且在 _ContentListState 类中重写 State 的 initState() 方法,以下所示:

@override
  void initState() {
    super.initState();

    _loadData();
  }
复制代码

Widget 生命周期相关的内容,咱们有机会再讲。

使用 ListView

至此,咱们已经有了列表数据,接下来就须要将数据显示在界面上了。Flutter 提供了 ListView Widget 来显示一个列表,这个 Widget 能很流畅地展现列表内容。

咱们先在 _ContentListState 类中添加一个私有方法 _buildRow(),以建立显示单元格的 widget:

Widget _buildRow(int i) {
    Feed feed = this._items[i];

    return ListTile(
        title: Text(
          feed.title,
          overflow: TextOverflow.fade,
        ),
        subtitle: Text(
          '${feed.postdate} @${feed.author}',
        ));
  }
复制代码

咱们暂且返回一个 ListTile 来显示内容的标题及发布日期和做者。接下来咱们修改 build() 方法中 Scaffold 的 body:

Widget build(BuildContext context) {
    
    return Scaffold(
      appBar: AppBar(title: Text(Strings.appTitle)),
      body: new ListView.builder(
        padding: const EdgeInsets.all(13.0),
        itemCount: _items.length * 2,
        itemBuilder: (BuildContext context, int position) {

          // 此处为添加分割线
          if (position.isOdd) return Divider();
          final index = position ~/ 2;

          return _buildRow(index);
        },
      ),
    );
  }
复制代码

在这段代码中,咱们经过 ListView.builder 来建立一个 ListView,并经过参数来配置列表的显示。这里咱们没有处理单元格点击等事件,后续咱们会作改进。

OK,保存代码,Hot Reload 后的效果以下:

很简单吧?这样,咱们的任务基本完成。

这里咱们只是获取了第1页的数据,分页处理后续再完善。

添加主题(Theme)

最后咱们来看看如何为 App 添加主题。能够说这很容易,只须要设置 main.dart 中 MaterialApp 的 theme 属性,咱们来试试:

class AwesomeTipsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      theme: ThemeData(primaryColor: Colors.red.shade800),
      home: ContentList(),
    );
  }
}
复制代码

咱们使用了 Material Design 颜色值来设置主题颜色,效果以下:

总结

在本文中,咱们经过一个简单的例子来了解了一下若是使用 Flutter 来构建 App,能够在 awesome-tips-flutter-app 下载完整的示例代码。固然,构建一个完整的 App 还须要作不少事情,还有许多技术学习。后期咱们会逐步来完善这个 App,并让其达到上线的标准,最终发布到应用市场上。

为了更方便你们获取 Flutter 相关的开发资源,咱们在 Github 上开了一个 repo flutter-resources,欢迎你们一块儿来维护这个 repo。

参考

知识小集是一个团队公众号,主要定位在移动开发领域,分享移动开发技术,包括 iOS、Android、小程序、移动前端、React Native、weex 等。每周都会有 原创 文章分享,咱们的文章都会在公众号首发。欢迎关注查看更多内容。

相关文章
相关标签/搜索