Flutter项目实操---资讯、发布动弹

资讯:

继续接着上一次https://www.cnblogs.com/webor2006/p/13186045.html的功能往下编写,这一篇算是这个操练小项目的最后一篇笔记了,基本上经过这么将近一年的漫长的蜗牛式地Flutter学习对它也有了更深一步的认识了,待这个完篇之后打算再来操练一个稍完整的Flutter项目,具体是啥项目到时再来看,接下来再来编写资讯这个页面了。

效果演示:

API查看:

先上开源中国查看一下这个列表的API接口:

具体实现:

界面框架搭建:

这样的列表界面在之前也已经练过几次了,这里则再来温故一下,先来看一下目前的样子:

,其这个界面的代码目前为:

import 'package:flutter/material.dart';

class NewsListPage extends StatefulWidget {
  @override
  _NewsListPageState createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('资讯'),
    );
  }
}

因为要支持上下拉加载刷新功能,所以需要用RefreshIndicator这个Widget,如下:

import 'package:flutter/material.dart';

class NewsListPage extends StatefulWidget {
  @override
  _NewsListPageState createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: ListView.builder(itemBuilder: (context, index) {
        return Column(
          children: <Widget>[],
        );
      }),
    );
  }
}

接下来则来构建好列表的布局:

而这个列表的Widget的构建细节就不一一说明了,不是太难,代码如下:

import 'package:flutter/material.dart';

class NewsListItem extends StatelessWidget {
  final Map<String, dynamic> newsList;

  NewsListItem({this.newsList});

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {},
      child: Container(
        margin: const EdgeInsets.only(left: 20.0),
        decoration: BoxDecoration(
          //分隔线
          border: Border(
            bottom: BorderSide(
              color: Color(0xffaaaaaa),
               1.0,
            ),
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.only(top: 10.0, bottom: 10.0, right: 20.0),
          child: Column(
            children: <Widget>[
              Text(
                '${newsList['title']}',
                style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
              SizedBox(height: 10.0),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween, //左右分布
                children: <Widget>[
                  Text(
                    '@${newsList['author']} ${newsList['pubDate'].toString().split(' ')[0]}',
                    style: TextStyle(color: Color(0xaaaaaaaa), fontSize: 14.0),
                  ),
                  Row(
                    children: <Widget>[
                      Icon(
                        Icons.message,
                        color: Color(0xaaaaaaaa),
                      ),
                      Text(
                        '${newsList['commentCount']}',
                        style:
                            TextStyle(color: Color(0xaaaaaaaa), fontSize: 14.0),
                      ),
                    ],
                  )
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
} 

请求API:

接下来则需要处理接口数据的请求,这个接口其实是需要登录的,因为它的入参需要传递一个token,如下:

所以此时则需要判断是否登录,其这块的逻辑在之前我的页面已经写过了,这里直接抄过来了,就不啰嗦了:

import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/widgets/news_list_item.dart';

class NewsListPage extends StatefulWidget {
  @override
  _NewsListPageState createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  bool isLogin = false;

  @override
  void initState() {
    super.initState();
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      //TODO:获取新闻列表
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: ListView.builder(itemBuilder: (context, index) {
        return NewsListItem();
      }),
    );
  }
}

接下来如果登录之后,则来请求一下API,具体如下:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/utils/net_utils.dart';
import 'package:flutter_osc_client/widgets/news_list_item.dart';

class NewsListPage extends StatefulWidget {
  @override
  _NewsListPageState createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  bool isLogin = false;
  int curPage = 1;
  List newsList;

  @override
  void initState() {
    super.initState();
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      getNewsList(true);
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  //NEWS_LIST
  getNewsList(bool isLoadMore) async {
    DataUtils.isLogin().then((isLogin) {
      if (isLogin) {
        DataUtils.getAccessToken().then((accessToken) {
          if (accessToken == null || accessToken.length == 0) {
            return;
          }
          Map<String, dynamic> params = Map<String, dynamic>();
          params['access_token'] = accessToken;
          params['catalog'] = 1;
          params['page'] = curPage;
          params['pageSize'] = 10;
          params['dataType'] = 'json';

          NetUtils.get(AppUrls.NEWS_LIST, params).then((data) {
            print('NEWS_LIST: $data');
            if (data != null && data.isNotEmpty) {
              Map<String, dynamic> map = json.decode(data);
              List _newsList = map['newslist'];
              if (!mounted) return;
              setState(() {
                if (isLoadMore) {
                  newsList.addAll(_newsList);
                } else {
                  newsList = _newsList;
                }
              });
            }
          });
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: () {},
      child: ListView.builder(itemBuilder: (context, index) {
        return NewsListItem();
      }),
    );
  }
}

接下来则需要根据是否登录需要处理一下界面的状态:

下面运行一下:

 

上拉分页、下拉刷新:

接下来先来处理上拉分页加载,也是之前写过的,具体可以参考:https://www.cnblogs.com/webor2006/p/12748524.html,下面来贴一下代码:

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/pages/login_web_page.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/utils/net_utils.dart';
import 'package:flutter_osc_client/widgets/news_list_item.dart';

class NewsListPage extends StatefulWidget {
  @override
  _NewsListPageState createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  bool isLogin = false;
  int curPage = 1;
  List newsList;
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController()
      ..addListener(() {
        var maxScroll = _scrollController.position.maxScrollExtent;
        var pixels = _scrollController.position.pixels;
        if (maxScroll == pixels) {
          curPage++;
          getNewsList(true);
        }
      });
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      getNewsList(true);
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  //NEWS_LIST
  getNewsList(bool isLoadMore) async {
    DataUtils.isLogin().then((isLogin) {
      if (isLogin) {
        DataUtils.getAccessToken().then((accessToken) {
          if (accessToken == null || accessToken.length == 0) {
            return;
          }
          Map<String, dynamic> params = Map<String, dynamic>();
          params['access_token'] = accessToken;
          params['catalog'] = 1;
          params['page'] = curPage;
          params['pageSize'] = 10;
          params['dataType'] = 'json';

          NetUtils.get(AppUrls.NEWS_LIST, params).then((data) {
            print('NEWS_LIST: $data');
            if (data != null && data.isNotEmpty) {
              Map<String, dynamic> map = json.decode(data);
              List _newsList = map['newslist'];
              if (!mounted) return;
              setState(() {
                if (isLoadMore) {
                  newsList.addAll(_newsList);
                } else {
                  newsList = _newsList;
                }
              });
            }
          });
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!isLogin) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('由于openapi限制,必须登录才能获取资讯!'),
            InkWell(
              onTap: () async {
                final result = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => LoginWebPage()));
                if (result != null && result == 'refresh') {
                  //登录成功
                  eventBus.fire(LoginEvent());
                }
              },
              child: Text('去登录'),
            ),
          ],
        ),
      );
    }
    return RefreshIndicator(
      onRefresh: () {},
      child: buildListView(),
    );
  }

  Widget buildListView() {
    if (newsList == null) {
      getNewsList(false);
      return CupertinoActivityIndicator();
    }
    return ListView.builder(
        controller: _scrollController,
        itemCount: newsList.length + 1, //多加一个是为了实现分页效果
        itemBuilder: (context, index) {
          if (index == newsList.length) {
            //如果是最后一行则增加个loading
            return Padding(
              padding: const EdgeInsets.all(10.0),
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
          return NewsListItem(newsList: newsList[index]);
        });
  }
}

运行看一下:

 

接下来则处理下拉刷新的处理,这个就比较简单了:

这里就不演示了,效果都懂的。 

列表项点击处理:

增加点击事件:

接下来增加列表的点击事件,查看资讯详情:

 

查看API:

返回的JSON格式如下:

{
    "id": 11, 
    "body": "语法高亮文本编辑器", 
    "pubDate": "2008-09-15 17:00:29.0", 
    "author": "总管", 
    "title": "VirtualBox 2.0.2 released!", 
    "authorid": 1, 
    "relativies": [
        {
            "title": "RSyntaxTextArea 2.0.2 发布 语法高亮文本编辑器", 
            "url": "http://liudong/news/26709/rsyntaxtextarea-2-0-2"
        }, 
        {
            "title": "Bootstrap 2.0.2 发布,Web 前端工具包", 
            "url": "http://liudong/news/26688/bootstrap-2-0-2"
        }, 
        {
            "title": "Airtime 2.0.2 发布 - 电台管理系统", 
            "url": "http://liudong/news/26516/airtime-202"
        }, 
        {
            "title": "Wayland and Weston 0.85.0 released", 
            "url": "http://liudong/news/25610"
        }, 
        {
            "title": "jOOQ 2.0.2 发布,Java的ORM框架", 
            "url": "http://liudong/news/24645/jooq-2-0-2-released"
        }, 
        {
            "title": "RemoteBox 1.2 发布,VirtualBox 管理工具", 
            "url": "http://liudong/news/24463/remotebox-1-2-released"
        }, 
        {
            "title": "ChromePlus 2.0.0.4 Released(for Windows)", 
            "url": "http://liudong/news/24457/chromeplus-2-0-0-4-for-windows"
        }, 
        {
            "title": "VirtualBox 4.1.8.75467 Final", 
            "url": "http://liudong/news/24172/virtualbox-4-1-8-released"
        }, 
        {
            "title": "Spring Roo 1.2.0.RELEASED 发布", 
            "url": "http://liudong/news/24114/spring-roo-1-2-0-released"
        }, 
        {
            "title": "MongoDB 2.0.2 发布", 
            "url": "http://liudong/news/24076"
        }
    ], 
    "notice": {
        "replyCount": 0, 
        "msgCount": 0, 
        "fansCount": 0, 
        "referCount": 0
    }, 
    "favorite": 0, 
    "commentCount": 0, 
    "url": "http://liudong/news/11"
} 

效果演示:

其实就是一个H5页面啦~~

页面逻辑实现:

这里没啥好说的,直接开始请求API,然后用WebView去加载既可,这块在之前登录已经用过了,直接贴代码:

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/utils/net_utils.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class NewsDetailPage extends StatefulWidget {
  final int id;

  NewsDetailPage({this.id}) : assert(id != null);

  @override
  _NewsDetailPageState createState() => _NewsDetailPageState();
}

class _NewsDetailPageState extends State<NewsDetailPage> {
  bool isLoading = true;
  FlutterWebviewPlugin _flutterWebviewPlugin = FlutterWebviewPlugin();
  String url;

  @override
  void initState() {
    super.initState();
    //监听url变化
    _flutterWebviewPlugin.onStateChanged.listen((state) {
      if (state.type == WebViewState.finishLoad) {
        if (!mounted) return;
        setState(() {
          isLoading = false;
        });
      } else if (state.type == WebViewState.startLoad) {
        if (mounted) {
          setState(() {
            isLoading = true;
          });
        }
      }
    });

    DataUtils.getAccessToken().then((token) {
      //token !=null
      Map<String, dynamic> params = Map<String, dynamic>();
      params['access_token'] = token;
      params['dataType'] = 'json';
      params['id'] = widget.id;
      NetUtils.get(AppUrls.NEWS_DETAIL, params).then((data) {
        if (data != null && data.isNotEmpty) {
          Map<String, dynamic> map = json.decode(data);
          print('NEWS_DETAIL: $map');
          if (!mounted) return;
          setState(() {
            url = map['url'];
          });
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> _appBarTitle = [
      Text(
        '资讯详情',
        style: TextStyle(
          color: Color(AppColors.APPBAR),
        ),
      ),
    ];
    if (isLoading) {
      _appBarTitle.add(SizedBox(
         10.0,
      ));
      _appBarTitle.add(CupertinoActivityIndicator());
    }

    return url == null
        ? Center(
            child: CupertinoActivityIndicator(),
          )
        : WebviewScaffold(
            url: url,
            appBar: AppBar(
              title: Row(
                children: _appBarTitle,
              ),
              iconTheme:
                  IconThemeData(color: Color(AppColors.APPBAR)), //0412 added
            ),
            withJavascript: true,
            //允许执行js
            withLocalStorage: true,
            //允许本地存储
            withZoom: true, //允许网页缩放
          );
  }
}

运行瞅一下:

动弹:

效果图:

 

API查看:

Tab主框架搭建:

先来看一下目前咱们这个页面的状态:

 

import 'package:flutter/material.dart';

class TweetPage extends StatefulWidget {
  @override
  _TweetPageState createState() => _TweetPageState();
}

class _TweetPageState extends State<TweetPage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('动弹'),
    );
  }
}

接下来构建Tab页面,这个在之前的“我的消息”中已经构建用,回忆一下:

在之前咱们是用的这种方式来构建的:

这里采用另一种写法:

import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';

class TweetPage extends StatefulWidget {
  @override
  _TweetPageState createState() => _TweetPageState();
}

class _TweetPageState extends State<TweetPage>
    with SingleTickerProviderStateMixin {
  List _tabTitles = ['最新', '热门'];
  List latestTweetList;
  List hotTweetList;
  int curPage = 1;
  ScrollController _controller = ScrollController();
  TabController _tabController;
  bool isLogin = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabTitles.length, vsync: this);
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixels = _controller.position.pixels;
      if (maxScroll == pixels) {
        curPage++;
        //TODO:请求列表数据
      }
    });
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
      //TODO:请求列表数据
    });
    eventBus.on<LogoutEvent>().listen((event) {});
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        //tabbar
        Container(
          color: Color(AppColors.APP_THEME),
          child: TabBar(
              controller: _tabController,
              indicatorColor: Color(0xffffffff),
              labelColor: Color(0xffffffff),
              tabs: _tabTitles.map((title) {
                return Tab(
                  text: title,
                );
              }).toList()),
        ),
      ],
    );
  }
}

上面init()代码都是之前用过的,就不过多的解释了,下面运行看一下:

其中这些页面也是需要登录才能看到的,所以跟资讯一下针对未登录状态做一下处理:

import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/pages/login_web_page.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';

class TweetPage extends StatefulWidget {
  @override
  _TweetPageState createState() => _TweetPageState();
}

class _TweetPageState extends State<TweetPage>
    with SingleTickerProviderStateMixin {
  List _tabTitles = ['最新', '热门'];
  List latestTweetList;
  List hotTweetList;
  int curPage = 1;
  ScrollController _controller = ScrollController();
  TabController _tabController;
  bool isLogin = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabTitles.length, vsync: this);
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixels = _controller.position.pixels;
      if (maxScroll == pixels) {
        curPage++;
        //TODO:请求列表数据
      }
    });
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      //TODO:请求列表数据
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!isLogin) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('必须登录才能查看动弹信息!'),
            InkWell(
              child: Container(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  '马上登录',
                  style: TextStyle(color: Color(0xff0000ff)),
                ),
              ),
              onTap: () async {
                final result = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => LoginWebPage()));
                if (result != null && result == 'refresh') {
                  //登录成功
                  eventBus.fire(LoginEvent());
                }
              },
            ),
          ],
        ),
      );
    }
    return Column(
      children: <Widget>[
        //tabbar
        Container(
          color: Color(AppColors.APP_THEME),
          child: TabBar(
              controller: _tabController,
              indicatorColor: Color(0xffffffff),
              labelColor: Color(0xffffffff),
              tabs: _tabTitles.map((title) {
                return Tab(
                  text: title,
                );
              }).toList()),
        ),
      ],
    );
  }
}

运行看一下:

动弹列表实现:

接下来则来实现两个TAB的列表显示,这里需要提前明确一下:

热门列表:

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/pages/login_web_page.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/utils/net_utils.dart';
import 'package:flutter_osc_client/widgets/tweet_list_item.dart';

class TweetPage extends StatefulWidget {
  @override
  _TweetPageState createState() => _TweetPageState();
}

class _TweetPageState extends State<TweetPage>
    with SingleTickerProviderStateMixin {
  List _tabTitles = ['最新', '热门'];
  List latestTweetList;
  List hotTweetList;
  int curPage = 1;
  ScrollController _controller = ScrollController();
  TabController _tabController;
  bool isLogin = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabTitles.length, vsync: this);
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixels = _controller.position.pixels;
      if (maxScroll == pixels) {
        curPage++;
        //TODO:请求列表数据
      }
    });
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      //TODO:请求列表数据
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!isLogin) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('必须登录才能查看动弹信息!'),
            InkWell(
              child: Container(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  '马上登录',
                  style: TextStyle(color: Color(0xff0000ff)),
                ),
              ),
              onTap: () async {
                final result = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => LoginWebPage()));
                if (result != null && result == 'refresh') {
                  //登录成功
                  eventBus.fire(LoginEvent());
                }
              },
            ),
          ],
        ),
      );
    }
    return Column(
      children: <Widget>[
        //tabbar
        Container(
          color: Color(AppColors.APP_THEME),
          child: TabBar(
              controller: _tabController,
              indicatorColor: Color(0xffffffff),
              labelColor: Color(0xffffffff),
              tabs: _tabTitles.map((title) {
                return Tab(
                  text: title,
                );
              }).toList()),
        ),
        Expanded(
            child: TabBarView(
          controller: _tabController,
          children: [_buildLatestTweetList(), _buildHotTweetList()],
        ))
      ],
    );
  }

  Widget _buildLatestTweetList() {
    return Container();
  }

  Widget _buildHotTweetList() {
    if (hotTweetList == null) {
      getTweetList(isLoadMore: false, isHot: true);
      return Center(
        child: CupertinoActivityIndicator(),
      );
    }

    return ListView.separated(
        itemBuilder: (context, index) {
          if (index == hotTweetList.length) {
            return Container(
              padding: const EdgeInsets.all(10.0),
              color: Color(0xaaaaaaaa),
              child: Center(child: Text('没有更多数据了')),
            );
          }
          return TweetListItem(tweetData: hotTweetList[index]);
        },
        separatorBuilder: (context, index) {
          return Container(
            height: 10.0,
            color: Color(0xaaaaaaaa),
          );
        },
        itemCount: hotTweetList.length + 1);
  }

  getTweetList({bool isLoadMore, bool isHot}) async {
    DataUtils.isLogin().then((isLogin) {
      if (isLogin) {
        DataUtils.getAccessToken().then((accessToken) {
          if (accessToken == null || accessToken.length == 0) {
            return;
          }
          Map<String, dynamic> params = Map<String, dynamic>();
          params['access_token'] = accessToken;
          params['user'] = isHot ? -1 : 0;
          params['page'] = curPage;
          params['pageSize'] = 10;
          params['dataType'] = 'json';

          NetUtils.get(AppUrls.TWEET_LIST, params).then((data) {
            print('TWEET_LIST: $data');
            if (data != null && data.isNotEmpty) {
              Map<String, dynamic> map = json.decode(data);
              List _tweetList = map['tweetlist'];
              if (!mounted) return;
              setState(() {
                if (isLoadMore) {
                  if (isHot) {
                    latestTweetList.addAll(_tweetList);
                    hotTweetList.addAll(_tweetList);
                  }
                } else {
                  if (isHot) {
                    hotTweetList = _tweetList;
                  } else {
                    latestTweetList = _tweetList;
                  }
                }
              });
            }
          });
        });
      }
    });
  }
}

上面的代码细节就不详细说了,基本也能看明白,然后接下来则需要来构建列表项TweenListItem,如下:

import 'package:flutter/material.dart';

class TweetListItem extends StatelessWidget {
  final Map<String, dynamic> tweetData;

  TweetListItem({Key key, @required this.tweetData})
      : assert(tweetData != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget _buildContent() {
      return Container();
    }

    return Container(
      margin: const EdgeInsets.only(bottom: 10.0),
      child: Column(
        children: <Widget>[
          _buildContent(),
          Divider(),
          _buildFunctionArea(),
        ],
      ),
    );
  }

  Widget _buildFunctionArea() {
    return Container();
  }
}

接下来则来定义布局中的元素:

import 'package:flutter/material.dart';

class TweetListItem extends StatelessWidget {
  final Map<String, dynamic> tweetData;
  final RegExp regExp1 = new RegExp("</.*>");
  final RegExp regExp2 = new RegExp("<.*>");

  TweetListItem({Key key, @required this.tweetData})
      : assert(tweetData != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // 去掉文本中的html代码
    String clearHtmlContent(String str) {
      if (str.startsWith("<emoji")) {
        return "[emoji]";
      }
      var s = str.replaceAll(regExp1, "");
      s = s.replaceAll(regExp2, "");
      s = s.replaceAll("
", "");
      return s;
    }

    Widget _buildContent() {
      var _columnChildren = <Widget>[
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: EdgeInsets.all(5.0),
              child: CircleAvatar(
                backgroundImage:
                    NetworkImage(tweetData['portrait'], scale: 1.5),
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  '${tweetData['author']}',
                  style: TextStyle(
//                  fontWeight: FontWeight.bold,
                    fontSize: 20.0,
                  ),
                ),
                Text(
                  '${tweetData['pubDate']}',
                  style: TextStyle(
                    color: Color(0xffaaaaaa),
                  ),
                )
              ],
            ),
          ],
        ),
        //动弹主体内容
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 5.0),
          child: Text(
            '${clearHtmlContent(tweetData['body'])}',
            style: TextStyle(fontSize: 18.0),
          ),
        ),
      ];
      return Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: _columnChildren,
        ),
      );
    }

    return Container(
      margin: const EdgeInsets.only(bottom: 10.0),
      child: Column(
        children: <Widget>[
          _buildContent(),
          Divider(),
          _buildFunctionArea(),
        ],
      ),
    );
  }

  Widget _buildFunctionArea() {
    return Container();
  }
}

看一下此时的效果:

接下来处理图片的显示,这块是有个坑的,这里直接上代码了,也就是有些图片地址是显示不出来的,需要做一些特殊的处理才行,具体这块图片处理的代码如下:

import 'package:flutter/material.dart';

class TweetListItem extends StatelessWidget {
  final Map<String, dynamic> tweetData;
  final RegExp regExp1 = new RegExp("</.*>");
  final RegExp regExp2 = new RegExp("<.*>");

  TweetListItem({Key key, @required this.tweetData})
      : assert(tweetData != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // 去掉文本中的html代码
    String clearHtmlContent(String str) {
      if (str.startsWith("<emoji")) {
        return "[emoji]";
      }
      var s = str.replaceAll(regExp1, "");
      s = s.replaceAll(regExp2, "");
      s = s.replaceAll("
", "");
      return s;
    }

    int getRow(int n) {
      int a = n % 3;
      int b = n ~/ 3;
      if (a != 0) {
        return b + 1;
      }
      return b;
    }

    Widget _buildContent() {
      var _columnChildren = <Widget>[
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: EdgeInsets.all(5.0),
              child: CircleAvatar(
                backgroundImage:
                    NetworkImage(tweetData['portrait'], scale: 1.5),
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  '${tweetData['author']}',
                  style: TextStyle(
//                  fontWeight: FontWeight.bold,
                    fontSize: 20.0,
                  ),
                ),
                Text(
                  '${tweetData['pubDate']}',
                  style: TextStyle(
                    color: Color(0xffaaaaaa),
                  ),
                )
              ],
            ),
          ],
        ),
        //动弹主体内容
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 5.0),
          child: Text(
            '${clearHtmlContent(tweetData['body'])}',
            style: TextStyle(fontSize: 18.0),
          ),
        ),
      ];

      //以下是用来处理图片的,可能会有多张
      String _imgSmall = tweetData['imgSmall'];
      if (_imgSmall != null && _imgSmall.length > 0) {
        List<String> list = _imgSmall.split(",");
        print('list: $list');
        List<String> imgUrlList = new List<String>();
        for (String s in list) {
          //!!
          if (s.startsWith('https://static.oschina.net/uploads/space/https')) {
            //当发现是这样的图片地址,则需要手动的替换一下才能显示
            s = s.replaceAll('https://static.oschina.net/uploads/space/', '');
          }
          imgUrlList.add(s);
        }
        // print(imgUrlList);
        List<Widget> imgList = [];
        List<List<Widget>> rows = [];
        num len = imgUrlList.length;
        for (var row = 0; row < getRow(len); row++) {
          List<Widget> rowArr = [];
          for (var col = 0; col < 3; col++) {
            //一行显示三张图
            num index = row * 3 + col;
            num screenWidth = MediaQuery.of(context).size.width;
            double cellWidth = (screenWidth - 100) / 3;
            if (index < len) {
              rowArr.add(new Padding(
                padding: const EdgeInsets.all(2.0),
                child: new Image.network(imgUrlList[index],
                     cellWidth, height: cellWidth),
              ));
            }
          }
          rows.add(rowArr);
        }
        for (var row in rows) {
          imgList.add(new Row(
            children: row,
          ));
        }
        _columnChildren.add(new Padding(
          //将处理的图片添加下内容区域上
          padding: const EdgeInsets.all(5.0),
          child: new Column(
            children: imgList,
          ),
        ));
      }

      return Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: _columnChildren,
        ),
      );
    }

    return Container(
      margin: const EdgeInsets.only(bottom: 10.0),
      child: Column(
        children: <Widget>[
          _buildContent(),
          Divider(),
          _buildFunctionArea(),
        ],
      ),
    );
  }

  Widget _buildFunctionArea() {
    return Container();
  }
}

此时就可以看到图片了:

最新列表:

而最新列表显示的样式其实跟最热列表是一样的,所以这里处理起来就比较简单了,而列表的上拉下拉处理咱们之前已经操练过好多次了,这里就直接上代码了:

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/common/event_bus.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/pages/login_web_page.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:flutter_osc_client/utils/net_utils.dart';
import 'package:flutter_osc_client/widgets/tweet_list_item.dart';

class TweetPage extends StatefulWidget {
  @override
  _TweetPageState createState() => _TweetPageState();
}

class _TweetPageState extends State<TweetPage>
    with SingleTickerProviderStateMixin {
  List _tabTitles = ['最新', '热门'];
  List latestTweetList;
  List hotTweetList;
  int curPage = 1;
  ScrollController _controller = ScrollController();
  TabController _tabController;
  bool isLogin = false;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabTitles.length, vsync: this);
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixels = _controller.position.pixels;
      if (maxScroll == pixels) {
        curPage++;
        getTweetList(isLoadMore: true, isHot: false);
      }
    });
    DataUtils.isLogin().then((isLogin) {
      if (!mounted) return;
      setState(() {
        this.isLogin = isLogin;
      });
    });
    eventBus.on<LoginEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = true;
      });
      getTweetList(isLoadMore: false, isHot: false);
    });
    eventBus.on<LogoutEvent>().listen((event) {
      if (!mounted) return;
      setState(() {
        this.isLogin = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!isLogin) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('必须登录才能查看动弹信息!'),
            InkWell(
              child: Container(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  '马上登录',
                  style: TextStyle(color: Color(0xff0000ff)),
                ),
              ),
              onTap: () async {
                final result = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => LoginWebPage()));
                if (result != null && result == 'refresh') {
                  //登录成功
                  eventBus.fire(LoginEvent());
                }
              },
            ),
          ],
        ),
      );
    }
    return Column(
      children: <Widget>[
        //tabbar
        Container(
          color: Color(AppColors.APP_THEME),
          child: TabBar(
              controller: _tabController,
              indicatorColor: Color(0xffffffff),
              labelColor: Color(0xffffffff),
              tabs: _tabTitles.map((title) {
                return Tab(
                  text: title,
                );
              }).toList()),
        ),
        Expanded(
            child: TabBarView(
          controller: _tabController,
          children: [_buildLatestTweetList(), _buildHotTweetList()],
        ))
      ],
    );
  }

  Widget _buildLatestTweetList() {
    if (latestTweetList == null) {
      getTweetList(isLoadMore: false, isHot: false);
      return new Center(
        child: new CircularProgressIndicator(),
      );
    }

    Future<Null> _pullToRefresh() async {
      curPage = 1;
      getTweetList(isLoadMore: false, isHot: false);
      return null;
    }

    return RefreshIndicator(
      onRefresh: _pullToRefresh,
      child: ListView.separated(
          controller: _controller,
          itemBuilder: (context, index) {
            if (index == latestTweetList.length) {
              return Padding(
                padding: const EdgeInsets.all(10.0),
                child: Center(
                    child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    CupertinoActivityIndicator(),
                    SizedBox(
                       20.0,
                    ),
                    Text('正在加载...'),
                  ],
                )),
              );
            }
            return TweetListItem(tweetData: latestTweetList[index]);
          },
          separatorBuilder: (context, index) {
            return Container(
              height: 10.0,
              color: Colors.grey[200],
            );
          },
          itemCount: latestTweetList.length + 1),
    );
  }

  Widget _buildHotTweetList() {
    if (hotTweetList == null) {
      getTweetList(isLoadMore: false, isHot: true);
      return Center(
        child: CupertinoActivityIndicator(),
      );
    }

    return ListView.separated(
        itemBuilder: (context, index) {
          if (index == hotTweetList.length) {
            return Container(
              padding: const EdgeInsets.all(10.0),
              color: Color(0xaaaaaaaa),
              child: Center(child: Text('没有更多数据了')),
            );
          }
          return TweetListItem(tweetData: hotTweetList[index]);
        },
        separatorBuilder: (context, index) {
          return Container(
            height: 10.0,
            color: Color(0xaaaaaaaa),
          );
        },
        itemCount: hotTweetList.length + 1);
  }

  getTweetList({bool isLoadMore, bool isHot}) async {
    DataUtils.isLogin().then((isLogin) {
      if (isLogin) {
        DataUtils.getAccessToken().then((accessToken) {
          if (accessToken == null || accessToken.length == 0) {
            return;
          }
          Map<String, dynamic> params = Map<String, dynamic>();
          params['access_token'] = accessToken;
          params['user'] = isHot ? -1 : 0;
          params['page'] = curPage;
          params['pageSize'] = 10;
          params['dataType'] = 'json';

          NetUtils.get(AppUrls.TWEET_LIST, params).then((data) {
            print('TWEET_LIST: $data');
            if (data != null && data.isNotEmpty) {
              Map<String, dynamic> map = json.decode(data);
              List _tweetList = map['tweetlist'];
              if (!mounted) return;
              setState(() {
                if (isLoadMore) {
                  if (isHot) {
                    hotTweetList.addAll(_tweetList);
                  } else {
                    latestTweetList.addAll(_tweetList);
                  }
                } else {
                  if (isHot) {
                    hotTweetList = _tweetList;
                  } else {
                    latestTweetList = _tweetList;
                  }
                }
              });
            }
          });
        });
      }
    });
  }
}

运行看一下:

发布动弹:

效果图:

           

它的入口是从这点击跳转的:

API查看:

具体实现:

界面搭建:

由于篇幅有限,界面的搭建也不过多解释了,先上代码:

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';

class PublishTweetPage extends StatefulWidget {
  @override
  _PublishTweetPageState createState() => _PublishTweetPageState();
}

class _PublishTweetPageState extends State<PublishTweetPage> {
  TextEditingController _controller = new TextEditingController();
  List<File> fileList = List<File>();
  Future<File> _imageFile;
  bool isLoading = false;

  Widget _bodyWidget() {
    List<Widget> _body = [
      ListView(
        children: <Widget>[
          //动弹内容输入框
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: _controller,
              decoration: InputDecoration(
                  hintText: '今天想动弹什么??',
                  hintStyle: TextStyle(
                    color: Color(0xaaaaaaaa),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(
                      const Radius.circular(10.0),
                    ),
                  )),
              maxLength: 150,
              maxLines: 6,
            ),
          ),
          //图片显示
          GridView.count(
            shrinkWrap: true,
            crossAxisCount: 4,
            children: List.generate(
              fileList.length + 1,
              (index) {
                if (index == fileList.length) {
                  return Builder(
                    builder: (context) {
                      return GestureDetector(
                        onTap: () {
                          //选择图片
                          _pickImage(context);
                        },
                        child: Image.asset(
                          'assets/images/ic_add_pics.png',
                        ),
                      );
                    },
                  );
                }
                return GestureDetector(
                  onTap: () {
                    //取消图片
                    setState(() {
                      fileList.removeAt(index);
                    });
                  },
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Image.file(
                      fileList[index],
                      fit: BoxFit.cover,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      )
    ];

    if (isLoading) {
      _body.add(
        Center(
          child: Container(
             MediaQuery.of(context).size.width / 3,
            height: MediaQuery.of(context).size.width / 3,
            decoration: BoxDecoration(
              color: Color(0x88000000),
              borderRadius: BorderRadius.all(Radius.circular(10.0)),
            ),
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  CupertinoActivityIndicator(),
                  SizedBox(
                    height: 10.0,
                  ),
                  Text('努力动弹中...', style: TextStyle(color: Colors.white))
                ],
              ),
            ),
          ),
        ),
      );
    }

    return Stack(
      children: _body,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomPadding: false, //防止键盘弹出 导致超出屏幕
      appBar: AppBar(
        elevation: 0.0,
        title: Text(
          '弹一弹',
          style: TextStyle(color: Colors.white),
        ),
        iconTheme: IconThemeData(color: Colors.white),
        actions: <Widget>[
          Builder(
            builder: (context) {
              return FlatButton(
                onPressed: () {
                  //发布动弹
                  DataUtils.getAccessToken().then((token) {
                    //网络请求
                    _publishTweet(context, token);
                  });
                },
                child: Text(
                  '发送',
                  style: TextStyle(color: Colors.white, fontSize: 20.0),
                ),
              );
            },
          )
        ],
      ),
      body: FutureBuilder(
        future: _imageFile,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done &&
              snapshot.data != null &&
              _imageFile != null) {
            fileList.add(snapshot.data);
            _imageFile = null;
          }
          return _bodyWidget();
        },
      ),
    );
  }

  void _publishTweet(BuildContext context, String token) async {
    //TODO:发布动弹接口请求
  }

  void _pickImage(BuildContext context) {
    // 如果已添加了9张图片,则提示不允许添加更多
    num size = fileList.length;
    if (size >= 9) {
      Scaffold.of(context).showSnackBar(new SnackBar(
        content: new Text("最多只能添加9张图片!"),
      ));
      return;
    }
    showModalBottomSheet<void>(
        context: context,
        builder: (context) {
          return new Container(
              height: 121.0,
              child: new Column(
                children: <Widget>[
                  InkWell(
                    onTap: () {},
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '相机拍照',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                  new Divider(
                    height: 1.0,
                  ),
                  InkWell(
                    onTap: () {},
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '图库选择照片',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                ],
              ));
        });
  }
}

其中需要一个图片上传的+的图片:

 

其中这里涉及到一个新的文本输入框为TextField,另外还用到了一个FutureBuilder,这个是干嘛的呢?网上搜一下:

 

由于咱们这边图片上传是一个异步任务,而这里当这个异步任务状态变化时,也就是当图片上传成功时咱们则应该将图片的地址放到集合当中进行本地已上传图片的回显的,所以这也是为啥要用FutureBuilder的原因之所在,下面运行看一下:

而且发现在Flutter对于文本输入框的文字个数的监听好简单,都不用咱们自己来手写:

咱们只指定了要输入的最大字符个数:

选择图片:

此时则需要用到图片选择三方库了,先加入工程来:

 

然后处理选择图片的事件:

 

void _pickImage(BuildContext context) {
    // 如果已添加了9张图片,则提示不允许添加更多
    num size = fileList.length;
    if (size >= 9) {
      Scaffold.of(context).showSnackBar(new SnackBar(
        content: new Text("最多只能添加9张图片!"),
      ));
      return;
    }
    showModalBottomSheet<void>(
        context: context,
        builder: (context) {
          return new Container(
              height: 121.0,
              child: new Column(
                children: <Widget>[
                  InkWell(
                    onTap: () {
                      Navigator.of(context).pop();
                      if (mounted) {
                        setState(() {
                          _imageFile =
                              ImagePicker.pickImage(source: ImageSource.camera);
                        });
                      }
                    },
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '相机拍照',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                  new Divider(
                    height: 1.0,
                  ),
                  InkWell(
                    onTap: () {
                      Navigator.of(context).pop();
                      if (mounted) {
                        setState(() {
                          _imageFile = ImagePicker.pickImage(
                              source: ImageSource.gallery);
                        });
                      }
                    },
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '图库选择照片',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                ],
              ));
        });
  }

其中看到了么,在上传时其实就给FutureBuilder关联的_imageFile这个Future进行了赋值,当其选择完毕之后则会触发这块的刷新:

这就是使用FutureBuilder的好处,这个可能说着还是有点模糊,木关系,在未来的项目实践中再去熟练,接下来运行看一下此时的效果:

发布上传处理:

最后则需要进行发布上传啦,那在Flutter中是如何进行附件上传的呢,其实很简单,都封装得特别好了,下面直接贴代码了:

import 'dart:convert';
import 'dart:io';

import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_osc_client/constants/constants.dart';
import 'package:flutter_osc_client/utils/data_utils.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';

class PublishTweetPage extends StatefulWidget {
  @override
  _PublishTweetPageState createState() => _PublishTweetPageState();
}

class _PublishTweetPageState extends State<PublishTweetPage> {
  TextEditingController _controller = new TextEditingController();
  List<File> fileList = List<File>();
  Future<File> _imageFile;
  bool isLoading = false;

  Widget _bodyWidget() {
    List<Widget> _body = [
      ListView(
        children: <Widget>[
          //动弹内容输入框
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: _controller,
              decoration: InputDecoration(
                  hintText: '今天想动弹什么??',
                  hintStyle: TextStyle(
                    color: Color(0xaaaaaaaa),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(
                      const Radius.circular(10.0),
                    ),
                  )),
              maxLength: 150,
              maxLines: 6,
            ),
          ),
          //图片显示
          GridView.count(
            shrinkWrap: true,
            crossAxisCount: 4,
            children: List.generate(
              fileList.length + 1,
              (index) {
                if (index == fileList.length) {
                  return Builder(
                    builder: (context) {
                      return GestureDetector(
                        onTap: () {
                          //选择图片
                          _pickImage(context);
                        },
                        child: Image.asset(
                          'assets/images/ic_add_pics.png',
                        ),
                      );
                    },
                  );
                }
                return GestureDetector(
                  onTap: () {
                    //取消图片
                    setState(() {
                      fileList.removeAt(index);
                    });
                  },
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Image.file(
                      fileList[index],
                      fit: BoxFit.cover,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      )
    ];

    if (isLoading) {
      _body.add(
        Center(
          child: Container(
             MediaQuery.of(context).size.width / 3,
            height: MediaQuery.of(context).size.width / 3,
            decoration: BoxDecoration(
              color: Color(0x88000000),
              borderRadius: BorderRadius.all(Radius.circular(10.0)),
            ),
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  CupertinoActivityIndicator(),
                  SizedBox(
                    height: 10.0,
                  ),
                  Text('努力动弹中...', style: TextStyle(color: Colors.white))
                ],
              ),
            ),
          ),
        ),
      );
    }

    return Stack(
      children: _body,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomPadding: false, //防止键盘弹出 导致超出屏幕
      appBar: AppBar(
        elevation: 0.0,
        title: Text(
          '弹一弹',
          style: TextStyle(color: Colors.white),
        ),
        iconTheme: IconThemeData(color: Colors.white),
        actions: <Widget>[
          Builder(
            builder: (context) {
              return FlatButton(
                onPressed: () {
                  //发布动弹
                  DataUtils.getAccessToken().then((token) {
                    //网络请求
                    _publishTweet(context, token);
                  });
                },
                child: Text(
                  '发送',
                  style: TextStyle(color: Colors.white, fontSize: 20.0),
                ),
              );
            },
          )
        ],
      ),
      body: FutureBuilder(
        future: _imageFile,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done &&
              snapshot.data != null &&
              _imageFile != null) {
            fileList.add(snapshot.data);
            _imageFile = null;
          }
          return _bodyWidget();
        },
      ),
    );
  }

  void _publishTweet(BuildContext context, String token) async {
    if (token == null) {
      _showSnackBar(context, '未登录!');
      return;
    }
    String tweetContent = _controller.text;
    if (tweetContent == null || tweetContent.isEmpty) {
      //未输入动弹内容
      _showSnackBar(context, '请输入动弹内容!');
      return;
    }
    Map<String, String> params = new Map();
    params['msg'] = tweetContent;
    params['access_token'] = token;
    print('动弹内容:$tweetContent');

    var multipartRequest =
        http.MultipartRequest('POST', Uri.parse(AppUrls.TWEET_PUB));
    multipartRequest.fields.addAll(params);
    if (fileList.length > 0) {
      for (File file in fileList) {
        var stream = http.ByteStream(DelegatingStream.typed(file.openRead()));
        var length = await file.length();
        print('${file.path}');
        var fileName = file.path.substring(file.path.lastIndexOf('/') + 1);
        // MultipartFile(this.field, Stream<List<int>> stream, this.length,
        //{this.filename, MediaType contentType})
        multipartRequest.files
            .add(http.MultipartFile('img', stream, length, filename: fileName));
      }
    }
    setState(() {
      isLoading = true;
    });
    var streamedResponse = await multipartRequest.send();
    streamedResponse.stream.transform(utf8.decoder).listen((response) {
      print('response: $response');
      setState(() {
        isLoading = false;
      });
      if (response != null) {
        var decode = json.decode(response);
        var errorCode = decode['error'];
        if (mounted) {
          setState(() {
            if (errorCode != null && errorCode == '200') {
              fileList.clear();
              _controller.clear();
              _showSnackBar(context, '发布成功!');
            } else {
              _showSnackBar(context, '发布失败: ${decode['error_description']}');
            }
          });
        }
      }
    });
  }

  void _showSnackBar(BuildContext context, String message) {
    Scaffold.of(context).showSnackBar(new SnackBar(
      content: new Text(message),
      duration: Duration(milliseconds: 500),
    ));
  }

  void _pickImage(BuildContext context) {
    // 如果已添加了9张图片,则提示不允许添加更多
    num size = fileList.length;
    if (size >= 9) {
      Scaffold.of(context).showSnackBar(new SnackBar(
        content: new Text("最多只能添加9张图片!"),
      ));
      return;
    }
    showModalBottomSheet<void>(
        context: context,
        builder: (context) {
          return new Container(
              height: 121.0,
              child: new Column(
                children: <Widget>[
                  InkWell(
                    onTap: () {
                      Navigator.of(context).pop();
                      if (mounted) {
                        setState(() {
                          _imageFile =
                              ImagePicker.pickImage(source: ImageSource.camera);
                        });
                      }
                    },
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '相机拍照',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                  new Divider(
                    height: 1.0,
                  ),
                  InkWell(
                    onTap: () {
                      Navigator.of(context).pop();
                      if (mounted) {
                        setState(() {
                          _imageFile = ImagePicker.pickImage(
                              source: ImageSource.gallery);
                        });
                      }
                    },
                    child: Container(
                      height: 60.0,
                      child: Center(
                        child: Text(
                          '图库选择照片',
                          style: TextStyle(fontSize: 20.0),
                        ),
                      ),
                    ),
                  ),
                ],
              ));
        });
  }
}

上面这些代码可能有些看着费力,没关系,先有了大概印象,熟能生巧的,下面来试一下发布能否成功?

嗯,木问题,最后项目收尾时再到ios上运行看一把,效果怎么样,发现报错了。。

aunching lib/main.dart on iPhone X in debug mode...
2020-07-29 09:28:35.903 defaults[82428:1355490] 
The domain/default pair of (/Users/xiongwei/Documents/workspace/flutterstudy/flutter_osc_client/ios/Runner/Info, CFBundleIdentifier) does not exist
Warning: CocoaPods installed but not initialized. Skipping pod install.
  CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
  Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
  For more info, see https://flutter.io/platform-plugins
To initialize CocoaPods, run:
  pod setup
once to finalize CocoaPods' installation.
Running Xcode build...
Xcode build done.                                            6.2s
Failed to build iOS app
Error output from Xcode build:
↳
    ** BUILD FAILED **


Xcode's output:
=== BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug ===
    Debug.xcconfig line 1: Unable to find included file "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
    Debug.xcconfig line 1: Unable to find included file "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
    === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug ===
    /Users/xiongwei/Documents/workspace/flutterstudy/flutter_osc_client/ios/Runner/GeneratedPluginRegistrant.m:6:9: fatal error: 'barcode_scan/BarcodeScanPlugin.h' file not found
    #import <barcode_scan/BarcodeScanPlugin.h>
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1 error generated.

Could not build the application for the simulator.
Error launching application on iPhone X.

然后按网上大神的指导方针https://www.jianshu.com/p/4ffcbdb025f8试了一下:

 

此时解决办法是进ios目录中增加一个这个配置:

然后再来安装一下:

xiongweideMacBook-Pro:ios xiongwei$ pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing MTBBarcodeScanner (5.0.11)
Installing barcode_scan (0.0.1)
Installing flutter_webview_plugin (0.0.1)
Installing image_picker (0.0.1)
Installing sensors (0.0.1)
Installing shared_preferences (0.0.1)
Installing vibration (1.2.4)
Generating Pods project
Integrating client project
Pod installation complete! There are 7 dependencies from the Podfile and 8 total pods installed.

[!] `<PBXGroup UUID=`97C146E51CF9000F007C117D`>` attempted to initialize an object with an unknown UUID. `CF3B75C9A7D2FA2A4C99F110` for attribute: `children`. This can be the result of a merge and  the unknown UUID is being discarded.

[!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target `Runner` to `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` or include the `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` in your build configuration (`Flutter/Release.xcconfig`).

不知道这个是干嘛的,能解决运行再说,貌似已经安装好了,下面再来运行到ios模拟器看一下,还是报错了。。

Launching lib/main.dart on iPhone X in debug mode...
2020-07-29 09:59:34.309 defaults[87443:1406649] 
The domain/default pair of (/Users/xiongwei/Documents/workspace/flutterstudy/flutter_osc_client/ios/Runner/Info, CFBundleIdentifier) does not exist
Warning: CocoaPods installed but not initialized. Skipping pod install.
  CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
  Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
  For more info, see https://flutter.io/platform-plugins
To initialize CocoaPods, run:
  pod setup
once to finalize CocoaPods' installation.
Running Xcode build...
Xcode build done.                                            5.2s
Failed to build iOS app
Error output from Xcode build:
↳
    ** BUILD FAILED **


Xcode's output:
=== BUILD TARGET barcode_scan OF PROJECT Pods WITH CONFIGURATION Debug ===
    /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration-1.2.4/ios/Classes/VibrationPlugin.m:2:9: fatal error: 'vibration/vibration-Swift.h' file not found
    #import <vibration/vibration-Swift.h>
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1 error generated.
    /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/BarcodeScannerViewController.m:10:17: warning: method definition for 'initWithOptions:' not found [-Wincomplete-implementation]
    @implementation BarcodeScannerViewController {
                    ^
    In file included from /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/BarcodeScannerViewController.m:5:
    /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/BarcodeScannerViewController.h:19:3: note: method 'initWithOptions:' declared here
      -(id) initWithOptions:(NSDictionary *) options;
      ^
    1 warning generated.
    In file included from /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/BarcodeScanPlugin.m:1:
    /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/BarcodeScanPlugin.h:12:1: warning: retain'ed block property does not copy the block - use copy attribute instead [-Wobjc-noncopy-retain-block-property]
    @property(nonatomic, retain) FlutterResult result;
    ^
    1 warning generated.
    /Users/xiongwei/Documents/software/3g Tools/flutter/flutter/.pub-cache/hosted/pub.flutter-io.cn/barcode_scan-1.0.0/ios/Classes/ScannerOverlay.m:25:12: warning: unused variable 'scanLineColor' [-Wunused-variable]
      UIColor *scanLineColor = UIColor.redColor;
               ^
    1 warning generated.

Could not build the application for the simulator.
Error launching application on iPhone X.

。。咋回事呢?其实是还需要在ios的这个文件增加一句这个:

然后如这个网友https://github.com/benjamindean/flutter_vibration/issues/1所说:

再来到ios目录下执行一下pod install,然后再运行,终于ok了:

来,看一下在ios上效果:

嗯,确实不错,基本上跟android上看到的效果一模一样,关于这个项目的操练就先告一段落,接下来Flutter的巩固还会继续加强!!!!

原文地址:https://www.cnblogs.com/webor2006/p/13336013.html