【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory

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视频教程-小部件

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第1张图片


⑫、豆瓣案例-2 篇

    • ①、首页ListView的搭建
      • 使用豆瓣的电影`最近热门`API作为首页的数据
      • 网络请求的抽取
        • 1.⌨️ 网络请求的抽取 httpRequest.dart
        • 2.⌨️ 网络请求的抽取 Config.dart
        • 3.⌨️ 网络请求的抽取 hoome_request.dart
        • 4.将JSON转换Model
          • 4.1 将API的数据转换成Model
          • 4.2 ⌨️ 模型类 home_model .dart
          • 小贴士-快捷键生成toString方法
          • 4.3⌨️ 首页内容 home_content.dart的代码
          • 4.4效果图 - 首页基本展示
    • ②、首页ListView的页面完善
      • 2.2 效果图-ListView的页面完善
    • ③、Json 转换模型工具
      • 1、Json to Dart工具
        • 1.1.复制一个Json的item的数据
        • 1.2、Json to Dart工具的使用
      • 2、FlutterJsonBeanFactory插件
        • 2.1 安装
        • 2.1 FlutterJsonBeanFactory的使用
        • 2.2 FlutterJsonBeanFactory生成的模型-效果

①、首页ListView的搭建

使用豆瓣的电影最近热门API作为首页的数据

https://movie.douban.com/j/search_subjects?type=movie&tag=热门&page_limit=50&page_start=0

网络请求的抽取

比如我们现在豆瓣有5个模块
我们可以考虑基于之前封装Dio的第三方库的二次封装再抽取5个模块的请求
: 如果你没有看过我之前的封装 可以到这篇文章里面看出 →⑩(Dart的Future和网络篇)
比如首页模块的请求、解析单独放到一个模块中。
这样我们改模块也比较方便

1.⌨️ 网络请求的抽取 httpRequest.dart

// 封装建议
// 命名最好使用 单词使用下划线分开

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);
        }



      }
}

2.⌨️ 网络请求的抽取 Config.dart

// 网络请求的配置 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;
}

3.⌨️ 网络请求的抽取 hoome_request.dart


// 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;
  }
}

4.将JSON转换Model

手动转换
优势:字段可以根据自己控制。不需要的数据可以忽略
劣势:麻烦 并且可能单词写错 。容易出错
自动转换

4.1 将API的数据转换成Model

将API的数据转换成Model
【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第2张图片
【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第3张图片

4.2 ⌨️ 模型类 home_model .dart

将解析的数据 挨个声明到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}';
  }


}
小贴士-快捷键生成toString方法
command+n

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第4张图片

4.3⌨️ 首页内容 home_content.dart的代码
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}"));
        });
  }
}

4.4效果图 - 首页基本展示

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第5张图片

②、首页ListView的页面完善

整体分成3部分进行拆解
1.排行
2.内容展示
3.描述【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第6张图片
整体使用一个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),),
    );
  }

}


2.2 效果图-ListView的页面完善

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第7张图片

③、Json 转换模型工具

1、Json to Dart工具

https://javiercbk.github.io/json_to_dart/

1.1.复制一个Json的item的数据

请添加图片描述

1.2、Json to Dart工具的使用

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第8张图片

2、FlutterJsonBeanFactory插件

安装完成之后 推出Android Studio

2.1 安装

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第9张图片

2.1 FlutterJsonBeanFactory的使用

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第10张图片

【Flutter从入门到实战】⑫、豆瓣案例-2、首页List搭建、首页List的完善、Json转换Dart模型工具和插件-Json to Dart-FlutterJsonBeanFactory_第11张图片

2.2 FlutterJsonBeanFactory生成的模型-效果

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);
  }
}

你可能感兴趣的:(Flutter,flutter,android,ios,android,studio,flutter案例)