从原生开发到Flutter教程(二)新闻列表布局

上篇文章从原生开发到Flutter教程(一)认识Flutter我们已经大概了解了Flutter的魅力并搭建好了开发环境,终于到了大展身手的时候了。
接下来我们来做一个App,是央视新闻客户端。
带着这个例子,功能挺齐全,相信大家学完这套教程,应对日常的开发应该就不会有大的问题了。但是除了学会写项目,笔者觉得,更重要的是,我们通过这个例子,一起来领略一下Google出品的沥血之作其中的奥妙,体会Google工程师对于一些问题的解决方案的理念,如UI构建、数据流传输、用户交互、数据异步处理等等。话不多说,Let's Get Started.

项目初始化

注意:我的开发工具是VSCode。

项目的初始化很简单,Shift + Cmd + p,选择Flutter: New Project,然后写上项目名称(比如cctv_news),再选择一个放置文件夹即可。
接下来,VSCode会自动初始化项目,等待大概10s即可完成。

了解文件夹构成

项目初始化好后,会看到一堆文件夹和文件,如果之前很少接触Flutter,对这些文件可能会比较陌生。其实很简单,下面我来简述一下文件夹构成。

Flutter文件目录
  • ios、android
    这两个文件夹望文知义,就是iOS、Android的工程文件夹。可以在里面写一些原生代码,如OC/Swift/Java/Kotlin等,做一些原生交互。
  • lib
    这里存放的是Dart语言编写的代码,这里是核心代码。我们做Flutter开发的大概98%的时间都是在这个文件夹下书写代码。我们可以在这个lib目录下面创建不同的文件夹,里面存放不同的文件。
  • pubspec.yaml
    看着些许陌生,但是很简单。它就是配置依赖项的地方,比如配置远程pub仓库的依赖库,或者指定本地资源(图片、字体、音频、视频等),有点类似iOS中的Podfile,当然比后者功能更强大。
  • test
    测试文件
  • build
    存储iOS和Android构建文件夹。

编码开始之前

万丈高楼平地起,如果想快速上手写Flutter项目,下面几个概念一定要先熟悉一下,磨刀不误砍柴工。

1、Widget

Flutter中,万物皆Widget
如果你了解React、VUE等,这个概念不难理解。如果你从iOS过来的,Widget很像UIView,但是绝对不能等同。Flutter中的Widget非常轻量,他们本身不是什么控件,也不会被直接绘制出什么,他们只是UI的描述,即"声明和构建UI的方法"。一定要理解这个概念,否则后面你会产生类似"App为什么继承自Widget"这样的困惑。

StatelessWidget和StatefulWidget

Widget分为两种,StatelessWidgetStatefulWidget
我们自定义控件大多继承自两者之一。他们的区别是,前者没有state状态的概念,而后者有。

  • StatelessWidget
    继承自StatelessWidget控件都是无状态的,不需状态管理,非常高效。有个必须重写的方法build,在这个方法中返回创建的Widget控件即可。
  • StatefulWidget
    继承自StatelessWidget控件都是有状态的。既然是有状态的,肯定得有个state对象。没错,在这个类中,必须重写一个方法返回自己的state对象,即createState方法。而在这个state类中,实现build方法返回需要的控件,然后在用户操作的时候,调用setState即可完成数据源改变页面刷新。
    另外值得注意的是state的生命周期:
    • 1、initState :初始化,理论上只有初始化一次。
    • 2、didChangeDependencies:在 initState 之后调用,此时可以获取其他 State 。
    • 3、dispose :销毁,只会调用一次。

下面以StatefulWidget为例,它有两个类组成,即widget本身和他的状态state。看下面代码实例:

小提示:stl和stf是创建StatelessWidgetStatefulWidget的快捷键。

class Counter extends StatefulWidget {
  Counter({Key key, this.title}) : super(key: key);
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
    int counter = 0;
    void increaseCount() {
        setState(() {
            this.counter++;
        }
    }

    Widget build(context) {
        return RaisedButton(
            onPressed: increaseCount,
            child: new Text('Tap to Increase'),
        );
    }
}

2、Material 和 Cupertino Widgets

上篇文章也谈到了这两个概念。Flutter之所以可以快速构建精美的页面,离不开这两个内建widget库。前者是安卓原生风格,后者是仿苹果风格。

3、常见控件

下面列一下常见的控件,为后面铺垫。

  • Text - 文字控件。类似UILabel
  • Image - 图片控件。类似UIImageView
  • ListView - 列表视图。类似UITableView
  • Icon - 图标控件。用来展示Material 和 Cupertino Widgets内建库的图标。
  • Container - 容器控件。可以为子控件添加padding, alignment, backgrounds等。
  • TextInput - 文字输入控件。类似UITextfield
  • Row, Column - 水平、垂直布局控件容器。类似于CSS3的Flex布局。
  • Stack - 层叠布局控件。这个控件对于原生开发人员来说比较陌生,flutter中如果想布局一个控件压在另一个控件上面,就用这个控件。
  • Scaffold - 内建页面控件。提供了navigations, appBars, back buttons等。

简单分析页面

先来简单分析一下央视新闻首页。


新闻首页分析

上面的内容比较多,我们先做最简单的iOS中的TableView视图,里面有多个Cell构成。
下面我们开始正式进入代码阶段。
(PS:由于笔者是主栈iOS开发的,所以一些名词术语暂以最熟悉的iOS平台的术语为准)

上文也提到了,我们以后大部分开发时间,都是在lib文件夹下。我们打开这个文件夹,发现里面有个main.dart文件,这是程序的入口文件,稍微懂点编程的都知道其作用。打开这个文件,发现已经存在示例代码,这是Flutter官方写的范例,你可以运行一下看看效果。当然,我们后面开始写代码之前,最好把他们清空,完全从0开始。

开始写代码

实现main.dart

  • 1、我们清空main.dart代码,开始写属于自己的第一行dart代码。
  • 2、先将material.dart引入进来,这是Flutter提供的安卓原生控件库,我们可以直接基于他们快速开发出精美的页面。
import 'package:flutter/material.dart';
  • 3、实现main函数
    main函数即程序入口,我们来实现main函数,实现内建函数runApp,该函数需要一个参数,是一个Widget,runApp会将这个Widget渲染到用户的屏幕。所以,我们的传入自己创建的App实例对象即可。

注意,当函数体只有一行代码时,我们可以用胖箭头=>代替花括号,语法更简洁。

void main() => runApp(MainApp());
  • 4、创建App类。我们可以随便命名自己的App类名称,这里叫MainApp,注意,他是继承自StatelessWidget,上文已经解释过,在Flutter世界中,万物皆Widget。
    我们知道,StatelessWidget必须重写实现一个方法,即build,返回一个Widget用于展示,我们在这个方法中,返回一个MaterialApp对象,他即是使用Material风格的App类。里面有一些很有用的属性,如titlehomethemeroutes等,基本上都可以望文知意。
    注意,我们在home属性传入的是一个Scaffold实例,他即是一个脚手架,可以简单把它当做我们的Material风格控件的容器,提供一些很有用的属性,如appBarbody等。body即是我们放置我们写的控件的地方。我们可以简单写个控件运行看看效果,如body: Text('CCTV News')

main.dart完整代码如下:

import 'package:flutter/material.dart';

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

class MainApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CCTV NRES',
      home: Scaffold(
        appBar: AppBar(
          title: Text('CCTV News'),
        ),
        body: Text('欢迎来到Flutter~'),
      ),
    );
  }
}

运行后会发现,'欢迎来到Flutter~'文字居于左上角,我们现在想将这段文字屏幕居中,跟我们之前原生开始的布局逻辑不同,Flutter是通过Widget来完成布局,关于布局的知识我们会在后面详细讲,这里只需要知道,要想居中控件,我们可以用Center控件包裹一下即可。
修改body属性如下:body: Center(child: Text('Test')),,输入r热加载一下,即可看到文字展示在中间了。

创建卡片视图(TableViewCell)

有了上面的铺垫,我们就可以正式开始写App了。先从最简单的入手,先实现一下新闻详情的列表的Cell的样式。

NewsCell

如上图,Cell里面的内容,是由两大部分构成,Row和Column。再次强调一下,Flutter的布局理念跟原生的Layout的概念完全不一样。形象点比喻就是,Flutter的布局就像装集装箱,先将一堆东西按照想要的规则放在一个盒子里,在将这个盒子按照想要的规则放在更大的盒子里面。好了,我们开始写代码,先创建home文件夹和HomeNewsCell.dart文件,如下图:

-lib/home
-lib/home/HomeNewsCell.dart

进入HomeNewsCell.dart文件中开始写Cell视图。这里我会分析得细一些,后面文章我们就快一些了。下面这样一层一层分析:

  • 由于内容承载视图和分割线属于上下布局,所以需要先用Column布局。
  • 内容承载视图,首先是左右布局,左边是标题和'听新闻'按钮视图,右边是新闻图片视图,所以用Row布局。
  • 内容承载视图的左半边,由于是上下布局,上面标题下面按钮,所以又得使用Column布局。

Column > Row > Column
注意,这里有几个需要注意的地方:

  • 文字撑开布局使用是Expended控件包裹
  • 加载本地资源图片,需要先将图片拖入images文件夹中,然后在pubspec.yaml中配置上才能使用。
assets:
 - images/news_image.jpg

其实整个布局比较基础,但是大家要通过这个简单的布局理解Flutter的布局思想,理解透了思想,再复杂的布局,也可以拆解成简单的单元。代码如下:

import 'package:flutter/material.dart';

class HomeNewsCell extends StatelessWidget {
  Widget get _cellContentView {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                '继山东编导艺考联考被曝疑似出现泄题和作弊的情况。江西编导艺考联考也被曝疑似出现泄题和作弊的情况。',
                style: TextStyle(
                  fontSize: 15.0,
                  color: Color(0xff111111),
                ),
                maxLines: 3,
                overflow: TextOverflow.ellipsis,
              ),
              Container(
                width: 50.0,
                height: 20.0,
                margin: EdgeInsets.only(top: 6.0),
                child: ButtonTheme(
                  buttonColor: Color(0xff1C64CF),
                  shape: StadiumBorder(),
                  child: RaisedButton(
                    onPressed: () => print('test'),
                    padding: EdgeInsets.all(2.0),
                    child: Text(
                      '听新闻',
                      style: TextStyle(
                          color: Colors.white,
                          fontSize: 11.0,
                          fontWeight: FontWeight.w300),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
        SizedBox(
          width: 10.0,
        ),
        Container(
          height: 85.0,
          width: 115.0,
          margin: EdgeInsets.only(top: 3.0),
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.circular(5.0),
            image: DecorationImage(
              image: AssetImage('images/news_image.jpg'),
              fit: BoxFit.cover,
            ),
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 115.0,
      child: Column(
        children: <Widget>[
          // 内容视图
          Container(
            padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
            child: _cellContentView,
          ),
          // 分割线
          Container(
            margin: EdgeInsets.only(top: 4.0),
            color: Color(0xffeaeaea),
            constraints: BoxConstraints.expand(height: 4.0),
          )
        ],
      ),
    );
  }
}

展示HomeNewsCell样式

上面已经写好了Cell的布局,我们现在改写一下main.dart,加载看一下Cell的样式。很简单,先将HomeNewsCell.dart引入进来后,改写body属性成Column,即可完成渲染。

...
body: Column(
  children: <Widget>[
    HomeNewsCell(),
    HomeNewsCell(),
    ],
),
...

flutter run 看一下效果,是不是很惊喜,这么快时间,就写好了横跨iOS/Android平台的代码,效果还不错,如下图:

新闻列表

使用ListView渲染列表

上面我们是把Cell放在了Column里面,但是在实际开发场景中,我们需要放在ListView里面,这样就可以多了很多如下拉刷新、滑动加载、阻尼效果等等的功能。下面我们继续改造main.dart,加入ListView
同上,改写body属性如下:

body: ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return HomeNewsCell();
  },
)),

使用ListView.builder很简单,itemCount是cell的个数,相当于iOS中TableViewnumberOfRowsInSectionitemBuilder是一个回调函数,需要外界告知需要渲染的Cell的样式,即相当于iOS中的cellForRowAtIndexPath。还有一些其他的属性,我们后面再介绍。
好了,改造完成后,flutter run一下,可以滑动了,效果还不错,见下图:

新闻滑动列表

总结

本节教程,抛砖引玉,完成了基础的新闻列表页的布局及展示,我们也了解到了Flutter的布局跟原生布局的思想的差异,其实,跟原生布局比起来,Flutter的这种布局方式刚一开始可能会觉得有些笨拙,但是写顺手之后会发现,这种堆积木、集装箱式的布局构建方式,另外配合上Flutter的数据绑定、响应式编程,这种方式写起来更得心应手,水到渠成。
当然,从上面列表页的小例子我们也不难很快就能发现其中的缺憾,这种UI构建方式,稍不注意,就很容易造成代码冗长,各种括号,各种回车等。颇有在写标记语言的的韵味,当然,注意好组件抽象封装隔离,就能在一定程度上很好避免上述问题。

下篇教程,我们会搭建Tab主UI框架,自定义组件,另外,本篇教程使用的是假数据,下篇教程,我们会请求网络真实数据,来体验一下,Dart作为优秀的现代化语言,异步任务处理是怎么样的一番景象。

推荐阅读更多精彩内容