練習(xí)高仿豆瓣電影列表

案例地址:Flutter_豆瓣案例

一. 數(shù)據(jù)請(qǐng)求和轉(zhuǎn)化

1.1. 首頁(yè)數(shù)據(jù)請(qǐng)求轉(zhuǎn)化

豆瓣數(shù)據(jù)的獲取

這里我使用豆瓣的API接口來(lái)請(qǐng)求數(shù)據(jù):

模型對(duì)象的封裝

在面向?qū)ο蟮拈_(kāi)發(fā)中,數(shù)據(jù)請(qǐng)求下來(lái)并不會(huì)像前端那樣直接使用,而是封裝成模型對(duì)象:

  • 前端開(kāi)發(fā)者很容易沒(méi)有面向?qū)ο蟮乃季S或者類型的思維。
  • 但是目前前端開(kāi)發(fā)正在向TypeScript發(fā)展,也在幫助我們強(qiáng)化這種思維方式。

為了方便之后使用請(qǐng)求下來(lái)的數(shù)據(jù),我將數(shù)據(jù)劃分成了如下的模型:

Person、Actor、Director模型:它們會(huì)被使用到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里面是演員,轉(zhuǎn)成List后,就可以使用map方法將演員map轉(zhuǎn)成演員模型
    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
  // 重寫(xiě)這個(gè)方法以后,打印的時(shí)候會(huì)把模型的所有屬性都信息都打印出來(lái)
  String toString() {
    return 'MovieItem{rank: $rank, imageURL: $imageURL, title: $title, playDate: $playDate, rating: $rating, genres: $genres, casts: $casts, director: $director, originalTitle: $originalTitle}';
  }
}

補(bǔ)充:鼠標(biāo)選中MovieItem,按command+n,可以快速生成構(gòu)造器以及toString方法。

首頁(yè)數(shù)據(jù)請(qǐng)求封裝以及模型轉(zhuǎn)化

這里我封裝了一個(gè)專門(mén)的類,用于請(qǐng)求首頁(yè)的數(shù)據(jù),這樣讓我們的請(qǐng)求代碼更加規(guī)范的管理:HomeRequest。

  • 目前類中只有一個(gè)方法requestMovieList;
  • 后續(xù)有其他首頁(yè)數(shù)據(jù)需要請(qǐng)求,就繼續(xù)在這里封裝請(qǐng)求的方法;
import 'package:learn_flutter/douban/model/home_model.dart';
import 'config.dart';
import 'http_request.dart';

class HomeRequest {
  // 類方法,返回一個(gè)Future
  static Future<List<MovieItem>> requestMovieList(int start) async {
    // 1.構(gòu)建URL
    final movieURL = "/movie/top250?start=$start&count=${HomeConfig.movieCount}";

    // 2.發(fā)送網(wǎng)絡(luò)請(qǐng)求獲取結(jié)果
    final result = await HttpRequest.request(movieURL);
    final subjects = result["subjects"];

    // 3.將Map轉(zhuǎn)成Model
    List<MovieItem> movies = [];
    for (var sub in subjects) {
      movies.add(MovieItem.fromMap(sub));
    }

    return movies;
  }
}

在home_content.dart文件中請(qǐng)求數(shù)據(jù)

二. 界面效果實(shí)現(xiàn)

2.1. 首頁(yè)整體代碼

首頁(yè)整體布局非常簡(jiǎn)單,使用一個(gè)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> {
  // 保存數(shù)據(jù)
  final List<MovieItem> movies = [];

  @override
  void initState() {
    super.initState();
    // 發(fā)送網(wǎng)絡(luò)請(qǐng)求
    HomeRequest.requestMovieList(0).then((res) {
      setState(() {
        movies.addAll(res);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: movies.length,
      itemBuilder: (ctx, index) {
        // 渲染數(shù)據(jù)
        return HYHomeMovieItem(movies[index]);
      }
    );
  }
}

2.2. 單獨(dú)Item局部

下面是針對(duì)界面結(jié)構(gòu)的分析:

大家按照對(duì)應(yīng)的結(jié)構(gòu),實(shí)現(xiàn)代碼,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是為了好設(shè)置內(nèi)邊距
      padding: EdgeInsets.all(8),
      // 底部加邊框,設(shè)置分隔條
      decoration: BoxDecoration(
          border:
              Border(bottom: BorderSide(width: 8, color: Color(0xffcccccc)))),
      child: Column(
        // 交叉軸左對(duì)齊
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          // 1. 頭部的布局
          buildHeader(),
          SizedBox(
            height: 8,
          ),
          // 2. 內(nèi)容布局
          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.內(nèi)容的布局
  Widget buildContent() {
    return Row(
      // 交叉軸從頭開(kāi)始
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        buildContentImage(),
        SizedBox(
          width: 8,
        ),
        Expanded(
          // 添加IntrinsicHeight組件就能保證內(nèi)容+虛線+想看高度都是一樣的,這樣我們就不需要設(shè)置虛線和想看的高度了
          child: IntrinsicHeight(
            child: Row(
              children: <Widget>[
                buildContentInfo(),
                SizedBox(
                  width: 8,
                ),
                // 虛線
                buildContentLine(),
                SizedBox(
                  width: 8,
                ),
                // 想看
                buildContentWish()
              ],
            ),
          ),
        )
      ],
    );
  }

  // 2.1.內(nèi)容的圖片
  Widget buildContentImage() {
    return ClipRRect( // 設(shè)置圓角,這種方式簡(jiǎn)單方便
        borderRadius: BorderRadius.circular(8),
        child: Image.network(
          movie.imageURL,
          // 設(shè)置高度之后寬度會(huì)自適應(yīng)比例,也就是寬高固定了
          height: 150,
        ));
  }

  // 2.2.內(nèi)容的信息
  Widget buildContentInfo() {
    // 因?yàn)樽筮叺膱D片和讓Column都是在一個(gè)row里面,如果文字過(guò)多,文字會(huì)超出屏幕外面
    // 所以我們讓Column變成可伸縮的,從而不超出屏幕
    return Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          buildContentInfoTitle(),
          SizedBox(
            height: 8,
          ),
          buildContentInfoRate(),
          SizedBox(
            height: 8,
          ),
          buildContentInfoDesc()
        ],
      ),
    );
  }

//  2.2.1 內(nèi)容的信息的標(biāo)題
  Widget buildContentInfoTitle() {
    List<InlineSpan> spans = [];

    // 圖標(biāo)+電影名稱+年份,使用row也是可以實(shí)現(xiàn)的,但是以后文字多的時(shí)候row無(wú)法換行,所以使用如下方式
    return Text.rich(
      TextSpan(children: [
        // 以前我們用的是WidgetSpan+textSpan+textSpan,但是這樣會(huì)導(dǎo)致前兩個(gè)不在一條水平線上
        // 現(xiàn)在我們?nèi)齻€(gè)都使用WidgetSpan,然后前兩個(gè)設(shè)置PlaceholderAlignment.middle,最后一個(gè)設(shè)置PlaceholderAlignment.bottom
        // 就可以讓前兩個(gè)中心點(diǎn)對(duì)齊,最后一個(gè)底部對(duì)齊
        // 圖標(biāo)
        WidgetSpan(
          child: Icon(
            Icons.play_circle_outline,
            color: Colors.pink,
            size: 40,
          ),
          baseline: TextBaseline.ideographic,
          alignment: PlaceholderAlignment.middle
        ),
        // 電影名稱
        // WidgetSpan要么都是一行顯示,要么就三行顯示,如果電影的名字過(guò)長(zhǎng),就無(wú)法做到兩行顯示的效果
        // 我們的解決辦法是讓每個(gè)文字都是一個(gè)WidgetSpan,runes就是每個(gè)文字組成的數(shù)組,使用map映射成WidgetSpan的 Iterable,再轉(zhuǎn)成數(shù)組,再展開(kāi)
        ...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 星級(jí)評(píng)分組件
  Widget buildContentInfoRate() {
    // 當(dāng)在iPhone5小尺寸手機(jī)上的時(shí)候,左邊圖片的寬度是固定的,右邊的虛線和想看的寬度也是固定的
    // 這時(shí)候剩下的寬度也許都不夠星星的寬度了,這時(shí)候就會(huì)報(bào)錯(cuò)
    // 我們添加FittedBox,這時(shí)候如果剩下的寬度不夠了,就可以稍微縮小一點(diǎn),這樣就不會(huì)報(bào)錯(cuò)了
    return FittedBox(
      child: Row(
        children: <Widget>[
          // 以前封裝的星級(jí)插件☆
          HYStarRating(
            rating: movie.rating,
            size: 20,
          ),
          SizedBox(
            width: 6,
          ),
          // 星級(jí)文字  
          Text(
            "${movie.rating}",
            style: TextStyle(fontSize: 16),
          )
        ],
      ),
    );
  }

  // 2.2.3 電影描述
  Widget buildContentInfoDesc() {
    // 1.字符串拼接
    // 數(shù)組元素以空格拼接
    final genresString = movie.genres.join(" ");
    // 導(dǎo)演
    final directorString = movie.director.name;
    List<Actor> casts = movie.casts;
    // 演員數(shù)組取出名字
    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.內(nèi)容的虛線
  Widget buildContentLine() {
    return Container(
//      height: 100,
      child: HYDashedLine(
        axis: Axis.vertical,
        dashedWidth: .4,
        dashedHeight: 6,
        count: 10,
        color: Colors.pink,
      ),
    );
  }

  // 2.4.內(nèi)容的想看
  Widget buildContentWish() {
    return Container(
//      height: 100,
      child: Column(
        // 包裹一個(gè)Container,再設(shè)置為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(
      // 默認(rèn)的寬度是內(nèi)容包裹的寬度,設(shè)置為最大
      width: double.infinity,
      // 內(nèi)邊距
      padding: EdgeInsets.all(8),
      // 圓角
      decoration: BoxDecoration(
        color: Color(0xfff2f2f2),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text(
        movie.originalTitle,
        style: TextStyle(fontSize: 20, color: Color(0xff666666)),
      ),
    );
  }
}

補(bǔ)充:Flutter默認(rèn)的print打印只會(huì)打印信息,并沒(méi)有所在行的信息,所以我們自定義一個(gè)打?。?/p>

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);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容