Flutter(十二)之練習高仿豆瓣電影列表

更新地點: 首發於公衆號,次日更新於掘金、思否、開發者頭條等地方;前端

更多交流: 能夠添加個人微信 372623326,關注個人微博:coderwhyvue

代碼地址: github.com/coderwhy/fl…git

因爲Mac的檔期問題(Mac忙其餘前些的),這裏給出一個我寫的階段性練習內容。github

時間問題,沒有很是詳細說明每一個步驟,後續但願能夠更新一個視頻爲你們講解。算法

image-20191002160439302

一. 數據請求和轉化

1.1. 網絡請求簡單封裝

目前我尚未詳細講解網絡請求相關的知識,開發中咱們更多選擇地方的dio。json

後面我會詳細講解網絡請求的幾種方式,我這裏基於dio進行了一個簡單工具的封裝:微信

配置文件存放:http_config.dart網絡

const 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;
    }
  }
}
複製代碼

1.2. 首頁數據請求轉化

豆瓣數據的獲取app

這裏我使用豆瓣的API接口來請求數據:

image-20191002155619948

模型對象的封裝

在面向對象的開發中,數據請求下來並不會像前端那樣直接使用,而是封裝成模型對象:

  • 前端開發者很容易沒有面向對象的思惟或者類型的思惟。
  • 可是目前前端開發正在向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.拼接URL
    final 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;
  }
}
複製代碼

在home.dart文件中請求數據

image-20191002160439302

二. 界面效果實現

2.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 {
  @override
  Widget 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 = [];

  @override
  void initState() {
    super.initState();

    // 請求電影列表數據
    getMovieTopList(_start, COUNT);
  }

  void getMovieTopList(start, count) {
    homeRequest.getMovieTopList(start, count).then((result) {
      setState(() {
        movies.addAll(result);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: movies.length,
      itemBuilder: (BuildContext context, int index) {
        return MovieListItem(movies[index]);
      }
    );
  }
}
複製代碼

2.2. 單獨Item局部

下面是針對界面結構的分析:

image-20191002163853864

你們按照對應的結構,實現代碼便可:

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

  @override
  Widget 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.建立Widget
    return 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),),
    );
  }
}
複製代碼

備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息