Flutter 完整示例

经过这一段对 Flutter 的了解和接触,掌握如何完整使用 Flutter 开发一个项目。实际上,在 Flutter 中,一切皆 widget,我们的界面都是由各种 widget 堆叠出来的。

一个 Flutter 工程涉及以下几个要点:

  • 工程项目代码分层
  • 主题风格
  • 插件
  • 路由
  • 网络数据交互
  • 界面布局及刷新



一、工程项目代码分层

一个正式的工程项目,它的代码必须做到分层,代码的分层体现了开始者的架构能力。



Flutter 工程的主要工作 lib 目录及 pubspec.yaml :

  • main.dart:Flutter 的入口函数
  • loading.dart:启动页,一般生存周期为3-5秒
  • app.dart:工程主文件
  • conf : 配置文件目前或一些宏定义数据文件目录
  • model : 数据模型目录
  • pages : 各 UI ,即 Widget 文件
  • service : 网络请求目录
  • style : 自定义风格文件(颜色、字体等)
  • utils : 工具目录

代码分层设计设计的合不合理,直接影响代码的可维护性和稳定性。

二、主题风格

Flutter 默认的主题是 蓝白 的风格,其他主题则需要配置。依项目而定,根据当前需要,配置一个 红灰 风格:

#main.dart
void main() => runApp(MaterialApp(
  debugShowCheckedModeBanner: false,
  title: 'Flutter实战',
  //自定义主题
  theme: mDefaultTheme,

));

//自定义主题
final ThemeData mDefaultTheme = ThemeData(
  primaryColor: Colors.redAccent,
);

Colors.redAccent 为系统主题,在 colors.dart 中定义:

static const MaterialAccentColor redAccent = MaterialAccentColor(
    _redAccentValue,
    <int, Color>{
      100: Color(0xFFFF8A80),
      200: Color(_redAccentValue),
      400: Color(0xFFFF1744),
      700: Color(0xFFD50000),
    },
  );
  static const int _redAccentValue = 0xFFFF5252;

当然也可以自定义一些风格或颜色,在工程 style 中 color.dart:

//产品颜色
class ProductColors{
  static const Color bgColor = Color(0xFFFFFFFF);
  static const divideLineColor = Color.fromRGBO(245, 245, 245, 1.0);
  static const typeColor = Color.fromRGBO(182, 9, 9, 1.0);
  static const piontColor = Color.fromRGBO(132, 95, 63, 1.0);
}

三、插件

开发者不可能对每个功能都自已造轮子,选取合适的轮子,对于项目来说,可以达到事半功倍的效果。

3.1 添加第三方库

打开 pubspec.yaml 文件添加三方库,可以 在 https://pub.dev/flutter (需要翻墙)上找到许多开源软件包

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  http: ^0.12.0
  flutter_webview_plugin: ^0.3.0
  flutter_swiper: 1.1.4
  • http : 网络请求插件
  • flutter_webview_plugin : 一些静态页面的加载,使用webview
  • flutter_swiper : 动画轮播效果

3.2 导入

swiper 插件

点击 Packages get 获取刚添加的包。

这样就可以使用这个库了。

四、路由

路由主要用于界面的切换及跳转,分为 静态路由 和 动态路由

  • 静态路由:界面的跳转不带附加参数
  • 动态路由:界面的跳转可携带附加参数

4.1 路由初始化

void main() => runApp(MaterialApp(
  debugShowCheckedModeBanner: false,
  title: 'Flutter实战',
  //自定义主题
  theme: mDefaultTheme,

  //添加路由
  routes: <String,WidgetBuilder>{
    "app": (BuildContext context) => App(),
    "company_info":(BuildContext context) => WebviewScaffold(
      url: "https://www.baidu.com",
      appBar: AppBar(
        title: Text('公司介绍'),
        leading: IconButton(
            icon: Icon(Icons.home),
            onPressed: (){
              //路由到主界面
              Navigator.of(context).pushReplacementNamed('app');
            },
        ),
      ),
    ),
  },
  //指定加载页面
  home: LoadingPage(),

));


4.2 静态路由

Navigator.of(context).pushReplacementNamed('company_info');

4.3 动态路由

Navigator.push(context,MaterialPageRoute(builder: (context) => AboutContactPage()));

或者

Navigator.push(context,MaterialPageRoute(builder: (context) => NewsDetailPage(item: item)),

数据接收处理:

class NewsDetailPage extends StatelessWidget{
  final NewsItemModal item;

  NewsDetailPage({Key key,@required this.item}) : super(key:key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(item.title),
      ),
      body:  Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(item.content),
      ),
    );
  }
}

五、网络数据交互

网络数据交互一般涉及 URL、数据模型、数据交互模式(Get、Post)等

5.1 URL 定义

在配置中定义宏变量

class Config{
  static const String IP = '192.168.2.5';
  static const String PORT = '8080';
}

5.2 数据模型

//新闻列表项数据转换
class NewsItemModal{
  String author;//作者
  String title;//标题
  String content;//内容

  NewsItemModal({
    this.author,
    this.title,
    this.content,
  });

  factory NewsItemModal.fromJson(dynamic json){
    return NewsItemModal(
      author: json['author'],
      title: json['title'],
      content: json['content'],
    );
  }
}

//新闻列表数据转换
class NewsListModal{

  List<NewsItemModal> data;
  NewsListModal(this.data);

  factory NewsListModal.fromJson(List json){
    return NewsListModal(
        json.map((i) => NewsItemModal.fromJson((i))).toList()
    );
  }
}

5.3 数据交互模式

import 'package:http/http.dart' as http;
import 'dart:convert';
import '../conf/configure.dart';

//获取新闻数据
getNewsResult() async {
  String url = 'http://' + Config.IP + ':' + Config.PORT + '/?action=getNews';

  var res = await http.get(url);
  String body = res.body;

  var json= jsonDecode(body);
  print(json);

  return json['items'] as List;
}

六、界面布局及刷新

6.1 启动页加载

一个存在 3 - 5 秒的界面

class LoadingPage extends StatefulWidget{
  @override
  _LoadingState createState() => _LoadingState();
}

class _LoadingState extends State<LoadingPage>{

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

    //在加载页面停顿3秒
    Future.delayed(Duration(seconds: 3),(){
      print('Flutter企业站启动...');
      Navigator.of(context).pushReplacementNamed("app");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Center(
        child: Stack(
          children: <Widget>[
            //加载页面背景图
            Image.asset(
              'assets/images/loading.jpeg'
            ),

            Center(
              child: Text(
                'Flutter',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 36.0,
                  decoration: TextDecoration.none
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

6.2 轮播图片

轮播图片应用很广泛,如广告宣传之类。

在资源配置图片中,添加图片

 # To add assets to your application, add an assets section, like this:
  assets:
  - assets/images/loading.jpeg
  - assets/images/company.jpg

  #轮播图片
  - assets/images/banners/1.jpeg
  - assets/images/banners/2.jpeg
  - assets/images/banners/3.jpeg
  - assets/images/banners/4.jpeg

使用 Swiper 插件

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

class BannerWidget extends StatelessWidget{

  //图片路径
  List<String> banners = <String>[
    'assets/images/banners/1.jpeg',
    'assets/images/banners/2.jpeg',
    'assets/images/banners/3.jpeg',
    'assets/images/banners/4.jpeg',
  ];

  @override
  Widget build(BuildContext context) {

    //计算宽高 按比例
    double width = MediaQuery.of(context).size.width;
    double height = width * 540.0 / 1080.0;
    return Container(
      width: width,
      height: height,
      //轮播组件
      child: Swiper(
        itemBuilder: (BuildContext context, index){
          return Container(
            //图片左右内边距
            margin: EdgeInsets.only(left: 5, right: 5),
            child: Image.asset(
              banners[index],
              width: width,
              height: height,
              fit: BoxFit.cover,
            ),
          );
        },
        //轮播数量
        itemCount: banners.length,
        //方向
        scrollDirection: Axis.horizontal,
        //是否自动播放
        autoplay: true,
      ),
    );
  }
}

6.3 主界面(含导航栏)

import 'package:flutter/material.dart';
import 'pages/about_us_page.dart';
import 'pages/home_page.dart';
import 'pages/news_page.dart';
import 'pages/product_page.dart';

class App extends StatefulWidget {
  @override
  AppState createState() => AppState();
}

class AppState extends State<App> {
  //当前选择页面索引
  var _currentIndex = 0;

  HomePage homePage;
  ProductPage productPage;
  NewsPage newsPage;
  AboutUsPage aboutUsPage;

  //根据当前索引返回不同的页面
  currentPage(){
    switch(_currentIndex){
      case 0:
        if(homePage == null){
          homePage = HomePage();
        }
        return homePage;
      case 1:
        if(productPage == null){
          productPage = ProductPage();
        }
        return productPage;

      case 2:
        if(newsPage == null){
          newsPage = NewsPage();
        }
        return newsPage;
      case 3:
        if(aboutUsPage == null){
          aboutUsPage = AboutUsPage();
        }
        return aboutUsPage;

    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter企业站实战'),
        leading: Icon(Icons.home),
        actions: <Widget>[
          //右侧内边距
          Padding(
            padding: EdgeInsets.only(right: 20.0),
            child: GestureDetector(
              onTap: () {},
              child: Icon(
                Icons.search,
              ),
            ),
          ),
        ],
      ),

      body: currentPage(),

      //底部导航栏
      bottomNavigationBar: BottomNavigationBar(
          //通过fixedColor设置选中item 的颜色
          type: BottomNavigationBarType.fixed,
          currentIndex: _currentIndex,
          onTap: ((index) {
            setState(() {
              _currentIndex = index;
            });
          }),
          //底部导航栏
          items: [
            BottomNavigationBarItem(
              title: Text(
                '首页',
              ),
              icon: Icon(Icons.home),
            ),
            BottomNavigationBarItem(
              title: Text(
                '产品',
              ),
              icon: Icon(Icons.apps),
            ),
            BottomNavigationBarItem(
              title: Text(
                '新闻',
              ),
              icon: Icon(Icons.fiber_new),
            ),
            BottomNavigationBarItem(
              title: Text(
                '关于我们',
              ),
              icon: Icon(Icons.insert_comment),
            ),
          ]),
    );
  }
}

6.4 ListView 的应用

import 'package:flutter/material.dart';
import '../model/news.dart';
import '../services/news.dart';
import 'news_detail_page.dart';

//新闻页面
class NewsPage extends StatefulWidget {
  @override
  NewsPageState createState() => NewsPageState();
}

class NewsPageState extends State<NewsPage> {
  NewsListModal listData = NewsListModal([]);

  //获取新闻列表数据
  void getNewsList() async {
    var data = await getNewsResult();
    NewsListModal list = NewsListModal.fromJson(data);

    setState(() {
      listData.data.addAll(list.data);
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //带分隔线的List
      body: ListView.separated(
        //排列方向 垂直和水平
        scrollDirection: Axis.vertical,
        //分隔线构建器
        separatorBuilder: (BuildContext contex, int index) => Divider(
              height: 1.0,
              color: Colors.grey,
            ),
        itemCount: listData.data.length,
        //列表项构建器
        itemBuilder: (BuildContext contex, int index) {

          //新闻列表项数据
          NewsItemModal item = listData.data[index];

          return ListTile(
            title: Text(item.title),
            subtitle: Text(item.content),
            leading: Icon(Icons.fiber_new),
            trailing: Icon(Icons.arrow_forward),
            contentPadding: EdgeInsets.all(10.0),
            enabled: true,
            //跳转至新闻详情页面
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) => NewsDetailPage(item: item)),
              );
            },
          );
        },
      ),
    );
  }
}

6.5 文本框操作

含文本框配置及数据操作

import 'package:flutter/material.dart';
import '../services/contact.dart';


class AboutContactPage extends StatefulWidget{
  @override
  AboutContactPageState createState() => AboutContactPageState();
}

class AboutContactPageState extends State<AboutContactPage>{

  //文本编辑控制器
  TextEditingController controller = TextEditingController();

  //提交数据
  void commit(){
    if(controller.text.length == 0){
      showDialog(context: context,builder: (context) => AlertDialog(title: Text('请输入内容'),),);
    } else{
      var info = contactCompany(controller.text);
      print(info);
    }
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text('给我留言'),
      ),
      body: Container(
        color: Colors.white,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Image.asset(
              'assets/images/company.jpg',
              fit: BoxFit.cover,
            ),
            SizedBox(
              height: 40.0,
            ),
            SizedBox(
              width: 380.0,
              child: TextField(
                controller: controller,
                decoration: InputDecoration(
                  hintText: '请留言',
                  labelText: '请留言',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.person),
                ),
              ),
            ),
            SizedBox(
              height: 40.0,
            ),
            SizedBox(
              width: 220.0,
              height: 48.0,
              child: RaisedButton(
                child: Text('给我们留言',style: TextStyle(fontSize: 16.0),),
                color: Theme.of(context).primaryColor,//Colors.redAccent,
                colorBrightness: Brightness.dark,
                textColor: Colors.white,
                padding: EdgeInsets.only(
                  left: 20.0,
                  right: 20.0,
                  top: 5.0,
                  bottom: 5.0,
                ),
                shape: RoundedRectangleBorder(
                  side: BorderSide(
                    width: 1.0,
                    color: Colors.white,
                    style: BorderStyle.solid,
                  ),
                  borderRadius: BorderRadius.only(
                    topRight: Radius.circular(4.0),
                    topLeft: Radius.circular(4.0),
                    bottomLeft: Radius.circular(4.0),
                    bottomRight: Radius.circular(4.0),
                  ),
                ),
                onPressed: (){
                  commit();
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

七、注意点

  • => 是 Dart 中单行函数的简写
  • StatelessWidget 代表只有一种状态的组件,与之对应的是 StatefulWidget(表示可能有多种状态)。
  • 在 Widget 组件中都是通过 build 方法来描述自己的内部结构。这里的 build 表示构建 MyApp 中使用的是 MaterialApp 的系统组件。
  • home标签的值:Scaffold 是 Material library 中提供的一个组件,我们可以在里面设置导航栏、标题和包含主屏幕 widget 树的 body 属性。可以看到这里是在页面上添加了AppBar 和一个 Text。
  • Center 是一个可以把子组件放在中心的组件

Refer

插件