Flutter从入门到实战
一共分为23个系列
①(Flutter、Dart环境搭建篇) 共3个内容 已更新
②(Dart语法1 篇) 共4个内容 已更新
③(Dart语法2 篇) 共2个内容 已更新
④(Flutter案例开发篇) 共4个内容 已更新
⑤(Flutter的StatelessWidget 共3个内容 已更新
⑥(Flutter的基础Widget篇) 共2个内容 已更新
⑦(布局Widget篇) 共1个内容 已更新
⑧(Flex、Row、Column以及Flexible、Stack篇) 共1个内容 已更新
⑨(滚动的Widget篇) 共4个内容 已更新
⑩(Dart的Future和网络篇) 共3个内容 已更新
⑪(豆瓣案例-1篇) 共3个内容 已更新
⑫(豆瓣案例-2篇) 共3个内容 已更新
官方文档说明
官方视频教程
Flutter的YouTube视频教程-小部件
最近热门
API作为首页的数据https://movie.douban.com/j/search_subjects?type=movie&tag=热门&page_limit=50&page_start=0
比如我们现在豆瓣有5个模块
我们可以考虑基于之前封装Dio的第三方库的二次封装再抽取5个模块的请求
: 如果你没有看过我之前的封装 可以到这篇文章里面看出 →⑩(Dart的Future和网络篇)
比如首页模块的请求、解析单独放到一个模块中。
这样我们改模块也比较方便
// 封装建议
// 命名最好使用 单词使用下划线分开
import 'package:dio/dio.dart';
import 'package:learn_flutter/service/config.dart';
class HttpRequest {
// Dio 有基本的配置
static final BaseOptions baseOptions = BaseOptions(
baseUrl: HttpConfig.baseUrl,connectTimeout: HttpConfig.timeout
);
static final Dio dio = Dio(baseOptions);
// static void request(String url,
// 由于异步请求 所以使用 Future
// 其中T代表泛型 因为不知道服务器返回什么具体的类型 所以使用泛型代表
static Future<T> request<T>(String url,
{String method="get",
Map<String,dynamic>? params,
Interceptor? inter}) async
{
// 1. 创建单独配置
final option = Options(method: method);
// 全局拦截器
// 创建默认的全局拦截器
Interceptor dInter = InterceptorsWrapper(
onRequest: (request,handle) {
print("请求拦截");
return handle.next(request);
},
onResponse: (response,handle) {
print("响应拦截");
return handle.next(response);
},
onError: (e, handler) async {
print("错误拦截");
return handler.next(e);
}
);
List<Interceptor> inters = [dInter];
// 请求单独拦截器
if (inter != null) {
inters.add(inter);
}
// 统一添加到拦截器中
dio.interceptors.addAll(inters);
// 2. 发送网络请求 url 参数 配置
// 请求可能发生错误 所以使用try 捕捉异常
try{
Response response = await dio.request(url,queryParameters: params,options: option);
// 3. 返回响应的数据
return response.data;
} on DioError catch(e) {
// 请求失败情况
return Future.error(e);
}
}
}
// 网络请求的配置 Url 和 请求超时等
class HttpConfig{
// static const String baseUrl = "https://httpbin.org";
static const String baseUrl = "https://movie.douban.com/j";
static const int timeout = 6000;
}
// 首页配置
class HomeConfig {
static const int movieCount = 20;
}
// https://movie.douban.com/j
// /search_subjects?type=movie&tag=热门&page_limit=50&page_start=0
import 'package:learn_flutter/service/config.dart';
import '../douban/pages/model/home_model.dart';
import 'http_request.dart';
class HomeRequest{
static Future<List<MovieItem>> requestMovieList(int start) async {
// 1.构建URL
// final movieURL = "/movie/top250?start=$start&count=${HomeConfig.movieCount}";
final movieURL = "/search_subjects?type=movie&tag=热门&page_limit=${HomeConfig.movieCount}&page_start=${start}";
// 2.发送网络请求获取结果
final result = await HttpRequest.request(movieURL);
final subjects = result["subjects"];
// // 3.将Map转成Model
List<MovieItem> movies = [];
for (var sub in subjects) {
movies.add(MovieItem.fromMap(sub));
}
print(movies);
return movies;
}
}
手动转换
优势:字段可以根据自己控制。不需要的数据可以忽略
劣势:麻烦 并且可能单词写错 。容易出错
自动转换
将解析的数据 挨个声明到model里面去
int counter = 1; // 用来记录每次排名加1的常量
class MovieItem {
int rank = 0; // 自己扩展 排名 数据是没有返回的 我们手动进行添加
String episodes_info = "";
double rate = 0.0;
int cover_x = 0;
int cover_y = 0;
String title = "";
String url = "";
String cover = "";
String id = "";
bool playable = false;
bool is_new = false;
MovieItem.fromMap(Map<String, dynamic> json) {
this.rank = counter++;
this.episodes_info = json["episodes_info"];
this.rate = double.parse(json["rate"].toString());
this.cover_x = int.parse(json["cover_x"].toString());
this.cover_y = int.parse(json["cover_y"].toString());
this.title = json["title"];
this.url = json["url"];
this.cover = json["cover"];
this.id = json["id"];
this.playable = json["playable"];
this.is_new = json["is_new"];
}
// 快捷键 command + n
// 生成toString方法
@override
String toString() {
return 'MovieItem{rank: $rank, episodes_info: $episodes_info, rate: $rate, cover_x: $cover_x, cover_y: $cover_y, title: $title, url: $url, cover: $cover, id: $id, playable: $playable, is_new: $is_new}';
}
}
command+n
import 'package:flutter/material.dart';
import 'package:learn_flutter/douban/pages/model/home_model.dart';
import 'package:learn_flutter/service/home_request.dart';
class YHHomeContent extends StatefulWidget {
@override
State<YHHomeContent> createState() => _YHHomeContentState();
}
class _YHHomeContentState extends State<YHHomeContent> {
final List<MovieItem> movies = [];
@override
void initState() {
// 1.网络请求
HomeRequest.requestMovieList(0).then((res){
print(res);
setState(() {
movies.addAll(res);
});
print(movies.length);
});
print("-------");
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: movies.length ,
itemBuilder: (BuildContext cxt,int index){
return ListTile(title: Text("${movies[index].title}"),
leading: Image.network("${movies[index].cover}"));
});
}
}
整体分成3部分进行拆解
1.排行
2.内容展示
3.描述
整体使用一个Column列布局
1.排行使用Container布局
2. 使用Row(行)布局使用Expanded包裹IntrinsicHeight(固定高度) 这样Row里面包含的Widget就是固定高度的
2.1 包含Image
2.2 包含 Expanded的Column 包含Expanded是因为文本可能需要换行
2.3 Container包含虚线
2.4 Column包含想看
3.使用Container布局
import 'package:flutter/material.dart';
import 'package:learn_flutter/douban/pages/model/home_model.dart';
import 'package:learn_flutter/douban/widgets/dashe_line.dart';
import 'package:learn_flutter/douban/widgets/star_rating.dart';
class YHHomeMovieItem extends StatelessWidget {
final MovieItem movie;
YHHomeMovieItem(this.movie);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8),
// 添加底部边框
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 8,color: Color(0xffcccccc))
)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildHeader(),
SizedBox(height: 6,),
buildContent(),
SizedBox(height: 6,),
buildFooter(),
],
),
);
}
// 1. 头部排名
Widget buildHeader() {
return Container(
padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
decoration: BoxDecoration(
color:Color.fromARGB(255, 238, 208, 144),
borderRadius: BorderRadius.circular(3)
),
child: Text("No.${movie.rank}",style: TextStyle(fontSize: 18,color: Color.fromARGB(255, 131, 95, 35))),
);
}
// 2.中间内容
Widget buildContent(){
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildContentImage(),
SizedBox(width: 8),
// 使用 IntrinsicHeight 固定高度也就是说 buildContentInfo buildContentLine buildContentWish 不需要设置高度 。直接等高
Expanded(
child: IntrinsicHeight(
child: Row(
children: [
buildContentInfo(),
SizedBox(width: 8),
buildContentLine(),
SizedBox(width: 8),
buildContentWish(),
],
),
),
),
],
);
}
// 2.1 内容的图片
Widget buildContentImage(){
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(movie.cover,height: 150,));
}
// 2.2 内容信息
Widget buildContentInfo(){
// Expanded 防止Column列表里面的数据超出范围 比如标题、或者是描述
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildContentInfoTitle(),
SizedBox(height: 8,),
buildContentInfoRate(),
SizedBox(height: 8,),
buildContentInfoDesc(),
],
),
);
}
// 2.2.1 内容信息 - 标题
Widget buildContentInfoTitle(){
// 由于文本不知道长度 需要换行 所以使用Text.rich 并且换行使用TextSpan
// 并且使用WidgetSpan 包裹其他Widget
return Text.rich(
TextSpan(
children: [
WidgetSpan(child:Icon(Icons.play_circle_outline,color: Colors.redAccent,size: 24,),alignment: PlaceholderAlignment.middle),
WidgetSpan(
child:Text("${movie.title}",
style: TextStyle(fontSize: 20,fontWeight: FontWeight.bold),),
alignment: PlaceholderAlignment.middle),
WidgetSpan(child:Text("(${movie.cover_x})",style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold,color: Colors.grey),), alignment: PlaceholderAlignment.bottom),
// TextSpan(text:movie.title,style: TextStyle(fontSize: 20,fontWeight: FontWeight.bold)),
// TextSpan(text: "(${movie.cover_x})",style: TextStyle(fontSize: 18,color: Colors.grey)
],
),
);
}
// 2.2.2 内容信息 - 评分
Widget buildContentInfoRate(){
return Row(
children: [
YHStarRating(rating: movie.rate,size: 20,),
SizedBox(width: 6,),
Text("${movie.rate}",style: TextStyle(fontSize:16)),
],
);
}
// 2.2.3 内容信息 - 描述
Widget buildContentInfoDesc() {
// 1.字符串拼接
// join是将数组进行以什么来切割
return Text("${movie.cover_y}",maxLines: 2);
}
// 2.3 内容的虚线
Widget buildContentLine(){
return Container(
// height: 100,
child: YHDasheLine(
axis: Axis.vertical,
dashedWidth: 1,
dashedHeight: 6,
count: 10,
color: Colors.red,
),
);
}
// 2.4 想看
Widget buildContentWish(){
return Container(
// height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/home/wish.png"),
Text("想看",style:TextStyle(fontSize: 16,color: Color.fromARGB(255, 235, 170, 60)),),
],
),
);
}
// 3 底部内容
Widget buildFooter(){
return Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Color(0xffe2e2e2),
borderRadius: BorderRadius.circular(6),
),
child: Text(movie.url,style: TextStyle(fontSize: 20,color: Colors.red),),
);
}
}
https://javiercbk.github.io/json_to_dart/
安装完成之后 推出Android Studio
import 'dart:convert';
import 'package:learn_flutter/generated/json/base/json_field.dart';
import 'package:learn_flutter/generated/json/home_movie_item_entity.g.dart';
@JsonSerializable()
class HomeMovieItemEntity {
@JSONField(name: "episodes_info")
late String episodesInfo;
late String rate;
@JSONField(name: "cover_x")
late int coverX;
late String title;
late String url;
late bool playable;
late String cover;
late String id;
@JSONField(name: "cover_y")
late int coverY;
@JSONField(name: "is_new")
late bool isNew;
HomeMovieItemEntity();
factory HomeMovieItemEntity.fromJson(Map<String, dynamic> json) => $HomeMovieItemEntityFromJson(json);
Map<String, dynamic> toJson() => $HomeMovieItemEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}