美文网首页
练习高仿豆瓣电影列表

练习高仿豆瓣电影列表

作者: Imkata | 来源:发表于2023-03-09 17:17 被阅读0次

    一. 数据请求和转化

    1.1. 首页数据请求转化

    豆瓣数据的获取

    这里我使用豆瓣的API接口来请求数据:

    模型对象的封装

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

    • 前端开发者很容易没有面向对象的思维或者类型的思维。
    • 但是目前前端开发正在向TypeScript发展,也在帮助我们强化这种思维方式。

    为了方便之后使用请求下来的数据,我将数据划分成了如下的模型:

    Person、Actor、Director模型:它们会被使用到MovieItem中,home_model.dart代码如下:

    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);
    }
    
    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>();
        // casts里面是演员,转成List后,就可以使用map方法将演员map转成演员模型
        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"];
      }
    
      @override
      // 重写这个方法以后,打印的时候会把模型的所有属性都信息都打印出来
      String toString() {
        return 'MovieItem{rank: $rank, imageURL: $imageURL, title: $title, playDate: $playDate, rating: $rating, genres: $genres, casts: $casts, director: $director, originalTitle: $originalTitle}';
      }
    }
    

    补充:鼠标选中MovieItem,按command+n,可以快速生成构造器以及toString方法。

    首页数据请求封装以及模型转化

    这里我封装了一个专门的类,用于请求首页的数据,这样让我们的请求代码更加规范的管理:HomeRequest。

    • 目前类中只有一个方法requestMovieList;
    • 后续有其他首页数据需要请求,就继续在这里封装请求的方法;
    import 'package:learn_flutter/douban/model/home_model.dart';
    import 'config.dart';
    import 'http_request.dart';
    
    class HomeRequest {
      // 类方法,返回一个Future
      static Future<List<MovieItem>> requestMovieList(int start) async {
        // 1.构建URL
        final movieURL = "/movie/top250?start=$start&count=${HomeConfig.movieCount}";
    
        // 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));
        }
    
        return movies;
      }
    }
    

    在home_content.dart文件中请求数据

    二. 界面效果实现

    2.1. 首页整体代码

    首页整体布局非常简单,使用一个ListView即可。

    import 'package:flutter/material.dart';
    import 'package:learn_flutter/douban/model/home_model.dart';
    import 'package:learn_flutter/_06_service//home_request.dart';
    import 'home_movie_item.dart';
    
    class HYHomeContent extends StatefulWidget {
      @override
      _HYHomeContentState createState() => _HYHomeContentState();
    }
    
    class _HYHomeContentState extend s State<HYHomeContent> {
      // 保存数据
      final List<MovieItem> movies = [];
    
      @override
      void initState() {
        super.initState();
        // 发送网络请求
        HomeRequest.requestMovieList(0).then((res) {
          setState(() {
            movies.addAll(res);
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          itemCount: movies.length,
          itemBuilder: (ctx, index) {
            // 渲染数据
            return HYHomeMovieItem(movies[index]);
          }
        );
      }
    }
    

    2.2. 单独Item局部

    下面是针对界面结构的分析:

    大家按照对应的结构,实现代码,home_movie_item.dart文件如下:

    import 'dart:math';
    import 'package:flutter/material.dart';
    import 'package:learn_flutter/douban/model/home_model.dart';
    import 'package:learn_flutter/douban/utils/log.dart';
    import 'package:learn_flutter/douban/widgets/dashed_line.dart';
    import 'package:learn_flutter/douban/widgets/star_rating.dart';
    
    class HYHomeMovieItem extends StatelessWidget {
      final MovieItem movie;
    
      HYHomeMovieItem(this.movie);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          // 包裹一层Container是为了好设置内边距
          padding: EdgeInsets.all(8),
          // 底部加边框,设置分隔条
          decoration: BoxDecoration(
              border:
                  Border(bottom: BorderSide(width: 8, color: Color(0xffcccccc)))),
          child: Column(
            // 交叉轴左对齐
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              // 1. 头部的布局
              buildHeader(),
              SizedBox(
                height: 8,
              ),
              // 2. 内容布局
              buildContent(),
              SizedBox(
                height: 8,
              ),
              // 3. 尾部布局
              buildFooter(),
            ],
          ),
        );
      }
    
      // 1.头部的布局
      Widget buildHeader() {
        return Container(
          padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
          decoration: BoxDecoration(
              color: Color.fromARGB(255, 238, 205, 144),
              borderRadius: BorderRadius.circular(3)),
          child: Text(
            "No.${movie.rank}",
            style: TextStyle(fontSize: 18, color: Color.fromARGB(255, 131, 95, 36)),
          ),
        );
      }
    
      // 2.内容的布局
      Widget buildContent() {
        return Row(
          // 交叉轴从头开始
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            buildContentImage(),
            SizedBox(
              width: 8,
            ),
            Expanded(
              // 添加IntrinsicHeight组件就能保证内容+虚线+想看高度都是一样的,这样我们就不需要设置虚线和想看的高度了
              child: IntrinsicHeight(
                child: Row(
                  children: <Widget>[
                    buildContentInfo(),
                    SizedBox(
                      width: 8,
                    ),
                    // 虚线
                    buildContentLine(),
                    SizedBox(
                      width: 8,
                    ),
                    // 想看
                    buildContentWish()
                  ],
                ),
              ),
            )
          ],
        );
      }
    
      // 2.1.内容的图片
      Widget buildContentImage() {
        return ClipRRect( // 设置圆角,这种方式简单方便
            borderRadius: BorderRadius.circular(8),
            child: Image.network(
              movie.imageURL,
              // 设置高度之后宽度会自适应比例,也就是宽高固定了
              height: 150,
            ));
      }
    
      // 2.2.内容的信息
      Widget buildContentInfo() {
        // 因为左边的图片和让Column都是在一个row里面,如果文字过多,文字会超出屏幕外面
        // 所以我们让Column变成可伸缩的,从而不超出屏幕
        return Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              buildContentInfoTitle(),
              SizedBox(
                height: 8,
              ),
              buildContentInfoRate(),
              SizedBox(
                height: 8,
              ),
              buildContentInfoDesc()
            ],
          ),
        );
      }
    
    //  2.2.1 内容的信息的标题
      Widget buildContentInfoTitle() {
        List<InlineSpan> spans = [];
    
        // 图标+电影名称+年份,使用row也是可以实现的,但是以后文字多的时候row无法换行,所以使用如下方式
        return Text.rich(
          TextSpan(children: [
            // 以前我们用的是WidgetSpan+textSpan+textSpan,但是这样会导致前两个不在一条水平线上
            // 现在我们三个都使用WidgetSpan,然后前两个设置PlaceholderAlignment.middle,最后一个设置PlaceholderAlignment.bottom
            // 就可以让前两个中心点对齐,最后一个底部对齐
            // 图标
            WidgetSpan(
              child: Icon(
                Icons.play_circle_outline,
                color: Colors.pink,
                size: 40,
              ),
              baseline: TextBaseline.ideographic,
              alignment: PlaceholderAlignment.middle
            ),
            // 电影名称
            // WidgetSpan要么都是一行显示,要么就三行显示,如果电影的名字过长,就无法做到两行显示的效果
            // 我们的解决办法是让每个文字都是一个WidgetSpan,runes就是每个文字组成的数组,使用map映射成WidgetSpan的 Iterable,再转成数组,再展开
            ...movie.title.runes.map((rune) {
              return WidgetSpan(child: Text(new String.fromCharCode(rune), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),), alignment: PlaceholderAlignment.middle);
            }).toList(),
            // 年份
            WidgetSpan(child: Text("(${movie.playDate})"), style: TextStyle(fontSize: 18, color: Colors.grey), alignment: PlaceholderAlignment.bottom)
          ])
        );
      }
    
      // 2.2.2 星级评分组件
      Widget buildContentInfoRate() {
        // 当在iPhone5小尺寸手机上的时候,左边图片的宽度是固定的,右边的虚线和想看的宽度也是固定的
        // 这时候剩下的宽度也许都不够星星的宽度了,这时候就会报错
        // 我们添加FittedBox,这时候如果剩下的宽度不够了,就可以稍微缩小一点,这样就不会报错了
        return FittedBox(
          child: Row(
            children: <Widget>[
              // 以前封装的星级插件☆
              HYStarRating(
                rating: movie.rating,
                size: 20,
              ),
              SizedBox(
                width: 6,
              ),
              // 星级文字  
              Text(
                "${movie.rating}",
                style: TextStyle(fontSize: 16),
              )
            ],
          ),
        );
      }
    
      // 2.2.3 电影描述
      Widget buildContentInfoDesc() {
        // 1.字符串拼接
        // 数组元素以空格拼接
        final genresString = movie.genres.join(" ");
        // 导演
        final directorString = movie.director.name;
        List<Actor> casts = movie.casts;
        // 演员数组取出名字
        final actorString = movie.casts.map((item) => item.name).join(" ");
    
        return Text(
          "$genresString / $directorString / $actorString",
          maxLines: 2, //最多两行
          overflow: TextOverflow.ellipsis, //超出以后显示...
          style: TextStyle(fontSize: 16),
        );
      }
    
      // 2.3.内容的虚线
      Widget buildContentLine() {
        return Container(
    //      height: 100,
          child: HYDashedLine(
            axis: Axis.vertical,
            dashedWidth: .4,
            dashedHeight: 6,
            count: 10,
            color: Colors.pink,
          ),
        );
      }
    
      // 2.4.内容的想看
      Widget buildContentWish() {
        return Container(
    //      height: 100,
          child: Column(
            // 包裹一个Container,再设置为center,让❤和想看垂直居中
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Image.asset("assets/images/home/wish.png"),
              Text(
                "想看",
                style: TextStyle(
                  fontSize: 18,
                  color: Color.fromARGB(255, 235, 170, 60)
                ),
              )
            ],
          ),
        );
      }
    
      // 3.尾部的布局
      Widget buildFooter() {
        return Container(
          // 默认的宽度是内容包裹的宽度,设置为最大
          width: double.infinity,
          // 内边距
          padding: EdgeInsets.all(8),
          // 圆角
          decoration: BoxDecoration(
            color: Color(0xfff2f2f2),
            borderRadius: BorderRadius.circular(6),
          ),
          child: Text(
            movie.originalTitle,
            style: TextStyle(fontSize: 20, color: Color(0xff666666)),
          ),
        );
      }
    }
    

    补充:Flutter默认的print打印只会打印信息,并没有所在行的信息,所以我们自定义一个打印:

    void hyLog(Object message, StackTrace current) {
      HYCustomTrace programInfo = HYCustomTrace(current);
      print("所在文件: ${programInfo.fileName}, 所在行: ${programInfo.lineNumber}, 打印信息: $message");
    }
    
    class HYCustomTrace {
      final StackTrace _trace;
    
      String fileName;
      int lineNumber;
      int columnNumber;
    
      HYCustomTrace(this._trace) {
        _parseTrace();
      }
    
      void _parseTrace() {
        var traceString = this._trace.toString().split("\n")[0];
        var indexOfFileName = traceString.indexOf(RegExp(r'[A-Za-z_]+.dart'));
        var fileInfo = traceString.substring(indexOfFileName);
        var listOfInfos = fileInfo.split(":");
        this.fileName = listOfInfos[0];
        this.lineNumber = int.parse(listOfInfos[1]);
        var columnStr = listOfInfos[2];
        columnStr = columnStr.replaceFirst(")", "");
        this.columnNumber = int.parse(columnStr);
      }
    }
    

    使用方式如下:

    hyLog("aaaaaa", StackTrace.current);
    

    相关文章

      网友评论

          本文标题:练习高仿豆瓣电影列表

          本文链接:https://www.haomeiwen.com/subject/mdyikdtx.html