一. 数据请求和转化
1.1. TabBar实现说明
Flutter中,会使用Scaffold来搭建页面的基本结构,可实现底部TabBar功能:bottomNavigationBar。
bottomNavigationBar对应的类型是BottomNavigationBar,属性:
- 属性非常多,但是都是设置底部TabBar相关的,我们介绍几个:
currentIndex:当前选中哪一个item;selectedFontSize:选中时的文本大小;unselectedFontSize:未选中时的文本大小;type:当item的数量超过2个时,需要设置为fixed;items:放入多个BottomNavigationBarItem类型;onTap:监听哪一个item被选中;class BottomNavigationBar extends StatefulWidget {BottomNavigationBar({Key key,@required this.items,this.onTap,this.currentIndex = 0,this.elevation = 8.0,BottomNavigationBarType type,Color fixedColor,this.backgroundColor,this.iconSize = 24.0,Color selectedItemColor,this.unselectedItemColor,this.selectedIconTheme = const IconThemeData(),this.unselectedIconTheme = const IconThemeData(),this.selectedFontSize = 14.0,this.unselectedFontSize = 12.0,this.selectedLabelStyle,this.unselectedLabelStyle,this.showSelectedLabels = true,bool showUnselectedLabels,})}
当实现了底部TabBar展示后,我们需要监听它的点击来切换显示不同的页面,这个时候我们可以使用IndexedStack来管理多个页面的切换:
body: IndexedStack(index: _currentIndex,children: <Widget>[Home(),Subject(),Group(),Mall(),Profile()],
1.2. TabBar代码实现
1、需要在其他地方创建对应要切换的页面;
2、需要引入对应的资源,并且在pubspec.yaml中引入;
import 'package:flutter/material.dart';import 'views/home/home.dart';import 'views/subject/subject.dart';import 'views/group/group.dart';import 'views/mall/mall.dart';import 'views/profile/profile.dart';class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: "豆瓣",theme: ThemeData(primaryColor: Colors.green,highlightColor: Colors.transparent,splashColor: Colors.transparent),home: MyStackPage(),);}}class MyStackPage extends StatefulWidget {@override_MyStackPageState createState() => _MyStackPageState();}class _MyStackPageState extends State<MyStackPage> {var _currentIndex = 0;@overrideWidget build(BuildContext context) {return Scaffold(bottomNavigationBar: BottomNavigationBar(currentIndex: _currentIndex,selectedFontSize: 14,unselectedFontSize: 14,type: BottomNavigationBarType.fixed,items: [createItem("home", "首页"),createItem("subject", "书影音"),createItem("group", "小组"),createItem("mall", "市集"),createItem("profile", "我的"),],onTap: (index) {setState(() {_currentIndex = index;});},),body: IndexedStack(index: _currentIndex,children: <Widget>[Home(),Subject(),Group(),Mall(),Profile()],),);}}BottomNavigationBarItem createItem(String iconName, String title) {return BottomNavigationBarItem(icon: Image.asset("assets/images/tabbar/$iconName.png", width: 30,),activeIcon: Image.asset("assets/images/tabbar/${iconName}_active.png", width: 30,),title: Text(title));}
二. 数据请求和转化
2.1. 网络请求简单封装
基于dio进行了一个简单工具的封装:
配置文件存放:http_config.dartconst baseURL = "http://123.207.32.32:8000";const timeout = 5000;
网络请求工具文件:http_request.dart
import 'package:dio/dio.dart';import 'http_config.dart';class HttpRequest {// 1.创建实例对象static BaseOptions baseOptions = BaseOptions(connectTimeout: timeout);static Dio dio = Dio(baseOptions);static Future<T> request<T>(String url, {String method = "get",Map<String, dynamic> params}) async {// 1.单独相关的设置Options options = Options();options.method = method;// 2.发送网络请求try {Response response = await dio.request<T>(url, queryParameters: params, options: options);return response.data;} on DioError catch (e) {throw e;}}}
2.2. 首页数据请求转化
豆瓣的API接口:
模型对象的封装
在面向对象的开发中,数据请求下来并不会像前端那样直接使用,而是封装成模型对象:
- 目前前端开发正在向TypeScript发展,也在帮助强化这种思维方式
为了方便之后使用请求下来的数据,将数据划分成了如下的模型:
Person、Actor、Director模型:会被使用到MovieItem中
class Person {String name;String avatarURL;Person.fromMap(Map<String, dynamic> json) {this.name = json["name"];this.avatarURL = json["avatars"]["medium"];}}class Actor extends Person {Actor.fromMap(Map<String, dynamic> json): super.fromMap(json);}class Director extends Person {Director.fromMap(Map<String, dynamic> json): super.fromMap(json);}
MovieItem模型:
int counter = 1;class MovieItem {int rank;String imageURL;String title;String playDate;double rating;List<String> genres;List<Actor> casts;Director director;String originalTitle;MovieItem.fromMap(Map<String, dynamic> json) {this.rank = counter++;this.imageURL = json["images"]["medium"];this.title = json["title"];this.playDate = json["year"];this.rating = json["rating"]["average"];this.genres = json["genres"].cast<String>();this.casts = (json["casts"] as List<dynamic>).map((item) {return Actor.fromMap(item);}).toList();this.director = Director.fromMap(json["directors"][0]);this.originalTitle = json["original_title"];}}
首页数据请求封装以及模型转化
这里封装了一个专门的类,用于请求首页的数据:HomeRequest
- 目前类中只有一个方法getMovieTopList;
- 后续有其他首页数据需要请求,就继续在这里封装请求的方法;
import 'package:douban_app/models/home_model.dart';import 'http_request.dart';class HomeRequest {Future<List<MovieItem>> getMovieTopList(int start, int count) async {// 1.拼接URLfinal url = "https://douban.uieee.com/v2/movie/top250?start=$start&count=$count";// 2.发送请求final result = await HttpRequest.request(url);// 3.转成模型对象final subjects = result["subjects"];List<MovieItem> movies = [];for (var sub in subjects) {movies.add(MovieItem.fromMap(sub));}return movies;}}
五. 界面效果实现
5.1. 首页整体代码
首页整体布局非常简单,使用一个ListView即可import 'package:douban_app/models/home_model.dart';import 'package:douban_app/network/home_request.dart';import 'package:douban_app/views/home/childCpns/movie_list_item.dart';import 'package:flutter/material.dart';const COUNT = 20;class Home extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("首页"),),body: Center(child: HomeContent(),),);}}class HomeContent extends StatefulWidget {@override_HomeContentState createState() => _HomeContentState();}class _HomeContentState extends State<HomeContent> {// 初始化首页的网络请求对象HomeRequest homeRequest = HomeRequest();int _start = 0;List<MovieItem> movies = [];@overridevoid initState() {super.initState();// 请求电影列表数据getMovieTopList(_start, COUNT);}void getMovieTopList(start, count) {homeRequest.getMovieTopList(start, count).then((result) {setState(() {movies.addAll(result);});});}@overrideWidget build(BuildContext context) {return ListView.builder(itemCount: movies.length,itemBuilder: (BuildContext context, int index) {return MovieListItem(movies[index]);});}}
5.2. 单独Item局部
按照对应的结构,实现代码:import 'package:douban_app/components/dash_line.dart';import 'package:flutter/material.dart';import 'package:douban_app/models/home_model.dart';import 'package:douban_app/components/star_rating.dart';class MovieListItem extends StatelessWidget {final MovieItem movie;MovieListItem(this.movie);@overrideWidget build(BuildContext context) {return Container(padding: EdgeInsets.all(10),decoration: BoxDecoration(border: Border(bottom: BorderSide(width: 10, color: Color(0xffe2e2e2)))),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[// 1.电影排名getMovieRankWidget(),SizedBox(height: 12),// 2.具体内容getMovieContentWidget(),SizedBox(height: 12),// 3.电影简介getMovieIntroduceWidget(),SizedBox(height: 12,)],),);}// 电影排名Widget getMovieRankWidget() {return Container(padding: EdgeInsets.fromLTRB(9, 4, 9, 4),decoration: BoxDecoration(borderRadius: BorderRadius.circular(3),color: Color.fromARGB(255, 238, 205, 144)),child: Text("No.${movie.rank}",style: TextStyle(fontSize: 18, color: Color.fromARGB(255, 131, 95, 36)),));}// 具体内容Widget getMovieContentWidget() {return Container(height: 150,child: Row(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[getContentImage(),getContentDesc(),getDashLine(),getContentWish()],),);}Widget getContentImage() {return ClipRRect(borderRadius: BorderRadius.circular(5),child: Image.network(movie.imageURL));}Widget getContentDesc() {return Expanded(child: Container(padding: EdgeInsets.symmetric(horizontal: 15),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[getTitleWidget(),SizedBox(height: 3,),getRatingWidget(),SizedBox(height: 3,),getInfoWidget()],),),);}Widget getDashLine() {return Container(width: 1,height: 100,child: DashedLine(axis: Axis.vertical,dashedHeight: 6,dashedWidth: .5,count: 12,),);}Widget getTitleWidget() {return Stack(children: <Widget>[Icon(Icons.play_circle_outline, color: Colors.redAccent,),Text.rich(TextSpan(children: [TextSpan(text: " " + movie.title,style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold)),TextSpan(text: "(${movie.playDate})",style: TextStyle(fontSize: 18,color: Colors.black54),)]),maxLines: 2,),],);}Widget getRatingWidget() {return Row(crossAxisAlignment: CrossAxisAlignment.end,children: <Widget>[StarRating(rating: movie.rating, size: 18,),SizedBox(width: 5),Text("${movie.rating}")],);}Widget getInfoWidget() {// 1.获取种类字符串final genres = movie.genres.join(" ");final director = movie.director.name;var castString = "";for (final cast in movie.casts) {castString += cast.name + " ";}// 2.创建Widgetreturn Text("$genres / $director / $castString",maxLines: 2,overflow: TextOverflow.ellipsis,style: TextStyle(fontSize: 16),);}Widget getContentWish() {return Container(width: 60,child: Column(mainAxisAlignment: MainAxisAlignment.start,children: <Widget>[SizedBox(height: 20,),Image.asset("assets/images/home/wish.png", width: 30,),SizedBox(height: 5,),Text("想看",style: TextStyle(fontSize: 16, color: Color.fromARGB(255, 235, 170, 60)),)],),);}// 电影简介(原生名称)Widget getMovieIntroduceWidget() {return Container(width: double.infinity,padding: EdgeInsets.all(12),decoration: BoxDecoration(color: Color(0xfff2f2f2),borderRadius: BorderRadius.circular(5)),child: Text(movie.originalTitle, style: TextStyle(fontSize: 18, color: Colors.black54),),);}}
