原文地址: https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
這篇文章中將使用The Movies DB網(wǎng)站上的popular movies來(lái)構(gòu)建要給簡(jiǎn)單的app。在開(kāi)始之前我假設(shè)你是已經(jīng)了解widgets以及如何在flutter中進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
在深入代碼之前,我們先從一張圖來(lái)了解一下該架構(gòu)。

上圖展示了data如何從UI層流向數(shù)據(jù)層,反之也是如此。BLoC永遠(yuǎn)不會(huì)引用UI層中的任何組件。UI層僅觀察來(lái)自BLoC類是否改變。
什么是BLoC模式
它是谷歌開(kāi)發(fā)人員推薦的Flutter狀態(tài)管理系統(tǒng),用于對(duì)state進(jìn)行管理以便在項(xiàng)目中集中訪問(wèn)數(shù)據(jù)。
可以將此架構(gòu)與其它架構(gòu)聯(lián)系起來(lái)嗎?
MVP和MVVM都是不錯(cuò)的選擇,只是BLoC會(huì)被MVVM中的ViewModel取代。
BLoC的核心是什么?
STREAMS 和 REACTIVE方法,一般而言,數(shù)據(jù)將以流的方式從BLoC流向UI或者從UI流向BLoC,如果你沒(méi)有聽(tīng)過(guò)流,閱讀以下Stack Overflow上的這個(gè)回答。
接下來(lái)開(kāi)始使用BLoC來(lái)構(gòu)建項(xiàng)目
- 創(chuàng)建項(xiàng)目
flutter create [myProjectName]
- 修改main.dart
import 'package:flutter/material.dart';
import 'src/app.dart';
void main(){
runApp(App());
}
- 在lib目錄下創(chuàng)建src文件夾,接著在該文件夾下創(chuàng)建app.dart文件,在該文件中添加下面的代碼:
import 'package:flutter/material.dart';
import 'ui/movie_list.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
theme: ThemeData.dark(),
home: Scaffold(
body: MovieList(),
),
);
}
}
-
在src包中創(chuàng)建一個(gè)resources包,按照如下圖示創(chuàng)建其余的包
4.png
BLoC包用來(lái)保存BLoC相關(guān)的代碼
models包用來(lái)存放POJO類
resources用來(lái)保存repository類和網(wǎng)絡(luò)調(diào)用相關(guān)的類
ui用來(lái)存放跟界面顯示相關(guān)的類
- 添加RxDart library,在pubspec.yaml 文件中添加rxdart: ^0.18.0
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
rxdart: ^0.18.0
http: ^0.12.0+1
接著運(yùn)行flutter packages get
- 現(xiàn)在我們已經(jīng)完成了項(xiàng)目的骨架,現(xiàn)在從網(wǎng)絡(luò)層開(kāi)始處理,在該鏈接中的設(shè)置頁(yè)面獲取Api Key,然后替換到該鏈接上**http://api.themoviedb.org/3/movie/popular?api_key=“your_api_key”
**,然后你就可以如下的json:
{
"page": 1,
"total_results": 19772,
"total_pages": 989,
"results": [
{
"vote_count": 6503,
"id": 299536,
"video": false,
"vote_average": 8.3,
"title": "Avengers: Infinity War",
"popularity": 350.154,
"poster_path": "\/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg",
"original_language": "en",
"original_title": "Avengers: Infinity War",
"genre_ids": [
12,
878,
14,
28
],
"backdrop_path": "\/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg",
"adult": false,
"overview": "As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.",
"release_date": "2018-04-25"
},
- 我們創(chuàng)建一個(gè)實(shí)體類item_model.dart,將下面的代碼復(fù)制到該類中
class ItemModel {
int _page;
int _total_results;
int _total_pages;
List<_Result> _results = [];
ItemModel.fromJson(Map<String, dynamic> parsedJson) {
print(parsedJson['results'].length);
_page = parsedJson['page'];
_total_results = parsedJson['total_results'];
_total_pages = parsedJson['total_pages'];
List<_Result> temp = [];
for (int i = 0; i < parsedJson['results'].length; i++) {
_Result result = _Result(parsedJson['results'][i]);
temp.add(result);
}
_results = temp;
}
List<_Result> get results => _results;
int get total_pages => _total_pages;
int get total_results => _total_results;
int get page => _page;
}
class _Result {
int _vote_count;
int _id;
bool _video;
var _vote_average;
String _title;
double _popularity;
String _poster_path;
String _original_language;
String _original_title;
List<int> _genre_ids = [];
String _backdrop_path;
bool _adult;
String _overview;
String _release_date;
_Result(result) {
_vote_count = result['vote_count'];
_id = result['id'];
_video = result['video'];
_vote_average = result['vote_average'];
_title = result['title'];
_popularity = result['popularity'];
_poster_path = result['poster_path'];
_original_language = result['original_language'];
_original_title = result['original_title'];
for (int i = 0; i < result['genre_ids'].length; i++) {
_genre_ids.add(result['genre_ids'][i]);
}
_backdrop_path = result['backdrop_path'];
_adult = result['adult'];
_overview = result['overview'];
_release_date = result['release_date'];
}
String get release_date => _release_date;
String get overview => _overview;
bool get adult => _adult;
String get backdrop_path => _backdrop_path;
List<int> get genre_ids => _genre_ids;
String get original_title => _original_title;
String get original_language => _original_language;
String get poster_path => _poster_path;
double get popularity => _popularity;
String get title => _title;
double get vote_average => _vote_average;
bool get video => _video;
int get id => _id;
int get vote_count => _vote_count;
}
將返回的json映射到該文件,fromJson方法時(shí)獲取解碼的json然后將屬性映射到相應(yīng)的字段中。
- 接下來(lái)開(kāi)始網(wǎng)絡(luò)實(shí)現(xiàn),在resources包中創(chuàng)建一個(gè)movie_api_provider.dart文件,然后編寫下面的代碼:
import 'dart:async';
import 'package:http/http.dart' show Client;
import 'dart:convert';
import '../models/item_model.dart';
class MovieApiProvider {
Client client = Client();
final _apiKey = 'your_api_key';
Future<ItemModel> fetchMovieList() async {
print("entered");
final response = await client
.get("http://api.themoviedb.org/3/movie/popular?api_key=$_apiKey");
print(response.body.toString());
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return ItemModel.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
}
fetchModelList方法發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,如果請(qǐng)求成功返回一個(gè)Future<ItemModel>對(duì)象,如果失敗,就拋出異常。
- 接著在resources包中創(chuàng)建repository.dart文件,編寫下面的代碼
import 'dart:async';
import 'movie_api_provider.dart';
import '../models/item_model.dart';
class Repository {
final moviesApiProvider = MovieApiProvider();
Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList();
}
此Repository類就是數(shù)據(jù)流向BLoC的中心點(diǎn)。
- 接下來(lái)讓我們實(shí)現(xiàn)BLoC邏輯,在bloc目錄下創(chuàng)建一個(gè)movies_bloc.dart文件,接著編寫下面的代碼:
import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/item_model.dart';
class MoviesBloc {
final _repository = Repository();
final _moviesFetcher = PublishSubject<ItemModel>();
Observable<ItemModel> get allMovies => _moviesFetcher.stream;
fetchAllMovies() async {
ItemModel itemModel = await _repository.fetchAllMovies();
_moviesFetcher.sink.add(itemModel);
}
dispose() {
_moviesFetcher.close();
}
}
final bloc = MoviesBloc();
在這個(gè)類中,我們創(chuàng)建了Repository對(duì)象用于訪問(wèn)fetchAllMovies。我們創(chuàng)建了一個(gè)PublishSubject對(duì)象,該對(duì)象的任務(wù)是將從服務(wù)器上獲取到的Item作為數(shù)據(jù)流傳遞給UI,要將ItemModel對(duì)象作為流傳遞,我們創(chuàng)建了一個(gè)allMovies方法,其返回類型為Observable。這樣我們就可以通過(guò)最后一行創(chuàng)建的bloc對(duì)象訪問(wèn)到數(shù)據(jù)流了。
如果你不理解什么是響應(yīng)式編程,請(qǐng)看這個(gè),簡(jiǎn)單的說(shuō)就是如果有從服務(wù)器上來(lái)的新數(shù)據(jù),我們就需要更新UI屏幕,為了使更新任務(wù)變得更簡(jiǎn)單,我們?cè)赨I中通過(guò)對(duì)BLoC進(jìn)行觀察從而更新UI的內(nèi)容。
- 最后一部分,在ui包中創(chuàng)建一個(gè)movie_list.dart文件。
import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';
class MovieList extends StatelessWidget {
@override
Widget build(BuildContext context) {
bloc.fetchAllMovies();
return Scaffold(
appBar: AppBar(
title: Text('Popular Movies'),
),
body: StreamBuilder(
stream: bloc.allMovies,
builder: (context, AsyncSnapshot<ItemModel> snapshot) {
if (snapshot.hasData) {
return buildList(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
),
);
}
Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
return GridView.builder(
itemCount: snapshot.data.results.length,
gridDelegate:
new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return Image.network(
'https://image.tmdb.org/t/p/w185${snapshot.data
.results[index].poster_path}',
fit: BoxFit.cover,
);
});
}
}
在這個(gè)類中,沒(méi)有使用StatefulWidget,而是使用了StreamBuilder,它將做和StatefulWidget相同的工作,用來(lái)更新UI。
我們?cè)赽uild方法中進(jìn)行了網(wǎng)絡(luò)調(diào)用,但是build方法時(shí)會(huì)執(zhí)行多次的,在這里進(jìn)行網(wǎng)絡(luò)調(diào)用明顯是不合適的。在下一篇文章中將會(huì)優(yōu)化這一點(diǎn)。
總結(jié)
MovieBloc將數(shù)據(jù)轉(zhuǎn)換成流,然后內(nèi)置的StreamBuilder將會(huì)對(duì)流進(jìn)行監(jiān)聽(tīng),如果數(shù)據(jù)流改變,就會(huì)去更新UI。StreamBuilder需要一個(gè)流參數(shù),所以在MovieBloc中提供了一個(gè)allMovies的流。
