直接開始幹,沒有爲何~android
在上一篇一個完整的登陸實踐,使用 connectivity 、 shared_preferences 、 Dio 實現一個完整的登陸操做,包含網絡鏈接檢測、請求、數據存儲等~git
列表在實際開發中必不可少,所以今天在以前的基礎上,實現一個"完整"列表界。項目主要功能包含 基礎Widget、Dio簡單封裝、Provider3實踐、列表下拉刷新與自動加載等github
固然,接口依舊不是我實現的,是由玩Android友情提供。依舊先看效果:json
Gif看不出請求了幾回數據,上面是請求接口圖,能夠看到請求了1~5頁。1頁請求了2次,由於最後又進行了一次刷新。後端
由於登陸和列表都須要網絡請求,仍是像登陸那樣作的話,就有點繁瑣了。這裏使用單例對 Dio 進行封裝。bash
網絡請求通常都會封裝爲單例,減小資源消耗。服務器
class Net {
Dio _dio;
factory Net() => instance;
static final Net instance = Net._();
// 私有構造器
Net._() {
...
}
...
Dio get dio {
return _dio;
}
}
複製代碼
在私有構造器中配置基本參數和攔截器等,由於登陸後接口須要 Cookie ,因此攔截器裏面也作了關於 Cookie 的處理。markdown
Net._() {
_dio = Dio();
// 基本配置
_dio.options.baseUrl = 'https://www.wanandroid.com/';
_dio.options.connectTimeout = 5000;
_dio.options.receiveTimeout = 5000;
// 攔截器
_dio.interceptors
..add(
InterceptorsWrapper(
onRequest: (RequestOptions options) async {
var prefs = await SharedPreferences.getInstance();
var userJson = prefs.getString('user');
if (userJson != null && userJson.isNotEmpty) {
UserData user = UserData.fromJson(jsonDecode(userJson));
options.headers
..addAll({
'userId': user.id ?? '',
'token': user.token ?? '',
});
}
// 添加cookie
var cookie = prefs.getString("login_cookies");
if (cookie != null) {
options.headers.addAll({"Cookie": cookie.toString()});
}
return options;
},
onResponse: (Response res) async {
// 保存cookie
var cookies = res.headers['Set-Cookie'];
var prefs = await SharedPreferences.getInstance();
if (cookies != null && cookies.isNotEmpty) {
prefs.setString("login_cookies", cookies.toString());
}
},
),
)
..add(LogInterceptor(requestBody: true, responseBody: true));
// // 設置代理
// var clientAdapter = (dio.httpClientAdapter as DefaultHttpClientAdapter);
//
// clientAdapter.onHttpClientCreate = (HttpClient client) {
// client.findProxy = (uri) {
// //proxy all request to localhost:8888
// return 'PROXY 192.168.10.82:8888';
// };
// client.badCertificateCallback =
// (X509Certificate cert, String host, int port) => true;
// };
}
複製代碼
封裝請求,Flutter 就不必用回調形式了,由於我們有 Future,好處是能夠連續處理,避免嵌套。而後在真正發起請求前仍是須要判斷一下網絡鏈接是否正常,請求完成後作一個簡單預判(實際能夠將後端返回 json 作下處理,通常返回格式都是{ "data": *** , "code": 0, "message": "" })。get請求大體相同,看源碼就好。cookie
Future post(String path, {data}) async {
// 檢測網絡鏈接
var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
throw Exception('網絡錯誤~');
}
// 發起請求
Response response = await dio.post(path, data: data);
if (response.statusCode == 200) {
return response.data;
} else {
throw Exception('服務器錯誤~');
}
}
複製代碼
最後看一下調用網絡
_doLogin() {
LoadingDialog.show(context);
Net.instance
.post('user/login',
data: FormData.fromMap({
"username": _accountController.text.trim(),
"password": _pwdController.text.trim(),
}))
.then((data) async {
UserEntity user = UserEntity.fromJson(data);
if (user.errorCode == 0) {
//登陸成功後 保存信息
LoadingDialog.hide(context);
...
} else {
LoadingDialog.hide(context);
...
}
}).catchError((e) {
print(e.toString());
});
}
複製代碼
class ProjectListPage extends StatefulWidget {
@override
_ProjectListPageState createState() => _ProjectListPageState();
}
class _ProjectListPageState extends State<ProjectListPage> {
List<ProjectListDataData> list = List();
@override
void initState() {
loadData();
super.initState();
}
/// 構建界面
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ...,
body: ListView.builder(
itemBuilder: (context, index) {
var item = list[index];
return _buildItem(item);
},
itemCount: list.length,
),
);
}
/// 構建 item
Widget _buildItem(ProjectListDataData item) {
return Card(
child: Padding(
padding: EdgeInsets.all(10),
child: Row(
children: <Widget>[
Image.network(
...
),
SizedBox(
...
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(...),
Text(...),
Text(...),
],
))
],
),
),
);
}
/// 請求列表數據
loadData() async {
Net.instance
.get('project/list/1/json', queryParameters: {'cid': 294}).then((res) {
ProjectListEntity entity = ProjectListEntity.fromJson(res);
if (entity.errorCode == 0) {
setState(() {
//登陸成功後 保存信息
list = entity.data.datas;
});
} else {
throw Exception('響應錯誤');
}
}).catchError((e) {
print(e.toString());
}).whenComplete(() {});
}
}
複製代碼
在 initState 中調用了 loadData 獲取數據,當數據獲取完畢後,調用 setState 更新界面數據,這樣一個簡單的列表就實現了,效果以下。
目前爲止咱們用到了新的 Widget 有:ListView、Card、Row、Expanded,接下來簡單介紹下:
比較經常使用的是前3種方式,少許列表用第一種,大量列表用第二種,須要分割線能夠用第三種,。
ListView.builder({
Key key,
Axis scrollDirection = Axis.vertical, // 滾動方向
bool reverse = false, // 是否反向
ScrollController controller, // 滾動控制和監聽
bool primary,
ScrollPhysics physics, // 滾動響應操做; ClampingScrollPhysics【Android 越界水波紋】、 BouncingScrollPhysics【iOS 越界回彈】
bool shrinkWrap = false, // 滾動視圖長度是否只包含內容
EdgeInsetsGeometry padding,
this.itemExtent, // item長度,豎向是高度 橫向是寬度 ,若是非空則強制 child 使用,不作測量
@required IndexedWidgetBuilder itemBuilder, // item 構建
int itemCount, // item 數
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
複製代碼
Card({
Key key,
this.color, // 背景色
this.elevation, // z 座標,簡單理解控制陰影顯示效果
this.shape, // 卡片形狀
this.borderOnForeground = true,
this.margin, // 外邊距
this.clipBehavior,
this.child,
this.semanticContainer = true,
})
複製代碼
Expanded 應用在 Flex 佈局中,如Row 和 Column 。使包裹的子 Widget 儘量多的佔用空間。
上面已經完成基礎了列表,接下來就是添加下拉刷新和加載更多。爲何叫加載更多,而不是上拉加載,由於是根據距離自動觸發。下面是加入下拉刷新和加載更多後的代碼(包含分頁):
...
/// 構建界面
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
title: Text('項目'),
backgroundColor: Colors.white,
),
body: NotificationListener(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
var item = list[index];
return _buildItem(item);
},
itemCount: list.length,
),
onRefresh: () {
// 下拉刷新
_pageIndex = 1;
return loadData(_pageIndex);
}),
onNotification: (ScrollNotification notify) {
/// 判斷滑動距離【小於等於400 】和 滾動方向
if (notify.metrics.pixels >= (notify.metrics.maxScrollExtent - 400) &&
notify.metrics.axis == Axis.vertical) {
// 加載更多
_pageIndex += 1;
loadData(_pageIndex);
}
return true;
},
),
);
}
...
/// 請求列表數據
loadData(int pageIndex) async {
await Net.instance.get('project/list/$pageIndex/json',
queryParameters: {'cid': 294}).then((res) {
ProjectListEntity entity = ProjectListEntity.fromJson(res);
if (entity.errorCode == 0) {
setState(() {
if(pageIndex == 1){
list = entity.data.datas;
}else{
list.addAll(entity.data.datas);
}
});
} else {
throw Exception('響應錯誤');
}
}).catchError((e) {
print(e.toString());
}).whenComplete(() {});
}
複製代碼
這一步完成後,就是頁面文章的效果了。這一步用到了 RefreshIndicator 和 NotificationListener 兩個 Widget,老規矩來個簡單說明。
RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0, // 刷新距離
@required this.onRefresh, // 刷新回調 須要有 async 和 await 關鍵字,缺乏 await,刷新圖標立馬消失,缺乏 async,刷新圖標不會消失
this.color, // 指示器前景色
this.backgroundColor, //指示器背景色
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
})
複製代碼
NotificationListener({
Key key,
@required this.child, // 監聽的子 Widget
this.onNotification, // 通知回調
})
複製代碼
經過上面的代碼能夠看到,請求完成後咱們經過 setState 重繪界面。在實際使用中咱們可能只須要繪製界面的某一起。爲了方便使用 Provider 控制刷新範圍,所以使用 ChangeNotifierProvider 和 Consumer 封裝了一個 ProviderWidget 。不熟悉 Provider 能夠先看下Flutter開始干係列-狀態管理Provider3
class ProviderWidget<T extends ChangeNotifier> extends StatefulWidget {
final T model;
final Widget child;
final ValueWidgetBuilder<T> builder;
final Function(T) onReady;
const ProviderWidget({Key key, this.model, this.builder, this.onReady, this.child})
: super(key: key);
@override
_ProviderWidgetState createState() => _ProviderWidgetState<T>();
}
class _ProviderWidgetState<T extends ChangeNotifier> extends State<ProviderWidget<T>> {
T model;
@override
void initState() {
model = widget.model;
//第一幀回調,避免build中異常,如顯示彈窗,固然根據我的實際狀況來
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.onReady != null) {
widget.onReady(model);
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (_) => model,
child: Consumer(
builder: widget.builder,
child: widget.child,
),
);
}
}
複製代碼
...
@override
Widget build(BuildContext context) {
print('--------build page----------');
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
title: Text('項目'),
backgroundColor: Colors.white,
),
body: ProviderWidget<ProjectViewModel>(
model: ProjectViewModel(),
onReady: (model) => model.refresh(),
builder: (context, model, _) {
print('--------build list----------');
return NotificationListener(
child: RefreshIndicator(
child: (model.list?.length ?? 0) == 0
? Container()
: ListView.builder(
itemBuilder: (context, index) {
ProjectListDataData item = model.list[index];
return _buildItem(item);
},
itemCount: model.list?.length ?? 0,
),
onRefresh: () => model.refresh(),
),
onNotification: (ScrollNotification notify) {
/// 判斷滑動距離和滾動方向
if (notify.metrics.pixels >=
(notify.metrics.maxScrollExtent - 400) &&
notify.metrics.axis == Axis.vertical) {
model.loadMore();
}
return true;
},
);
},
),
);
}
...
複製代碼
示例代碼中我使用了日誌輸出以下,能夠發現除了初次構建,都不會再輸出 build page。
2019-10-22 15:10:35.239 11686-11742/com.joker.flutter_widgets I/flutter: --------build page----------
2019-10-22 15:10:35.243 11686-11742/com.joker.flutter_widgets I/flutter: --------build list----------
2019-10-22 15:10:42.126 11686-11742/com.joker.flutter_widgets I/flutter: --------build list----------
複製代碼
可能經過日誌有的朋友仍是不能理解,這裏咱們在列表頂部再加一個妹子,列表更新的時候妹子是不須要更新的,這時就可使用 Provider ,將刷新粒度控制在列表上。這部分代碼作了在項目中作了屏蔽,能夠取消註釋看看。
說明幾點: