一. 数据请求和转化

1.1. TabBar实现说明

Flutter中,会使用Scaffold来搭建页面的基本结构,可实现底部TabBar功能:bottomNavigationBar。
bottomNavigationBar对应的类型是BottomNavigationBar,属性:

  • 属性非常多,但是都是设置底部TabBar相关的,我们介绍几个:
  • currentIndex:当前选中哪一个item;
  • selectedFontSize:选中时的文本大小;
  • unselectedFontSize:未选中时的文本大小;
  • type:当item的数量超过2个时,需要设置为fixed;
  • items:放入多个BottomNavigationBarItem类型;
  • onTap:监听哪一个item被选中;

    1. class BottomNavigationBar extends StatefulWidget {
    2. BottomNavigationBar({
    3. Key key,
    4. @required this.items,
    5. this.onTap,
    6. this.currentIndex = 0,
    7. this.elevation = 8.0,
    8. BottomNavigationBarType type,
    9. Color fixedColor,
    10. this.backgroundColor,
    11. this.iconSize = 24.0,
    12. Color selectedItemColor,
    13. this.unselectedItemColor,
    14. this.selectedIconTheme = const IconThemeData(),
    15. this.unselectedIconTheme = const IconThemeData(),
    16. this.selectedFontSize = 14.0,
    17. this.unselectedFontSize = 12.0,
    18. this.selectedLabelStyle,
    19. this.unselectedLabelStyle,
    20. this.showSelectedLabels = true,
    21. bool showUnselectedLabels,
    22. })
    23. }

    当实现了底部TabBar展示后,我们需要监听它的点击来切换显示不同的页面,这个时候我们可以使用IndexedStack来管理多个页面的切换:

    1. body: IndexedStack(
    2. index: _currentIndex,
    3. children: <Widget>[
    4. Home(),
    5. Subject(),
    6. Group(),
    7. Mall(),
    8. Profile()
    9. ],

    1.2. TabBar代码实现

  • 1、需要在其他地方创建对应要切换的页面;

  • 2、需要引入对应的资源,并且在pubspec.yaml中引入;

    1. import 'package:flutter/material.dart';
    2. import 'views/home/home.dart';
    3. import 'views/subject/subject.dart';
    4. import 'views/group/group.dart';
    5. import 'views/mall/mall.dart';
    6. import 'views/profile/profile.dart';
    7. class MyApp extends StatelessWidget {
    8. @override
    9. Widget build(BuildContext context) {
    10. return MaterialApp(
    11. title: "豆瓣",
    12. theme: ThemeData(
    13. primaryColor: Colors.green,
    14. highlightColor: Colors.transparent,
    15. splashColor: Colors.transparent
    16. ),
    17. home: MyStackPage(),
    18. );
    19. }
    20. }
    21. class MyStackPage extends StatefulWidget {
    22. @override
    23. _MyStackPageState createState() => _MyStackPageState();
    24. }
    25. class _MyStackPageState extends State<MyStackPage> {
    26. var _currentIndex = 0;
    27. @override
    28. Widget build(BuildContext context) {
    29. return Scaffold(
    30. bottomNavigationBar: BottomNavigationBar(
    31. currentIndex: _currentIndex,
    32. selectedFontSize: 14,
    33. unselectedFontSize: 14,
    34. type: BottomNavigationBarType.fixed,
    35. items: [
    36. createItem("home", "首页"),
    37. createItem("subject", "书影音"),
    38. createItem("group", "小组"),
    39. createItem("mall", "市集"),
    40. createItem("profile", "我的"),
    41. ],
    42. onTap: (index) {
    43. setState(() {
    44. _currentIndex = index;
    45. });
    46. },
    47. ),
    48. body: IndexedStack(
    49. index: _currentIndex,
    50. children: <Widget>[
    51. Home(),
    52. Subject(),
    53. Group(),
    54. Mall(),
    55. Profile()
    56. ],
    57. ),
    58. );
    59. }
    60. }
    61. BottomNavigationBarItem createItem(String iconName, String title) {
    62. return BottomNavigationBarItem(
    63. icon: Image.asset("assets/images/tabbar/$iconName.png", width: 30,),
    64. activeIcon: Image.asset("assets/images/tabbar/${iconName}_active.png", width: 30,),
    65. title: Text(title)
    66. );
    67. }

    二. 数据请求和转化

    2.1. 网络请求简单封装

    基于dio进行了一个简单工具的封装:
    配置文件存放:http_config.dart

    1. const baseURL = "http://123.207.32.32:8000";
    2. const timeout = 5000;

    网络请求工具文件:http_request.dart

    1. import 'package:dio/dio.dart';
    2. import 'http_config.dart';
    3. class HttpRequest {
    4. // 1.创建实例对象
    5. static BaseOptions baseOptions = BaseOptions(connectTimeout: timeout);
    6. static Dio dio = Dio(baseOptions);
    7. static Future<T> request<T>(String url, {String method = "get",Map<String, dynamic> params}) async {
    8. // 1.单独相关的设置
    9. Options options = Options();
    10. options.method = method;
    11. // 2.发送网络请求
    12. try {
    13. Response response = await dio.request<T>(url, queryParameters: params, options: options);
    14. return response.data;
    15. } on DioError catch (e) {
    16. throw e;
    17. }
    18. }
    19. }

    2.2. 首页数据请求转化

    豆瓣的API接口:

  • https://douban.uieee.com/v2/movie/top250?start=0&count=20

模型对象的封装
在面向对象的开发中,数据请求下来并不会像前端那样直接使用,而是封装成模型对象:

  • 目前前端开发正在向TypeScript发展,也在帮助强化这种思维方式

为了方便之后使用请求下来的数据,将数据划分成了如下的模型:
Person、Actor、Director模型:会被使用到MovieItem中

  1. class Person {
  2. String name;
  3. String avatarURL;
  4. Person.fromMap(Map<String, dynamic> json) {
  5. this.name = json["name"];
  6. this.avatarURL = json["avatars"]["medium"];
  7. }
  8. }
  9. class Actor extends Person {
  10. Actor.fromMap(Map<String, dynamic> json): super.fromMap(json);
  11. }
  12. class Director extends Person {
  13. Director.fromMap(Map<String, dynamic> json): super.fromMap(json);
  14. }

MovieItem模型:

  1. int counter = 1;
  2. class MovieItem {
  3. int rank;
  4. String imageURL;
  5. String title;
  6. String playDate;
  7. double rating;
  8. List<String> genres;
  9. List<Actor> casts;
  10. Director director;
  11. String originalTitle;
  12. MovieItem.fromMap(Map<String, dynamic> json) {
  13. this.rank = counter++;
  14. this.imageURL = json["images"]["medium"];
  15. this.title = json["title"];
  16. this.playDate = json["year"];
  17. this.rating = json["rating"]["average"];
  18. this.genres = json["genres"].cast<String>();
  19. this.casts = (json["casts"] as List<dynamic>).map((item) {
  20. return Actor.fromMap(item);
  21. }).toList();
  22. this.director = Director.fromMap(json["directors"][0]);
  23. this.originalTitle = json["original_title"];
  24. }
  25. }

首页数据请求封装以及模型转化
这里封装了一个专门的类,用于请求首页的数据:HomeRequest

  • 目前类中只有一个方法getMovieTopList;
  • 后续有其他首页数据需要请求,就继续在这里封装请求的方法;
    1. import 'package:douban_app/models/home_model.dart';
    2. import 'http_request.dart';
    3. class HomeRequest {
    4. Future<List<MovieItem>> getMovieTopList(int start, int count) async {
    5. // 1.拼接URL
    6. final url = "https://douban.uieee.com/v2/movie/top250?start=$start&count=$count";
    7. // 2.发送请求
    8. final result = await HttpRequest.request(url);
    9. // 3.转成模型对象
    10. final subjects = result["subjects"];
    11. List<MovieItem> movies = [];
    12. for (var sub in subjects) {
    13. movies.add(MovieItem.fromMap(sub));
    14. }
    15. return movies;
    16. }
    17. }

    五. 界面效果实现

    5.1. 首页整体代码

    首页整体布局非常简单,使用一个ListView即可
    1. import 'package:douban_app/models/home_model.dart';
    2. import 'package:douban_app/network/home_request.dart';
    3. import 'package:douban_app/views/home/childCpns/movie_list_item.dart';
    4. import 'package:flutter/material.dart';
    5. const COUNT = 20;
    6. class Home extends StatelessWidget {
    7. @override
    8. Widget build(BuildContext context) {
    9. return Scaffold(
    10. appBar: AppBar(
    11. title: Text("首页"),
    12. ),
    13. body: Center(
    14. child: HomeContent(),
    15. ),
    16. );
    17. }
    18. }
    19. class HomeContent extends StatefulWidget {
    20. @override
    21. _HomeContentState createState() => _HomeContentState();
    22. }
    23. class _HomeContentState extends State<HomeContent> {
    24. // 初始化首页的网络请求对象
    25. HomeRequest homeRequest = HomeRequest();
    26. int _start = 0;
    27. List<MovieItem> movies = [];
    28. @override
    29. void initState() {
    30. super.initState();
    31. // 请求电影列表数据
    32. getMovieTopList(_start, COUNT);
    33. }
    34. void getMovieTopList(start, count) {
    35. homeRequest.getMovieTopList(start, count).then((result) {
    36. setState(() {
    37. movies.addAll(result);
    38. });
    39. });
    40. }
    41. @override
    42. Widget build(BuildContext context) {
    43. return ListView.builder(
    44. itemCount: movies.length,
    45. itemBuilder: (BuildContext context, int index) {
    46. return MovieListItem(movies[index]);
    47. }
    48. );
    49. }
    50. }

    5.2. 单独Item局部

    按照对应的结构,实现代码:
    1. import 'package:douban_app/components/dash_line.dart';
    2. import 'package:flutter/material.dart';
    3. import 'package:douban_app/models/home_model.dart';
    4. import 'package:douban_app/components/star_rating.dart';
    5. class MovieListItem extends StatelessWidget {
    6. final MovieItem movie;
    7. MovieListItem(this.movie);
    8. @override
    9. Widget build(BuildContext context) {
    10. return Container(
    11. padding: EdgeInsets.all(10),
    12. decoration: BoxDecoration(
    13. border: Border(bottom: BorderSide(width: 10, color: Color(0xffe2e2e2)))
    14. ),
    15. child: Column(
    16. crossAxisAlignment: CrossAxisAlignment.start,
    17. children: <Widget>[
    18. // 1.电影排名
    19. getMovieRankWidget(),
    20. SizedBox(height: 12),
    21. // 2.具体内容
    22. getMovieContentWidget(),
    23. SizedBox(height: 12),
    24. // 3.电影简介
    25. getMovieIntroduceWidget(),
    26. SizedBox(height: 12,)
    27. ],
    28. ),
    29. );
    30. }
    31. // 电影排名
    32. Widget getMovieRankWidget() {
    33. return Container(
    34. padding: EdgeInsets.fromLTRB(9, 4, 9, 4),
    35. decoration: BoxDecoration(
    36. borderRadius: BorderRadius.circular(3),
    37. color: Color.fromARGB(255, 238, 205, 144)
    38. ),
    39. child: Text(
    40. "No.${movie.rank}",
    41. style: TextStyle(fontSize: 18, color: Color.fromARGB(255, 131, 95, 36)),
    42. )
    43. );
    44. }
    45. // 具体内容
    46. Widget getMovieContentWidget() {
    47. return Container(
    48. height: 150,
    49. child: Row(
    50. crossAxisAlignment: CrossAxisAlignment.start,
    51. children: <Widget>[
    52. getContentImage(),
    53. getContentDesc(),
    54. getDashLine(),
    55. getContentWish()
    56. ],
    57. ),
    58. );
    59. }
    60. Widget getContentImage() {
    61. return ClipRRect(
    62. borderRadius: BorderRadius.circular(5),
    63. child: Image.network(movie.imageURL)
    64. );
    65. }
    66. Widget getContentDesc() {
    67. return Expanded(
    68. child: Container(
    69. padding: EdgeInsets.symmetric(horizontal: 15),
    70. child: Column(
    71. crossAxisAlignment: CrossAxisAlignment.start,
    72. children: <Widget>[
    73. getTitleWidget(),
    74. SizedBox(height: 3,),
    75. getRatingWidget(),
    76. SizedBox(height: 3,),
    77. getInfoWidget()
    78. ],
    79. ),
    80. ),
    81. );
    82. }
    83. Widget getDashLine() {
    84. return Container(
    85. width: 1,
    86. height: 100,
    87. child: DashedLine(
    88. axis: Axis.vertical,
    89. dashedHeight: 6,
    90. dashedWidth: .5,
    91. count: 12,
    92. ),
    93. );
    94. }
    95. Widget getTitleWidget() {
    96. return Stack(
    97. children: <Widget>[
    98. Icon(Icons.play_circle_outline, color: Colors.redAccent,),
    99. Text.rich(
    100. TextSpan(
    101. children: [
    102. TextSpan(
    103. text: " " + movie.title,
    104. style: TextStyle(
    105. fontSize: 18,
    106. fontWeight: FontWeight.bold
    107. )
    108. ),
    109. TextSpan(
    110. text: "(${movie.playDate})",
    111. style: TextStyle(
    112. fontSize: 18,
    113. color: Colors.black54
    114. ),
    115. )
    116. ]
    117. ),
    118. maxLines: 2,
    119. ),
    120. ],
    121. );
    122. }
    123. Widget getRatingWidget() {
    124. return Row(
    125. crossAxisAlignment: CrossAxisAlignment.end,
    126. children: <Widget>[
    127. StarRating(rating: movie.rating, size: 18,),
    128. SizedBox(width: 5),
    129. Text("${movie.rating}")
    130. ],
    131. );
    132. }
    133. Widget getInfoWidget() {
    134. // 1.获取种类字符串
    135. final genres = movie.genres.join(" ");
    136. final director = movie.director.name;
    137. var castString = "";
    138. for (final cast in movie.casts) {
    139. castString += cast.name + " ";
    140. }
    141. // 2.创建Widget
    142. return Text(
    143. "$genres / $director / $castString",
    144. maxLines: 2,
    145. overflow: TextOverflow.ellipsis,
    146. style: TextStyle(fontSize: 16),
    147. );
    148. }
    149. Widget getContentWish() {
    150. return Container(
    151. width: 60,
    152. child: Column(
    153. mainAxisAlignment: MainAxisAlignment.start,
    154. children: <Widget>[
    155. SizedBox(height: 20,),
    156. Image.asset("assets/images/home/wish.png", width: 30,),
    157. SizedBox(height: 5,),
    158. Text(
    159. "想看",
    160. style: TextStyle(fontSize: 16, color: Color.fromARGB(255, 235, 170, 60)),
    161. )
    162. ],
    163. ),
    164. );
    165. }
    166. // 电影简介(原生名称)
    167. Widget getMovieIntroduceWidget() {
    168. return Container(
    169. width: double.infinity,
    170. padding: EdgeInsets.all(12),
    171. decoration: BoxDecoration(
    172. color: Color(0xfff2f2f2),
    173. borderRadius: BorderRadius.circular(5)
    174. ),
    175. child: Text(movie.originalTitle, style: TextStyle(fontSize: 18, color: Colors.black54),),
    176. );
    177. }
    178. }