美文网首页RN And FlutterFlutterFlutter跨平台移动开发
从原生开发到Flutter教程(二)新闻列表布局

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

作者: KavinZhou | 来源:发表于2018-12-14 21:37 被阅读149次

    上篇文章从原生开发到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作为优秀的现代化语言,异步任务处理是怎么样的一番景象。

    相关文章

      网友评论

        本文标题:从原生开发到Flutter教程(二)新闻列表布局

        本文链接:https://www.haomeiwen.com/subject/dtunhqtx.html