更新地點: 首發於公眾號,第二天更新於掘金、思否、開發者頭條等地方;
更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy
程式碼地址: github.com/coderwhy/fl…
由於Mac的檔期問題(Mac忙其他前些的),這裡給出一個我寫的階段性練習內容。
時間問題,沒有非常詳細說明每個步驟,後續希望可以更新一個視訊為大家講解。
一. 資料請求和轉化
1.1. 網路請求簡單封裝
目前我還沒有詳細講解網路請求相關的知識,開發中我們更多選擇地方的dio。
後面我會詳細講解網路請求的幾種方式,我這裡基於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. 首頁資料請求轉化
豆瓣資料的獲取
這裡我使用豆瓣的API介面來請求資料:
模型物件的封裝
在物件導向的開發中,資料請求下來並不會像前端那樣直接使用,而是封裝成模型物件:
- 前端開發者很容易沒有物件導向的思維或者型別的思維。
- 但是目前前端開發正在向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檔案中請求資料
二. 介面效果實現
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區域性
下面是針對介面結構的分析:
大家按照對應的結構,實現程式碼即可:
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、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注